Skip to content

Commit 0020f4e

Browse files
committed
support importing camt.052 bank statement file
1 parent b470cb6 commit 0020f4e

26 files changed

Lines changed: 214 additions & 5 deletions

pkg/converters/camt/camt_data.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,20 @@ const (
99
CAMT_INDICATOR_DEBIT camtCreditDebitIndicator = "DBIT"
1010
)
1111

12+
type camt052File struct {
13+
XMLName xml.Name `xml:"Document"`
14+
BankToCustomerAccountReport *camtBankToCustomerAccountReport `xml:"BkToCstmrAcctRpt"`
15+
}
16+
1217
type camt053File struct {
1318
XMLName xml.Name `xml:"Document"`
1419
BankToCustomerStatement *camtBankToCustomerStatement `xml:"BkToCstmrStmt"`
1520
}
1621

22+
type camtBankToCustomerAccountReport struct {
23+
Statements []*camtStatement `xml:"Rpt"`
24+
}
25+
1726
type camtBankToCustomerStatement struct {
1827
Statements []*camtStatement `xml:"Stmt"`
1928
}

pkg/converters/camt/camt_data_reader.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,30 @@ import (
1010
"github.com/mayswind/ezbookkeeping/pkg/errs"
1111
)
1212

13+
// camt052FileReader defines the structure of camt.052 file reader
14+
type camt052FileReader struct {
15+
xmlDecoder *xml.Decoder
16+
}
17+
1318
// camt053FileReader defines the structure of camt.053 file reader
1419
type camt053FileReader struct {
1520
xmlDecoder *xml.Decoder
1621
}
1722

23+
// read returns the imported camt.052 data
24+
// Reference: https://www.iso20022.org/message-set/1196/download
25+
func (r *camt052FileReader) read(ctx core.Context) (*camt052File, error) {
26+
file := &camt052File{}
27+
28+
err := r.xmlDecoder.Decode(&file)
29+
30+
if err != nil {
31+
return nil, err
32+
}
33+
34+
return file, nil
35+
}
36+
1837
// read returns the imported camt.053 data
1938
// Reference: https://www.iso20022.org/message-set/1196/download
2039
func (r *camt053FileReader) read(ctx core.Context) (*camt053File, error) {
@@ -29,6 +48,19 @@ func (r *camt053FileReader) read(ctx core.Context) (*camt053File, error) {
2948
return file, nil
3049
}
3150

51+
func createNewCamt052FileReader(data []byte) (*camt052FileReader, error) {
52+
if len(data) > 5 && data[0] == 0x3C && data[1] == 0x3F && data[2] == 0x78 && data[3] == 0x6D && data[4] == 0x6C { // <?xml
53+
xmlDecoder := xml.NewDecoder(bytes.NewReader(data))
54+
xmlDecoder.CharsetReader = charset.NewReaderLabel
55+
56+
return &camt052FileReader{
57+
xmlDecoder: xmlDecoder,
58+
}, nil
59+
}
60+
61+
return nil, errs.ErrInvalidXmlFile
62+
}
63+
3264
func createNewCamt053FileReader(data []byte) (*camt053FileReader, error) {
3365
if len(data) > 5 && data[0] == 0x3C && data[1] == 0x3F && data[2] == 0x78 && data[3] == 0x6D && data[4] == 0x6C { // <?xml
3466
xmlDecoder := xml.NewDecoder(bytes.NewReader(data))

pkg/converters/camt/camt_statement_transaction_data_table.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -303,12 +303,12 @@ func (t *camtStatementTransactionDataRowIterator) parseTransaction(ctx core.Cont
303303
return data, nil
304304
}
305305

306-
func createNewCamtStatementTransactionDataTable(file *camt053File) (*camtStatementTransactionDataTable, error) {
307-
if file == nil || file.BankToCustomerStatement == nil || len(file.BankToCustomerStatement.Statements) == 0 {
306+
func createNewCamtStatementTransactionDataTable(camtStatements []*camtStatement) (*camtStatementTransactionDataTable, error) {
307+
if len(camtStatements) == 0 {
308308
return nil, errs.ErrNotFoundTransactionDataInFile
309309
}
310310

311311
return &camtStatementTransactionDataTable{
312-
allStatements: file.BankToCustomerStatement.Statements,
312+
allStatements: camtStatements,
313313
}, nil
314314
}

pkg/converters/camt/camt_transaction_data_file_importer.go

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55

66
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
77
"github.com/mayswind/ezbookkeeping/pkg/core"
8+
"github.com/mayswind/ezbookkeeping/pkg/errs"
89
"github.com/mayswind/ezbookkeeping/pkg/models"
910
"github.com/mayswind/ezbookkeeping/pkg/utils"
1011
)
@@ -15,15 +16,49 @@ var camtTransactionTypeNameMapping = map[models.TransactionType]string{
1516
models.TRANSACTION_TYPE_TRANSFER: utils.IntToString(int(models.TRANSACTION_TYPE_TRANSFER)),
1617
}
1718

19+
// camt052TransactionDataImporter defines the structure of camt.052 file importer for transaction data
20+
type camt052TransactionDataImporter struct {
21+
}
22+
1823
// camt053TransactionDataImporter defines the structure of camt.053 file importer for transaction data
1924
type camt053TransactionDataImporter struct {
2025
}
2126

22-
// Initialize a camt.053 transaction data importer singleton instance
27+
// Initialize camt.052 and camt.053 transaction data importer singleton instances
2328
var (
29+
Camt052TransactionDataImporter = &camt052TransactionDataImporter{}
2430
Camt053TransactionDataImporter = &camt053TransactionDataImporter{}
2531
)
2632

33+
// ParseImportedData returns the imported data by parsing the camt.052 file transaction data
34+
func (c *camt052TransactionDataImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezone *time.Location, additionalOptions converter.TransactionDataImporterOptions, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) {
35+
camt052DataReader, err := createNewCamt052FileReader(data)
36+
37+
if err != nil {
38+
return nil, nil, nil, nil, nil, nil, err
39+
}
40+
41+
camt052Data, err := camt052DataReader.read(ctx)
42+
43+
if err != nil {
44+
return nil, nil, nil, nil, nil, nil, err
45+
}
46+
47+
if camt052Data.BankToCustomerAccountReport == nil || camt052Data.BankToCustomerAccountReport.Statements == nil {
48+
return nil, nil, nil, nil, nil, nil, errs.ErrNotFoundTransactionDataInFile
49+
}
50+
51+
transactionDataTable, err := createNewCamtStatementTransactionDataTable(camt052Data.BankToCustomerAccountReport.Statements)
52+
53+
if err != nil {
54+
return nil, nil, nil, nil, nil, nil, err
55+
}
56+
57+
dataTableImporter := converter.CreateNewSimpleImporterWithTypeNameMapping(camtTransactionTypeNameMapping)
58+
59+
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
60+
}
61+
2762
// ParseImportedData returns the imported data by parsing the camt.053 file transaction data
2863
func (c *camt053TransactionDataImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezone *time.Location, additionalOptions converter.TransactionDataImporterOptions, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) {
2964
camt053DataReader, err := createNewCamt053FileReader(data)
@@ -38,7 +73,11 @@ func (c *camt053TransactionDataImporter) ParseImportedData(ctx core.Context, use
3873
return nil, nil, nil, nil, nil, nil, err
3974
}
4075

41-
transactionDataTable, err := createNewCamtStatementTransactionDataTable(camt053Data)
76+
if camt053Data.BankToCustomerStatement == nil || camt053Data.BankToCustomerStatement.Statements == nil {
77+
return nil, nil, nil, nil, nil, nil, errs.ErrNotFoundTransactionDataInFile
78+
}
79+
80+
transactionDataTable, err := createNewCamtStatementTransactionDataTable(camt053Data.BankToCustomerStatement.Statements)
4281

4382
if err != nil {
4483
return nil, nil, nil, nil, nil, nil, err

pkg/converters/camt/camt_transaction_data_file_importer_test.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,109 @@ import (
1313
"github.com/mayswind/ezbookkeeping/pkg/utils"
1414
)
1515

16+
func TestCamt052TransactionDataFileParseImportedData_MinimumValidData(t *testing.T) {
17+
importer := Camt052TransactionDataImporter
18+
context := core.NewNullContext()
19+
20+
user := &models.User{
21+
Uid: 1234567890,
22+
DefaultCurrency: "CNY",
23+
}
24+
25+
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := importer.ParseImportedData(context, user, []byte(
26+
`<?xml version="1.0" encoding="UTF-8"?>
27+
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.052.001.02">
28+
<BkToCstmrAcctRpt>
29+
<Rpt>
30+
<Acct>
31+
<Id>
32+
<IBAN>123</IBAN>
33+
</Id>
34+
<Ccy>CNY</Ccy>
35+
</Acct>
36+
<Ntry>
37+
<BookgDt>
38+
<DtTm>2024-09-01T01:23:45+08:00</DtTm>
39+
</BookgDt>
40+
<CdtDbtInd>CRDT</CdtDbtInd>
41+
<Amt Ccy="CNY">123.45</Amt>
42+
</Ntry>
43+
<Ntry>
44+
<BookgDt>
45+
<DtTm>2024-09-01T12:34:56+08:00</DtTm>
46+
</BookgDt>
47+
<CdtDbtInd>DBIT</CdtDbtInd>
48+
<Amt Ccy="CNY">0.12</Amt>
49+
</Ntry>
50+
</Rpt>
51+
<Rpt>
52+
<Acct>
53+
<Id>
54+
<Othr>
55+
<Id>456</Id>
56+
</Othr>
57+
</Id>
58+
<Ccy>USD</Ccy>
59+
</Acct>
60+
<Ntry>
61+
<BookgDt>
62+
<DtTm>2024-09-01T23:59:59+08:00</DtTm>
63+
</BookgDt>
64+
<CdtDbtInd>CRDT</CdtDbtInd>
65+
<Amt Ccy="USD">1.23</Amt>
66+
</Ntry>
67+
</Rpt>
68+
</BkToCstmrAcctRpt>
69+
</Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
70+
71+
assert.Nil(t, err)
72+
73+
assert.Equal(t, 3, len(allNewTransactions))
74+
assert.Equal(t, 2, len(allNewAccounts))
75+
assert.Equal(t, 1, len(allNewSubExpenseCategories))
76+
assert.Equal(t, 1, len(allNewSubIncomeCategories))
77+
assert.Equal(t, 0, len(allNewSubTransferCategories))
78+
assert.Equal(t, 0, len(allNewTags))
79+
80+
assert.Equal(t, int64(1234567890), allNewTransactions[0].Uid)
81+
assert.Equal(t, models.TRANSACTION_DB_TYPE_INCOME, allNewTransactions[0].Type)
82+
assert.Equal(t, int64(1725125025), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
83+
assert.Equal(t, int64(12345), allNewTransactions[0].Amount)
84+
assert.Equal(t, "123", allNewTransactions[0].OriginalSourceAccountName)
85+
assert.Equal(t, "CNY", allNewTransactions[0].OriginalSourceAccountCurrency)
86+
assert.Equal(t, "", allNewTransactions[0].OriginalCategoryName)
87+
88+
assert.Equal(t, int64(1234567890), allNewTransactions[1].Uid)
89+
assert.Equal(t, models.TRANSACTION_DB_TYPE_EXPENSE, allNewTransactions[1].Type)
90+
assert.Equal(t, int64(1725165296), utils.GetUnixTimeFromTransactionTime(allNewTransactions[1].TransactionTime))
91+
assert.Equal(t, int64(12), allNewTransactions[1].Amount)
92+
assert.Equal(t, "123", allNewTransactions[1].OriginalSourceAccountName)
93+
assert.Equal(t, "CNY", allNewTransactions[1].OriginalSourceAccountCurrency)
94+
assert.Equal(t, "", allNewTransactions[1].OriginalCategoryName)
95+
96+
assert.Equal(t, int64(1234567890), allNewTransactions[2].Uid)
97+
assert.Equal(t, models.TRANSACTION_DB_TYPE_INCOME, allNewTransactions[2].Type)
98+
assert.Equal(t, int64(1725206399), utils.GetUnixTimeFromTransactionTime(allNewTransactions[2].TransactionTime))
99+
assert.Equal(t, int64(123), allNewTransactions[2].Amount)
100+
assert.Equal(t, "456", allNewTransactions[2].OriginalSourceAccountName)
101+
assert.Equal(t, "USD", allNewTransactions[2].OriginalSourceAccountCurrency)
102+
assert.Equal(t, "", allNewTransactions[2].OriginalCategoryName)
103+
104+
assert.Equal(t, int64(1234567890), allNewAccounts[0].Uid)
105+
assert.Equal(t, "123", allNewAccounts[0].Name)
106+
assert.Equal(t, "CNY", allNewAccounts[0].Currency)
107+
108+
assert.Equal(t, int64(1234567890), allNewAccounts[1].Uid)
109+
assert.Equal(t, "456", allNewAccounts[1].Name)
110+
assert.Equal(t, "USD", allNewAccounts[1].Currency)
111+
112+
assert.Equal(t, int64(1234567890), allNewSubExpenseCategories[0].Uid)
113+
assert.Equal(t, "", allNewSubExpenseCategories[0].Name)
114+
115+
assert.Equal(t, int64(1234567890), allNewSubIncomeCategories[0].Uid)
116+
assert.Equal(t, "", allNewSubIncomeCategories[0].Name)
117+
}
118+
16119
func TestCamt053TransactionDataFileParseImportedData_MinimumValidData(t *testing.T) {
17120
importer := Camt053TransactionDataImporter
18121
context := core.NewNullContext()

pkg/converters/transaction_data_converters.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ func GetTransactionDataImporter(fileType string) (converter.TransactionDataImpor
5252
return qif.QifDayMonthYearTransactionDataImporter, nil
5353
} else if fileType == "iif" {
5454
return iif.IifTransactionDataFileImporter, nil
55+
} else if fileType == "camt052" {
56+
return camt.Camt052TransactionDataImporter, nil
5557
} else if fileType == "camt053" {
5658
return camt.Camt053TransactionDataImporter, nil
5759
} else if fileType == "mt940" {

src/consts/file.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,11 @@ export const SUPPORTED_IMPORT_FILE_CATEGORY_AND_TYPES: ImportFileCategoryAndType
229229
{
230230
categoryName: 'General Bank Statement Format',
231231
fileTypes: [
232+
{
233+
type: 'camt052',
234+
name: 'Camt.052 Bank to Customer Statement File',
235+
extensions: '.xml'
236+
},
232237
{
233238
type: 'camt053',
234239
name: 'Camt.053 Bank to Customer Statement File',

src/locales/de.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1981,6 +1981,7 @@
19811981
"Month-day-year format": "Monat-Tag-Jahr-Format",
19821982
"Day-month-year format": "Tag-Monat-Jahr-Format",
19831983
"Intuit Interchange Format (IIF) File": "Intuit Interchange Format (IIF)-Datei",
1984+
"Camt.052 Bank to Customer Statement File": "Camt.052 Bank to Customer Statement File",
19841985
"Camt.053 Bank to Customer Statement File": "Camt.053 Bank to Customer Statement File",
19851986
"MT940 Consumer Statement Message File": "MT940 Consumer Statement Message File",
19861987
"Delimiter-separated Values (DSV) File": "Delimiter-separated Values (DSV) File",

src/locales/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1981,6 +1981,7 @@
19811981
"Month-day-year format": "Month-day-year format",
19821982
"Day-month-year format": "Day-month-year format",
19831983
"Intuit Interchange Format (IIF) File": "Intuit Interchange Format (IIF) File",
1984+
"Camt.052 Bank to Customer Statement File": "Camt.052 Bank to Customer Statement File",
19841985
"Camt.053 Bank to Customer Statement File": "Camt.053 Bank to Customer Statement File",
19851986
"MT940 Consumer Statement Message File": "MT940 Consumer Statement Message File",
19861987
"Delimiter-separated Values (DSV) File": "Delimiter-separated Values (DSV) File",

src/locales/es.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1981,6 +1981,7 @@
19811981
"Month-day-year format": "Formato mes-día-año",
19821982
"Day-month-year format": "Formato día-mes-año",
19831983
"Intuit Interchange Format (IIF) File": "Archivo IIF (Intuit Interchange Format)",
1984+
"Camt.052 Bank to Customer Statement File": "Extracto Camt.052 (Extracto de cuenta del cliente)",
19841985
"Camt.053 Bank to Customer Statement File": "Extracto Camt.053 (Extracto de cuenta del cliente)",
19851986
"MT940 Consumer Statement Message File": "Extracto MT940 (Extracto de cuenta del cliente)",
19861987
"Delimiter-separated Values (DSV) File": "Archivo DSV (valores separados por delimirtadores)",

0 commit comments

Comments
 (0)