mirror of
https://github.com/aclindsa/ofxgo.git
synced 2024-11-22 11:30:05 -05:00
Add validation of banking requests and responses
This commit is contained in:
parent
7f2ca5db0f
commit
1ee7197340
170
bank.go
170
bank.go
@ -1,6 +1,7 @@
|
|||||||
package ofxgo
|
package ofxgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"github.com/aclindsa/go/src/encoding/xml"
|
"github.com/aclindsa/go/src/encoding/xml"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -30,8 +31,16 @@ func (r *StatementRequest) Name() string {
|
|||||||
// Valid returns (true, nil) if this struct would be valid OFX if marshalled
|
// Valid returns (true, nil) if this struct would be valid OFX if marshalled
|
||||||
// into XML/SGML
|
// into XML/SGML
|
||||||
func (r *StatementRequest) Valid(version ofxVersion) (bool, error) {
|
func (r *StatementRequest) Valid(version ofxVersion) (bool, error) {
|
||||||
// TODO implement
|
if ok, err := r.TrnUID.Valid(); !ok {
|
||||||
return true, nil
|
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Type returns which message set this message belongs to (which Request
|
// Type returns which message set this message belongs to (which Request
|
||||||
@ -54,6 +63,26 @@ type Payee struct {
|
|||||||
Phone String `xml:"PHONE"`
|
Phone String `xml:"PHONE"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
// ImageData represents the metadata surrounding a check or other image file,
|
// ImageData represents the metadata surrounding a check or other image file,
|
||||||
// including how to retrieve the image
|
// including how to retrieve the image
|
||||||
type ImageData struct {
|
type ImageData struct {
|
||||||
@ -100,6 +129,49 @@ type Transaction struct {
|
|||||||
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.)
|
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.)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
// TransactionList represents a list of bank transactions, and also includes
|
// TransactionList represents a list of bank transactions, and also includes
|
||||||
// the date range its transactions cover.
|
// the date range its transactions cover.
|
||||||
type TransactionList struct {
|
type TransactionList struct {
|
||||||
@ -109,6 +181,23 @@ type TransactionList struct {
|
|||||||
Transactions []Transaction `xml:"STMTTRN,omitempty"`
|
Transactions []Transaction `xml:"STMTTRN,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
// PendingTransaction represents a single pending transaction. It is similar to
|
// PendingTransaction represents a single pending transaction. It is similar to
|
||||||
// Transaction, but is not finalized (and may never be). For instance, it lacks
|
// Transaction, but is not finalized (and may never be). For instance, it lacks
|
||||||
// FiTID and DtPosted fields.
|
// FiTID and DtPosted fields.
|
||||||
@ -127,6 +216,24 @@ type PendingTransaction struct {
|
|||||||
OrigCurrency CurrSymbol `xml:"ORIGCURRENCY,omitempty"` // If different from CURDEF in STMTTRS
|
OrigCurrency CurrSymbol `xml:"ORIGCURRENCY,omitempty"` // If different from CURDEF in STMTTRS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
// PendingTransactionList represents a list of pending transactions, along with
|
// PendingTransactionList represents a list of pending transactions, along with
|
||||||
// the date they were generated
|
// the date they were generated
|
||||||
type PendingTransactionList struct {
|
type PendingTransactionList struct {
|
||||||
@ -135,6 +242,21 @@ type PendingTransactionList struct {
|
|||||||
Transactions []PendingTransaction `xml:"STMTTRNP,omitempty"`
|
Transactions []PendingTransaction `xml:"STMTTRNP,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
// Balance represents a generic (free-form) balance defined by an FI.
|
// Balance represents a generic (free-form) balance defined by an FI.
|
||||||
type Balance struct {
|
type Balance struct {
|
||||||
XMLName xml.Name `xml:"BAL"`
|
XMLName xml.Name `xml:"BAL"`
|
||||||
@ -152,6 +274,17 @@ type Balance struct {
|
|||||||
Currency *Currency `xml:"CURRENCY,omitempty"` // if BALTYPE is DOLLAR
|
Currency *Currency `xml:"CURRENCY,omitempty"` // if BALTYPE is DOLLAR
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
// StatementResponse represents a bank account statement, including its
|
// StatementResponse represents a bank account statement, including its
|
||||||
// balances and possibly transactions. It is a response to StatementRequest, or
|
// balances and possibly transactions. It is a response to StatementRequest, or
|
||||||
// sometimes provided as part of an OFX file downloaded manually from an FI.
|
// sometimes provided as part of an OFX file downloaded manually from an FI.
|
||||||
@ -182,7 +315,38 @@ func (sr *StatementResponse) Name() string {
|
|||||||
|
|
||||||
// Valid returns (true, nil) if this struct was valid OFX when unmarshalled
|
// Valid returns (true, nil) if this struct was valid OFX when unmarshalled
|
||||||
func (sr *StatementResponse) Valid(version ofxVersion) (bool, error) {
|
func (sr *StatementResponse) Valid(version ofxVersion) (bool, error) {
|
||||||
//TODO implement
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
20
common.go
20
common.go
@ -274,6 +274,19 @@ type BankAcct struct {
|
|||||||
AcctKey String `xml:"ACCTKEY,omitempty"` // Unused in USA
|
AcctKey String `xml:"ACCTKEY,omitempty"` // Unused in USA
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b BankAcct) Valid() (bool, error) {
|
||||||
|
if len(b.BankID) == 0 {
|
||||||
|
return false, errors.New("BankAcct.BankID empty")
|
||||||
|
}
|
||||||
|
if len(b.AcctID) == 0 {
|
||||||
|
return false, errors.New("BankAcct.AcctID empty")
|
||||||
|
}
|
||||||
|
if !b.AcctType.Valid() {
|
||||||
|
return false, errors.New("Invalid or unspecified BankAcct.AcctType")
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
// CCAcct represents the identifying information for one checking account
|
// CCAcct represents the identifying information for one checking account
|
||||||
type CCAcct struct {
|
type CCAcct struct {
|
||||||
XMLName xml.Name // CCACCTTO or CCACCTFROM
|
XMLName xml.Name // CCACCTTO or CCACCTFROM
|
||||||
@ -281,6 +294,13 @@ type CCAcct struct {
|
|||||||
AcctKey String `xml:"ACCTKEY,omitempty"` // Unused in USA
|
AcctKey String `xml:"ACCTKEY,omitempty"` // Unused in USA
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c CCAcct) Valid() (bool, error) {
|
||||||
|
if len(c.AcctID) == 0 {
|
||||||
|
return false, errors.New("CCAcct.AcctID empty")
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
// InvAcct represents the identifying information for one investment account
|
// InvAcct represents the identifying information for one investment account
|
||||||
type InvAcct struct {
|
type InvAcct struct {
|
||||||
XMLName xml.Name // INVACCTTO or INVACCTFROM
|
XMLName xml.Name // INVACCTTO or INVACCTFROM
|
||||||
|
9
types.go
9
types.go
@ -304,6 +304,15 @@ func (ou UID) RecommendedFormat() (bool, error) {
|
|||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Valid returns true, nil if the UID is valid. This is less strict than
|
||||||
|
// RecommendedFormat, and will always return true, nil if it does.
|
||||||
|
func (ou UID) Valid() (bool, error) {
|
||||||
|
if len(ou) == 0 || len(ou) > 36 {
|
||||||
|
return false, errors.New("UID invalid length")
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Equal returns true if the two UIDs are the same
|
// 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
|
||||||
|
@ -364,6 +364,21 @@ func TestUIDRecommendedFormat(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUIDValid(t *testing.T) {
|
||||||
|
var u ofxgo.UID = ""
|
||||||
|
if ok, err := u.Valid(); ok || err == nil {
|
||||||
|
t.Fatalf("Empty UID unexpectedly valid\n")
|
||||||
|
}
|
||||||
|
u = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||||
|
if ok, err := u.Valid(); ok || err == nil {
|
||||||
|
t.Fatalf("Too-long UID unexpectedly valid\n")
|
||||||
|
}
|
||||||
|
u = "7be37076-623a-425f-ae6b-a5465b7e93b0"
|
||||||
|
if ok, err := u.Valid(); !ok || err != nil {
|
||||||
|
t.Fatalf("Good UID unexpectedly invalid: %s\n", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRandomUID(t *testing.T) {
|
func TestRandomUID(t *testing.T) {
|
||||||
uid, err := ofxgo.RandomUID()
|
uid, err := ofxgo.RandomUID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
Loading…
Reference in New Issue
Block a user