2017-03-14 10:31:14 -04:00
package ofxgo
import (
2017-04-18 19:46:23 -04:00
"errors"
2017-10-02 09:21:56 -04:00
"github.com/aclindsa/xml"
2017-03-14 10:31:14 -04:00
)
2017-04-12 21:21:05 -04:00
// StatementRequest represents a request for a bank statement. It is used to
// request balances and/or transactions for checking, savings, money market,
// and line of credit accounts. See CCStatementRequest for the analog for
// credit card accounts.
2017-03-14 10:31:14 -04:00
type StatementRequest struct {
2017-03-30 10:24:26 -04:00
XMLName xml . Name ` xml:"STMTTRNRQ" `
TrnUID UID ` xml:"TRNUID" `
CltCookie String ` xml:"CLTCOOKIE,omitempty" `
TAN String ` xml:"TAN,omitempty" ` // Transaction authorization number
// TODO `xml:"OFXEXTENSION,omitempty"`
2017-03-22 20:01:30 -04:00
BankAcctFrom BankAcct ` xml:"STMTRQ>BANKACCTFROM" `
2017-03-29 05:31:01 -04:00
DtStart * Date ` xml:"STMTRQ>INCTRAN>DTSTART,omitempty" `
DtEnd * Date ` xml:"STMTRQ>INCTRAN>DTEND,omitempty" `
2017-03-22 20:01:30 -04:00
Include Boolean ` xml:"STMTRQ>INCTRAN>INCLUDE" ` // Include transactions (instead of just balance)
IncludePending Boolean ` xml:"STMTRQ>INCLUDEPENDING,omitempty" ` // Include pending transactions
IncTranImg Boolean ` xml:"STMTRQ>INCTRANIMG,omitempty" ` // Include transaction images
2017-03-14 10:31:14 -04:00
}
2017-04-12 21:21:05 -04:00
// Name returns the name of the top-level transaction XML/SGML element
2017-03-14 10:31:14 -04:00
func ( r * StatementRequest ) Name ( ) string {
return "STMTTRNRQ"
}
2017-04-12 21:21:05 -04:00
// Valid returns (true, nil) if this struct would be valid OFX if marshalled
// into XML/SGML
2017-04-17 11:11:46 -04:00
func ( r * StatementRequest ) Valid ( version ofxVersion ) ( bool , error ) {
2017-04-18 19:46:23 -04:00
if ok , err := r . TrnUID . Valid ( ) ; ! ok {
return false , err
}
if r . IncludePending && version < OfxVersion220 {
return false , errors . New ( "StatementRequest.IncludePending invalid for OFX < 2.2" )
}
if r . IncTranImg && version < OfxVersion210 {
return false , errors . New ( "StatementRequest.IncTranImg invalid for OFX < 2.1" )
}
return r . BankAcctFrom . Valid ( )
2017-03-14 10:31:14 -04:00
}
2017-04-12 21:21:05 -04:00
// Type returns which message set this message belongs to (which Request
// element of type []Message it should appended to)
2017-03-31 09:25:07 -04:00
func ( r * StatementRequest ) Type ( ) messageType {
return BankRq
}
2017-04-12 21:21:05 -04:00
// Payee specifies a complete billing address for a payee
2017-03-14 10:31:14 -04:00
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" `
}
2017-04-18 19:46:23 -04:00
// Valid returns (true, nil) if this struct is valid OFX
func ( p Payee ) Valid ( ) ( bool , error ) {
if len ( p . Name ) == 0 {
return false , errors . New ( "Payee.Name empty" )
} else if len ( p . Addr1 ) == 0 {
return false , errors . New ( "Payee.Addr1 empty" )
} else if len ( p . City ) == 0 {
return false , errors . New ( "Payee.City empty" )
} else if len ( p . State ) == 0 {
return false , errors . New ( "Payee.State empty" )
} else if len ( p . PostalCode ) == 0 {
return false , errors . New ( "Payee.PostalCode empty" )
} else if len ( p . Country ) != 0 && len ( p . Country ) != 3 {
return false , errors . New ( "Payee.Country invalid length" )
} else if len ( p . Phone ) == 0 {
return false , errors . New ( "Payee.Phone empty" )
}
return true , nil
}
2017-04-12 21:21:05 -04:00
// ImageData represents the metadata surrounding a check or other image file,
// including how to retrieve the image
2017-03-17 21:35:42 -04:00
type ImageData struct {
2017-04-06 05:58:22 -04:00
XMLName xml . Name ` xml:"IMAGEDATA" `
ImageType imageType ` xml:"IMAGETYPE" ` // One of STATEMENT, TRANSACTION, TAX
ImageRef String ` xml:"IMAGEREF" ` // URL or identifier, depending on IMAGEREFTYPE
ImageRefType imageRefType ` xml:"IMAGEREFTYPE" ` // One of OPAQUE, URL, FORMURL (see spec for more details on how to access images of each of these types)
2017-03-17 21:35:42 -04:00
// Only one of the next two should be valid at any given time
2017-04-06 05:58:22 -04:00
ImageDelay Int ` xml:"IMAGEDELAY,omitempty" ` // Number of calendar days from DTSERVER (for statement images) or DTPOSTED (for transaction image) the image will become available
DtImageAvail * Date ` xml:"DTIMAGEAVAIL,omitempty" ` // Date image will become available
ImageTTL Int ` xml:"IMAGETTL,omitempty" ` // Number of days after image becomes available that it will remain available
CheckSup checkSup ` xml:"CHECKSUP,omitempty" ` // What is contained in check images. One of FRONTONLY, BACKONLY, FRONTANDBACK
2017-03-17 21:35:42 -04:00
}
2017-04-12 21:21:05 -04:00
// Transaction represents a single banking transaction. At a minimum, it
// identifies the type of transaction (TrnType) and the date it was posted
// (DtPosted). Ideally it also provides metadata to help the user recognize
// this transaction (i.e. CheckNum, Name or Payee, Memo, etc.)
2017-03-14 10:31:14 -04:00
type Transaction struct {
2017-04-06 05:58:22 -04:00
XMLName xml . Name ` xml:"STMTTRN" `
TrnType trnType ` 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, OTHER
DtPosted Date ` xml:"DTPOSTED" `
DtUser * Date ` xml:"DTUSER,omitempty" `
DtAvail * Date ` xml:"DTAVAIL,omitempty" `
TrnAmt Amount ` xml:"TRNAMT" `
2017-04-12 21:40:42 -04:00
FiTID String ` xml:"FITID" ` // Client uses FITID to detect whether it has previously downloaded the transaction
CorrectFiTID String ` xml:"CORRECTFITID,omitempty" ` // Transaction ID that this transaction corrects, if present
2017-04-06 05:58:22 -04:00
CorrectAction correctAction ` xml:"CORRECTACTION,omitempty" ` // One of DELETE, REPLACE
2017-04-12 21:40:42 -04:00
SrvrTID String ` xml:"SRVRTID,omitempty" `
2017-04-06 05:58:22 -04:00
CheckNum String ` xml:"CHECKNUM,omitempty" `
RefNum String ` xml:"REFNUM,omitempty" `
SIC Int ` xml:"SIC,omitempty" ` // Standard Industrial Code
2017-04-12 21:21:05 -04:00
PayeeID String ` xml:"PAYEEID,omitempty" `
2017-03-14 10:31:14 -04:00
// Note: Servers should provide NAME or PAYEE, but not both
2017-04-19 21:23:37 -04:00
Name String ` xml:"NAME,omitempty" `
Payee * Payee ` xml:"PAYEE,omitempty" `
ExtdName String ` xml:"EXTDNAME,omitempty" ` // Extended name of payee or transaction description
BankAcctTo * BankAcct ` xml:"BANKACCTTO,omitempty" ` // If the transfer was to a bank account we have the account information for
CCAcctTo * CCAcct ` xml:"CCACCTTO,omitempty" ` // If the transfer was to a credit card account we have the account information for
Memo String ` xml:"MEMO,omitempty" ` // Extra information (not in NAME)
ImageData [ ] ImageData ` xml:"IMAGEDATA,omitempty" `
// Only one of Currency and OrigCurrency can ever be Valid() for the same transaction
2019-03-06 05:58:46 -05:00
Currency * Currency ` xml:"CURRENCY,omitempty" ` // Represents the currency of TrnAmt (instead of CURDEF in STMTRS) if Valid
OrigCurrency * Currency ` xml:"ORIGCURRENCY,omitempty" ` // Represents the currency TrnAmt was converted to STMTRS' CURDEF from if Valid
2017-04-06 05:58:22 -04:00
Inv401kSource inv401kSource ` 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.)
2017-03-14 10:31:14 -04:00
}
2017-04-18 19:46:23 -04:00
// Valid returns (true, nil) if this struct is valid OFX
func ( t Transaction ) Valid ( version ofxVersion ) ( bool , error ) {
var emptyDate Date
if ! t . TrnType . Valid ( ) || t . TrnType == TrnTypeHold {
return false , errors . New ( "Transaction.TrnType invalid" )
} else if t . DtPosted . Equal ( emptyDate ) {
return false , errors . New ( "Transaction.DtPosted not filled" )
} else if len ( t . FiTID ) == 0 {
return false , errors . New ( "Transaction.FiTID empty" )
} else if len ( t . CorrectFiTID ) > 0 && t . CorrectAction . Valid ( ) {
return false , errors . New ( "Transaction.CorrectFiTID nonempty but CorrectAction invalid" )
} else if len ( t . Name ) > 0 && t . Payee != nil {
return false , errors . New ( "Only one of Transaction.Name and Payee may be specified" )
}
if t . Payee != nil {
if ok , err := t . Payee . Valid ( ) ; ! ok {
return false , err
}
}
if t . BankAcctTo != nil && t . CCAcctTo != nil {
return false , errors . New ( "Only one of Transaction.BankAcctTo and CCAcctTo may be specified" )
} else if t . BankAcctTo != nil {
if ok , err := t . BankAcctTo . Valid ( ) ; ! ok {
return false , err
}
} else if t . CCAcctTo != nil {
if ok , err := t . CCAcctTo . Valid ( ) ; ! ok {
return false , err
}
}
if version < OfxVersion220 && len ( t . ImageData ) > 0 {
return false , errors . New ( "Transaction.ImageData only supportd for OFX > 220" )
} else if len ( t . ImageData ) > 2 {
return false , errors . New ( "Only 2 of ImageData allowed in Transaction" )
}
2019-03-06 05:58:46 -05:00
var ok1 , ok2 bool
if t . Currency != nil {
ok1 , _ = t . Currency . Valid ( )
}
if t . OrigCurrency != nil {
ok2 , _ = t . OrigCurrency . Valid ( )
}
2017-04-18 19:46:23 -04:00
if ok1 && ok2 {
return false , errors . New ( "Currency and OrigCurrency both supplied for Pending Transaction, only one allowed" )
}
return true , nil
}
2017-04-12 21:21:05 -04:00
// TransactionList represents a list of bank transactions, and also includes
// the date range its transactions cover.
2017-03-14 10:31:14 -04:00
type TransactionList struct {
XMLName xml . Name ` xml:"BANKTRANLIST" `
2017-04-12 21:21:05 -04:00
DtStart Date ` xml:"DTSTART" ` // Start date for transaction data
DtEnd Date ` xml:"DTEND" ` // Value that client should send in next <DTSTART> request to ensure that it does not miss any transactions
2017-03-17 21:35:42 -04:00
Transactions [ ] Transaction ` xml:"STMTTRN,omitempty" `
}
2017-04-18 19:46:23 -04:00
// Valid returns (true, nil) if this struct is valid OFX
func ( l TransactionList ) Valid ( version ofxVersion ) ( bool , error ) {
var emptyDate Date
if l . DtStart . Equal ( emptyDate ) {
return false , errors . New ( "TransactionList.DtStart not filled" )
} else if l . DtEnd . Equal ( emptyDate ) {
return false , errors . New ( "TransactionList.DtEnd not filled" )
}
for _ , t := range l . Transactions {
if ok , err := t . Valid ( version ) ; ! ok {
return false , err
}
}
return true , nil
}
2017-04-12 21:21:05 -04:00
// PendingTransaction represents a single pending transaction. It is similar to
// Transaction, but is not finalized (and may never be). For instance, it lacks
2017-04-12 21:40:42 -04:00
// FiTID and DtPosted fields.
2017-03-17 21:35:42 -04:00
type PendingTransaction struct {
2017-04-19 21:23:37 -04:00
XMLName xml . Name ` xml:"STMTTRNP" `
TrnType trnType ` 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, OTHER
DtTran Date ` xml:"DTTRAN" `
DtExpire * Date ` xml:"DTEXPIRE,omitempty" ` // only valid for TrnType==HOLD, the date the hold will expire
TrnAmt Amount ` xml:"TRNAMT" `
RefNum String ` xml:"REFNUM,omitempty" `
Name String ` xml:"NAME,omitempty" `
ExtdName String ` xml:"EXTDNAME,omitempty" ` // Extended name of payee or transaction description
Memo String ` xml:"MEMO,omitempty" ` // Extra information (not in NAME)
ImageData [ ] ImageData ` xml:"IMAGEDATA,omitempty" `
// Only one of Currency and OrigCurrency can ever be Valid() for the same transaction
Currency Currency ` xml:"CURRENCY,omitempty" ` // Represents the currency of TrnAmt (instead of CURDEF in STMTRS) if Valid
OrigCurrency Currency ` xml:"ORIGCURRENCY,omitempty" ` // Represents the currency TrnAmt was converted to STMTRS' CURDEF from if Valid
2017-03-17 21:35:42 -04:00
}
2017-04-18 19:46:23 -04:00
// Valid returns (true, nil) if this struct is valid OFX
func ( t PendingTransaction ) Valid ( ) ( bool , error ) {
var emptyDate Date
if ! t . TrnType . Valid ( ) {
return false , errors . New ( "PendingTransaction.TrnType invalid" )
} else if t . DtTran . Equal ( emptyDate ) {
return false , errors . New ( "PendingTransaction.DtTran not filled" )
} else if len ( t . Name ) == 0 {
return false , errors . New ( "PendingTransaction.Name empty" )
}
ok1 , _ := t . Currency . Valid ( )
ok2 , _ := t . OrigCurrency . Valid ( )
if ok1 && ok2 {
return false , errors . New ( "Currency and OrigCurrency both supplied for Pending Transaction, only one allowed" )
}
return true , nil
}
2017-04-12 21:21:05 -04:00
// PendingTransactionList represents a list of pending transactions, along with
// the date they were generated
2017-03-17 21:35:42 -04:00
type PendingTransactionList struct {
XMLName xml . Name ` xml:"BANKTRANLISTP" `
2017-04-12 21:21:05 -04:00
DtAsOf Date ` xml:"DTASOF" ` // Date and time this set of pending transactions was generated
2017-03-17 21:35:42 -04:00
Transactions [ ] PendingTransaction ` xml:"STMTTRNP,omitempty" `
}
2017-04-18 19:46:23 -04:00
// Valid returns (true, nil) if this struct is valid OFX
func ( l PendingTransactionList ) Valid ( ) ( bool , error ) {
var emptyDate Date
if l . DtAsOf . Equal ( emptyDate ) {
return false , errors . New ( "PendingTransactionList.DtAsOf not filled" )
}
for _ , t := range l . Transactions {
if ok , err := t . Valid ( ) ; ! ok {
return false , err
}
}
return true , nil
}
2017-04-12 21:21:05 -04:00
// Balance represents a generic (free-form) balance defined by an FI.
2017-03-17 21:35:42 -04:00
type Balance struct {
XMLName xml . Name ` xml:"BAL" `
Name String ` xml:"NAME" `
Desc String ` xml:"DESC" `
// Balance type:
// DOLLAR = dollar (value formatted DDDD.cc)
// PERCENT = percentage (value formatted XXXX.YYYY)
// NUMBER = number (value formatted as is)
2017-04-06 05:58:22 -04:00
BalType balType ` xml:"BALTYPE" `
2017-03-17 21:35:42 -04:00
2017-03-29 05:31:01 -04:00
Value Amount ` xml:"VALUE" `
DtAsOf * Date ` xml:"DTASOF,omitempty" `
Currency * Currency ` xml:"CURRENCY,omitempty" ` // if BALTYPE is DOLLAR
2017-03-14 10:31:14 -04:00
}
2017-04-18 19:46:23 -04:00
// Valid returns (true, nil) if this struct is valid OFX
func ( b Balance ) Valid ( ) ( bool , error ) {
if len ( b . Name ) == 0 || len ( b . Desc ) == 0 {
return false , errors . New ( "Balance Name and Desc not supplied" )
}
if ! b . BalType . Valid ( ) {
return false , errors . New ( "Balance BALTYPE not specified" )
}
return true , nil
}
2017-04-12 21:21:05 -04:00
// StatementResponse represents a bank account statement, including its
// balances and possibly transactions. It is a response to StatementRequest, or
// sometimes provided as part of an OFX file downloaded manually from an FI.
2017-03-14 10:31:14 -04:00
type StatementResponse struct {
2017-03-30 10:24:26 -04:00
XMLName xml . Name ` xml:"STMTTRNRS" `
TrnUID UID ` xml:"TRNUID" `
Status Status ` xml:"STATUS" `
CltCookie String ` xml:"CLTCOOKIE,omitempty" `
// TODO `xml:"OFXEXTENSION,omitempty"`
2017-04-17 20:20:22 -04:00
CurDef CurrSymbol ` xml:"STMTRS>CURDEF" `
2017-03-29 05:31:01 -04:00
BankAcctFrom BankAcct ` xml:"STMTRS>BANKACCTFROM" `
BankTranList * TransactionList ` xml:"STMTRS>BANKTRANLIST,omitempty" `
BankTranListP * PendingTransactionList ` xml:"STMTRS>BANKTRANLISTP,omitempty" `
BalAmt Amount ` xml:"STMTRS>LEDGERBAL>BALAMT" `
DtAsOf Date ` xml:"STMTRS>LEDGERBAL>DTASOF" `
AvailBalAmt * Amount ` xml:"STMTRS>AVAILBAL>BALAMT,omitempty" `
AvailDtAsOf * Date ` xml:"STMTRS>AVAILBAL>DTASOF,omitempty" `
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,omitempty" ` // Marketing information
2017-03-14 10:31:14 -04:00
}
2017-04-12 21:21:05 -04:00
// Name returns the name of the top-level transaction XML/SGML element
2017-03-31 11:54:43 -04:00
func ( sr * StatementResponse ) Name ( ) string {
2017-03-14 10:31:14 -04:00
return "STMTTRNRS"
}
2017-04-12 21:23:57 -04:00
// Valid returns (true, nil) if this struct was valid OFX when unmarshalled
2017-04-17 11:11:46 -04:00
func ( sr * StatementResponse ) Valid ( version ofxVersion ) ( bool , error ) {
2017-04-18 19:46:23 -04:00
var emptyDate Date
if ok , err := sr . TrnUID . Valid ( ) ; ! ok {
return false , err
} else if ok , err := sr . Status . Valid ( ) ; ! ok {
return false , err
} else if ok , err := sr . CurDef . Valid ( ) ; ! ok {
return false , err
} else if ok , err := sr . BankAcctFrom . Valid ( ) ; ! ok {
return false , err
} else if sr . DtAsOf . Equal ( emptyDate ) {
return false , errors . New ( "StatementResponse.DtAsOf not filled" )
} else if ( sr . AvailBalAmt == nil ) != ( sr . AvailDtAsOf == nil ) {
return false , errors . New ( "StatementResponse.Avail* must both either be present or absent" )
}
if sr . BankTranList != nil {
if ok , err := sr . BankTranList . Valid ( version ) ; ! ok {
return false , err
}
}
if sr . BankTranListP != nil {
if version < OfxVersion220 {
return false , errors . New ( "StatementResponse.BankTranListP invalid for OFX < 2.2" )
}
if ok , err := sr . BankTranListP . Valid ( ) ; ! ok {
return false , err
}
}
for _ , bal := range sr . BalList {
if ok , err := bal . Valid ( ) ; ! ok {
return false , err
}
}
2017-03-14 10:31:14 -04:00
return true , nil
}
2017-03-17 21:35:42 -04:00
2017-04-12 21:21:05 -04:00
// Type returns which message set this message belongs to (which Response
// element of type []Message it belongs to)
2017-03-31 11:54:43 -04:00
func ( sr * StatementResponse ) Type ( ) messageType {
2017-03-31 09:25:07 -04:00
return BankRs
}