Skip to content

Commit

Permalink
Merge pull request #35 from LucaBernstein/simpletx-default-today
Browse files Browse the repository at this point in the history
Simpletx default today
  • Loading branch information
LucaBernstein authored Nov 25, 2021
2 parents 14ebf1a + 181ee71 commit ad6700a
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 76 deletions.
13 changes: 9 additions & 4 deletions bot/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func (bc *BotController) commandMappings() []*CMD {
{Command: CMD_HELP, Handler: bc.commandHelp, Help: "List this command help"},
{Command: CMD_START, Handler: bc.commandStart, Help: "Give introduction into this bot"},
{Command: CMD_CANCEL, Handler: bc.commandCancel, Help: "Cancel any running commands"},
{Command: CMD_SIMPLE, Handler: bc.commandCreateSimpleTx, Help: "Record a simple transaction"},
{Command: CMD_SIMPLE, Handler: bc.commandCreateSimpleTx, Help: "Record a simple transaction, optionally with date as YYYY-MM-DD, default today"},
{Command: CMD_LIST, Handler: bc.commandList, Help: "List your recorded transactions"},
{Command: CMD_SUGGEST, Handler: bc.commandSuggestions, Help: "List, add or remove suggestions"},
{Command: CMD_CURRENCY, Handler: bc.commandCurrency, Help: "Set the currency to use globally for subsequent transactions"},
Expand Down Expand Up @@ -117,9 +117,14 @@ func (bc *BotController) commandCreateSimpleTx(m *tb.Message) {
"I will guide you through.\n\n",
clearKeyboard(),
)
hint := bc.State.
SimpleTx(m). // create new tx
NextHint(bc.Repo, m) // get first hint
tx, err := bc.State.SimpleTx(m) // create new tx
if err != nil {
bc.Bot.Send(m.Sender, "Something went wrong creating your transactions ("+err.Error()+"). Please check /help for usage."+
"\n\nYou can create a simple transaction using this command: /simple [YYYY-MM-DD]\ne.g. /simple 2021-01-24\n"+
"The date parameter is non-mandatory, if not specified, today's date will be taken.", clearKeyboard())
return
}
hint := tx.NextHint(bc.Repo, m) // get first hint
bc.Bot.Send(m.Sender, hint.Prompt, ReplyKeyboard(hint.KeyboardOptions))
}

Expand Down
9 changes: 4 additions & 5 deletions bot/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,10 @@ func TestTextHandlingWithoutPriorState(t *testing.T) {
// Create simple tx and fill it completely
bc.commandCreateSimpleTx(&tb.Message{Chat: chat})
tx := bc.State.states[12345]
tx.Input(&tb.Message{Text: "17.34"}) // amount
tx.Input(&tb.Message{Text: "Assets:Wallet"}) // from
tx.Input(&tb.Message{Text: "Expenses:Groceries"}) // to
tx.Input(&tb.Message{Text: "Buy something in the grocery store"}) // description
bc.handleTextState(&tb.Message{Chat: chat, Text: "today"}) // date (via handleTextState)
tx.Input(&tb.Message{Text: "17.34"}) // amount
tx.Input(&tb.Message{Text: "Assets:Wallet"}) // from
tx.Input(&tb.Message{Text: "Expenses:Groceries"}) // to
bc.handleTextState(&tb.Message{Chat: chat, Text: "Buy something in the grocery store"}) // description (via handleTextState)

// After the first tx is done, send some command
m := &tb.Message{Chat: chat}
Expand Down
9 changes: 6 additions & 3 deletions bot/stateHandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,11 @@ func (s *StateHandler) Get(m *tb.Message) Tx {
return s.states[(chatId)(m.Chat.ID)]
}

func (s *StateHandler) SimpleTx(m *tb.Message) Tx {
tx := CreateSimpleTx()
func (s *StateHandler) SimpleTx(m *tb.Message) (Tx, error) {
tx, err := CreateSimpleTx(m)
if err != nil {
return nil, err
}
s.states[(chatId)(m.Chat.ID)] = tx
return tx
return tx, nil
}
6 changes: 3 additions & 3 deletions bot/suggestions.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"fmt"
"strings"

. "github.com/LucaBernstein/beancount-bot-tg/helpers"
h "github.com/LucaBernstein/beancount-bot-tg/helpers"
tb "gopkg.in/tucnak/telebot.v2"
)

Expand Down Expand Up @@ -32,7 +32,7 @@ func (bc *BotController) suggestionsHandler(m *tb.Message) {
return
}

if !ArrayContainsC(AllowedSuggestionTypes(), suggType, false) {
if !h.ArrayContainsC(h.AllowedSuggestionTypes(), suggType, false) {
bc.suggestionsHelp(m)
return
}
Expand All @@ -50,7 +50,7 @@ func (bc *BotController) suggestionsHandler(m *tb.Message) {
}

func (bc *BotController) suggestionsHelp(m *tb.Message) {
suggestionTypes := strings.Join(AllowedSuggestionTypes(), ", ")
suggestionTypes := strings.Join(h.AllowedSuggestionTypes(), ", ")
bc.Bot.Send(m.Sender, fmt.Sprintf(`Usage help for /suggestions:
/suggestions list <type>
/suggestions add <type> <value>
Expand Down
75 changes: 42 additions & 33 deletions bot/transactionBuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,11 @@ import (
"unicode/utf8"

"github.com/LucaBernstein/beancount-bot-tg/db/crud"
. "github.com/LucaBernstein/beancount-bot-tg/helpers"
"github.com/LucaBernstein/beancount-bot-tg/helpers"
c "github.com/LucaBernstein/beancount-bot-tg/helpers"
tb "gopkg.in/tucnak/telebot.v2"
)

const DOT_INDENT = 47
const (
BEANCOUNT_DATE_FORMAT = "2006-01-02"
)

type Hint struct {
Prompt string
KeyboardOptions []string
Expand Down Expand Up @@ -60,21 +56,16 @@ func HandleRaw(m *tb.Message) (string, error) {
return m.Text, nil
}

func HandleDate(m *tb.Message) (string, error) {
// Handle "today" string
if strings.HasPrefix("today", strings.TrimSpace(strings.ToLower(m.Text))) {
return time.Now().Format(BEANCOUNT_DATE_FORMAT), nil
}
func ParseDate(m string) (string, error) {
// Handle YYYY-MM-DD
matched, err := regexp.MatchString("\\d{4}-\\d{2}-\\d{2}", m.Text)
matched, err := regexp.MatchString("\\d{4}-\\d{2}-\\d{2}", m)
if err != nil {
return "", err
}
if !matched {
return "", fmt.Errorf("Input did not match pattern 'YYYY-MM-DD'")
}
// TODO: Try to parse date and check if valid
return m.Text, nil
return m, nil
}

type Tx interface {
Expand All @@ -87,24 +78,38 @@ type Tx interface {
DataKeys() map[string]string

addStep(command command, hint string, handler func(m *tb.Message) (string, error)) Tx
setDate(*tb.Message) (Tx, error)
}

type SimpleTx struct {
steps []command
stepDetails map[command]Input
data []data
date string
step int
}

func CreateSimpleTx() Tx {
return (&SimpleTx{
func CreateSimpleTx(m *tb.Message) (Tx, error) {
tx := (&SimpleTx{
stepDetails: make(map[command]Input),
}).
addStep("amount", "Please enter the amount of money (e.g. '12.34' or '12.34 USD')", HandleFloat).
addStep("from", "Please enter the account the money came from (or select one from the list)", HandleRaw).
addStep("to", "Please enter the account the money went to (or select one from the list)", HandleRaw).
addStep("description", "Please enter a description (or select one from the list)", HandleRaw).
addStep("date", "Please enter the transaction data in the format YYYY-MM-DD (or type 't' / 'today')", HandleDate)
addStep("description", "Please enter a description (or select one from the list)", HandleRaw)
return tx.setDate(m)
}

func (tx *SimpleTx) setDate(m *tb.Message) (Tx, error) {
command := strings.Split(m.Text, " ")
if len(command) >= 2 {
date, err := ParseDate(command[1])
if err != nil {
return nil, err
}
tx.date = date
}
return tx, nil
}

func (tx *SimpleTx) addStep(command command, hint string, handler func(m *tb.Message) (string, error)) Tx {
Expand Down Expand Up @@ -140,7 +145,7 @@ func (tx *SimpleTx) EnrichHint(r *crud.Repo, m *tb.Message, i Input) *Hint {
if i.key == "date" {
return tx.hintDate(i.hint)
}
if ArrayContains([]string{"from", "to"}, i.key) {
if c.ArrayContains([]string{"from", "to"}, i.key) {
return tx.hintAccount(r, m, i)
}
return i.hint
Expand All @@ -153,9 +158,9 @@ func (tx *SimpleTx) hintAccount(r *crud.Repo, m *tb.Message, i Input) *Hint {
err error = nil
)
if i.key == "from" {
res, err = r.GetCacheHints(m, STX_ACCF)
res, err = r.GetCacheHints(m, c.STX_ACCF)
} else if i.key == "to" {
res, err = r.GetCacheHints(m, STX_ACCT)
res, err = r.GetCacheHints(m, c.STX_ACCT)
}
if err != nil {
log.Printf("Error occurred getting cached hint (hintAccount): %s", err.Error())
Expand All @@ -166,7 +171,7 @@ func (tx *SimpleTx) hintAccount(r *crud.Repo, m *tb.Message, i Input) *Hint {
}

func (tx *SimpleTx) hintDescription(r *crud.Repo, m *tb.Message, h *Hint) *Hint {
res, err := r.GetCacheHints(m, STX_DESC)
res, err := r.GetCacheHints(m, c.STX_DESC)
if err != nil {
log.Printf("Error occurred getting cached hint (hintDescription): %s", err.Error())
}
Expand All @@ -180,12 +185,16 @@ func (tx *SimpleTx) hintDate(h *Hint) *Hint {
}

func (tx *SimpleTx) DataKeys() map[string]string {
if tx.date == "" {
// set today as fallback/default date
tx.date = time.Now().Format(helpers.BEANCOUNT_DATE_FORMAT)
}
return map[string]string{
STX_DATE: string(tx.data[4]),
STX_DESC: string(tx.data[3]),
STX_ACCF: string(tx.data[1]),
STX_AMTF: string(tx.data[0]),
STX_ACCT: string(tx.data[2]),
c.STX_DATE: tx.date,
c.STX_DESC: string(tx.data[3]),
c.STX_ACCF: string(tx.data[1]),
c.STX_AMTF: string(tx.data[0]),
c.STX_ACCT: string(tx.data[2]),
}
}

Expand All @@ -199,7 +208,7 @@ func (tx *SimpleTx) FillTemplate(currency string) (string, error) {
}
// Variables
txRaw := tx.DataKeys()
f, err := strconv.ParseFloat(strings.Split(string(txRaw[STX_AMTF]), " ")[0], 64)
f, err := strconv.ParseFloat(strings.Split(string(txRaw[c.STX_AMTF]), " ")[0], 64)
if err != nil {
return "", err
}
Expand All @@ -212,9 +221,9 @@ func (tx *SimpleTx) FillTemplate(currency string) (string, error) {
amountF = fmt.Sprintf("%.2f", f)
}
// Add spaces
spacesNeeded := DOT_INDENT - (utf8.RuneCountInString(string(txRaw[STX_ACCF]))) // accFrom
spacesNeeded -= CountLeadingDigits(f) // float length before point
spacesNeeded -= 2 // additional space in template + negative sign
spacesNeeded := c.DOT_INDENT - (utf8.RuneCountInString(string(txRaw[c.STX_ACCF]))) // accFrom
spacesNeeded -= CountLeadingDigits(f) // float length before point
spacesNeeded -= 2 // additional space in template + negative sign
if spacesNeeded < 0 {
spacesNeeded = 0
}
Expand All @@ -224,12 +233,12 @@ func (tx *SimpleTx) FillTemplate(currency string) (string, error) {
%s%s -%s %s
%s
`
amount := strings.Split(txRaw[STX_AMTF], " ")
amount := strings.Split(txRaw[c.STX_AMTF], " ")
if len(amount) >= 2 {
// amount input contains currency
currency = amount[1]
}
return fmt.Sprintf(tpl, txRaw[STX_DATE], txRaw[STX_DESC], txRaw[STX_ACCF], addSpacesFrom, amountF, currency, txRaw[STX_ACCT]), nil
return fmt.Sprintf(tpl, txRaw[c.STX_DATE], txRaw[c.STX_DESC], txRaw[c.STX_ACCF], addSpacesFrom, amountF, currency, txRaw[c.STX_ACCT]), nil
}

func (tx *SimpleTx) Debug() string {
Expand Down
60 changes: 32 additions & 28 deletions bot/transactionBuilder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,14 @@ func TestHandleFloat(t *testing.T) {
}

func TestTransactionBuilding(t *testing.T) {
tx := bot.CreateSimpleTx()
tx, err := bot.CreateSimpleTx(&tb.Message{Text: "/simple"})
if err != nil {
t.Errorf("Error creating simple tx: %s", err.Error())
}
tx.Input(&tb.Message{Text: "17"}) // amount
tx.Input(&tb.Message{Text: "Assets:Wallet"}) // from
tx.Input(&tb.Message{Text: "Expenses:Groceries"}) // to
tx.Input(&tb.Message{Text: "Buy something in the grocery store"}) // description
tx.Input(&tb.Message{Text: "2021-01-24"}) // date

if !tx.IsDone() {
t.Errorf("With given input transaction data should be complete for SimpleTx")
Expand All @@ -52,19 +54,22 @@ func TestTransactionBuilding(t *testing.T) {
if err != nil {
t.Errorf("There should be no error raised during templating: %s", err.Error())
}
helpers.TestExpect(t, templated, `2021-01-24 * "Buy something in the grocery store"
today := time.Now().Format(helpers.BEANCOUNT_DATE_FORMAT)
helpers.TestExpect(t, templated, today+` * "Buy something in the grocery store"
Assets:Wallet -17.00 USD
Expenses:Groceries
`, "Templated string should be filled with variables as expected.")
}

func TestTransactionBuildingCustomCurrencyInAmount(t *testing.T) {
tx := bot.CreateSimpleTx()
tx, err := bot.CreateSimpleTx(&tb.Message{Text: "/simple"})
if err != nil {
t.Errorf("Error creating simple tx: %s", err.Error())
}
tx.Input(&tb.Message{Text: "17.3456 USD_TEST"}) // amount
tx.Input(&tb.Message{Text: "Assets:Wallet"}) // from
tx.Input(&tb.Message{Text: "Expenses:Groceries"}) // to
tx.Input(&tb.Message{Text: "Buy something in the grocery store"}) // description
tx.Input(&tb.Message{Text: "2021-01-24"}) // date

if !tx.IsDone() {
t.Errorf("With given input transaction data should be complete for SimpleTx")
Expand All @@ -74,40 +79,39 @@ func TestTransactionBuildingCustomCurrencyInAmount(t *testing.T) {
if err != nil {
t.Errorf("There should be no error raised during templating: %s", err.Error())
}
helpers.TestExpect(t, templated, `2021-01-24 * "Buy something in the grocery store"
today := time.Now().Format(helpers.BEANCOUNT_DATE_FORMAT)
helpers.TestExpect(t, templated, today+` * "Buy something in the grocery store"
Assets:Wallet -17.3456 USD_TEST
Expenses:Groceries
`, "Templated string should be filled with variables as expected.")
}

func TestCountLeadingDigits(t *testing.T) {
helpers.TestExpect(t, bot.CountLeadingDigits(12.34), 2, "")
helpers.TestExpect(t, bot.CountLeadingDigits(0.34), 1, "")
helpers.TestExpect(t, bot.CountLeadingDigits(1244.0), 4, "")
}

func TestDateSpecialNow(t *testing.T) {
h, err := bot.HandleDate(&tb.Message{Text: " ToDaY "})
func TestTransactionBuildingWithDate(t *testing.T) {
tx, err := bot.CreateSimpleTx(&tb.Message{Text: "/simple 2021-01-24"})
if err != nil {
t.Errorf("There should be no error handling date 'today': %s", err.Error())
t.Errorf("Error creating simple tx: %s", err.Error())
}
helpers.TestExpect(t, h, time.Now().Format("2006-01-02"), "")
tx.Input(&tb.Message{Text: "17.3456 USD_TEST"}) // amount
tx.Input(&tb.Message{Text: "Assets:Wallet"}) // from
tx.Input(&tb.Message{Text: "Expenses:Groceries"}) // to
tx.Input(&tb.Message{Text: "Buy something in the grocery store"}) // description

// GitHub-Issue #14: Also accept parts of "today" string for quicker recording
h, err = bot.HandleDate(&tb.Message{Text: " t"})
if err != nil {
t.Errorf("There should be no error handling date 't': %s", err.Error())
if !tx.IsDone() {
t.Errorf("With given input transaction data should be complete for SimpleTx")
}
helpers.TestExpect(t, h, time.Now().Format("2006-01-02"), "")

h, err = bot.HandleDate(&tb.Message{Text: " tod"})
templated, err := tx.FillTemplate("EUR")
if err != nil {
t.Errorf("There should be no error handling date 'tod': %s", err.Error())
t.Errorf("There should be no error raised during templating: %s", err.Error())
}
helpers.TestExpect(t, h, time.Now().Format("2006-01-02"), "")
helpers.TestExpect(t, templated, `2021-01-24 * "Buy something in the grocery store"
Assets:Wallet -17.3456 USD_TEST
Expenses:Groceries
`, "Templated string should be filled with variables as expected.")
}

_, err = bot.HandleDate(&tb.Message{Text: "tomorrow"})
if err == nil {
t.Errorf("There should be an error processing the date 'tomorrow': %s", err.Error())
}
func TestCountLeadingDigits(t *testing.T) {
helpers.TestExpect(t, bot.CountLeadingDigits(12.34), 2, "")
helpers.TestExpect(t, bot.CountLeadingDigits(0.34), 1, "")
helpers.TestExpect(t, bot.CountLeadingDigits(1244.0), 4, "")
}
4 changes: 4 additions & 0 deletions helpers/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ const (
STX_ACCF = "accFrom"
STX_AMTF = "amountFrom"
STX_ACCT = "accTo"

DOT_INDENT = 47

BEANCOUNT_DATE_FORMAT = "2006-01-02"
)

func AllowedSuggestionTypes() []string {
Expand Down

0 comments on commit ad6700a

Please sign in to comment.