From 0e62af64e324b723dff1d36bf10360cc460ae431 Mon Sep 17 00:00:00 2001 From: Aaron Lindsay Date: Sat, 11 Mar 2017 21:13:06 -0500 Subject: [PATCH] Implement Signup message parsing --- acctinfo.go | 23 ------- ofx.go | 16 ++--- profile.go | 8 +-- signon.go | 8 +-- signup.go | 170 ++++++++++++++++++++++++++++++++++++++++++++++++++++ types.go | 4 ++ 6 files changed, 190 insertions(+), 39 deletions(-) delete mode 100644 acctinfo.go create mode 100644 signup.go diff --git a/acctinfo.go b/acctinfo.go deleted file mode 100644 index 9863389..0000000 --- a/acctinfo.go +++ /dev/null @@ -1,23 +0,0 @@ -package ofxgo - -import ( - "github.com/golang/go/src/encoding/xml" -) - -type AcctInfoRequest struct { - XMLName xml.Name `xml:"ACCTINFOTRNRQ"` - TrnUID UID `xml:"TRNUID"` - CltCookie Int `xml:"CLTCOOKIE"` - DtAcctup Date `xml:"ACCTINFORQ>DTACCTUP"` -} - -func (r *AcctInfoRequest) Name() string { - return "ACCTINFOTRNRQ" -} - -func (r *AcctInfoRequest) Valid() (bool, error) { - if ok, err := r.TrnUID.Valid(); !ok { - return false, err - } - return true, nil -} diff --git a/ofx.go b/ofx.go index 22e1f31..be75ed7 100644 --- a/ofx.go +++ b/ofx.go @@ -127,7 +127,7 @@ NEWFILEUID:NONE } func (oq *Request) Request() (*Response, error) { - oq.Signon.Dtclient = Date(time.Now()) + oq.Signon.DtClient = Date(time.Now()) b, err := oq.Marshal() if err != nil { @@ -368,12 +368,12 @@ func (or *Response) Unmarshal(reader io.Reader, xmlVersion bool) error { } else if start, ok := tok.(xml.StartElement); ok { // TODO decode other types switch start.Name.Local { - // case "SIGNUPMSGSRSV1": - // msgs, err := DecodeSignupMessageSet(decoder, start) - // if err != nil { - // return err - // } - // or.Signup = msgs + case "SIGNUPMSGSRSV1": + msgs, err := DecodeSignupMessageSet(decoder, start) + if err != nil { + return err + } + or.Signup = msgs //case "BANKMSGSRSV1": //case "CREDITCARDMSGSRSV1": //case "LOANMSGSRSV1": @@ -391,7 +391,7 @@ func (or *Response) Unmarshal(reader io.Reader, xmlVersion bool) error { return err } or.Profile = msgs - //case "IMAGEMSGSRSV1": + //case "IMAGEMSGSRSV1": default: return errors.New("Unsupported message set: " + start.Name.Local) } diff --git a/profile.go b/profile.go index 15118fd..f572f24 100644 --- a/profile.go +++ b/profile.go @@ -9,7 +9,7 @@ type ProfileRequest struct { XMLName xml.Name `xml:"PROFTRNRQ"` TrnUID UID `xml:"TRNUID"` ClientRouting String `xml:"PROFRQ>CLIENTROUTING"` // Forced to NONE - DtProfup Date `xml:"PROFRQ>DTPROFUP"` + DtProfUp Date `xml:"PROFRQ>DTPROFUP"` } func (r *ProfileRequest) Name() string { @@ -29,7 +29,7 @@ type SignonInfo struct { SignonRealm String `xml:"SIGNONREALM"` Min Int `xml:"MIN"` // Minimum number of password characters Max Int `xml:"MAX"` // Maximum number of password characters - Chartype String `xml:"CHARTYPE"` // ALPHAONLY, NUMERICONLY, ALPHAORNUMERIC, ALPHAANDNUMERIC + CharType String `xml:"CHARTYPE"` // ALPHAONLY, NUMERICONLY, ALPHAORNUMERIC, ALPHAANDNUMERIC CaseSen Boolean `xml:"CASESEN"` // Password is case-sensitive? Special Boolean `xml:"SPECIAL"` // Special characters allowed? Spaces Boolean `xml:"SPACES"` // Spaces allowed? @@ -101,8 +101,8 @@ type ProfileResponse struct { TrnUID UID `xml:"TRNUID"` MessageSetList MessageSetList `xml:"PROFRS>MSGSETLIST"` SignonInfoList []SignonInfo `xml:"PROFRS>SIGNONINFOLIST>SIGNONINFO"` - DtProfup Date `xml:"PROFRS>DTPROFUP"` - Finame String `xml:"PROFRS>FINAME"` + DtProfUp Date `xml:"PROFRS>DTPROFUP"` + FiName String `xml:"PROFRS>FINAME"` Addr1 String `xml:"PROFRS>ADDR1"` Addr2 String `xml:"PROFRS>ADDR2,omitempty"` Addr3 String `xml:"PROFRS>ADDR3,omitempty"` diff --git a/signon.go b/signon.go index a28c108..910bfba 100644 --- a/signon.go +++ b/signon.go @@ -7,7 +7,7 @@ import ( type SignonRequest struct { XMLName xml.Name `xml:"SONRQ"` - Dtclient Date `xml:"DTCLIENT"` // Overridden in Request.Request() + DtClient Date `xml:"DTCLIENT"` // Overridden in Request.Request() UserId String `xml:"USERID"` UserPass String `xml:"USERPASS,omitempty"` UserKey String `xml:"USERKEY,omitempty"` @@ -78,12 +78,12 @@ func (s *Status) Valid() (bool, error) { type SignonResponse struct { XMLName xml.Name `xml:"SONRS"` Status Status `xml:"STATUS"` - Dtserver Date `xml:"DTSERVER"` + DtServer Date `xml:"DTSERVER"` UserKey String `xml:"USERKEY,omitempty"` TsKeyExpire Date `xml:"TSKEYEXPIRE,omitempty"` Language String `xml:"LANGUAGE"` - Dtprofup Date `xml:"DTPROFUP,omitempty"` - Dtacctup Date `xml:"DTACCTUP,omitempty"` + DtProfUp Date `xml:"DTPROFUP,omitempty"` + DtAcctUp Date `xml:"DTACCTUP,omitempty"` Org String `xml:"FI>ORG"` Fid String `xml:"FI>FID"` SessCookie String `xml:"SESSCOOKIE,omitempty"` diff --git a/signup.go b/signup.go new file mode 100644 index 0000000..0463843 --- /dev/null +++ b/signup.go @@ -0,0 +1,170 @@ +package ofxgo + +import ( + "errors" + "fmt" + "github.com/golang/go/src/encoding/xml" +) + +type AcctInfoRequest struct { + XMLName xml.Name `xml:"ACCTINFOTRNRQ"` + TrnUID UID `xml:"TRNUID"` + CltCookie Int `xml:"CLTCOOKIE"` + DtAcctUp Date `xml:"ACCTINFORQ>DTACCTUP"` +} + +func (r *AcctInfoRequest) Name() string { + return "ACCTINFOTRNRQ" +} + +func (r *AcctInfoRequest) Valid() (bool, error) { + if ok, err := r.TrnUID.Valid(); !ok { + return false, err + } + return true, nil +} + +type HolderInfo struct { + XMLName xml.Name + FirstName String `xml:"FIRSTNAME"` + MiddleName String `xml:"MIDDLENAME,omitempty"` + LastName String `xml:"LASTNAME"` + 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"` + DayPhone String `xml:"DAYPHONE,omitempty"` + EvePhone String `xml:"EVEPHONE,omitempty"` + Email String `xml:"EMAIL,omitempty"` + 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"` + SupTxDl Boolean `xml:"SUPTXDL"` // Supports downloading transactions (as opposed to balance only) + XferSrc Boolean `xml:"XFERSRC"` // Enabled as source for intra/interbank transfer + XferDest Boolean `xml:"XFERDEST"` // Enabled as destination for intra/interbank transfer + MaturityDate Date `xml:"MATURITYDATE,omitempty"` // Maturity date for CD, if CD + MaturityAmt Amount `xml:"MATURITYAMOUNT,omitempty"` // Maturity amount for CD, if CD + MinBalReq Amount `xml:"MINBALREQ,omitempty"` // Minimum balance required to avoid service fees + AcctClassification String `xml:"ACCTCLASSIFICATION,omitempty"` // One of PERSONAL, BUSINESS, CORPORATE, OTHER + OverdraftLimit Amount `xml:"OVERDRAFTLIMIT,omitempty"` + SvcStatus String `xml:"SVCSTATUS"` // One of AVAIL (available, but not yet requested), PEND (requested, but not yet available), ACTIVE +} + +// Make pointers to these structs print nicely +func (bai *BankAcctInfo) String() string { + return fmt.Sprintf("%+v", *bai) +} + +type CCAcct struct { + XMLName xml.Name // CCACCTTO or CCACCTFROM + AcctId String `xml:"ACCTID"` + AcctKey String `xml:"ACCTKEY,omitempty"` // Unused in USA +} + +type CCAcctInfo struct { + XMLName xml.Name `xml:"CCACCTINFO"` + CCAcctFrom CCAcct `xml:"CCACCTFROM"` + SupTxDl Boolean `xml:"SUPTXDL"` // Supports downloading transactions (as opposed to balance only) + XferSrc Boolean `xml:"XFERSRC"` // Enabled as source for intra/interbank transfer + XferDest Boolean `xml:"XFERDEST"` // Enabled as destination for intra/interbank transfer + AcctClassification String `xml:"ACCTCLASSIFICATION,omitempty"` // One of PERSONAL, BUSINESS, CORPORATE, OTHER + SvcStatus String `xml:"SVCSTATUS"` // One of AVAIL (available, but not yet requested), PEND (requested, but not yet available), ACTIVE +} + +// Make pointers to these structs print nicely +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"` + 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 + InvAcctType String `xml:"INVACCTTYPE,omitempty"` // One of INDIVIDUAL, JOINT, TRUST, CORPORATE + OptionLevel String `xml:"OPTIONLEVEL,omitempty"` // Text desribing option trading privileges +} + +// Make pointers to these structs print nicely +func (iai *InvAcctInfo) String() string { + return fmt.Sprintf("%+v", *iai) +} + +type AcctInfo struct { + XMLName xml.Name `xml:"ACCTINFO"` + Name String `xml:"NAME,omitempty"` + Desc String `xml:"DESC,omitempty"` + Phone String `xml:"PHONE,omitempty"` + PrimaryHolder HolderInfo `xml:"HOLDERINFO>PRIMARYHOLDER,omitempty"` + SecondaryHolder HolderInfo `xml:"HOLDERINFO>SECONDARYHOLDER,omitempty"` + // Only one of the rest of the fields will be valid for any given AcctInfo + BankAcctInfo *BankAcctInfo `xml:"BANKACCTINFO,omitempty"` + CCAcctInfo *CCAcctInfo `xml:"CCACCTINFO,omitempty"` + InvAcctInfo *InvAcctInfo `xml:"INVACCTINFO,omitempty"` + // TODO LOANACCTINFO + // TODO BPACCTINFO? +} + +type AcctInfoResponse struct { + XMLName xml.Name `xml:"ACCTINFOTRNRS"` + TrnUID UID `xml:"TRNUID"` + DtAcctUp Date `xml:"ACCTINFORS>DTACCTUP"` + AcctInfo []AcctInfo `xml:"ACCTINFORS>ACCTINFO,omitempty"` +} + +func (air AcctInfoResponse) Name() string { + return "ACCTINFORS" +} + +func (air AcctInfoResponse) Valid() (bool, error) { + //TODO implement + return true, nil +} + +func DecodeSignupMessageSet(d *xml.Decoder, start xml.StartElement) ([]Message, error) { + var msgs []Message + for { + tok, err := d.Token() + 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 "ACCTINFOTRNRS": + var info AcctInfoResponse + if err := d.DecodeElement(&info, &startElement); err != nil { + return nil, err + } + msgs = append(msgs, Message(info)) + default: + return nil, errors.New("Unsupported signup response tag: " + startElement.Name.Local) + } + } else { + return nil, errors.New("Didn't find an opening element") + } + } +} diff --git a/types.go b/types.go index ce1f79e..8829f91 100644 --- a/types.go +++ b/types.go @@ -27,6 +27,10 @@ func (oi *Int) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { return nil } +type Amount string + +// TODO parse Amount into big.Rat? + type Date time.Time var ofxDateFormats = []string{