support ofx 1.x

This commit is contained in:
MaysWind
2024-11-02 02:05:55 +08:00
parent ac29f0bf98
commit f2e89da724
3 changed files with 1415 additions and 59 deletions
+45 -48
View File
@@ -1,8 +1,6 @@
package ofx package ofx
import ( import (
"encoding/xml"
"github.com/mayswind/ezbookkeeping/pkg/models" "github.com/mayswind/ezbookkeeping/pkg/models"
) )
@@ -71,10 +69,9 @@ var ofxTransactionTypeMapping = map[ofxTransactionType]models.TransactionType{
// ofxFile represents the struct of open financial exchange (ofx) file // ofxFile represents the struct of open financial exchange (ofx) file
type ofxFile struct { type ofxFile struct {
XMLName xml.Name `xml:"OFX"`
FileHeader *ofxFileHeader FileHeader *ofxFileHeader
BankMessageResponseV1 *ofxBankMessageResponseV1 `xml:"BANKMSGSRSV1"` BankMessageResponseV1 *ofxBankMessageResponseV1
CreditCardMessageResponseV1 *ofxCreditCardMessageResponseV1 `xml:"CREDITCARDMSGSRSV1"` CreditCardMessageResponseV1 *ofxCreditCardMessageResponseV1
} }
// ofxFileHeader represents the struct of open financial exchange (ofx) file header // ofxFileHeader represents the struct of open financial exchange (ofx) file header
@@ -88,101 +85,101 @@ type ofxFileHeader struct {
// ofxBankMessageResponseV1 represents the struct of open financial exchange (ofx) bank message response v1 // ofxBankMessageResponseV1 represents the struct of open financial exchange (ofx) bank message response v1
type ofxBankMessageResponseV1 struct { type ofxBankMessageResponseV1 struct {
StatementTransactionResponse *ofxBankStatementTransactionResponse `xml:"STMTTRNRS"` StatementTransactionResponse *ofxBankStatementTransactionResponse
} }
// ofxCreditCardMessageResponseV1 represents the struct of open financial exchange (ofx) credit card message response v1 // ofxCreditCardMessageResponseV1 represents the struct of open financial exchange (ofx) credit card message response v1
type ofxCreditCardMessageResponseV1 struct { type ofxCreditCardMessageResponseV1 struct {
StatementTransactionResponse *ofxCreditCardStatementTransactionResponse `xml:"CCSTMTTRNRS"` StatementTransactionResponse *ofxCreditCardStatementTransactionResponse
} }
// ofxBankStatementTransactionResponse represents the struct of open financial exchange (ofx) bank statement transaction response // ofxBankStatementTransactionResponse represents the struct of open financial exchange (ofx) bank statement transaction response
type ofxBankStatementTransactionResponse struct { type ofxBankStatementTransactionResponse struct {
StatementResponse *ofxBankStatementResponse `xml:"STMTRS"` StatementResponse *ofxBankStatementResponse
} }
// ofxCreditCardStatementTransactionResponse represents the struct of open financial exchange (ofx) credit card statement transaction response // ofxCreditCardStatementTransactionResponse represents the struct of open financial exchange (ofx) credit card statement transaction response
type ofxCreditCardStatementTransactionResponse struct { type ofxCreditCardStatementTransactionResponse struct {
StatementResponse *ofxCreditCardStatementResponse `xml:"CCSTMTRS"` StatementResponse *ofxCreditCardStatementResponse
} }
// ofxBankStatementResponse represents the struct of open financial exchange (ofx) bank statement response // ofxBankStatementResponse represents the struct of open financial exchange (ofx) bank statement response
type ofxBankStatementResponse struct { type ofxBankStatementResponse struct {
DefaultCurrency string `xml:"CURDEF"` DefaultCurrency string
AccountFrom *ofxBankAccount `xml:"BANKACCTFROM"` AccountFrom *ofxBankAccount
TransactionList *ofxBankTransactionList `xml:"BANKTRANLIST"` TransactionList *ofxBankTransactionList
} }
// ofxCreditCardStatementResponse represents the struct of open financial exchange (ofx) credit card statement response // ofxCreditCardStatementResponse represents the struct of open financial exchange (ofx) credit card statement response
type ofxCreditCardStatementResponse struct { type ofxCreditCardStatementResponse struct {
DefaultCurrency string `xml:"CURDEF"` DefaultCurrency string
AccountFrom *ofxCreditCardAccount `xml:"CCACCTFROM"` AccountFrom *ofxCreditCardAccount
TransactionList *ofxCreditCardTransactionList `xml:"BANKTRANLIST"` TransactionList *ofxCreditCardTransactionList
} }
// ofxBankAccount represents the struct of open financial exchange (ofx) bank account // ofxBankAccount represents the struct of open financial exchange (ofx) bank account
type ofxBankAccount struct { type ofxBankAccount struct {
BankId string `xml:"BANKID"` BankId string
BranchId string `xml:"BRANCHID"` BranchId string
AccountId string `xml:"ACCTID"` AccountId string
AccountType ofxAccountType `xml:"ACCTTYPE"` AccountType ofxAccountType
AccountKey string `xml:"ACCTKEY"` AccountKey string
} }
// ofxCreditCardAccount represents the struct of open financial exchange (ofx) credit card account // ofxCreditCardAccount represents the struct of open financial exchange (ofx) credit card account
type ofxCreditCardAccount struct { type ofxCreditCardAccount struct {
AccountId string `xml:"ACCTID"` AccountId string
AccountKey string `xml:"ACCTKEY"` AccountKey string
} }
// ofxBankTransactionList represents the struct of open financial exchange (ofx) bank transaction list // ofxBankTransactionList represents the struct of open financial exchange (ofx) bank transaction list
type ofxBankTransactionList struct { type ofxBankTransactionList struct {
StartDate string `xml:"DTSTART"` StartDate string
EndDate string `xml:"DTEND"` EndDate string
StatementTransactions []*ofxBankStatementTransaction `xml:"STMTTRN"` StatementTransactions []*ofxBankStatementTransaction
} }
// ofxCreditCardTransactionList represents the struct of open financial exchange (ofx) credit card transaction list // ofxCreditCardTransactionList represents the struct of open financial exchange (ofx) credit card transaction list
type ofxCreditCardTransactionList struct { type ofxCreditCardTransactionList struct {
StartDate string `xml:"DTSTART"` StartDate string
EndDate string `xml:"DTEND"` EndDate string
StatementTransactions []*ofxCreditCardStatementTransaction `xml:"STMTTRN"` StatementTransactions []*ofxCreditCardStatementTransaction
} }
// ofxBaseStatementTransaction represents the struct of open financial exchange (ofx) base statement transaction // ofxBaseStatementTransaction represents the struct of open financial exchange (ofx) base statement transaction
type ofxBaseStatementTransaction struct { type ofxBaseStatementTransaction struct {
TransactionId string `xml:"FITID"` TransactionId string
TransactionType ofxTransactionType `xml:"TRNTYPE"` TransactionType ofxTransactionType
PostedDate string `xml:"DTPOSTED"` PostedDate string
Amount string `xml:"TRNAMT"` Amount string
Name string `xml:"NAME"` Name string
Payee *ofxPayee `xml:"PAYEE"` Payee *ofxPayee
Memo string `xml:"MEMO"` Memo string
Currency string `xml:"CURRENCY"` Currency string
OriginalCurrency string `xml:"ORIGCURRENCY"` OriginalCurrency string
} }
// ofxBankStatementTransaction represents the struct of open financial exchange (ofx) bank statement transaction // ofxBankStatementTransaction represents the struct of open financial exchange (ofx) bank statement transaction
type ofxBankStatementTransaction struct { type ofxBankStatementTransaction struct {
ofxBaseStatementTransaction ofxBaseStatementTransaction
AccountTo *ofxBankAccount `xml:"BANKACCTTO"` AccountTo *ofxBankAccount
} }
// ofxCreditCardStatementTransaction represents the struct of open financial exchange (ofx) credit card statement transaction // ofxCreditCardStatementTransaction represents the struct of open financial exchange (ofx) credit card statement transaction
type ofxCreditCardStatementTransaction struct { type ofxCreditCardStatementTransaction struct {
ofxBaseStatementTransaction ofxBaseStatementTransaction
AccountTo *ofxCreditCardAccount `xml:"CCACCTTO"` AccountTo *ofxCreditCardAccount
} }
// ofxPayee represents the struct of open financial exchange (ofx) payee info // ofxPayee represents the struct of open financial exchange (ofx) payee info
type ofxPayee struct { type ofxPayee struct {
Name string `xml:"NAME"` Name string
Address1 string `xml:"ADDR1"` Address1 string
Address2 string `xml:"ADDR2"` Address2 string
Address3 string `xml:"ADDR3"` Address3 string
City string `xml:"CITY"` City string
State string `xml:"STATE"` State string
PostalCode string `xml:"POSTALCODE"` PostalCode string
Country string `xml:"COUNTRY"` Country string
Phone string `xml:"PHONE"` Phone string
} }
File diff suppressed because it is too large Load Diff
+216 -2
View File
@@ -9,6 +9,191 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/errs" "github.com/mayswind/ezbookkeeping/pkg/errs"
) )
func TestCreateNewOFXFileReader_OFX1(t *testing.T) {
context := core.NewNullContext()
reader, err := createNewOFXFileReader(context, []byte(
"OFXHEADER:100\n"+
"DATA:OFXSGML\n"+
"VERSION:103\n"+
"SECURITY:NONE\n"+
"ENCODING:USASCII\n"+
"CHARSET:1252\n"+
"COMPRESSION:NONE\n"+
"OLDFILEUID:NONE\n"+
"NEWFILEUID:NONE\n"+
"\n"+
"<OFX>\n"+
"<BANKMSGSRSV1>\n"+
"<STMTTRNRS>\n"+
"<STMTRS>\n"+
"<CURDEF>CNY\n"+
"<BANKACCTFROM>\n"+
"<ACCTID>123\n"+
"</BANKACCTFROM>\n"+
"<BANKTRANLIST>\n"+
"<STMTTRN>\n"+
"<TRNTYPE>DEP\n"+
"<DTPOSTED>20240901012345.000[+8:CST]\n"+
"<TRNAMT>123.45\n"+
"</STMTTRN>\n"+
"</BANKTRANLIST>\n"+
"</STMTRS>\n"+
"</STMTTRNRS>\n"+
"</BANKMSGSRSV1>\n"+
"</OFX>"))
assert.Nil(t, err)
ofxFile, err := reader.read(context)
assert.Nil(t, err)
assert.NotNil(t, ofxFile)
assert.NotNil(t, ofxFile.FileHeader)
assert.Equal(t, ofxVersion1, ofxFile.FileHeader.OFXDeclarationVersion)
assert.Equal(t, "103", ofxFile.FileHeader.OFXDataVersion)
assert.Equal(t, "NONE", ofxFile.FileHeader.Security)
assert.Equal(t, "NONE", ofxFile.FileHeader.OldFileUid)
assert.Equal(t, "NONE", ofxFile.FileHeader.NewFileUid)
assert.NotNil(t, ofxFile.BankMessageResponseV1)
assert.NotNil(t, ofxFile.BankMessageResponseV1.StatementTransactionResponse)
assert.NotNil(t, ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse)
assert.Equal(t, "CNY", ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse.DefaultCurrency)
assert.NotNil(t, ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse.AccountFrom)
assert.Equal(t, "123", ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse.AccountFrom.AccountId)
assert.Equal(t, 1, len(ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse.TransactionList.StatementTransactions))
assert.Equal(t, ofxDepositTransaction, ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse.TransactionList.StatementTransactions[0].TransactionType)
assert.Equal(t, "20240901012345.000[+8:CST]", ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse.TransactionList.StatementTransactions[0].PostedDate)
assert.Equal(t, "123.45", ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse.TransactionList.StatementTransactions[0].Amount)
}
func TestCreateNewOFXFileReader_OFX1WithoutBreakLine(t *testing.T) {
context := core.NewNullContext()
reader, err := createNewOFXFileReader(context, []byte(
"OFXHEADER:100\n"+
"DATA:OFXSGML\n"+
"VERSION:103\n"+
"SECURITY:NONE\n"+
"ENCODING:USASCII\n"+
"CHARSET:1252\n"+
"COMPRESSION:NONE\n"+
"OLDFILEUID:NONE\n"+
"NEWFILEUID:NONE\n"+
"\n"+
"<OFX>"+
"<BANKMSGSRSV1>"+
"<STMTTRNRS>"+
"<STMTRS>"+
"<CURDEF>CNY"+
"<BANKACCTFROM>"+
"<ACCTID>123"+
"</BANKACCTFROM>"+
"<BANKTRANLIST>"+
"<STMTTRN>"+
"<TRNTYPE>DEP"+
"<DTPOSTED>20240901012345.000[+8:CST]"+
"<TRNAMT>123.45"+
"</STMTTRN>"+
"</BANKTRANLIST>"+
"</STMTRS>"+
"</STMTTRNRS>"+
"</BANKMSGSRSV1>"+
"</OFX>"))
assert.Nil(t, err)
ofxFile, err := reader.read(context)
assert.Nil(t, err)
assert.NotNil(t, ofxFile)
assert.NotNil(t, ofxFile.FileHeader)
assert.Equal(t, ofxVersion1, ofxFile.FileHeader.OFXDeclarationVersion)
assert.Equal(t, "103", ofxFile.FileHeader.OFXDataVersion)
assert.Equal(t, "NONE", ofxFile.FileHeader.Security)
assert.Equal(t, "NONE", ofxFile.FileHeader.OldFileUid)
assert.Equal(t, "NONE", ofxFile.FileHeader.NewFileUid)
assert.NotNil(t, ofxFile.BankMessageResponseV1)
assert.NotNil(t, ofxFile.BankMessageResponseV1.StatementTransactionResponse)
assert.NotNil(t, ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse)
assert.Equal(t, "CNY", ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse.DefaultCurrency)
assert.NotNil(t, ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse.AccountFrom)
assert.Equal(t, "123", ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse.AccountFrom.AccountId)
assert.Equal(t, 1, len(ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse.TransactionList.StatementTransactions))
assert.Equal(t, ofxDepositTransaction, ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse.TransactionList.StatementTransactions[0].TransactionType)
assert.Equal(t, "20240901012345.000[+8:CST]", ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse.TransactionList.StatementTransactions[0].PostedDate)
assert.Equal(t, "123.45", ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse.TransactionList.StatementTransactions[0].Amount)
}
func TestCreateNewOFXFileReader_OFX1WithBlanklines(t *testing.T) {
context := core.NewNullContext()
reader, err := createNewOFXFileReader(context, []byte(
"\n"+
"\n"+
"OFXHEADER:100\n"+
"DATA:OFXSGML\n"+
"VERSION:103\n"+
"SECURITY:NONE\n"+
"ENCODING:USASCII\n"+
"CHARSET:1252\n"+
"COMPRESSION:NONE\n"+
"OLDFILEUID:NONE\n"+
"NEWFILEUID:NONE\n"+
"\n"+
"<OFX>"+
"<BANKMSGSRSV1>"+
"<STMTTRNRS>"+
"<STMTRS>"+
"<CURDEF>CNY"+
"<BANKACCTFROM>"+
"<ACCTID>123"+
"</BANKACCTFROM>"+
"<BANKTRANLIST>"+
"<STMTTRN>"+
"<TRNTYPE>DEP"+
"<DTPOSTED>20240901012345.000[+8:CST]"+
"<TRNAMT>123.45"+
"</STMTTRN>"+
"</BANKTRANLIST>"+
"</STMTRS>"+
"</STMTTRNRS>"+
"</BANKMSGSRSV1>"+
"</OFX>"))
assert.Nil(t, err)
ofxFile, err := reader.read(context)
assert.Nil(t, err)
assert.NotNil(t, ofxFile)
assert.NotNil(t, ofxFile.FileHeader)
assert.Equal(t, ofxVersion1, ofxFile.FileHeader.OFXDeclarationVersion)
assert.Equal(t, "103", ofxFile.FileHeader.OFXDataVersion)
assert.Equal(t, "NONE", ofxFile.FileHeader.Security)
assert.Equal(t, "NONE", ofxFile.FileHeader.OldFileUid)
assert.Equal(t, "NONE", ofxFile.FileHeader.NewFileUid)
assert.NotNil(t, ofxFile.BankMessageResponseV1)
assert.NotNil(t, ofxFile.BankMessageResponseV1.StatementTransactionResponse)
assert.NotNil(t, ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse)
assert.Equal(t, "CNY", ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse.DefaultCurrency)
assert.NotNil(t, ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse.AccountFrom)
assert.Equal(t, "123", ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse.AccountFrom.AccountId)
assert.Equal(t, 1, len(ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse.TransactionList.StatementTransactions))
assert.Equal(t, ofxDepositTransaction, ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse.TransactionList.StatementTransactions[0].TransactionType)
assert.Equal(t, "20240901012345.000[+8:CST]", ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse.TransactionList.StatementTransactions[0].PostedDate)
assert.Equal(t, "123.45", ofxFile.BankMessageResponseV1.StatementTransactionResponse.StatementResponse.TransactionList.StatementTransactions[0].Amount)
}
func TestCreateNewOFXFileReader_OFX2(t *testing.T) { func TestCreateNewOFXFileReader_OFX2(t *testing.T) {
context := core.NewNullContext() context := core.NewNullContext()
reader, err := createNewOFXFileReader(context, []byte( reader, err := createNewOFXFileReader(context, []byte(
@@ -146,19 +331,48 @@ func TestCreateNewOFXFileReader_OFX2WithInvalidHeader(t *testing.T) {
_, err = createNewOFXFileReader(context, []byte( _, err = createNewOFXFileReader(context, []byte(
"<?xml version=\"1.0\" encoding=\"US-ASCII\"?>"+ "<?xml version=\"1.0\" encoding=\"US-ASCII\"?>"+
"<?OFX OFXHEADER=100?>"+ "<?OFX OFXHEADER=200?>"+
"<OFX>"+ "<OFX>"+
"</OFX>")) "</OFX>"))
assert.EqualError(t, err, errs.ErrInvalidOFXFile.Message) assert.EqualError(t, err, errs.ErrInvalidOFXFile.Message)
_, err = createNewOFXFileReader(context, []byte( _, err = createNewOFXFileReader(context, []byte(
"<?xml version=\"1.0\" encoding=\"US-ASCII\"?>"+ "<?xml version=\"1.0\" encoding=\"US-ASCII\"?>"+
"<?OFX OFXHEADER=\"100\" VERSION=\"211\" SECURITY=\"NONE\" OLDFILEUID=\"NONE\" NEWFILEUID=\"NONE\" test=\"\"?>"+ "<?OFX OFXHEADER=\"200\" VERSION=\"211\" SECURITY=\"NONE\" OLDFILEUID=\"NONE\" NEWFILEUID=\"NONE\" test=\"\"?>"+
"<OFX>"+ "<OFX>"+
"</OFX>")) "</OFX>"))
assert.EqualError(t, err, errs.ErrInvalidOFXFile.Message) assert.EqualError(t, err, errs.ErrInvalidOFXFile.Message)
} }
func TestCreateNewOFXFileReader_OFX2WithSGML(t *testing.T) {
context := core.NewNullContext()
reader, err := createNewOFXFileReader(context, []byte(
"<?xml version=\"1.0\" encoding=\"US-ASCII\"?>\n"+
"<?OFX OFXHEADER=\"200\" VERSION=\"211\" SECURITY=\"NONE\" OLDFILEUID=\"NONE\" NEWFILEUID=\"NONE\"?>\n"+
"<BANKMSGSRSV1>\n"+
"<STMTTRNRS>\n"+
"<STMTRS>\n"+
"<CURDEF>CNY\n"+
"<BANKACCTFROM>\n"+
"<ACCTID>123\n"+
"</BANKACCTFROM>\n"+
"<BANKTRANLIST>\n"+
"<STMTTRN>\n"+
"<TRNTYPE>DEP\n"+
"<DTPOSTED>20240901012345.000[+8:CST]\n"+
"<TRNAMT>123.45\n"+
"</STMTTRN>\n"+
"</BANKTRANLIST>\n"+
"</STMTRS>\n"+
"</STMTTRNRS>\n"+
"</BANKMSGSRSV1>\n"+
"</OFX>"))
assert.Nil(t, err)
_, err = reader.read(context)
assert.EqualError(t, err, errs.ErrInvalidOFXFile.Message)
}
func TestCreateNewOFXFileReader_OFX2WithoutAnyHeader(t *testing.T) { func TestCreateNewOFXFileReader_OFX2WithoutAnyHeader(t *testing.T) {
context := core.NewNullContext() context := core.NewNullContext()
reader, err := createNewOFXFileReader(context, []byte( reader, err := createNewOFXFileReader(context, []byte(