Comment investments, profiles, seclist, signon, signup, and types

This commit is contained in:
Aaron Lindsay 2017-04-13 10:18:07 -04:00
parent a1aec204a8
commit 1ff64a9d55
8 changed files with 170 additions and 20 deletions

View File

@ -468,7 +468,8 @@ type InvTranList struct {
BankTransactions []InvBankTransaction BankTransactions []InvBankTransaction
} }
// UnmarshalXML handles unmarshalling an InvTranList element from an XML string // UnmarshalXML handles unmarshalling an InvTranList element from an SGML/XML
// string
func (l *InvTranList) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { func (l *InvTranList) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
for { for {
tok, err := nextNonWhitespaceToken(d) tok, err := nextNonWhitespaceToken(d)

View File

@ -5,6 +5,9 @@ import (
"github.com/aclindsa/go/src/encoding/xml" "github.com/aclindsa/go/src/encoding/xml"
) )
// ProfileRequest represents a request for a server to provide a profile of its
// capabilities (which message sets and versions it supports, how to access
// them, which languages and which types of synchronization they support, etc.)
type ProfileRequest struct { type ProfileRequest struct {
XMLName xml.Name `xml:"PROFTRNRQ"` XMLName xml.Name `xml:"PROFTRNRQ"`
TrnUID UID `xml:"TRNUID"` TrnUID UID `xml:"TRNUID"`
@ -12,26 +15,35 @@ type ProfileRequest struct {
TAN String `xml:"TAN,omitempty"` // Transaction authorization number TAN String `xml:"TAN,omitempty"` // Transaction authorization number
// TODO `xml:"OFXEXTENSION,omitempty"` // TODO `xml:"OFXEXTENSION,omitempty"`
ClientRouting String `xml:"PROFRQ>CLIENTROUTING"` // Forced to NONE ClientRouting String `xml:"PROFRQ>CLIENTROUTING"` // Forced to NONE
DtProfUp Date `xml:"PROFRQ>DTPROFUP"` DtProfUp Date `xml:"PROFRQ>DTPROFUP"` // Date and time client last received a profile update
} }
// Name returns the name of the top-level transaction XML/SGML element
func (r *ProfileRequest) Name() string { func (r *ProfileRequest) Name() string {
return "PROFTRNRQ" return "PROFTRNRQ"
} }
// Valid returns (true, nil) if this struct would be valid OFX if marshalled
// into XML/SGML
func (r *ProfileRequest) Valid() (bool, error) { func (r *ProfileRequest) Valid() (bool, error) {
// TODO implement // TODO implement
r.ClientRouting = "NONE" r.ClientRouting = "NONE"
return true, nil return true, nil
} }
// Type returns which message set this message belongs to (which Request
// element of type []Message it should appended to)
func (r *ProfileRequest) Type() messageType { func (r *ProfileRequest) Type() messageType {
return ProfRq return ProfRq
} }
// SignonInfo provides the requirements to login to a single signon realm. A
// signon realm consists of all MessageSets which can be accessed using one set
// of login credentials. Most FI's only use one signon realm to make it easier
// and less confusing for the user.
type SignonInfo struct { type SignonInfo struct {
XMLName xml.Name `xml:"SIGNONINFO"` XMLName xml.Name `xml:"SIGNONINFO"`
SignonRealm String `xml:"SIGNONREALM"` SignonRealm String `xml:"SIGNONREALM"` // The SignonRealm for which this SignonInfo provides information. This SignonInfo is valid for all MessageSets with SignonRealm fields matching this one
Min Int `xml:"MIN"` // Minimum number of password characters Min Int `xml:"MIN"` // Minimum number of password characters
Max Int `xml:"MAX"` // Maximum number of password characters Max Int `xml:"MAX"` // Maximum number of password characters
CharType charType `xml:"CHARTYPE"` // One of ALPHAONLY, NUMERICONLY, ALPHAORNUMERIC, ALPHAANDNUMERIC CharType charType `xml:"CHARTYPE"` // One of ALPHAONLY, NUMERICONLY, ALPHAORNUMERIC, ALPHAANDNUMERIC
@ -51,11 +63,13 @@ type SignonInfo struct {
AccessTokenReq Boolean `xml:"ACCESSTOKENREQ,omitempty"` // Server requires ACCESSTOKEN to be sent with all requests except profile AccessTokenReq Boolean `xml:"ACCESSTOKENREQ,omitempty"` // Server requires ACCESSTOKEN to be sent with all requests except profile
} }
// MessageSet represents one message set supported by an FI and its
// capabilities
type MessageSet struct { type MessageSet struct {
XMLName xml.Name // <xxxMSGSETVn> XMLName xml.Name // <xxxMSGSETVn>
Name string // <xxxMSGSETVn> (copy of XMLName.Local) Name string // <xxxMSGSETVn> (copy of XMLName.Local)
Ver Int `xml:"MSGSETCORE>VER"` // Message set version - should always match 'n' in <xxxMSGSETVn> Ver Int `xml:"MSGSETCORE>VER"` // Message set version - should always match 'n' in <xxxMSGSETVn> of Name
Url String `xml:"MSGSETCORE>URL"` // URL where messages in this set are to be set URL String `xml:"MSGSETCORE>URL"` // URL where messages in this set are to be set
OfxSec ofxSec `xml:"MSGSETCORE>OFXSEC"` // NONE or 'TYPE 1' OfxSec ofxSec `xml:"MSGSETCORE>OFXSEC"` // NONE or 'TYPE 1'
TranspSec Boolean `xml:"MSGSETCORE>TRANSPSEC"` // Transport-level security must be used TranspSec Boolean `xml:"MSGSETCORE>TRANSPSEC"` // Transport-level security must be used
SignonRealm String `xml:"MSGSETCORE>SIGNONREALM"` // Used to identify which SignonInfo to use for to this MessageSet SignonRealm String `xml:"MSGSETCORE>SIGNONREALM"` // Used to identify which SignonInfo to use for to this MessageSet
@ -67,8 +81,11 @@ type MessageSet struct {
// TODO MessageSet-specific stuff? // TODO MessageSet-specific stuff?
} }
// MessageSetList is a list of MessageSets (necessary because they must be
// manually parsed)
type MessageSetList []MessageSet type MessageSetList []MessageSet
// UnmarshalXML handles unmarshalling a MessageSetList element from an XML string
func (msl *MessageSetList) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { func (msl *MessageSetList) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
for { for {
var msgset MessageSet var msgset MessageSet
@ -106,6 +123,11 @@ func (msl *MessageSetList) UnmarshalXML(d *xml.Decoder, start xml.StartElement)
} }
} }
// ProfileResponse contains a requested profile of the server's capabilities
// (which message sets and versions it supports, how to access them, which
// languages and which types of synchronization they support, etc.). Note that
// if the server does not support ClientRouting=NONE (as we always send with
// ProfileRequest), this may be an error)
type ProfileResponse struct { type ProfileResponse struct {
XMLName xml.Name `xml:"PROFTRNRS"` XMLName xml.Name `xml:"PROFTRNRS"`
TrnUID UID `xml:"TRNUID"` TrnUID UID `xml:"TRNUID"`
@ -130,15 +152,19 @@ type ProfileResponse struct {
Email String `xml:"PROFRS>EMAIL,omitempty"` Email String `xml:"PROFRS>EMAIL,omitempty"`
} }
// Name returns the name of the top-level transaction XML/SGML element
func (pr *ProfileResponse) Name() string { func (pr *ProfileResponse) Name() string {
return "PROFTRNRS" return "PROFTRNRS"
} }
// Valid returns (true, nil) if this struct was valid OFX when unmarshalled
func (pr *ProfileResponse) Valid() (bool, error) { func (pr *ProfileResponse) Valid() (bool, error) {
//TODO implement //TODO implement
return true, nil return true, nil
} }
// Type returns which message set this message belongs to (which Response
// element of type []Message it belongs to)
func (pr *ProfileResponse) Type() messageType { func (pr *ProfileResponse) Type() messageType {
return ProfRs return ProfRs
} }

View File

@ -232,7 +232,7 @@ NEWFILEUID:NONE
ofxgo.MessageSet{ ofxgo.MessageSet{
Name: "SIGNONMSGSETV1", Name: "SIGNONMSGSETV1",
Ver: 1, Ver: 1,
Url: "https://ofx.example.com/cgi-ofx/exampleofx", URL: "https://ofx.example.com/cgi-ofx/exampleofx",
OfxSec: ofxgo.OfxSecNone, OfxSec: ofxgo.OfxSecNone,
TranspSec: true, TranspSec: true,
SignonRealm: "Example Trade", SignonRealm: "Example Trade",
@ -244,7 +244,7 @@ NEWFILEUID:NONE
ofxgo.MessageSet{ ofxgo.MessageSet{
Name: "SIGNUPMSGSETV1", Name: "SIGNUPMSGSETV1",
Ver: 1, Ver: 1,
Url: "https://ofx.example.com/cgi-ofx/exampleofx", URL: "https://ofx.example.com/cgi-ofx/exampleofx",
OfxSec: ofxgo.OfxSecNone, OfxSec: ofxgo.OfxSecNone,
TranspSec: true, TranspSec: true,
SignonRealm: "Example Trade", SignonRealm: "Example Trade",
@ -256,7 +256,7 @@ NEWFILEUID:NONE
ofxgo.MessageSet{ ofxgo.MessageSet{
Name: "INVSTMTMSGSETV1", Name: "INVSTMTMSGSETV1",
Ver: 1, Ver: 1,
Url: "https://ofx.example.com/cgi-ofx/exampleofx", URL: "https://ofx.example.com/cgi-ofx/exampleofx",
OfxSec: ofxgo.OfxSecNone, OfxSec: ofxgo.OfxSecNone,
TranspSec: true, TranspSec: true,
SignonRealm: "Example Trade", SignonRealm: "Example Trade",
@ -268,7 +268,7 @@ NEWFILEUID:NONE
ofxgo.MessageSet{ ofxgo.MessageSet{
Name: "SECLISTMSGSETV1", Name: "SECLISTMSGSETV1",
Ver: 1, Ver: 1,
Url: "https://ofx.example.com/cgi-ofx/exampleofx", URL: "https://ofx.example.com/cgi-ofx/exampleofx",
OfxSec: ofxgo.OfxSecNone, OfxSec: ofxgo.OfxSecNone,
TranspSec: true, TranspSec: true,
SignonRealm: "Example Trade", SignonRealm: "Example Trade",
@ -280,7 +280,7 @@ NEWFILEUID:NONE
ofxgo.MessageSet{ ofxgo.MessageSet{
Name: "PROFMSGSETV1", Name: "PROFMSGSETV1",
Ver: 1, Ver: 1,
Url: "https://ofx.example.com/cgi-ofx/exampleofx", URL: "https://ofx.example.com/cgi-ofx/exampleofx",
OfxSec: ofxgo.OfxSecNone, OfxSec: ofxgo.OfxSecNone,
TranspSec: true, TranspSec: true,
SignonRealm: "Example Trade", SignonRealm: "Example Trade",

View File

@ -60,7 +60,8 @@ func marshalMessageSet(e *xml.Encoder, requests []Message, set messageType) erro
return nil return nil
} }
// Overwrite the fields in this Request object controlled by the Client // SetClientFields overwrites the fields in this Request object controlled by
// the Client
func (oq *Request) SetClientFields(c *Client) { func (oq *Request) SetClientFields(c *Client) {
oq.Signon.DtClient.Time = time.Now() oq.Signon.DtClient.Time = time.Now()

View File

@ -5,12 +5,17 @@ import (
"github.com/aclindsa/go/src/encoding/xml" "github.com/aclindsa/go/src/encoding/xml"
) )
// SecurityID identifies a security by its CUSIP (for US-based FI's, others may
// use UniqueID types other than CUSIP)
type SecurityID struct { type SecurityID struct {
XMLName xml.Name `xml:"SECID"` XMLName xml.Name `xml:"SECID"`
UniqueID String `xml:"UNIQUEID"` // CUSIP for US FI's UniqueID String `xml:"UNIQUEID"` // CUSIP for US FI's
UniqueIDType String `xml:"UNIQUEIDTYPE"` // Should always be "CUSIP" for US FI's UniqueIDType String `xml:"UNIQUEIDTYPE"` // Should always be "CUSIP" for US FI's
} }
// SecurityRequest represents a request for one security. It is specified with
// a SECID aggregate, a ticker symbol, or an FI assigned identifier (but no
// more than one of them at a time)
type SecurityRequest struct { type SecurityRequest struct {
XMLName xml.Name `xml:"SECRQ"` XMLName xml.Name `xml:"SECRQ"`
// Only one of the next three should be present // Only one of the next three should be present
@ -19,6 +24,8 @@ type SecurityRequest struct {
FiID String `xml:"FIID,omitempty"` FiID String `xml:"FIID,omitempty"`
} }
// SecListRequest represents a request for information (namely price) about one
// or more securities
type SecListRequest struct { type SecListRequest struct {
XMLName xml.Name `xml:"SECLISTTRNRQ"` XMLName xml.Name `xml:"SECLISTTRNRQ"`
TrnUID UID `xml:"TRNUID"` TrnUID UID `xml:"TRNUID"`
@ -28,19 +35,29 @@ type SecListRequest struct {
Securities []SecurityRequest `xml:"SECLISTRQ>SECRQ,omitempty"` Securities []SecurityRequest `xml:"SECLISTRQ>SECRQ,omitempty"`
} }
// Name returns the name of the top-level transaction XML/SGML element
func (r *SecListRequest) Name() string { func (r *SecListRequest) Name() string {
return "SECLISTTRNRQ" return "SECLISTTRNRQ"
} }
// Valid returns (true, nil) if this struct would be valid OFX if marshalled
// into XML/SGML
func (r *SecListRequest) Valid() (bool, error) { func (r *SecListRequest) Valid() (bool, error) {
// TODO implement // TODO implement
return true, nil return true, nil
} }
// Type returns which message set this message belongs to (which Request
// element of type []Message it should appended to)
func (r *SecListRequest) Type() messageType { func (r *SecListRequest) Type() messageType {
return SecListRq return SecListRq
} }
// SecListResponse is always empty (except for the transaction UID, status, and
// optional client cookie). Its presence signifies that the SecurityList (a
// different element from this one) immediately after this element in
// Response.SecList was been generated in response to the same SecListRequest
// this is a response to.
type SecListResponse struct { type SecListResponse struct {
XMLName xml.Name `xml:"SECLISTTRNRS"` XMLName xml.Name `xml:"SECLISTTRNRS"`
TrnUID UID `xml:"TRNUID"` TrnUID UID `xml:"TRNUID"`
@ -50,23 +67,31 @@ type SecListResponse struct {
// SECLISTRS is always empty, so we don't parse it here. The actual securities list will be in a top-level element parallel to SECLISTTRNRS // SECLISTRS is always empty, so we don't parse it here. The actual securities list will be in a top-level element parallel to SECLISTTRNRS
} }
// Name returns the name of the top-level transaction XML/SGML element
func (r *SecListResponse) Name() string { func (r *SecListResponse) Name() string {
return "SECLISTTRNRS" return "SECLISTTRNRS"
} }
// Valid returns (true, nil) if this struct was valid OFX when unmarshalled
func (r *SecListResponse) Valid() (bool, error) { func (r *SecListResponse) Valid() (bool, error) {
// TODO implement // TODO implement
return true, nil return true, nil
} }
// Type returns which message set this message belongs to (which Response
// element of type []Message it belongs to)
func (r *SecListResponse) Type() messageType { func (r *SecListResponse) Type() messageType {
return SecListRs return SecListRs
} }
// Security is satisfied by all *Info elements providing information about
// securities for SecurityList
type Security interface { type Security interface {
SecurityType() string SecurityType() string
} }
// SecInfo represents the generic information about a security. It is included
// in most other *Info elements.
type SecInfo struct { type SecInfo struct {
XMLName xml.Name `xml:"SECINFO"` XMLName xml.Name `xml:"SECINFO"`
SecID SecurityID `xml:"SECID"` SecID SecurityID `xml:"SECID"`
@ -80,6 +105,7 @@ type SecInfo struct {
Memo String `xml:"MEMO,omitempty"` Memo String `xml:"MEMO,omitempty"`
} }
// DebtInfo provides information about a debt security
type DebtInfo struct { type DebtInfo struct {
XMLName xml.Name `xml:"DEBTINFO"` XMLName xml.Name `xml:"DEBTINFO"`
SecInfo SecInfo `xml:"SECINFO"` SecInfo SecInfo `xml:"SECINFO"`
@ -99,22 +125,29 @@ type DebtInfo struct {
FiAssetClass String `xml:"FIASSETCLASS,omitempty"` // FI-defined asset class FiAssetClass String `xml:"FIASSETCLASS,omitempty"` // FI-defined asset class
} }
// SecurityType returns a string representation of this security's type
func (i DebtInfo) SecurityType() string { func (i DebtInfo) SecurityType() string {
return "DEBTINFO" return "DEBTINFO"
} }
// AssetPortion represents the percentage of a mutual fund with the given asset
// classification
type AssetPortion struct { type AssetPortion struct {
XMLName xml.Name `xml:"PORTION"` XMLName xml.Name `xml:"PORTION"`
AssetClass assetClass `xml:"ASSETCLASS"` // One of DOMESTICBOND, INTLBOND, LARGESTOCK, SMALLSTOCK, INTLSTOCK, MONEYMRKT, OTHER AssetClass assetClass `xml:"ASSETCLASS"` // One of DOMESTICBOND, INTLBOND, LARGESTOCK, SMALLSTOCK, INTLSTOCK, MONEYMRKT, OTHER
Percent Amount `xml:"PERCENT"` // Percentage of the fund that falls under this asset class Percent Amount `xml:"PERCENT"` // Percentage of the fund that falls under this asset class
} }
// FiAssetPortion represents the percentage of a mutual fund with the given
// FI-defined asset classification (AssetPortion should be used for all asset
// classifications defined by the assetClass enum)
type FiAssetPortion struct { type FiAssetPortion struct {
XMLName xml.Name `xml:"FIPORTION"` XMLName xml.Name `xml:"FIPORTION"`
FiAssetClass String `xml:"FIASSETCLASS,omitempty"` // FI-defined asset class FiAssetClass String `xml:"FIASSETCLASS,omitempty"` // FI-defined asset class
Percent Amount `xml:"PERCENT"` // Percentage of the fund that falls under this asset class Percent Amount `xml:"PERCENT"` // Percentage of the fund that falls under this asset class
} }
// MFInfo provides information about a mutual fund
type MFInfo struct { type MFInfo struct {
XMLName xml.Name `xml:"MFINFO"` XMLName xml.Name `xml:"MFINFO"`
SecInfo SecInfo `xml:"SECINFO"` SecInfo SecInfo `xml:"SECINFO"`
@ -125,10 +158,12 @@ type MFInfo struct {
FiAssetClasses []FiAssetPortion `xml:"FIMFASSETCLASS>FIPORTION"` FiAssetClasses []FiAssetPortion `xml:"FIMFASSETCLASS>FIPORTION"`
} }
// SecurityType returns a string representation of this security's type
func (i MFInfo) SecurityType() string { func (i MFInfo) SecurityType() string {
return "MFINFO" return "MFINFO"
} }
// OptInfo provides information about an option
type OptInfo struct { type OptInfo struct {
XMLName xml.Name `xml:"OPTINFO"` XMLName xml.Name `xml:"OPTINFO"`
SecInfo SecInfo `xml:"SECINFO"` SecInfo SecInfo `xml:"SECINFO"`
@ -141,10 +176,13 @@ type OptInfo struct {
FiAssetClass String `xml:"FIASSETCLASS,omitempty"` // FI-defined asset class FiAssetClass String `xml:"FIASSETCLASS,omitempty"` // FI-defined asset class
} }
// SecurityType returns a string representation of this security's type
func (i OptInfo) SecurityType() string { func (i OptInfo) SecurityType() string {
return "OPTINFO" return "OPTINFO"
} }
// OtherInfo provides information about a security type not covered by the
// other *Info elements
type OtherInfo struct { type OtherInfo struct {
XMLName xml.Name `xml:"OTHERINFO"` XMLName xml.Name `xml:"OTHERINFO"`
SecInfo SecInfo `xml:"SECINFO"` SecInfo SecInfo `xml:"SECINFO"`
@ -153,10 +191,12 @@ type OtherInfo struct {
FiAssetClass String `xml:"FIASSETCLASS,omitempty"` // FI-defined asset class FiAssetClass String `xml:"FIASSETCLASS,omitempty"` // FI-defined asset class
} }
// SecurityType returns a string representation of this security's type
func (i OtherInfo) SecurityType() string { func (i OtherInfo) SecurityType() string {
return "OTHERINFO" return "OTHERINFO"
} }
// StockInfo provides information about a security type
type StockInfo struct { type StockInfo struct {
XMLName xml.Name `xml:"STOCKINFO"` XMLName xml.Name `xml:"STOCKINFO"`
SecInfo SecInfo `xml:"SECINFO"` SecInfo SecInfo `xml:"SECINFO"`
@ -167,27 +207,35 @@ type StockInfo struct {
FiAssetClass String `xml:"FIASSETCLASS,omitempty"` // FI-defined asset class FiAssetClass String `xml:"FIASSETCLASS,omitempty"` // FI-defined asset class
} }
// SecurityType returns a string representation of this security's type
func (i StockInfo) SecurityType() string { func (i StockInfo) SecurityType() string {
return "STOCKINFO" return "STOCKINFO"
} }
// SecurityList is a container for Security objects containaing information
// about securities
type SecurityList struct { type SecurityList struct {
Securities []Security Securities []Security
} }
// Name returns the name of the top-level transaction XML/SGML element
func (r *SecurityList) Name() string { func (r *SecurityList) Name() string {
return "SECLIST" return "SECLIST"
} }
// Valid returns (true, nil) if this struct was valid OFX when unmarshalled
func (r *SecurityList) Valid() (bool, error) { func (r *SecurityList) Valid() (bool, error) {
// TODO implement // TODO implement
return true, nil return true, nil
} }
// Type returns which message set this message belongs to (which Response
// element of type []Message it belongs to)
func (r *SecurityList) Type() messageType { func (r *SecurityList) Type() messageType {
return SecListRs return SecListRs
} }
// UnmarshalXML handles unmarshalling a SecurityList from an SGML/XML string
func (r *SecurityList) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { func (r *SecurityList) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
for { for {
tok, err := nextNonWhitespaceToken(d) tok, err := nextNonWhitespaceToken(d)

View File

@ -6,6 +6,8 @@ import (
"github.com/aclindsa/go/src/encoding/xml" "github.com/aclindsa/go/src/encoding/xml"
) )
// SignonRequest identifies and authenticates a user to their FI and is
// provided with every Request
type SignonRequest struct { type SignonRequest struct {
XMLName xml.Name `xml:"SONRQ"` XMLName xml.Name `xml:"SONRQ"`
DtClient Date `xml:"DTCLIENT"` // Current time on client, overwritten in Client.Request() DtClient Date `xml:"DTCLIENT"` // Current time on client, overwritten in Client.Request()
@ -20,10 +22,13 @@ type SignonRequest struct {
ClientUID UID `xml:"CLIENTUID,omitempty"` ClientUID UID `xml:"CLIENTUID,omitempty"`
} }
// Name returns the name of the top-level transaction XML/SGML element
func (r *SignonRequest) Name() string { func (r *SignonRequest) Name() string {
return "SONRQ" return "SONRQ"
} }
// Valid returns (true, nil) if this struct would be valid OFX if marshalled
// into XML/SGML
func (r *SignonRequest) Valid() (bool, error) { func (r *SignonRequest) Valid() (bool, error) {
if len(r.UserID) < 1 || len(r.UserID) > 32 { if len(r.UserID) < 1 || len(r.UserID) > 32 {
return false, errors.New("SONRQ>USERID invalid length") return false, errors.New("SONRQ>USERID invalid length")
@ -40,7 +45,7 @@ func (r *SignonRequest) Valid() (bool, error) {
if len(r.Language) == 0 { if len(r.Language) == 0 {
r.Language = "ENG" r.Language = "ENG"
} else if len(r.Language) != 3 { } else if len(r.Language) != 3 {
return false, fmt.Errorf("SONRQ>LANGUAGE invalid length: \"%s\"\n", r.Language) return false, fmt.Errorf("SONRQ>LANGUAGE invalid length: \"%s\"", r.Language)
} }
if len(r.AppID) < 1 || len(r.AppID) > 5 { if len(r.AppID) < 1 || len(r.AppID) > 5 {
return false, errors.New("SONRQ>APPID invalid length") return false, errors.New("SONRQ>APPID invalid length")
@ -51,6 +56,8 @@ func (r *SignonRequest) Valid() (bool, error) {
return true, nil return true, nil
} }
// SignonResponse is provided with every Response and indicates the success or
// failure of the SignonRequest in the corresponding Request
type SignonResponse struct { type SignonResponse struct {
XMLName xml.Name `xml:"SONRS"` XMLName xml.Name `xml:"SONRS"`
Status Status `xml:"STATUS"` Status Status `xml:"STATUS"`
@ -66,13 +73,15 @@ type SignonResponse struct {
AccessKey String `xml:"ACCESSKEY,omitempty"` AccessKey String `xml:"ACCESSKEY,omitempty"`
} }
// Name returns the name of the top-level transaction XML/SGML element
func (r *SignonResponse) Name() string { func (r *SignonResponse) Name() string {
return "SONRS" return "SONRS"
} }
// Valid returns (true, nil) if this struct was valid OFX when unmarshalled
func (r *SignonResponse) Valid() (bool, error) { func (r *SignonResponse) Valid() (bool, error) {
if len(r.Language) != 3 { if len(r.Language) != 3 {
return false, fmt.Errorf("SONRS>LANGUAGE invalid length: \"%s\"\n", r.Language) return false, fmt.Errorf("SONRS>LANGUAGE invalid length: \"%s\"", r.Language)
} }
return r.Status.Valid() return r.Status.Valid()
} }

View File

@ -5,6 +5,8 @@ import (
"github.com/aclindsa/go/src/encoding/xml" "github.com/aclindsa/go/src/encoding/xml"
) )
// AcctInfoRequest represents a request for the server to provide information
// for all of the user's available accounts at this FI
type AcctInfoRequest struct { type AcctInfoRequest struct {
XMLName xml.Name `xml:"ACCTINFOTRNRQ"` XMLName xml.Name `xml:"ACCTINFOTRNRQ"`
TrnUID UID `xml:"TRNUID"` TrnUID UID `xml:"TRNUID"`
@ -14,19 +16,25 @@ type AcctInfoRequest struct {
DtAcctUp Date `xml:"ACCTINFORQ>DTACCTUP"` DtAcctUp Date `xml:"ACCTINFORQ>DTACCTUP"`
} }
// Name returns the name of the top-level transaction XML/SGML element
func (r *AcctInfoRequest) Name() string { func (r *AcctInfoRequest) Name() string {
return "ACCTINFOTRNRQ" return "ACCTINFOTRNRQ"
} }
// Valid returns (true, nil) if this struct would be valid OFX if marshalled
// into XML/SGML
func (r *AcctInfoRequest) Valid() (bool, error) { func (r *AcctInfoRequest) Valid() (bool, error) {
// TODO implement // TODO implement
return true, nil return true, nil
} }
// Type returns which message set this message belongs to (which Request
// element of type []Message it should appended to)
func (r *AcctInfoRequest) Type() messageType { func (r *AcctInfoRequest) Type() messageType {
return SignupRq return SignupRq
} }
// HolderInfo contains the information a FI has about an account-holder
type HolderInfo struct { type HolderInfo struct {
XMLName xml.Name XMLName xml.Name
FirstName String `xml:"FIRSTNAME"` FirstName String `xml:"FIRSTNAME"`
@ -45,6 +53,9 @@ type HolderInfo struct {
HolderType holderType `xml:"HOLDERTYPE,omitempty"` // One of INDIVIDUAL, JOINT, CUSTODIAL, TRUST, OTHER HolderType holderType `xml:"HOLDERTYPE,omitempty"` // One of INDIVIDUAL, JOINT, CUSTODIAL, TRUST, OTHER
} }
// BankAcctInfo contains information about a bank account, including how to
// access it (BankAcct), and whether it supports downloading transactions
// (SupTxDl).
type BankAcctInfo struct { type BankAcctInfo struct {
XMLName xml.Name `xml:"BANKACCTINFO"` XMLName xml.Name `xml:"BANKACCTINFO"`
BankAcctFrom BankAcct `xml:"BANKACCTFROM"` BankAcctFrom BankAcct `xml:"BANKACCTFROM"`
@ -59,11 +70,14 @@ type BankAcctInfo struct {
SvcStatus svcStatus `xml:"SVCSTATUS"` // One of AVAIL (available, but not yet requested), PEND (requested, but not yet available), ACTIVE SvcStatus svcStatus `xml:"SVCSTATUS"` // One of AVAIL (available, but not yet requested), PEND (requested, but not yet available), ACTIVE
} }
// Make pointers to these structs print nicely // String makes pointers to BankAcctInfo structs print nicely
func (bai *BankAcctInfo) String() string { func (bai *BankAcctInfo) String() string {
return fmt.Sprintf("%+v", *bai) return fmt.Sprintf("%+v", *bai)
} }
// CCAcctInfo contains information about a credit card account, including how
// to access it (CCAcct), and whether it supports downloading transactions
// (SupTxDl).
type CCAcctInfo struct { type CCAcctInfo struct {
XMLName xml.Name `xml:"CCACCTINFO"` XMLName xml.Name `xml:"CCACCTINFO"`
CCAcctFrom CCAcct `xml:"CCACCTFROM"` CCAcctFrom CCAcct `xml:"CCACCTFROM"`
@ -74,11 +88,14 @@ type CCAcctInfo struct {
SvcStatus svcStatus `xml:"SVCSTATUS"` // One of AVAIL (available, but not yet requested), PEND (requested, but not yet available), ACTIVE SvcStatus svcStatus `xml:"SVCSTATUS"` // One of AVAIL (available, but not yet requested), PEND (requested, but not yet available), ACTIVE
} }
// Make pointers to these structs print nicely // String makes pointers to CCAcctInfo structs print nicely
func (ci *CCAcctInfo) String() string { func (ci *CCAcctInfo) String() string {
return fmt.Sprintf("%+v", *ci) return fmt.Sprintf("%+v", *ci)
} }
// InvAcctInfo contains information about an investment account, including how
// to access it (InvAcct), and whether it supports downloading transactions
// (SupTxDl).
type InvAcctInfo struct { type InvAcctInfo struct {
XMLName xml.Name `xml:"INVACCTINFO"` XMLName xml.Name `xml:"INVACCTINFO"`
InvAcctFrom InvAcct `xml:"INVACCTFROM"` InvAcctFrom InvAcct `xml:"INVACCTFROM"`
@ -89,11 +106,14 @@ type InvAcctInfo struct {
OptionLevel String `xml:"OPTIONLEVEL,omitempty"` // Text desribing option trading privileges OptionLevel String `xml:"OPTIONLEVEL,omitempty"` // Text desribing option trading privileges
} }
// Make pointers to these structs print nicely // String makes pointers to InvAcctInfo structs print nicely
func (iai *InvAcctInfo) String() string { func (iai *InvAcctInfo) String() string {
return fmt.Sprintf("%+v", *iai) return fmt.Sprintf("%+v", *iai)
} }
// AcctInfo represents generic account information. It should contain one (and
// only one) *AcctInfo element corresponding to the tyep of account it
// represents.
type AcctInfo struct { type AcctInfo struct {
XMLName xml.Name `xml:"ACCTINFO"` XMLName xml.Name `xml:"ACCTINFO"`
Name String `xml:"NAME,omitempty"` Name String `xml:"NAME,omitempty"`
@ -101,6 +121,7 @@ type AcctInfo struct {
Phone String `xml:"PHONE,omitempty"` Phone String `xml:"PHONE,omitempty"`
PrimaryHolder HolderInfo `xml:"HOLDERINFO>PRIMARYHOLDER,omitempty"` PrimaryHolder HolderInfo `xml:"HOLDERINFO>PRIMARYHOLDER,omitempty"`
SecondaryHolder HolderInfo `xml:"HOLDERINFO>SECONDARYHOLDER,omitempty"` SecondaryHolder HolderInfo `xml:"HOLDERINFO>SECONDARYHOLDER,omitempty"`
// Only one of the rest of the fields will be valid for any given AcctInfo // Only one of the rest of the fields will be valid for any given AcctInfo
BankAcctInfo *BankAcctInfo `xml:"BANKACCTINFO,omitempty"` BankAcctInfo *BankAcctInfo `xml:"BANKACCTINFO,omitempty"`
CCAcctInfo *CCAcctInfo `xml:"CCACCTINFO,omitempty"` CCAcctInfo *CCAcctInfo `xml:"CCACCTINFO,omitempty"`
@ -109,6 +130,8 @@ type AcctInfo struct {
// TODO BPACCTINFO? // TODO BPACCTINFO?
} }
// AcctInfoResponse contains the information about all a user's accounts
// accessible from this FI
type AcctInfoResponse struct { type AcctInfoResponse struct {
XMLName xml.Name `xml:"ACCTINFOTRNRS"` XMLName xml.Name `xml:"ACCTINFOTRNRS"`
TrnUID UID `xml:"TRNUID"` TrnUID UID `xml:"TRNUID"`
@ -119,15 +142,19 @@ type AcctInfoResponse struct {
AcctInfo []AcctInfo `xml:"ACCTINFORS>ACCTINFO,omitempty"` AcctInfo []AcctInfo `xml:"ACCTINFORS>ACCTINFO,omitempty"`
} }
// Name returns the name of the top-level transaction XML/SGML element
func (air *AcctInfoResponse) Name() string { func (air *AcctInfoResponse) Name() string {
return "ACCTINFOTRNRS" return "ACCTINFOTRNRS"
} }
// Valid returns (true, nil) if this struct was valid OFX when unmarshalled
func (air *AcctInfoResponse) Valid() (bool, error) { func (air *AcctInfoResponse) Valid() (bool, error) {
//TODO implement //TODO implement
return true, nil return true, nil
} }
// Type returns which message set this message belongs to (which Response
// element of type []Message it belongs to)
func (air *AcctInfoResponse) Type() messageType { func (air *AcctInfoResponse) Type() messageType {
return SignupRs return SignupRs
} }

View File

@ -12,8 +12,11 @@ import (
"time" "time"
) )
// Int provides helper methods to unmarshal int64 values from SGML/XML
type Int int64 type Int int64
// UnmarshalXML handles unmarshalling an Int from an SGML/XML string. Leading
// and trailing whitespace is ignored.
func (i *Int) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { func (i *Int) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
var value string var value string
@ -33,14 +36,19 @@ func (i *Int) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
return nil return nil
} }
// Equal returns true if the two Ints are equal in value
func (i Int) Equal(o Int) bool { func (i Int) Equal(o Int) bool {
return i == o return i == o
} }
// Amount represents non-integer values (or at least values for fields that may
// not necessarily be integers)
type Amount struct { type Amount struct {
big.Rat big.Rat
} }
// UnmarshalXML handles unmarshalling an Amount from an SGML/XML string.
// Leading and trailing whitespace is ignored.
func (a *Amount) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { func (a *Amount) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
var value string var value string
@ -61,18 +69,22 @@ func (a *Amount) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
return nil return nil
} }
// String prints a string representation of an Amount
func (a Amount) String() string { func (a Amount) String() string {
return strings.TrimRight(strings.TrimRight(a.FloatString(100), "0"), ".") return strings.TrimRight(strings.TrimRight(a.FloatString(100), "0"), ".")
} }
// MarshalXML marshals an Amount to SGML/XML
func (a *Amount) MarshalXML(e *xml.Encoder, start xml.StartElement) error { func (a *Amount) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
return e.EncodeElement(a.String(), start) return e.EncodeElement(a.String(), start)
} }
// Equal returns true if two Amounts are equal in value
func (a Amount) Equal(o Amount) bool { func (a Amount) Equal(o Amount) bool {
return (&a).Cmp(&o.Rat) == 0 return (&a).Cmp(&o.Rat) == 0
} }
// Date represents OFX date/time values
type Date struct { type Date struct {
time.Time time.Time
} }
@ -86,6 +98,10 @@ var ofxDateFormats = []string{
} }
var ofxDateZoneRegex = regexp.MustCompile(`^([+-]?[0-9]+)(\.([0-9]{2}))?(:([A-Z]+))?$`) var ofxDateZoneRegex = regexp.MustCompile(`^([+-]?[0-9]+)(\.([0-9]{2}))?(:([A-Z]+))?$`)
// UnmarshalXML handles unmarshalling a Date from an SGML/XML string. It
// attempts to unmarshal the valid date formats in order of decreasing length
// and defaults to GMT if a time zone is not provided, as per the OFX spec.
// Leading and trailing whitespace is ignored.
func (od *Date) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { func (od *Date) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
var value, zone, zoneFormat string var value, zone, zoneFormat string
err := d.DecodeElement(&value, &start) err := d.DecodeElement(&value, &start)
@ -148,6 +164,7 @@ func (od *Date) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
return errors.New("OFX: Couldn't parse date:" + value) return errors.New("OFX: Couldn't parse date:" + value)
} }
// String returns a string representation of the Date abiding by the OFX spec
func (od Date) String() string { func (od Date) String() string {
format := od.Format(ofxDateFormats[0]) format := od.Format(ofxDateFormats[0])
zonename, zoneoffset := od.Zone() zonename, zoneoffset := od.Zone()
@ -165,31 +182,39 @@ func (od Date) String() string {
if len(zonename) > 0 { if len(zonename) > 0 {
return format + ":" + zonename + "]" return format + ":" + zonename + "]"
} else {
return format + "]"
} }
return format + "]"
} }
// MarshalXML marshals a Date to XML
func (od *Date) MarshalXML(e *xml.Encoder, start xml.StartElement) error { func (od *Date) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
return e.EncodeElement(od.String(), start) return e.EncodeElement(od.String(), start)
} }
// Equal returns true if the two Dates represent the same time (time zones are
// accounted for when comparing, but are not required to match)
func (od Date) Equal(o Date) bool { func (od Date) Equal(o Date) bool {
return od.Time.Equal(o.Time) return od.Time.Equal(o.Time)
} }
// NewDate returns a new Date object with the provided date, time, and timezone
func NewDate(year int, month time.Month, day, hour, min, sec, nsec int, loc *time.Location) *Date { func NewDate(year int, month time.Month, day, hour, min, sec, nsec int, loc *time.Location) *Date {
return &Date{Time: time.Date(year, month, day, hour, min, sec, nsec, loc)} return &Date{Time: time.Date(year, month, day, hour, min, sec, nsec, loc)}
} }
var gmt = time.FixedZone("GMT", 0) var gmt = time.FixedZone("GMT", 0)
// NewDateGMT returns a new Date object with the provided date and time in the
// GMT timezone
func NewDateGMT(year int, month time.Month, day, hour, min, sec, nsec int) *Date { func NewDateGMT(year int, month time.Month, day, hour, min, sec, nsec int) *Date {
return &Date{Time: time.Date(year, month, day, hour, min, sec, nsec, gmt)} return &Date{Time: time.Date(year, month, day, hour, min, sec, nsec, gmt)}
} }
// String provides helper methods to unmarshal OFX string values from SGML/XML
type String string type String string
// UnmarshalXML handles unmarshalling a String from an SGML/XML string. Leading
// and trailing whitespace is ignored.
func (os *String) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { func (os *String) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
var value string var value string
err := d.DecodeElement(&value, &start) err := d.DecodeElement(&value, &start)
@ -200,16 +225,21 @@ func (os *String) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
return nil return nil
} }
// String returns the string
func (os *String) String() string { func (os *String) String() string {
return string(*os) return string(*os)
} }
// Equal returns true if the two Strings are equal in value
func (os String) Equal(o String) bool { func (os String) Equal(o String) bool {
return os == o return os == o
} }
// Boolean provides helper methods to unmarshal bool values from OFX SGML/XML
type Boolean bool type Boolean bool
// UnmarshalXML handles unmarshalling a Boolean from an SGML/XML string.
// Leading and trailing whitespace is ignored.
func (ob *Boolean) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { func (ob *Boolean) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
var value string var value string
err := d.DecodeElement(&value, &start) err := d.DecodeElement(&value, &start)
@ -228,6 +258,7 @@ func (ob *Boolean) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
return nil return nil
} }
// MarshalXML marshals a Boolean to XML
func (ob *Boolean) MarshalXML(e *xml.Encoder, start xml.StartElement) error { func (ob *Boolean) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
if *ob { if *ob {
return e.EncodeElement("Y", start) return e.EncodeElement("Y", start)
@ -235,16 +266,21 @@ func (ob *Boolean) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
return e.EncodeElement("N", start) return e.EncodeElement("N", start)
} }
// String returns a string representation of a Boolean value
func (ob *Boolean) String() string { func (ob *Boolean) String() string {
return fmt.Sprintf("%v", *ob) return fmt.Sprintf("%v", *ob)
} }
// Equal returns true if the two Booleans are the same
func (ob Boolean) Equal(o Boolean) bool { func (ob Boolean) Equal(o Boolean) bool {
return ob == o return ob == o
} }
// UID represents an UID according to the OFX spec
type UID string type UID string
// UnmarshalXML handles unmarshalling an UID from an SGML/XML string. Leading
// and trailing whitespace is ignored.
func (ou *UID) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { func (ou *UID) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
var value string var value string
err := d.DecodeElement(&value, &start) err := d.DecodeElement(&value, &start)
@ -255,8 +291,8 @@ func (ou *UID) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
return nil return nil
} }
// The OFX specification recommends that UIDs follow the standard UUID // RecommendedFormat returns true iff this UID meets the OFX specification's
// 36-character format // recommendation that UIDs follow the standard UUID 36-character format
func (ou UID) RecommendedFormat() (bool, error) { func (ou UID) RecommendedFormat() (bool, error) {
if len(ou) != 36 { if len(ou) != 36 {
return false, errors.New("UID not 36 characters long") return false, errors.New("UID not 36 characters long")
@ -267,10 +303,12 @@ func (ou UID) RecommendedFormat() (bool, error) {
return true, nil return true, nil
} }
// Equal returns true if the two UIDs are the same
func (ou UID) Equal(o UID) bool { func (ou UID) Equal(o UID) bool {
return ou == o return ou == o
} }
// RandomUID creates a new randomly-generated UID
func RandomUID() (*UID, error) { func RandomUID() (*UID, error) {
uidbytes := make([]byte, 16) uidbytes := make([]byte, 16)
n, err := rand.Read(uidbytes[:]) n, err := rand.Read(uidbytes[:])