From cd1e7b480ab25f1f6dc90a467f0bfc80966b9b5e Mon Sep 17 00:00:00 2001 From: Aaron Lindsay Date: Tue, 14 Mar 2017 10:31:14 -0400 Subject: [PATCH] Add the ability to download Bank Transactions --- banking.go | 130 +++++++++++++++++++++++++++++++++++++++++++++++++++++ ofx.go | 27 ++++++++--- signup.go | 9 ---- 3 files changed, 152 insertions(+), 14 deletions(-) create mode 100644 banking.go diff --git a/banking.go b/banking.go new file mode 100644 index 0000000..0778e46 --- /dev/null +++ b/banking.go @@ -0,0 +1,130 @@ +package ofxgo + +import ( + "errors" + "github.com/golang/go/src/encoding/xml" +) + +type BankAcct struct { + XMLName xml.Name // BANKACCTTO or BANKACCTFROM + BankId String `xml:"BANKID"` + BranchId String `xml:"BRANCHID,omitempty"` // Unused in USA + AcctId String `xml:"ACCTID"` + AcctType String `xml:"ACCTTYPE"` // One of CHECKING, SAVINGS, MONEYMRKT, CREDITLINE, CD + AcctKey String `xml:"ACCTKEY,omitempty"` // Unused in USA +} + +type StatementRequest struct { + XMLName xml.Name `xml:"STMTTRNRQ"` + TrnUID UID `xml:"TRNUID"` + BankAcctFrom BankAcct `xml:"STMTRQ>BANKACCTFROM"` + DtStart Date `xml:"STMTRQ>INCTRAN>DTSTART,omitempty"` + DtEnd Date `xml:"STMTRQ>INCTRAN>DTEND,omitempty"` + Include Boolean `xml:"STMTRQ>INCTRAN>INCLUDE"` // Include transactions (instead of just balance) + IncludePending Boolean `xml:"STMTRQ>INCLUDEPENDING,omitempty"` // Include pending transactions + IncludeTranImage Boolean `xml:"STMTRQ>INCLUDETRANIMAGE,omitempty"` // Include transaction images +} + +func (r *StatementRequest) Name() string { + return "STMTTRNRQ" +} + +func (r *StatementRequest) Valid() (bool, error) { + if ok, err := r.TrnUID.Valid(); !ok { + return false, err + } + return true, nil +} + +type Payee struct { + XMLName xml.Name `xml:"PAYEE"` + Name String `xml:"NAME"` + Addr1 String `xml:"ADDR1"` + Addr2 String `xml:"ADDR2,omitempty"` + Addr3 String `xml:"ADDR3,omitempty"` + City String `xml:"CITY"` + State String `xml:"STATE"` + PostalCode String `xml:"POSTALCODE"` + Country String `xml:"COUNTRY,omitempty"` + Phone String `xml:"PHONE"` +} + +type Transaction struct { + XMLName xml.Name `xml:"STMTTRN"` + TrnType String `xml:"TRNTYPE"` // One of CREDIT, DEBIT, INT (interest earned or paid. Note: Depends on signage of amount), DIV, FEE, SRVCHG (service charge), DEP (deposit), ATM (Note: Depends on signage of amount), POS (Note: Depends on signage of amount), XFER, CHECK, PAYMENT, CASH, DIRECTDEP, DIRECTDEBIT, REPEATPMT, HOLD (Only valid in ), OTHER + DtPosted Date `xml:"DTPOSTED"` + DtUser Date `xml:"DTUSER,omitempty"` + DtAvail Date `xml:"DTAVAIL,omitempty"` + TrnAmt Amount `xml:"TRNAMT"` + FiTId String `xml:"FITID"` + CorrectFiTId String `xml:"CORRECTFITID,omitempty"` // Transaction Id that this transaction corrects, if present + CorrectAction String `xml:"CORRECTACTION,omitempty"` // One of DELETE, REPLACE + SrvrTId String `xml:"SRVRTID,omitempty"` + CheckNum String `xml:"CHECKNUM,omitempty"` + RefNum String `xml:"REFNUM,omitempty"` + SIC Int `xml:"SIC,omitempty"` // Standard Industrial Code + PayeeId String `xml:"PAYEEID,omitempty"` + // Note: Servers should provide NAME or PAYEE, but not both + Name String `xml:"NAME,omitempty"` + Payee Payee `xml:"PAYEE,omitempty"` + ExtdName String `xml:"EXTDNAME,omitempty"` // Extended name of payee or transaction description + // TODO BANKACCTTO + // TODO CCACCTTO + Memo String `xml:"MEMO,omitempty"` // Extra information (not in NAME) + // TODO IMAGEDATA + Currency String `xml:"CURRENCY,omitempty"` // If different from CURDEF in STMTTRS + OrigCurrency String `xml:"ORIGCURRENCY,omitempty"` // If different from CURDEF in STMTTRS + Inv401kSource String `xml:"INV401KSOURCE,omitempty"` // One of PRETAX, AFTERTAX, MATCH, PROFITSHARING, ROLLOVER, OTHERVEST, OTHERNONVEST (Default if not present is OTHERNONVEST. The following cash source types are subject to vesting: MATCH, PROFITSHARING, and OTHERVEST.) +} + +type TransactionList struct { + XMLName xml.Name `xml:"BANKTRANLIST"` + DtStart Date `xml:"DTSTART"` + DtEnd Date `xml:"DTEND"` + Transactions []Transaction `xml:"STMTTRN"` +} + +type StatementResponse struct { + XMLName xml.Name `xml:"STMTTRNRS"` + TrnUID UID `xml:"TRNUID"` + CurDef String `xml:"STMTRS>CURDEF"` + BankAcctFrom BankAcct `xml:"STMTRS>BANKACCTFROM"` + BankTranList TransactionList `xml:"STMTRS>BANKTRANLIST,omitempty"` + BalAmt Amount `xml:"STMTRS>LEDGERBAL>BALAMT"` + DtAsOf Date `xml:"STMTRS>LEDGERBAL>DTASOF"` + // TODO AVAILBAL et. al. +} + +func (sr StatementResponse) Name() string { + return "STMTTRNRS" +} + +func (sr StatementResponse) Valid() (bool, error) { + //TODO implement + return true, nil +} +func DecodeBankingMessageSet(d *xml.Decoder, start xml.StartElement) ([]Message, error) { + var msgs []Message + for { + tok, err := nextNonWhitespaceToken(d) + if err != nil { + return nil, err + } else if end, ok := tok.(xml.EndElement); ok && end.Name.Local == start.Name.Local { + // If we found the end of our starting element, we're done parsing + return msgs, nil + } else if startElement, ok := tok.(xml.StartElement); ok { + switch startElement.Name.Local { + case "STMTTRNRS": + var info StatementResponse + if err := d.DecodeElement(&info, &startElement); err != nil { + return nil, err + } + msgs = append(msgs, Message(info)) + default: + return nil, errors.New("Unsupported banking response tag: " + startElement.Name.Local) + } + } else { + return nil, errors.New("Didn't find an opening element") + } + } +} diff --git a/ofx.go b/ofx.go index 87efb67..d3cc305 100644 --- a/ofx.go +++ b/ofx.go @@ -21,7 +21,7 @@ type Request struct { Version string // String for OFX header, defaults to 203 Signon SignonRequest // Signup []Message // - // + Banking []Message // // // // @@ -112,6 +112,9 @@ NEWFILEUID:NONE if err := oq.marshalMessageSet(encoder, oq.Signup, "SIGNUPMSGSRQV1"); err != nil { return nil, err } + if err := oq.marshalMessageSet(encoder, oq.Banking, "BANKMSGSRQV1"); err != nil { + return nil, err + } if err := oq.marshalMessageSet(encoder, oq.Profile, "PROFMSGSRQV1"); err != nil { return nil, err } @@ -126,7 +129,7 @@ NEWFILEUID:NONE return &b, nil } -func (oq *Request) Request() (*Response, error) { +func (oq *Request) Request() (*http.Response, error) { oq.Signon.DtClient = Date(time.Now()) b, err := oq.Marshal() @@ -137,12 +140,21 @@ func (oq *Request) Request() (*Response, error) { if err != nil { return nil, err } - defer response.Body.Close() if response.StatusCode != 200 { return nil, errors.New("OFXQuery request status: " + response.Status) } + return response, nil +} + +func (oq *Request) RequestAndParse() (*Response, error) { + response, err := oq.Request() + if err != nil { + return nil, err + } + defer response.Body.Close() + // Help the parser out by giving it a clue about what header format to // expect var xmlVersion bool = true @@ -163,7 +175,7 @@ type Response struct { Version string // String for OFX header, defaults to 203 Signon SignonResponse // Signup []Message // - // + Banking []Message // // // // @@ -394,7 +406,12 @@ func (or *Response) Unmarshal(reader io.Reader, xmlVersion bool) error { return err } or.Signup = msgs - //case "BANKMSGSRSV1": + case "BANKMSGSRSV1": + msgs, err := DecodeBankingMessageSet(decoder, start) + if err != nil { + return err + } + or.Banking = msgs //case "CREDITCARDMSGSRSV1": //case "LOANMSGSRSV1": //case "INVSTMTMSGSRSV1": diff --git a/signup.go b/signup.go index f56798b..f356010 100644 --- a/signup.go +++ b/signup.go @@ -42,15 +42,6 @@ type HolderInfo struct { HolderType String `xml:"HOLDERTYPE,omitempty"` // One of INDIVIDUAL, JOINT, CUSTODIAL, TRUST, OTHER } -type BankAcct struct { - XMLName xml.Name // BANKACCTTO or BANKACCTFROM - BankId String `xml:"BANKID"` - BranchId String `xml:"BRANCHID,omitempty"` // Unused in USA - AcctId String `xml:"ACCTID"` - AcctType String `xml:"ACCTTYPE"` // One of CHECKING, SAVINGS, MONEYMRKT, CREDITLINE, CD - AcctKey String `xml:"ACCTKEY,omitempty"` // Unused in USA -} - type BankAcctInfo struct { XMLName xml.Name `xml:"BANKACCTINFO"` BankAcctFrom BankAcct `xml:"BANKACCTFROM"`