diff --git a/banking.go b/banking.go index 37dbe01..1c3ce51 100644 --- a/banking.go +++ b/banking.go @@ -167,7 +167,7 @@ type StatementResponse struct { CashAdvBalAmt Amount `xml:"STMTRS>CASHADVBALAMT,omitempty"` // Only for CREDITLINE accounts, available balance for cash advances IntRate Amount `xml:"STMTRS>INTRATE,omitempty"` // Current interest rate BalList []Balance `xml:"STMTRS>BALLIST>BAL,omitempty"` - MktgInfo String `xml:"STMTRS>MKTGINFO"` // Marketing information + MktgInfo String `xml:"STMTRS>MKTGINFO,omitempty"` // Marketing information } func (sr StatementResponse) Name() string { @@ -198,7 +198,7 @@ type CCStatementResponse struct { RewardBal Amount `xml:"CCSTMTRS>REWARDINFO>REWARDBAL,omitempty"` // Current balance of the reward program RewardEarned Amount `xml:"CCSTMTRS>REWARDINFO>REWARDEARNED,omitempty"` // Reward amount earned YTD BalList []Balance `xml:"CCSTMTRS>BALLIST>BAL,omitempty"` - MktgInfo String `xml:"CCSTMTRS>MKTGINFO"` // Marketing information + MktgInfo String `xml:"CCSTMTRS>MKTGINFO,omitempty"` // Marketing information } func (sr CCStatementResponse) Name() string { diff --git a/common.go b/common.go index 7849bd1..85131b5 100644 --- a/common.go +++ b/common.go @@ -1,6 +1,7 @@ package ofxgo import ( + "errors" "github.com/golang/go/src/encoding/xml" ) @@ -11,6 +12,22 @@ type Message interface { // it's unmarshaled to ensure the request or response is valid } +type Status struct { + XMLName xml.Name `xml:"STATUS"` + Code Int `xml:"CODE"` + Severity String `xml:"SEVERITY"` + Message String `xml:"MESSAGE,omitempty"` +} + +func (s *Status) Valid() (bool, error) { + switch s.Severity { + case "INFO", "WARN", "ERROR": + return true, nil + default: + return false, errors.New("Invalid STATUS>SEVERITY") + } +} + type BankAcct struct { XMLName xml.Name // BANKACCTTO or BANKACCTFROM BankId String `xml:"BANKID"` @@ -25,3 +42,9 @@ type CCAcct struct { AcctId String `xml:"ACCTID"` AcctKey String `xml:"ACCTKEY,omitempty"` // Unused in USA } + +type InvAcct struct { + XMLName xml.Name // INVACCTTO or INVACCTFROM + BrokerId String `xml:"BROKERID"` + AcctId String `xml:"ACCTID"` +} diff --git a/investments.go b/investments.go new file mode 100644 index 0000000..dfb27b1 --- /dev/null +++ b/investments.go @@ -0,0 +1,111 @@ +package ofxgo + +import ( + "errors" + "github.com/golang/go/src/encoding/xml" +) + +type InvStatementRequest struct { + XMLName xml.Name `xml:"INVSTMTTRNRQ"` + TrnUID UID `xml:"TRNUID"` + CltCookie String `xml:"CLTCOOKIE,omitempty"` + TAN String `xml:"TAN,omitempty"` // Transaction authorization number + // TODO `xml:"OFXEXTENSION,omitempty"` + InvAcctFrom InvAcct `xml:"INVSTMTRQ>INVACCTFROM"` + DtStart Date `xml:"INVSTMTRQ>INCTRAN>DTSTART,omitempty"` + DtEnd Date `xml:"INVSTMTRQ>INCTRAN>DTEND,omitempty"` + Include Boolean `xml:"INVSTMTRQ>INCTRAN>INCLUDE"` // Include transactions (instead of just balance) + IncludeOO Boolean `xml:"INVSTMTRQ>INCOO"` // Include open orders + PosDtAsOf Date `xml:"INVSTMTRQ>INCPOS>DTASOF,omitempty"` // Date that positions should be sent down for, if present + IncludePos Boolean `xml:"INVSTMTRQ>INCPOS>INCLUDE"` // Include position data in response + IncludeBalance Boolean `xml:"INVSTMTRQ>INCBAL"` // Include investment balance in response + Include401K Boolean `xml:"INVSTMTRQ>INC401K,omitempty"` // Include 401k information + Include401KBal Boolean `xml:"INVSTMTRQ>INC401KBAL,omitempty"` // Include 401k balance information + IncludeTranImage Boolean `xml:"INVSTMTRQ>INCTRANIMAGE,omitempty"` // Include transaction images +} + +func (r *InvStatementRequest) Name() string { + return "INVSTMTTRNRQ" +} + +func (r *InvStatementRequest) Valid() (bool, error) { + if ok, err := r.TrnUID.Valid(); !ok { + return false, err + } + return true, nil +} + +type InvBankTransaction struct { + XMLName xml.Name `xml:"INVBANKTRAN"` + Transactions []Transaction `xml:"STMTTRN,omitempty"` + SubAcctFund String `xml:"SUBACCTFUND"` // Where did the money for the transaction come from or go to? CASH, MARGIN, SHORT, OTHER +} + +type InvTransactionList struct { + XMLName xml.Name `xml:"INVTRANLIST"` + DtStart Date `xml:"DTSTART"` + DtEnd Date `xml:"DTEND"` + Transactions []InvBankTransaction `xml:"INVBANKTRAN,omitempty"` +} + +type InvBalance struct { + XMLName xml.Name `xml:"INVBAL"` + AvailCash Amount `xml:"AVAILCASH"` // Available cash across all sub-accounts, including sweep funds + MarginBalance Amount `xml:"MARGINBALANCE"` // Negative means customer has borrowed funds + ShortBalance Amount `xml:"SHORTBALANCE"` // Always positive, market value of all short positions + BuyPower Amount `xml:"BUYPOWER"` + BalList []Balance `xml:"BALLIST>BAL,omitempty"` +} + +type InvStatementResponse struct { + XMLName xml.Name `xml:"INVSTMTTRNRS"` + TrnUID UID `xml:"TRNUID"` + Status Status `xml:"STATUS"` + CltCookie String `xml:"CLTCOOKIE,omitempty"` + // TODO OFXEXTENSION + DtAsOf Date `xml:"INVSTMTRS>DTASOF"` + CurDef String `xml:"INVSTMTRS>CURDEF"` + InvAcctFrom InvAcct `xml:"INVSTMTRS>INVACCTFROM"` + InvTranList InvTransactionList `xml:"INVSTMTRS>INVTRANLIST,omitempty"` + // TODO INVPOSLIST + InvBal InvBalance `xml:"INVSTMTRS>INVBAL,omitempty"` + // TODO INVOOLIST + MktgInfo String `xml:"INVSTMTRS>MKTGINFO,omitempty"` // Marketing information + // TODO INV401K + // TODO INV401KBAL +} + +func (sr InvStatementResponse) Name() string { + return "INVSTMTTRNRS" +} + +func (sr InvStatementResponse) Valid() (bool, error) { + //TODO implement + return true, nil +} + +func DecodeInvestmentsMessageSet(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 "INVSTMTTRNRS": + var info InvStatementResponse + if err := d.DecodeElement(&info, &startElement); err != nil { + return nil, err + } + msgs = append(msgs, Message(info)) + default: + return nil, errors.New("Unsupported investments response tag: " + startElement.Name.Local) + } + } else { + return nil, errors.New("Didn't find an opening element") + } + } +} diff --git a/request.go b/request.go index c8ffc7e..0425b67 100644 --- a/request.go +++ b/request.go @@ -14,7 +14,7 @@ type Request struct { Banking []Message // // // - // + Investments []Message // // // // @@ -106,6 +106,9 @@ NEWFILEUID:NONE if err := marshalMessageSet(encoder, oq.Banking, "BANKMSGSRQV1"); err != nil { return nil, err } + if err := marshalMessageSet(encoder, oq.Investments, "INVSTMTMSGSRQV1"); err != nil { + return nil, err + } if err := marshalMessageSet(encoder, oq.Profile, "PROFMSGSRQV1"); err != nil { return nil, err } diff --git a/response.go b/response.go index 50cb71e..a8d5224 100644 --- a/response.go +++ b/response.go @@ -16,7 +16,7 @@ type Response struct { Banking []Message // // // - // + Investments []Message // // // // @@ -271,7 +271,12 @@ func ParseResponse(reader io.Reader) (*Response, error) { or.Banking = msgs //case "CREDITCARDMSGSRSV1": //case "LOANMSGSRSV1": - //case "INVSTMTMSGSRSV1": + case "INVSTMTMSGSRSV1": + msgs, err := DecodeInvestmentsMessageSet(decoder, start) + if err != nil { + return nil, err + } + or.Investments = msgs //case "INTERXFERMSGSRSV1": //case "WIREXFERMSGSRSV1": //case "BILLPAYMSGSRSV1": diff --git a/signon.go b/signon.go index 536df50..87662cf 100644 --- a/signon.go +++ b/signon.go @@ -55,22 +55,6 @@ func (r *SignonRequest) Valid() (bool, error) { return true, nil } -type Status struct { - XMLName xml.Name `xml:"STATUS"` - Code Int `xml:"CODE"` - Severity String `xml:"SEVERITY"` - Message String `xml:"MESSAGE,omitempty"` -} - -func (s *Status) Valid() (bool, error) { - switch s.Severity { - case "INFO", "WARN", "ERROR": - return true, nil - default: - return false, errors.New("Invalid STATUS>SEVERITY") - } -} - type SignonResponse struct { XMLName xml.Name `xml:"SONRS"` Status Status `xml:"STATUS"` diff --git a/signup.go b/signup.go index f38054a..8c44468 100644 --- a/signup.go +++ b/signup.go @@ -76,15 +76,9 @@ func (ci *CCAcctInfo) String() string { return fmt.Sprintf("%+v", *ci) } -type InvAcct struct { - XMLName xml.Name // INVACCTTO or INVACCTFROM - BrokerId String `xml:"BROKERID"` - AcctId String `xml:"ACCTID"` -} - type InvAcctInfo struct { XMLName xml.Name `xml:"INVACCTINFO"` - INVAcctFrom InvAcct `xml:"INVACCTFROM"` + InvAcctFrom InvAcct `xml:"INVACCTFROM"` UsProductType String `xml:"USPRODUCTTYPE"` // One of 401K, 403B, IRA, KEOGH, OTHER, SARSEP, SIMPLE, NORMAL, TDA, TRUST, UGMA Checking Boolean `xml:"CHECKING"` // Has check-writing privileges SvcStatus String `xml:"SVCSTATUS"` // One of AVAIL (available, but not yet requested), PEND (requested, but not yet available), ACTIVE