mirror of
				https://github.com/aclindsa/ofxgo.git
				synced 2025-11-01 02:03:26 -04:00 
			
		
		
		
	Add validation of banking requests and responses
This commit is contained in:
		
							
								
								
									
										170
									
								
								bank.go
									
									
									
									
									
								
							
							
						
						
									
										170
									
								
								bank.go
									
									
									
									
									
								
							| @@ -1,6 +1,7 @@ | ||||
| package ofxgo | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"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 | ||||
| // into XML/SGML | ||||
| func (r *StatementRequest) Valid(version ofxVersion) (bool, error) { | ||||
| 	// TODO implement | ||||
| 	return true, nil | ||||
| 	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() | ||||
| } | ||||
|  | ||||
| // Type returns which message set this message belongs to (which Request | ||||
| @@ -54,6 +63,26 @@ type Payee struct { | ||||
| 	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, | ||||
| // including how to retrieve the image | ||||
| 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.) | ||||
| } | ||||
|  | ||||
| // 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 | ||||
| // the date range its transactions cover. | ||||
| type TransactionList struct { | ||||
| @@ -109,6 +181,23 @@ type TransactionList struct { | ||||
| 	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 | ||||
| // Transaction, but is not finalized (and may never be). For instance, it lacks | ||||
| // FiTID and DtPosted fields. | ||||
| @@ -127,6 +216,24 @@ type PendingTransaction struct { | ||||
| 	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 | ||||
| // the date they were generated | ||||
| type PendingTransactionList struct { | ||||
| @@ -135,6 +242,21 @@ type PendingTransactionList struct { | ||||
| 	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. | ||||
| type Balance struct { | ||||
| 	XMLName xml.Name `xml:"BAL"` | ||||
| @@ -152,6 +274,17 @@ type Balance struct { | ||||
| 	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 | ||||
| // balances and possibly transactions. It is a response to StatementRequest, or | ||||
| // 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 | ||||
| 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 | ||||
| } | ||||
|  | ||||
|   | ||||
							
								
								
									
										20
									
								
								common.go
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								common.go
									
									
									
									
									
								
							| @@ -274,6 +274,19 @@ type BankAcct struct { | ||||
| 	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 | ||||
| type CCAcct struct { | ||||
| 	XMLName xml.Name // CCACCTTO or CCACCTFROM | ||||
| @@ -281,6 +294,13 @@ type CCAcct struct { | ||||
| 	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 | ||||
| type InvAcct struct { | ||||
| 	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 | ||||
| } | ||||
|  | ||||
| // 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 | ||||
| func (ou UID) Equal(o UID) bool { | ||||
| 	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) { | ||||
| 	uid, err := ofxgo.RandomUID() | ||||
| 	if err != nil { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user