5 Commits
v0.6 ... v0.9

Author SHA1 Message Date
Bel LaPointe
10bc441e1e bankofamerica impl charges 2021-09-07 15:46:01 -06:00
Bel LaPointe
6f3bf1f6a4 dont negate payment 2021-09-07 15:03:38 -06:00
Bel LaPointe
09f14ec44c chase payments a go, update password to app password 2021-07-31 11:35:15 -06:00
bel
3077343c89 add anti-patterns, print acc for ledger, fix chase 2021 from body.read double 2021-07-30 06:28:21 -06:00
bel
24b66d1c46 filter accounts by pattern 2021-07-30 00:07:34 -06:00
9 changed files with 654 additions and 30 deletions

View File

@@ -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:

View File

@@ -7,6 +7,7 @@ import (
"local/args"
"local/oauth2"
"local/storage"
"log"
"net/http"
"strings"
)
@@ -24,16 +25,18 @@ var uploaders = map[string]Uploader{
}
type Config struct {
EmailUser string
EmailPass string
EmailIMAP string
TodoAddr string
TodoToken string
TodoList string
TodoTag string
Uploader Uploader
Storage storage.DB
Banks map[Bank]bool
EmailUser string
EmailPass string
EmailIMAP string
TodoAddr string
TodoToken string
TodoList string
TodoTag string
Uploader Uploader
Storage storage.DB
Banks map[Bank]bool
AccountsPattern string
AccountsAntiPattern string
}
var config Config
@@ -42,7 +45,7 @@ func NewConfig() Config {
as := args.NewArgSet()
as.Append(args.STRING, "emailuser", "email username", "breellocaldev@gmail.com")
as.Append(args.STRING, "emailpass", "email password", "ML3WQRFSqe9rQ8qNkm")
as.Append(args.STRING, "emailpass", "email password", "diblloewfncwssof")
as.Append(args.STRING, "emailimap", "email imap", "imap.gmail.com:993")
as.Append(args.STRING, "uploader", "todo, ledger", "todo")
@@ -52,7 +55,10 @@ func NewConfig() Config {
as.Append(args.STRING, "todotoken", "todo token", "")
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")
as.Append(args.STRING, "authaddr", "auth addr", "https://auth.remote.blapointe.com")
as.Append(args.STRING, "store", "store type", "map")
@@ -76,19 +82,23 @@ func NewConfig() Config {
}
config = Config{
EmailUser: as.GetString("emailuser"),
EmailPass: as.GetString("emailpass"),
EmailIMAP: as.GetString("emailimap"),
TodoAddr: as.GetString("todoaddr"),
TodoTag: as.GetString("todotag"),
Storage: storage,
Uploader: ul,
EmailUser: as.GetString("emailuser"),
EmailPass: as.GetString("emailpass"),
EmailIMAP: as.GetString("emailimap"),
TodoAddr: as.GetString("todoaddr"),
TodoTag: as.GetString("todotag"),
AccountsPattern: as.GetString("accounts"),
AccountsAntiPattern: as.GetString("not-accounts"),
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")

11
main.go
View File

@@ -3,6 +3,7 @@ package main
import (
"local/sandbox/contact/contact"
"log"
"regexp"
)
func main() {
@@ -16,12 +17,22 @@ func main() {
if err != nil {
panic(err)
}
patterns := regexp.MustCompile(config.AccountsPattern)
antipatterns := regexp.MustCompile(config.AccountsAntiPattern)
for email := range emails {
transactions, err := Scrape(email, config.Banks)
if err != nil {
log.Println("failed to scrape email:", err)
}
for _, transaction := range transactions {
if !patterns.MatchString(transaction.Account) {
log.Printf("skipping unmatching account pattern %q vs %q", config.AccountsPattern, transaction.Account)
continue
}
if antipatterns.MatchString(transaction.Account) {
log.Printf("skipping match account antipattern %q vs %q", config.AccountsAntiPattern, transaction.Account)
continue
}
if _, err := config.Storage.Get(transaction.ID); err == nil {
log.Println("skipping duplicate transaction:", transaction)
} else {

View File

@@ -15,6 +15,7 @@ type scraper interface {
scrape(*mail.Message) ([]*Transaction, error)
}
type bankOfAmericaScraper struct{}
type chaseScraper struct{}
type citiScraper struct{}
type uccuScraper struct{}
@@ -29,13 +30,16 @@ func Scrape(m *mail.Message, banks map[Bank]bool) ([]*Transaction, error) {
func buildScraper(m *mail.Message, banks map[Bank]bool) (scraper, error) {
subject := fmt.Sprint(m.Header["Subject"])
if !containsAny(subject, "transaction", "report", "Transaction") {
if !containsAny(subject, "transaction", "report", "Transaction", "payment") {
return nil, errors.New("cannot build scraper for subject " + subject)
}
from := fmt.Sprint(m.Header["From"])
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{}
}
@@ -67,14 +75,46 @@ func containsAny(a string, b ...string) bool {
}
func (c *chaseScraper) scrape(m *mail.Message) ([]*Transaction, error) {
transactions, err := c.scrape2020(m)
transactions, err := c.scrape2021(m)
if err == nil && len(transactions) > 0 {
return transactions, err
}
return c.scrape2021(m)
return c.scrape2020(m)
}
func (c *chaseScraper) scrape2021(m *mail.Message) ([]*Transaction, error) {
if t, err := c.scrape2021Payment(m); err == nil {
return t, err
}
return c.scrape2021Charge(m)
}
func (c *chaseScraper) scrape2021Payment(m *mail.Message) ([]*Transaction, error) {
re := regexp.MustCompile(`^We've received your .* payment$`)
if !re.Match([]byte(m.Header["Subject"][0])) {
return nil, errors.New("no match subject search")
}
b, err := ioutil.ReadAll(m.Body)
if err != nil {
return nil, err
}
re = regexp.MustCompile(`\$[0-9]+\.[0-9]{2}`)
amount := "-" + strings.TrimLeft(string(re.Find(b)), "$")
amount = strings.TrimLeft(string(re.Find(b)), "$")
vendor := "Payment"
re = regexp.MustCompile(`\(\.\.\.[0-9]{4}\)`)
match := re.Find(b)
re = regexp.MustCompile(`[0-9]{4}`)
account := string(re.Find(match))
return []*Transaction{NewTransaction(account, amount, vendor, fmt.Sprint(m.Header["Date"]), Chase)}, nil
}
func (c *chaseScraper) scrape2021Charge(m *mail.Message) ([]*Transaction, error) {
re := regexp.MustCompile(`^Your \$(?P<amount>[0-9\.]*) transaction with (?P<vendor>.*)$`)
matches := re.FindSubmatch([]byte(m.Header["Subject"][0]))
if len(matches) < 1 {
@@ -206,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
}

View File

@@ -7,6 +7,41 @@ import (
"testing"
)
func TestScrapeChase2021Payment(t *testing.T) {
b, err := ioutil.ReadFile("./testdata/chase.2021.payment.txt")
if err != nil {
t.Fatal(err)
}
message := &mail.Message{
Header: map[string][]string{
"Subject": []string{"We've received your AARP from Chase payment"},
},
Body: bytes.NewReader(b),
}
chase := &chaseScraper{}
gots, err := chase.scrape2021(message)
if err != nil {
t.Fatal(err)
}
if len(gots) != 1 {
t.Fatal(gots)
}
got := gots[0]
if got.Account != "8824" {
t.Fatalf("bad account: %v: %+v", got.Account, got)
}
if got.Amount != "100.00" {
t.Fatalf("bad amount: %v: %+v", got.Amount, got)
}
if got.Vendor != "Payment" {
t.Fatalf("bad vendor: %v: %+v", got.Vendor, got)
}
t.Logf("%+v", got)
}
func TestScrapeChase2021(t *testing.T) {
b, err := ioutil.ReadFile("./testdata/chase.2021.txt")
if err != nil {
@@ -73,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)
}
}

46
testdata/bofa.charge.txt vendored Normal file
View File

@@ -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

411
testdata/chase.2021.payment.txt vendored Normal file
View File

@@ -0,0 +1,411 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.=
w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns=3D"http://www.w3.org/1999/xhtml" lang=3D"en">
<head>
<meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3DUTF-8" />
<meta name=3D"viewport" content=3D"width=3Ddevice-width, initial-scale=3D1.=
0"/>
<title>This payment has been applied to your account.</title>
<style type=3D"text/css">
* {
=09line-height: normal !important;
}
strong {
=09font-weight: bold !important;
}
em {
=09font-style: italic !important;
}
body {
=09background-color: #d7dbe0 !important;
=09-webkit-text-size-adjust: none !important;
}
.ExternalClass * {
=09line-height: 112%
}
.ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass =
td {
=09line-height: 112%
}
td {
=09-webkit-text-size-adjust: none;
}
a[href^=3Dtel] {
=09color: inherit;
=09text-decoration: none;
}
.applelinksgray41 a {
=09color: #414042 !important;
=09text-decoration: none;
}
.applelinksgray a {
=09color: #717171 !important;
=09text-decoration: none;
}
.wordBreak {
=09overflow-wrap: break-word;
=09word-wrap: break-word;
=09word-break: break-all;
=09word-break: break-word;
}
@media screen and (max-width: 800px) {
.fullWidth {
=09width: 100% !important;
=09min-width: 100% !important;
=09margin-left: auto !important;
=09margin-right: auto !important;
=09padding: 0px !important;
=09text-align: center !important;
}
.hero {
=09width: 100% !important;
=09height: auto !important;
}
.moPad {
=09padding-right: 20px !important;
=09padding-left: 20px !important;
}
.zeroPad {
=09padding-right: 0px !important;
=09padding-left: 0px !important;
}
.font14 {
=09font-size: 14px !important;
}
.font24 {
=09font-size: 24px !important;
}
}
@media print and (max-width: 800px) {
.fullWidth {
=09width: 100% !important;
=09min-width: 100% !important;
=09margin-left: auto !important;
=09margin-right: auto !important;
=09padding: 0px !important;
=09text-align: center !important;
}
.hero {
=09width: 100% !important;
=09height: auto !important;
}
.moPad {
=09padding-right: 20px !important;
=09padding-left: 20px !important;
}
.zeroPad {
=09padding-right: 0px !important;
=09padding-left: 0px !important;
}
.font14 {
=09font-size: 14px !important;
}
.font24 {
=09font-size: 24px !important;
}
}
</style>
</head>
<body style=3D"padding: 0px;margin: 0px; background-color:#d7dbe0;">
<table align=3D"center" width=3D"100%" border=3D"0" cellspacing=3D"0" cellp=
adding=3D"0" style=3D"min-width:800px; background-color:#d7dbe0;" class=3D"=
fullWidth">
<tr>
<td align=3D"center" style=3D"vertical-align:top; padding:0px 0px 20px =
0px; min-width:800px; background-color:#d7dbe0;" class=3D"fullWidth"><table=
align=3D"center" width=3D"800" cellpadding=3D"0" cellspacing=3D"0" border=
=3D"0" class=3D"fullWidth" style=3D"background-color:#FFFFFF;">
<!-- Start of Content -->
<tr>
<td align=3D"center" style=3D"vertical-align:top; padding: 23px 0=
px 0px;background-color: #005EB8;"><table cellpadding=3D"0" cellspacing=3D"=
0" border=3D"0">
<tr>
<td align=3D"right" style=3D"vertical-align:bottom; padding=
:0px 0px; width:12px;"><img src=3D"https://www.chase.com/content/dam/email/=
images/blue-left.jpg" width=3D"12" height=3D"226" border=3D"0" style=3D"dis=
play:block;" alt=3D""/></td>
<td align=3D"center" style=3D"vertical-align:bottom; paddin=
g: 0px 0px 0px;width:616px; background-color: #FFFFFF;"><table width=3D"100=
%" cellpadding=3D"0" cellspacing=3D"0" border=3D"0">
<tr>
<td align=3D"left" style=3D"vertical-align:top; paddi=
ng: 0px 0px; background-color: #ffffff;"><table width=3D"100%" cellpadding=
=3D"0" cellspacing=3D"0" border=3D"0">
<!-- Start hidden preview text -->
<div style=3D"display: none; max-height: 0px; ove=
rflow: hidden;">This payment has been applied to your account.</div>
<!-- Insert &zwnj;&nbsp; after hidden preview tex=
t -->
<div style=3D"display: none; max-height: 0px; ove=
rflow: hidden;"> &nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwn=
j;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&=
nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwn=
j;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&=
nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbs=
p;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&=
zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwn=
j;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&=
nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbs=
p;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&=
zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&nbsp;&zwnj;&nbs=
p;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&=
zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwn=
j;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&=
nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbs=
p;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&=
zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwn=
j;&nbsp;</div>
<!-- End hidden preview text -->
<tr>
<td align=3D"left" style=3D"vertical-align:top;=
padding-left: 30px; background-color: #ffffff;" class=3D"moPad"><table widt=
h=3D"100%" cellpadding=3D"0" cellspacing=3D"0" border=3D"0">
<tr>
<td align=3D"left" style=3D"vertical-alig=
n:bottom; padding:36px 0px 20px;"><img src=3D"https://www.chase.com/content=
/dam/email/images/chase-logo-h-rgb.png" width=3D"104" height=3D"20" border=
=3D"0" style=3D"display:block;" alt=3D"Chase Logo"/></td>
</tr>
</table></td>
</tr>
<tr>
<td align=3D"left" style=3D"vertical-align:top;=
padding: 20px 28px 0px;" class=3D"moPad"><table align=3D"left" cellpadding=
=3D"0" cellspacing=3D"0" border=3D"0">
<tr>
<td align=3D"left" style=3D"vertical-alig=
n:top;"><table width=3D"100%" cellpadding=3D"0" cellspacing=3D"0" border=3D=
"0">
<tr>
<td align=3D"left" style=3D"vertica=
l-align:top; padding:5px 10px; font-family:Arial, Helvetica, sans-serif; fo=
nt-size:12px; font-weight:bold; color:#000000; background-color:#24e16b; bo=
rder-radius:20px; -moz-border-radius: 20px; -webkit-border-radius:20px; whi=
te-space: nowrap;" class=3D"font14">Payment received</td>
</tr>
</table></td>
</tr>
</table></td>
</tr>
<tr>
<td align=3D"left" style=3D"vertical-align:top;=
background-color: #ffffff;"><table width=3D"100%" cellpadding=3D"0" cellsp=
acing=3D"0" border=3D"0">
<tr>
<td align=3D"left" style=3D"vertical-alig=
n:top; padding: 20px 30px 28px;" class=3D"moPad"><table width=3D"100%" cell=
padding=3D"0" cellspacing=3D"0" border=3D"0">
<tr>
<td align=3D"left" style=3D"vertica=
l-align:top; padding: 0px 20px 0px 0px;"><img src=3D"https://static.chasecd=
n.com/content/services/rendition/image.small.png/unified-assets/digital-car=
ds/aarp/41473417018.png" width=3D"57" height=3D"auto" alt=3D"" border=3D"0=
" style=3D"display:block;"/></td>
<td align=3D"left" style=3D"vertica=
l-align:top; padding:0px 50px 0px 0px; font-family:Arial, Helvetica, sans-s=
erif; font-size:30px; font-weight: bold; color:#414042;" class=3D"zeroPad">=
We've received your credit card payment</td>
</tr>
</table></td>
</tr>
</table></td>
</tr>
</table></td>
</tr>
</table></td>
<td align=3D"left" style=3D"vertical-align:bottom; padding:=
0px 0px;width:12px; "><img src=3D"https://www.chase.com/content/dam/email/i=
mages/blue-right.jpg " width=3D"12" height=3D"226" border=3D"0" style=3D"di=
splay:block;" alt=3D""/></td>
</tr>
</table></td>
</tr>
<tr>
<td align=3D"center" style=3D"vertical-align:top; padding: 0px 0p=
x 0px; background-color: #FFFFFF;"><table cellpadding=3D"0" cellspacing=3D"=
0" border=3D"0">
<tr>
<td align=3D"right" style=3D"vertical-align:top; padding:0p=
x 0px; width:12px;"><img src=3D"https://www.chase.com/content/dam/email/ima=
ges/white-left.jpg" width=3D"12" height=3D"77" border=3D"0" style=3D"displa=
y:block;" alt=3D""/></td>
<td align=3D"center" style=3D"vertical-align:top; padding: =
0px 0px 0px;width:616px;"><table width=3D"100%" cellpadding=3D"0" cellspaci=
ng=3D"0" border=3D"0">
<tr>
<td align=3D"left" style=3D"vertical-align:top; paddi=
ng:0px 150px 20px 30px; font-family:Arial, Helvetica, sans-serif; font-size=
:16px; color:#414042;" class=3D"moPad">This payment has been applied to you=
r account.</td>
</tr>
<tr>
<td align=3D"left" style=3D"vertical-align:top; paddi=
ng: 0px 150px 0px 30px;" class=3D"moPad"><table width=3D"100%" cellpadding=
=3D"0" cellspacing=3D"0" border=3D"0">
<tr>
<td align=3D"left" style=3D"vertical-align:top;=
padding: 10px 0px;border-bottom: solid 1px #414042;"><table width=3D"100%"=
cellpadding=3D"0" cellspacing=3D"0" border=3D"0">
<tr>
<td align=3D"left" style=3D"vertical-alig=
n:top; padding:0px 0px 0px 0px; font-family:Arial, Helvetica, sans-serif; f=
ont-size:16px; color:#414042;" class=3D"font14">Account</td>
<td align=3D"right" style=3D"vertical-ali=
gn:top; padding:0px 0px 0px 5px; font-family:Arial, Helvetica, sans-serif; =
font-size:16px; font-weight:bold; color:#414042;" class=3D"font14">AARP fro=
m Chase (...8824)</td>
</tr>
</table></td>
</tr>
<tr>
<td align=3D"left" style=3D"vertical-align:top;=
padding: 10px 0px;border-bottom: solid 1px #414042;"><table width=3D"100%"=
cellpadding=3D"0" cellspacing=3D"0" border=3D"0">
<tr>
<td align=3D"left" style=3D"vertical-alig=
n:top; padding:0px 0px 0px 0px; font-family:Arial, Helvetica, sans-serif; f=
ont-size:16px; color:#414042;" class=3D"font14">Posted date</td>
<td align=3D"right" style=3D"vertical-ali=
gn:top; padding:0px 0px 0px 5px; font-family:Arial, Helvetica, sans-serif; =
font-size:16px; font-weight:bold; color:#414042;" class=3D"font14">Jul 30, =
2021</td>
</tr>
</table></td>
</tr>
<tr>
<td align=3D"left" style=3D"vertical-align:top;=
padding: 10px 0px;border-bottom: solid 1px #414042;"><table width=3D"100%"=
cellpadding=3D"0" cellspacing=3D"0" border=3D"0">
<tr>
<td align=3D"left" style=3D"vertical-alig=
n:top; padding:0px 0px 0px 0px; font-family:Arial, Helvetica, sans-serif; f=
ont-size:16px; color:#414042;" class=3D"font14">Payment amount</td>
<td align=3D"right" style=3D"vertical-ali=
gn:top; padding:0px 0px 0px 5px; font-family:Arial, Helvetica, sans-serif; =
font-size:16px; font-weight:bold; color:#414042;" class=3D"font14"><span cl=
ass=3D"applelinksgray41"><a style=3D"color:#414042;text-decoration: none;">=
$100.00</a></span></td>
</tr>
</table></td>
</tr>
</table></td>
</tr>
<tr>
<td align=3D"left" style=3D"vertical-align:top; paddi=
ng:40px 150px 40px 30px; font-family:Arial, Helvetica, sans-serif; font-siz=
e:16px; color:#414042;" class=3D"moPad">Find <a style=3D"text-decoration: u=
nderline; color:#0060F0;" href=3D"https://www.chase.com/personal/credit-car=
ds/login-epay" rel=3D"noopener noreferrer" target=3D"_blank">more informat=
ion</a> about the credit card payments process.</td>
</tr>
<tr>
<td align=3D"left" style=3D"vertical-align:top;"><tab=
le width=3D"100%" align=3D"left" cellpadding=3D"0" cellspacing=3D"0" border=
=3D"0">
<tr>
<td align=3D"left" style=3D"vertical-align:top;=
"><table width=3D"100%" align=3D"left" cellpadding=3D"0" cellspacing=3D"0" =
border=3D"0" class=3D"fullWidth">
<tr>
<td align=3D"left" style=3D"padding:0px; =
vertical-align:top; padding: 0px 0px 30px 30px;" class=3D"moPad"><table ali=
gn=3D"left" cellpadding=3D"0" cellspacing=3D"0" border=3D"0" style=3D"verti=
cal-align:top;">
<tr>
<td role=3D"button" align=3D"center=
" style=3D"background-color:#0060f0; color: #fffffe; font-size: 16px; font-=
family: Arial, Helvetica, sans-serif; padding: 10px 0px; border: 1px solid =
#0060f0; vertical-align:top; border-radius:4px; -moz-border-radius: 4px; -w=
ebkit-border-radius:4px;width: 200px;"><a href=3D"https://www.chase.com/per=
sonal/mobile-online-banking/payment-activity" target=3D"_blank" style=3D"co=
lor: #fffffe; text-decoration:none;">See payment activity</a></td>
</tr>
</table></td>
</tr>
</table></td>
</tr>
</table></td>
</tr>
<tr>
<td align=3D"left" style=3D"vertical-align:top; paddi=
ng:0px 30px 20px; font-family:Arial, Helvetica, sans-serif; font-size:12px;=
color:#717171;" class=3D"moPad font14">Securely access your accounts with =
the <a style=3D"text-decoration: underline; color:#0060F0;" href=3D"https:/=
/www.chase.com/digital/mobile-banking" rel=3D"noopener noreferrer" target=
=3D"_blank">Chase&nbsp;Mobile<span style=3D"font-size:70%; line-height:0; v=
ertical-align:3px; text-decoration: none;">&reg;</span> app</a> or <a style=
=3D"text-decoration: underline; color:#0060F0;" href=3D"https://secure.chas=
e.com/web/auth/nav?navKey=3DrequestDashboard" rel=3D"noopener noreferrer" =
target=3D"_blank">chase.com</a>. </td>
</tr>
<tr>
<td align=3D"left" style=3D"vertical-align:top; paddi=
ng: 0px 0px; background-color: #F6F6F6;"><table width=3D"100%" cellpadding=
=3D"0" cellspacing=3D"0" border=3D"0">
<tr>
<td align=3D"left" style=3D"vertical-align:top;=
padding:20px 30px 60px; font-family:Arial, Helvetica, sans-serif; font-siz=
e:12px; color:#717171;" class=3D"moPad font14"><span role=3D"heading" style=
=3D"text-transform: uppercase; font-weight: bold;">About this message</span=
><br />
<br />
Chase&nbsp;Mobile<span style=3D"font-size:70%=
; line-height:0; vertical-align:3px;">&reg;</span> app is available for se=
lect mobile devices. Message and data rates may apply.<br />
<br />
This service email was sent based on your ale=
rt settings. Use the Chase&nbsp;Mobile app or visit <a href=3D"https://www.=
chase.com/personal/mobile-online-banking/login-alerts" target=3D"_blank" st=
yle=3D"text-decoration: underline; color:#0060F0;" rel=3D"noopener noreferr=
er">chase.com/alerts</a> to view or manage your settings.<br />
<br />
Chase cannot guarantee the delivery of alerts=
and notifications. Wireless or internet service provider outages or other =
circumstances could delay them. You can always check <span class=3D"appleli=
nksgray"><a style=3D"color:#717171;text-decoration: none;">chase.com</a></s=
pan> or the Chase&nbsp;Mobile app for the status of your accounts including=
your latest account balances and transaction details.<br />
<br />
To protect your personal information, please =
don't reply to this message. Chase won't ask for confidential information i=
n an email. <br />
<br />
If you have concerns about the authenticity o=
f this message or have questions about your account visit <a style=3D"text-=
decoration: underline; color:#0060F0;" href=3D"https://www.chase.com/digita=
l/customer-service" target=3D"_blank" rel=3D"noopener noreferrer">chase.com=
/CustomerService</a> for ways to contact us.<br />
<br />
Your privacy is important to us. See our onli=
ne <a style=3D"text-decoration: underline; color:#0060F0;" href=3D"https://=
www.chase.com/digital/resources/privacy-security" target=3D"_blank" rel=3D"=
noopener noreferrer">Security Center</a> to learn how to protect your infor=
mation.<br />
<br />
&copy; 2021 JPMorgan Chase &amp; Co. </td>
</tr>
</table></td>
</tr>
</table></td>
<td align=3D"left" style=3D"vertical-align:top; padding:0px=
0px; width:12px;"><img src=3D"https://www.chase.com/content/dam/email/imag=
es/white-right.jpg" width=3D"12" height=3D"77" border=3D"0" style=3D"displa=
y:block;" alt=3D""/></td>
</tr>
</table></td>
</tr>
<!--End of Content -->
</table></td>
</tr>
</table>
</body>
</html>

View File

@@ -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 {

View File

@@ -66,7 +66,7 @@ func uploadLedger(config Config, transaction *Transaction) error {
remote += substr
}
fmt.Fprintf(f, "%-50s%-s\n", formatGMailDate(transaction.Date), transaction.Vendor)
fmt.Fprintf(f, "%-50s%-50s$%.2f\n", "", "AssetAccount:"+transaction.Bank.String(), amount)
fmt.Fprintf(f, "%-50s%-50s$%.2f\n", "", "AssetAccount:"+transaction.Bank.String()+":"+transaction.Account, amount)
fmt.Fprintf(f, "%-50s%-s\n", "", remote)
return nil
}