From 10bc441e1ee70f685f24b285708939c08b0108b7 Mon Sep 17 00:00:00 2001 From: Bel LaPointe Date: Tue, 7 Sep 2021 15:46:01 -0600 Subject: [PATCH] bankofamerica impl charges --- bank.go | 9 +++++--- config.go | 11 ++++++---- scrape.go | 41 +++++++++++++++++++++++++++++++++++ scrape_test.go | 35 ++++++++++++++++++++++++++++++ testdata/bofa.charge.txt | 46 ++++++++++++++++++++++++++++++++++++++++ transaction.go | 2 +- 6 files changed, 136 insertions(+), 8 deletions(-) create mode 100644 testdata/bofa.charge.txt diff --git a/bank.go b/bank.go index fe3d8cc..738f8d3 100755 --- a/bank.go +++ b/bank.go @@ -3,13 +3,16 @@ package main type Bank int const ( - Chase Bank = iota + 1 - Citi Bank = iota + 1 - UCCU Bank = iota + 1 + Chase Bank = iota + 1 + Citi Bank = iota + 1 + UCCU Bank = iota + 1 + BankOfAmerica Bank = iota + 1 ) func (b Bank) String() string { switch b { + case BankOfAmerica: + return "BankOfAmerica" case Chase: return "Chase" case Citi: diff --git a/config.go b/config.go index 9b20b3d..8421b2a 100755 --- a/config.go +++ b/config.go @@ -7,6 +7,7 @@ import ( "local/args" "local/oauth2" "local/storage" + "log" "net/http" "strings" ) @@ -55,7 +56,7 @@ func NewConfig() Config { as.Append(args.STRING, "todolist", "todo list", "") as.Append(args.STRING, "todotag", "todo tag", "expense") - as.Append(args.STRING, "banks", "uccu,citi,chase", "uccu,citi,chase") + as.Append(args.STRING, "banks", "uccu,citi,chase,bankofamerica", "uccu,citi,chase,bankofamerica") as.Append(args.STRING, "accounts", "regex to include filter accounts", ".*") as.Append(args.STRING, "not-accounts", "regex to exclude filter accounts", "zzzzzz") @@ -91,11 +92,13 @@ func NewConfig() Config { Storage: storage, Uploader: ul, Banks: map[Bank]bool{ - Chase: strings.Contains(strings.ToLower(as.GetString("banks")), strings.ToLower(Chase.String())), - Citi: strings.Contains(strings.ToLower(as.GetString("banks")), strings.ToLower(Citi.String())), - UCCU: strings.Contains(strings.ToLower(as.GetString("banks")), strings.ToLower(UCCU.String())), + BankOfAmerica: strings.Contains(strings.ToLower(as.GetString("banks")), strings.ToLower(BankOfAmerica.String())), + Chase: strings.Contains(strings.ToLower(as.GetString("banks")), strings.ToLower(Chase.String())), + Citi: strings.Contains(strings.ToLower(as.GetString("banks")), strings.ToLower(Citi.String())), + UCCU: strings.Contains(strings.ToLower(as.GetString("banks")), strings.ToLower(UCCU.String())), }, } + log.Printf("config: %+v", config) if config.Uploader == UploaderTodo { token := as.GetString("todotoken") diff --git a/scrape.go b/scrape.go index c8159a7..62ff3a4 100755 --- a/scrape.go +++ b/scrape.go @@ -15,6 +15,7 @@ type scraper interface { scrape(*mail.Message) ([]*Transaction, error) } +type bankOfAmericaScraper struct{} type chaseScraper struct{} type citiScraper struct{} type uccuScraper struct{} @@ -36,6 +37,9 @@ func buildScraper(m *mail.Message, banks map[Bank]bool) (scraper, error) { if strings.Contains(from, "Chase") && banks[Chase] { return newChaseScraper(), nil } + if strings.Contains(from, "Bank of America") && banks[BankOfAmerica] { + return newBankOfAmericaScraper(), nil + } if strings.Contains(from, "Citi") && banks[Citi] { return newCitiScraper(), nil } @@ -45,6 +49,10 @@ func buildScraper(m *mail.Message, banks map[Bank]bool) (scraper, error) { return nil, errors.New("unknown sender: " + from) } +func newBankOfAmericaScraper() scraper { + return &bankOfAmericaScraper{} +} + func newChaseScraper() scraper { return &chaseScraper{} } @@ -238,3 +246,36 @@ func (c *uccuScraper) scrape(m *mail.Message) ([]*Transaction, error) { transaction := NewTransaction(UCCU.String(), fmt.Sprintf("%.2f", f), "?", fmt.Sprint(m.Header["Date"]), UCCU) return []*Transaction{transaction}, nil } + +func (c *bankOfAmericaScraper) scrape(m *mail.Message) ([]*Transaction, error) { + subject := fmt.Sprint(m.Header["Subject"]) + if strings.Contains(subject, "Credit card transaction") { + return c.scrapeCharge(m) + } + return nil, errors.New("not impl") +} + +func (c *bankOfAmericaScraper) scrapeCharge(m *mail.Message) ([]*Transaction, error) { + b, err := ioutil.ReadAll(m.Body) + if err != nil { + return nil, err + } + amount := "" + acc := "" + for _, line := range bytes.Split(b, []byte("\n")) { + if amount == "" && bytes.HasPrefix(line, []byte("Amount: $")) { + words := bytes.Split(bytes.TrimSpace(line), []byte(" ")) + lastword := words[len(words)-1][1:] + escapedfloat := bytes.TrimPrefix(lastword, []byte("$")) + fixEscape := bytes.ReplaceAll(escapedfloat, []byte("=2E"), []byte(".")) + amount = string(fixEscape) + } else if acc == "" && bytes.HasPrefix(line, []byte("Where: ")) { + acc = string(bytes.TrimSpace(bytes.TrimPrefix(line, []byte("Where: ")))) + } + } + if amount == "" || acc == "" { + return nil, errors.New("no amount/account found") + } + transaction := NewTransaction(BankOfAmerica.String(), amount, acc, fmt.Sprint(m.Header["Date"]), BankOfAmerica) + return []*Transaction{transaction}, nil +} diff --git a/scrape_test.go b/scrape_test.go index a3f850c..f145fdb 100644 --- a/scrape_test.go +++ b/scrape_test.go @@ -108,3 +108,38 @@ func TestScrapeChase2020(t *testing.T) { } t.Logf("%+v", got) } + +func TestScrapeBofACharge(t *testing.T) { + b, err := ioutil.ReadFile("./testdata/bofa.charge.txt") + if err != nil { + t.Fatal(err) + } + message := &mail.Message{ + Header: map[string][]string{ + "Subject": []string{"Credit card transaction exceeds alert limit you set"}, + }, + Body: bytes.NewReader(b), + } + bofa := &bankOfAmericaScraper{} + + gots, err := bofa.scrape(message) + if err != nil { + t.Fatal(err) + } + + if len(gots) != 1 { + t.Fatal(len(gots)) + } + got := gots[0] + want := Transaction{ + ID: got.ID, + Bank: BankOfAmerica, + Amount: "75.08", + Vendor: "PAYPAL GIBBDOGENTE MA", + Date: "[]", + Account: BankOfAmerica.String(), + } + if *got != want { + t.Fatalf("want:\n\t%+v, got\n\t%+v", want, *got) + } +} diff --git a/testdata/bofa.charge.txt b/testdata/bofa.charge.txt new file mode 100644 index 0000000..7d852de --- /dev/null +++ b/testdata/bofa.charge.txt @@ -0,0 +1,46 @@ + + +Credit card transaction exceeds alert limit you set + +National Education Association World Mas ending in 7522 + +Amount: $75=2E08 +Date: September 05, 2021 +Where: PAYPAL GIBBDOGENTE MA + +View details by going to +https://www=2Ebankofamerica=2Ecom/deeplink/redirect=2Ego?target=3Dbofasigni= +n&screen=3DAccounts:Home&version=3D7=2E0=2E0 + +If you made this purchase or payment but don=27t recognize the amount, +wait until the final purchase amount has posted before filing a dispute +claim=2E + +If you don=27t recognize this activity, please contact us at the number +on the back of your card=2E + +Did you know? +You can choose how you get alerts from us including text messages and +mobile notifications=2E Go to Alert Settings at +https://www=2Ebankofamerica=2Ecom/deeplink/redirect=2Ego?target=3Dalerts_se= +ttings&screen=3DAlerts:Home&gotoSetting=3Dtrue&version=3D7=2E1=2E0 + + +We'll never ask for your personal information such as SSN or ATM PIN in +email messages=2E If you get an email that looks suspicious or you are not = + +the intended recipient of this email, don't click on any links=2E Instead, = + +forward to abuse@bankofamerica=2Ecom then delete it=2E + +Please don't reply to this automatically generated service email=2E +Read our Privacy Notice https://www=2Ebankofamerica=2Ecom/privacy/consumer-= +privacy-notice=2Ego +Equal Housing Lender: https://www=2Ebankofamerica=2Ecom/help/equalhousing= +=2Ecfm +Bank of America, N=2EA=2E Member FDIC +(C) 2021 Bank of America Corporation + + + +=20 diff --git a/transaction.go b/transaction.go index 39f0de7..645e1f0 100755 --- a/transaction.go +++ b/transaction.go @@ -22,7 +22,7 @@ func (t *Transaction) Format() string { } func (t *Transaction) String() string { - return fmt.Sprint(*t) + return fmt.Sprintf("%+v", *t) } func NewTransaction(account, amount, vendor, date string, bank Bank) *Transaction {