Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc0b0a64e2 | ||
|
|
1e01058c7c | ||
|
|
5c557ea713 | ||
|
|
8b226294a2 | ||
|
|
f6fc366dd4 | ||
|
|
1139fef0ab | ||
|
|
2728e8c4a2 | ||
|
|
fc95242c94 |
3
bank.go
3
bank.go
@@ -7,10 +7,13 @@ const (
|
||||
Citi Bank = iota + 1
|
||||
UCCU Bank = iota + 1
|
||||
BankOfAmerica Bank = iota + 1
|
||||
Fidelity Bank = iota + 1
|
||||
)
|
||||
|
||||
func (b Bank) String() string {
|
||||
switch b {
|
||||
case Fidelity:
|
||||
return "Fidelity"
|
||||
case BankOfAmerica:
|
||||
return "BankOfAmerica"
|
||||
case Chase:
|
||||
|
||||
10
config.go
10
config.go
@@ -17,17 +17,20 @@ type Uploader int
|
||||
const (
|
||||
UploaderTodo = Uploader(iota)
|
||||
UploaderLedger
|
||||
UploaderPTTodo
|
||||
)
|
||||
|
||||
var uploaders = map[string]Uploader{
|
||||
"todo": UploaderTodo,
|
||||
"ledger": UploaderLedger,
|
||||
"pttodo": UploaderPTTodo,
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
EmailUser string
|
||||
EmailPass string
|
||||
EmailIMAP string
|
||||
EmailLimit int
|
||||
TodoAddr string
|
||||
TodoToken string
|
||||
TodoList string
|
||||
@@ -47,8 +50,9 @@ func NewConfig() Config {
|
||||
as.Append(args.STRING, "emailuser", "email username", "breellocaldev@gmail.com")
|
||||
as.Append(args.STRING, "emailpass", "email password", "diblloewfncwssof")
|
||||
as.Append(args.STRING, "emailimap", "email imap", "imap.gmail.com:993")
|
||||
as.Append(args.INT, "emaillimit", "email limit", 0)
|
||||
|
||||
as.Append(args.STRING, "uploader", "todo, ledger", "todo")
|
||||
as.Append(args.STRING, "uploader", "todo, ledger, pttodo", "todo")
|
||||
|
||||
as.Append(args.STRING, "todoaddr", "todo addr", "https://todo-server.remote.blapointe.com")
|
||||
as.Append(args.STRING, "todopass", "todo pass", "gJtEXbbLHLf54yS9EdujtVN2n6Y")
|
||||
@@ -56,7 +60,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,bankofamerica", "uccu,citi,chase,bankofamerica")
|
||||
as.Append(args.STRING, "banks", "uccu,citi,chase,bankofamerica,fidelity", "uccu,citi,chase,bankofamerica,fidelity")
|
||||
as.Append(args.STRING, "accounts", "regex to include filter accounts", ".*")
|
||||
as.Append(args.STRING, "not-accounts", "regex to exclude filter accounts", "zzzzzz")
|
||||
|
||||
@@ -85,6 +89,7 @@ func NewConfig() Config {
|
||||
EmailUser: as.GetString("emailuser"),
|
||||
EmailPass: as.GetString("emailpass"),
|
||||
EmailIMAP: as.GetString("emailimap"),
|
||||
EmailLimit: as.GetInt("emaillimit"),
|
||||
TodoAddr: as.GetString("todoaddr"),
|
||||
TodoTag: as.GetString("todotag"),
|
||||
AccountsPattern: as.GetString("accounts"),
|
||||
@@ -96,6 +101,7 @@ func NewConfig() Config {
|
||||
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())),
|
||||
Fidelity: strings.Contains(strings.ToLower(as.GetString("banks")), strings.ToLower(Fidelity.String())),
|
||||
},
|
||||
}
|
||||
log.Printf("config: %+v", config)
|
||||
|
||||
7
go.mod
7
go.mod
@@ -4,6 +4,7 @@ go 1.17
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.3.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
local/args v0.0.0-00010101000000-000000000000
|
||||
local/oauth2 v0.0.0-00010101000000-000000000000
|
||||
local/sandbox/contact/contact v0.0.0-00010101000000-000000000000
|
||||
@@ -16,10 +17,7 @@ require (
|
||||
github.com/abbot/go-http-auth v0.4.0 // indirect
|
||||
github.com/aws/aws-sdk-go v1.15.81 // indirect
|
||||
github.com/boltdb/bolt v1.3.1 // indirect
|
||||
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b // indirect
|
||||
github.com/buraksezer/consistent v0.9.0 // indirect
|
||||
github.com/bytbox/go-pop3 v0.0.0-20120201222208-3046caf0763e // indirect
|
||||
github.com/cespare/xxhash v1.1.0 // indirect
|
||||
github.com/emersion/go-imap v1.2.0 // indirect
|
||||
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect
|
||||
github.com/go-stack/stack v1.8.0 // indirect
|
||||
@@ -52,13 +50,12 @@ require (
|
||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20181120190819-8f65e3013eba // indirect
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect
|
||||
golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2 // indirect
|
||||
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
|
||||
google.golang.org/api v0.0.0-20181120235003-faade3cbb06a // indirect
|
||||
google.golang.org/appengine v1.3.0 // indirect
|
||||
gopkg.in/ini.v1 v1.42.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
local/logb v0.0.0-00010101000000-000000000000 // indirect
|
||||
)
|
||||
|
||||
|
||||
13
go.sum
13
go.sum
@@ -5,8 +5,6 @@ github.com/Azure/azure-pipeline-go v0.1.8/go.mod h1:XA1kFWRVhSK+KNFiOhfv83Fv8L9a
|
||||
github.com/Azure/azure-storage-blob-go v0.0.0-20181023070848-cf01652132cc/go.mod h1:oGfmITT1V6x//CswqY2gtAHND+xIP64/qL7a5QJix0Y=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/Unknwon/goconfig v0.0.0-20181105214110-56bd8ab18619 h1:6X8iB881g299aNEv6KXrcjL31iLOH7yA6NXoQX+MbDg=
|
||||
github.com/Unknwon/goconfig v0.0.0-20181105214110-56bd8ab18619/go.mod h1:wngxua9XCNjvHjDiTiV26DaKDT+0c63QR6H5hjVUUxw=
|
||||
github.com/a8m/tree v0.0.0-20180321023834-3cf936ce15d6/go.mod h1:FSdwKX97koS5efgm8WevNf7XS3PqtyFkKDDXrz778cg=
|
||||
@@ -18,14 +16,8 @@ github.com/aws/aws-sdk-go v1.15.81/go.mod h1:E3/ieXAlvM0XWO57iftYVDLLvQ824smPP3A
|
||||
github.com/billziss-gh/cgofuse v1.1.0/go.mod h1:LJjoaUojlVjgo5GQoEJTcJNqZJeRU0nCR84CyxKt2YM=
|
||||
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
|
||||
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b h1:L/QXpzIa3pOvUGt1D1lA5KjYhPBAN/3iWdP7xeFS9F0=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
|
||||
github.com/buraksezer/consistent v0.9.0 h1:Zfs6bX62wbP3QlbPGKUhqDw7SmNkOzY5bHZIYXYpR5g=
|
||||
github.com/buraksezer/consistent v0.9.0/go.mod h1:6BrVajWq7wbKZlTOUPs/XVfR8c0maujuPowduSpZqmw=
|
||||
github.com/bytbox/go-pop3 v0.0.0-20120201222208-3046caf0763e h1:mQTN05gz0rDZSABqKMzAPMb5ATWcvvdMljRzEh0LjBo=
|
||||
github.com/bytbox/go-pop3 v0.0.0-20120201222208-3046caf0763e/go.mod h1:alXX+s7a4cKaIprgjeEboqi4Tm7XR/HXEwUTxUV/ywU=
|
||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/coreos/bbolt v0.0.0-20180318001526-af9db2027c98/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
github.com/cpuguy83/go-md2man v1.0.8/go.mod h1:N6JayAiVKtlHSnuTCeuLSQVs75hb8q+dYQLjr7cDsKY=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -172,8 +164,6 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1
|
||||
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
|
||||
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a h1:pa8hGb/2YqsZKovtsgrwcDH1RZhVbTKCjLp47XpqCDs=
|
||||
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
@@ -232,8 +222,9 @@ golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2 h1:T5DasATyLQfmbTpfEXx/IOL9vfjzW6up+ZDkmHvIf2s=
|
||||
golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 h1:nonptSpoQ4vQjyraW20DXPAglgQfVnM9ZC6MmNLMR60=
|
||||
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
|
||||
3
main.go
3
main.go
@@ -12,6 +12,7 @@ func main() {
|
||||
IMAP: config.EmailIMAP,
|
||||
From: config.EmailUser,
|
||||
Password: config.EmailPass,
|
||||
Limit: config.EmailLimit,
|
||||
}
|
||||
emails, err := emailer.Read()
|
||||
if err != nil {
|
||||
@@ -33,7 +34,7 @@ func main() {
|
||||
log.Printf("skipping match account antipattern %q vs %q", config.AccountsAntiPattern, transaction.Account)
|
||||
continue
|
||||
}
|
||||
if _, err := config.Storage.Get(transaction.ID); err == nil {
|
||||
if v, err := config.Storage.Get(transaction.ID); err == nil || string(v) == transaction.String() {
|
||||
log.Println("skipping duplicate transaction:", transaction)
|
||||
} else {
|
||||
if err := Upload(config, transaction); err != nil {
|
||||
|
||||
80
scrape.go
80
scrape.go
@@ -15,6 +15,7 @@ type scraper interface {
|
||||
scrape(*mail.Message) ([]*Transaction, error)
|
||||
}
|
||||
|
||||
type fidelityScraper struct{}
|
||||
type bankOfAmericaScraper struct{}
|
||||
type chaseScraper struct{}
|
||||
type citiScraper struct{}
|
||||
@@ -30,13 +31,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", "payment", "Payment") {
|
||||
if !containsAny(subject, "transaction", "report", "Transaction", "payment", "Payment", "Deposit", "Withdrawal") {
|
||||
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, "Fidelity") && banks[Fidelity] {
|
||||
return newFidelityScraper(), nil
|
||||
}
|
||||
if strings.Contains(from, "Bank of America") && banks[BankOfAmerica] {
|
||||
return newBankOfAmericaScraper(), nil
|
||||
}
|
||||
@@ -49,6 +53,10 @@ func buildScraper(m *mail.Message, banks map[Bank]bool) (scraper, error) {
|
||||
return nil, errors.New("unknown sender: " + from)
|
||||
}
|
||||
|
||||
func newFidelityScraper() scraper {
|
||||
return &fidelityScraper{}
|
||||
}
|
||||
|
||||
func newBankOfAmericaScraper() scraper {
|
||||
return &bankOfAmericaScraper{}
|
||||
}
|
||||
@@ -210,6 +218,52 @@ func (c *uccuScraper) scrape(m *mail.Message) ([]*Transaction, error) {
|
||||
return []*Transaction{transaction}, nil
|
||||
}
|
||||
|
||||
func (c *fidelityScraper) scrape(m *mail.Message) ([]*Transaction, error) {
|
||||
subject := fmt.Sprint(m.Header["Subject"])
|
||||
if strings.Contains(subject, "Debit Withdrawal") {
|
||||
return c.scrapeWithdrawal(m)
|
||||
}
|
||||
if strings.Contains(subject, "Deposit Received") {
|
||||
return c.scrapeDeposit(m)
|
||||
}
|
||||
panic(nil)
|
||||
}
|
||||
|
||||
func (c *fidelityScraper) scrapeDeposit(m *mail.Message) ([]*Transaction, error) {
|
||||
b, err := ioutil.ReadAll(m.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fidelAcc, _ := findSubstringBetween(b, "Account: XXXXX", "\n")
|
||||
|
||||
transaction := NewTransaction(
|
||||
fmt.Sprintf("%s-%s", Fidelity, fidelAcc),
|
||||
"?.??",
|
||||
"misc",
|
||||
fmt.Sprint(m.Header["Date"]),
|
||||
Fidelity,
|
||||
)
|
||||
return []*Transaction{transaction}, nil
|
||||
}
|
||||
|
||||
func (c *fidelityScraper) scrapeWithdrawal(m *mail.Message) ([]*Transaction, error) {
|
||||
b, err := ioutil.ReadAll(m.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
amount, amountOk := findSubstringBetween(b, "in the amount of $", " ")
|
||||
fidelAcc, fidelAccOk := findSubstringBetween(b, "For account ending in ", ":")
|
||||
acc, accOk := findSubstringBetween(b, "in the amount of $"+amount+" by ", ".")
|
||||
|
||||
if amount == "" || acc == "" {
|
||||
return nil, fmt.Errorf("no amount/account found: fidelAcc=%v,fidelAccOk=%v, acc=%v,accOk=%v, amount=%v,amountOk=%v", fidelAcc, fidelAccOk, acc, accOk, amount, amountOk)
|
||||
}
|
||||
transaction := NewTransaction(fmt.Sprintf("%s-%s", Fidelity, fidelAcc), amount, acc, fmt.Sprint(m.Header["Date"]), Fidelity)
|
||||
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") {
|
||||
@@ -227,8 +281,8 @@ func (c *bankOfAmericaScraper) scrapeCharge(m *mail.Message) ([]*Transaction, er
|
||||
return nil, err
|
||||
}
|
||||
|
||||
amount := c.findFloatAfter(b, "Amount: $")
|
||||
acc := string(c.findLineAfter(b, "Where: "))
|
||||
amount := findFloatAfter(b, "Amount: $")
|
||||
acc := string(findLineAfter(b, "Where: "))
|
||||
|
||||
if amount == "" || acc == "" {
|
||||
return nil, errors.New("no amount/account found")
|
||||
@@ -242,7 +296,7 @@ func (c *bankOfAmericaScraper) scrapePayment(m *mail.Message) ([]*Transaction, e
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
amount := "-" + c.findFloatAfter(b, "Payment: $")
|
||||
amount := "-" + findFloatAfter(b, "Payment: $")
|
||||
acc := "Payment"
|
||||
if amount == "" || acc == "" {
|
||||
return nil, errors.New("no amount/account found")
|
||||
@@ -251,8 +305,20 @@ func (c *bankOfAmericaScraper) scrapePayment(m *mail.Message) ([]*Transaction, e
|
||||
return []*Transaction{transaction}, nil
|
||||
}
|
||||
|
||||
func (c *bankOfAmericaScraper) findFloatAfter(b []byte, prefix string) string {
|
||||
amount := string(c.findLineAfter(b, prefix))
|
||||
func findSubstringBetween(b []byte, prefix, suffix string) (string, bool) {
|
||||
byPre := bytes.Split(b, []byte(prefix))
|
||||
if len(byPre) < 2 {
|
||||
return "", false
|
||||
}
|
||||
bySuff := bytes.Split(byPre[1], []byte(suffix))
|
||||
if len(bySuff) < 2 {
|
||||
return "", false
|
||||
}
|
||||
return string(bySuff[0]), true
|
||||
}
|
||||
|
||||
func findFloatAfter(b []byte, prefix string) string {
|
||||
amount := string(findLineAfter(b, prefix))
|
||||
words := strings.Split(amount, " ")
|
||||
lastword := words[len(words)-1]
|
||||
escapedfloat := strings.TrimPrefix(lastword, "$")
|
||||
@@ -261,7 +327,7 @@ func (c *bankOfAmericaScraper) findFloatAfter(b []byte, prefix string) string {
|
||||
return amount
|
||||
}
|
||||
|
||||
func (c *bankOfAmericaScraper) findLineAfter(b []byte, prefix string) []byte {
|
||||
func findLineAfter(b []byte, prefix string) []byte {
|
||||
for _, line := range bytes.Split(b, []byte("\n")) {
|
||||
if bytes.HasPrefix(line, []byte(prefix)) {
|
||||
return bytes.TrimSpace(bytes.TrimPrefix(line, []byte(prefix)))
|
||||
|
||||
@@ -179,6 +179,76 @@ func TestScrapeBofAPayment(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestScrapeFidelityDeposit(t *testing.T) {
|
||||
b, err := ioutil.ReadFile("./testdata/fidelity.deposit.txt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
message := &mail.Message{
|
||||
Header: map[string][]string{
|
||||
"Subject": []string{"Fidelity Alerts: Deposit Received"},
|
||||
},
|
||||
Body: bytes.NewReader(b),
|
||||
}
|
||||
fidelity := &fidelityScraper{}
|
||||
|
||||
gots, err := fidelity.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: Fidelity,
|
||||
Amount: "?.??",
|
||||
Vendor: "misc",
|
||||
Date: "[]",
|
||||
Account: Fidelity.String() + "-5576",
|
||||
}
|
||||
if *got != want {
|
||||
t.Fatalf("want:\n\t%+v, got\n\t%+v", want, *got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScrapeFidelityWithdrawal(t *testing.T) {
|
||||
b, err := ioutil.ReadFile("./testdata/fidelity.withdrawal.txt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
message := &mail.Message{
|
||||
Header: map[string][]string{
|
||||
"Subject": []string{"Fidelity Alerts - Direct Debit Withdrawal"},
|
||||
},
|
||||
Body: bytes.NewReader(b),
|
||||
}
|
||||
fidelity := &fidelityScraper{}
|
||||
|
||||
gots, err := fidelity.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: Fidelity,
|
||||
Amount: "1.00",
|
||||
Vendor: "CHASE CREDIT CRD",
|
||||
Date: "[]",
|
||||
Account: Fidelity.String() + "-5576",
|
||||
}
|
||||
if *got != want {
|
||||
t.Fatalf("want:\n\t%+v, got\n\t%+v", want, *got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScrapeBofACharge(t *testing.T) {
|
||||
b, err := ioutil.ReadFile("./testdata/bofa.charge.txt")
|
||||
if err != nil {
|
||||
|
||||
29
testdata/fidelity.deposit.txt
vendored
Normal file
29
testdata/fidelity.deposit.txt
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
|
||||
Account: XXXXX5576
|
||||
|
||||
A deposit to your account was received on 12/08/2022.
|
||||
|
||||
|
||||
|
||||
Important information:
|
||||
Fidelity automatically emails certain alerts to customers who have provided
|
||||
an email address. You can see the terms which govern these alerts at
|
||||
https://www.fidelity.com/customer-service/alerts-agreement. If you would
|
||||
prefer to not receive these alerts, please change your preferences at
|
||||
https://scs.fidelity.com/customeronly/alerts.shtml.
|
||||
|
||||
Review Fidelity's Terms of Use for Third Party Content and Research at
|
||||
https://www.fidelity.com/terms-of-use#Third.
|
||||
|
||||
If your email has changed, please update your email address
|
||||
at https://alertable.fidelity.com/ftgw/alerts/GetUserDeliveryDevices to continue to
|
||||
receive your alerts.
|
||||
|
||||
Fidelity Brokerage Services LLC, Member NYSE, SIPC
|
||||
|
||||
EMAIL REF# 537048
|
||||
|
||||
Copyright 2022 FMR LLC
|
||||
All rights reserved. Important Legal Information
|
||||
at http://www.fidelity.com/terms-of-use.
|
||||
|
||||
49
testdata/fidelity.withdrawal.txt
vendored
Normal file
49
testdata/fidelity.withdrawal.txt
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
|
||||
For account ending in 5576:
|
||||
Money was withdrawn from your account through a direct debit in the amount of $1.00 by CHASE CREDIT CRD.
|
||||
|
||||
If you authorized this transaction, no action is needed.
|
||||
If you did not authorize this transaction, please contact us immediately at 800-343-3548.
|
||||
|
||||
|
||||
|
||||
Important information:
|
||||
Fidelity automatically emails certain alerts to customers who have provided
|
||||
an email address. You can see the terms which govern these alerts at
|
||||
https://alertable.fidelity.com/alerts/help/agreement.html. If you would
|
||||
prefer to not receive these alerts, please change your preferences at
|
||||
https://scs.fidelity.com/customeronly/alerts.shtml.
|
||||
This new alerts service will not affect delivery of paper communications
|
||||
you are scheduled to receive.
|
||||
|
||||
To stop receipt of alerts, modify your preferences on the Existing Alerts page
|
||||
at https://scs.fidelity.com/customeronly/fens.shtml, or temporarily
|
||||
stop/restart alerts at
|
||||
https://scs.fidelity.com/customeronly/fens_alertstatus.shtml.
|
||||
|
||||
Fidelity offers access to a broader range of third-party research at
|
||||
http://personal.fidelity.com/research/stocks/content/stocksindex.shtml.
|
||||
Fidelity is not recommending or endorsing any third-party research by making it
|
||||
available to its customers or by notifying customers of its availability.
|
||||
|
||||
Review Fidelity's Terms of Use for Third Party Content and Research at
|
||||
http://activequote.fidelity.com/rtrnews/terms.html.
|
||||
|
||||
If your email has changed, please update your email address
|
||||
at https://scs.fidelity.com/customeronly/fens_profile.shtml to continue to
|
||||
receive your alerts.
|
||||
|
||||
Read Fidelity's Commitment to Privacy
|
||||
at http://personal.fidelity.com/global/search/content/privacy.html.tvsr.
|
||||
|
||||
Visit Fidelity's Home Page
|
||||
http://www.fidelity.com/
|
||||
|
||||
Fidelity Brokerage Services LLC, Member NYSE, SIPC, 900 Salem Street, Smithfield, RI 02917
|
||||
|
||||
EMAIL REF# 537048
|
||||
|
||||
Copyright 2022 FMR LLC
|
||||
All rights reserved. Important Legal Information
|
||||
at http://personal.fidelity.com/misc/legal/legal.html.tvsr.
|
||||
|
||||
45
upload.go
45
upload.go
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
@@ -8,10 +9,13 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func Upload(config Config, transaction *Transaction) error {
|
||||
@@ -20,6 +24,8 @@ func Upload(config Config, transaction *Transaction) error {
|
||||
return uploadTodo(config, transaction)
|
||||
case UploaderLedger:
|
||||
return uploadLedger(config, transaction)
|
||||
case UploaderPTTodo:
|
||||
return uploadPTTodo(config, transaction)
|
||||
default:
|
||||
return errors.New("not impl: uploader")
|
||||
}
|
||||
@@ -49,6 +55,45 @@ func uploadTodo(config Config, transaction *Transaction) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func uploadPTTodo(config Config, transaction *Transaction) error {
|
||||
b, err := ioutil.ReadFile(config.TodoAddr)
|
||||
if os.IsNotExist(err) {
|
||||
b = []byte("todo:\n")
|
||||
} else if err != nil {
|
||||
return err
|
||||
} else if len(b) == 0 {
|
||||
b = []byte("todo:\n")
|
||||
}
|
||||
f, err := ioutil.TempFile(path.Dir(config.TodoAddr), path.Base("."+config.TodoAddr))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
sep := []byte{'\n'}
|
||||
seek := []byte("todo:")
|
||||
for len(b) > 0 {
|
||||
idx := bytes.Index(b, sep)
|
||||
if idx == -1 {
|
||||
idx = len(b) - 1
|
||||
}
|
||||
fmt.Fprintf(f, "%s\n", b[:idx])
|
||||
if bytes.Equal(bytes.TrimSpace(b[:idx]), seek) {
|
||||
fmt.Fprintf(f, `- {"todo":%q, "tags":%q}%s`, transaction.Format(), config.TodoTag, "\n")
|
||||
}
|
||||
b = b[idx+1:]
|
||||
}
|
||||
f.Close()
|
||||
var v interface{}
|
||||
b, err = ioutil.ReadFile(f.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := yaml.Unmarshal(b, &v); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(f.Name(), config.TodoAddr)
|
||||
}
|
||||
|
||||
func uploadLedger(config Config, transaction *Transaction) error {
|
||||
f, err := os.OpenFile(config.TodoAddr, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
|
||||
@@ -4,12 +4,92 @@ import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"local/storage"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func TestUploadPTTodo(t *testing.T) {
|
||||
addr := path.Join(t.TempDir(), "test.upload.pttodo")
|
||||
config := Config{TodoAddr: addr, TodoTag: "expense"}
|
||||
xaction := func() *Transaction {
|
||||
return &Transaction{
|
||||
ID: "id",
|
||||
Bank: UCCU,
|
||||
Amount: "1.23",
|
||||
Vendor: "vendor vendor",
|
||||
Date: "today",
|
||||
}
|
||||
}
|
||||
t.Run("full file", func(t *testing.T) {
|
||||
if err := ioutil.WriteFile(addr, []byte(`
|
||||
todo:
|
||||
- first
|
||||
- todo: second
|
||||
scheduled: []
|
||||
done: []
|
||||
`), os.ModePerm); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err := uploadPTTodo(config, xaction())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
b, err := ioutil.ReadFile(addr)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if bytes.Compare(bytes.TrimSpace(b), bytes.TrimSpace([]byte(`
|
||||
todo:
|
||||
- {"todo":"(today) /UCCU: 1.23 @ vendor vendor", "tags":"expense"}
|
||||
- first
|
||||
- todo: second
|
||||
scheduled: []
|
||||
done: []
|
||||
`))) != 0 {
|
||||
t.Errorf("full file came out wrong: got %s", b)
|
||||
}
|
||||
if !bytes.Contains(b, []byte(xaction().Format())) {
|
||||
t.Errorf("full file didnt get target: %s", string(b))
|
||||
}
|
||||
t.Logf("%s", b)
|
||||
})
|
||||
t.Run("no file", func(t *testing.T) {
|
||||
os.Remove(addr)
|
||||
err := uploadPTTodo(config, xaction())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
b, err := ioutil.ReadFile(addr)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if !bytes.Contains(b, []byte(xaction().Format())) {
|
||||
t.Errorf("no file didnt get target: %s", string(b))
|
||||
}
|
||||
t.Logf("%s", b)
|
||||
})
|
||||
t.Run("empty file", func(t *testing.T) {
|
||||
if err := ioutil.WriteFile(addr, []byte{}, os.ModePerm); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err := uploadPTTodo(config, xaction())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
b, err := ioutil.ReadFile(addr)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if !bytes.Contains(b, []byte(xaction().Format())) {
|
||||
t.Errorf("empty file didnt get target: %s", string(b))
|
||||
}
|
||||
t.Logf("%s", b)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUploadLedger(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
transaction Transaction
|
||||
|
||||
Reference in New Issue
Block a user