Skip to content

Commit

Permalink
import transactions from ofx 2.x file
Browse files Browse the repository at this point in the history
  • Loading branch information
mayswind committed Oct 27, 2024
1 parent 22d653c commit c372272
Show file tree
Hide file tree
Showing 11 changed files with 645 additions and 0 deletions.
183 changes: 183 additions & 0 deletions pkg/converters/ofx/ofx_data.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package ofx

import (
"encoding/xml"

"github.com/mayswind/ezbookkeeping/pkg/models"
)

const ofxVersion1 = "100"
const ofxVersion2 = "200"

const ofxDefaultTimezoneOffset = "+00:00"

// ofxAccountType represents account type in open financial exchange (ofx) file
type ofxAccountType string

// OFX account types
const (
ofxCheckingAccount ofxAccountType = "CHECKING"
ofxSavingsAccount ofxAccountType = "SAVINGS"
ofxMoneyMarketAccount ofxAccountType = "MONEYMRKT"
ofxLineOfCreditAccount ofxAccountType = "CREDITLINE"
ofxCertificateOfDepositAccount ofxAccountType = "CD"
)

// ofxTransactionType represents transaction type in open financial exchange (ofx) file
type ofxTransactionType string

// OFX transaction types
const (
ofxGenericCreditTransaction ofxTransactionType = "CREDIT"
ofxGenericDebitTransaction ofxTransactionType = "DEBIT"
ofxInterestTransaction ofxTransactionType = "INT"
ofxDividendTransaction ofxTransactionType = "DIV"
ofxFIFeeTransaction ofxTransactionType = "FEE"
ofxServiceChargeTransaction ofxTransactionType = "SRVCHG"
ofxDepositTransaction ofxTransactionType = "DEP"
ofxATMTransaction ofxTransactionType = "ATM"
ofxPOSTransaction ofxTransactionType = "POS"
ofxTransferTransaction ofxTransactionType = "XFER"
ofxCheckTransaction ofxTransactionType = "CHECK"
ofxElectronicPaymentTransaction ofxTransactionType = "PAYMENT"
ofxCashWithdrawalTransaction ofxTransactionType = "CASH"
ofxDirectDepositTransaction ofxTransactionType = "DIRECTDEP"
ofxMerchantInitiatedDebitTransaction ofxTransactionType = "DIRECTDEBIT"
ofxRepeatingPaymentTransaction ofxTransactionType = "REPEATPMT"
ofxHoldTransaction ofxTransactionType = "HOLD"
ofxOtherTransaction ofxTransactionType = "OTHER"
)

var ofxTransactionTypeMapping = map[ofxTransactionType]models.TransactionType{
ofxGenericCreditTransaction: models.TRANSACTION_TYPE_EXPENSE,
ofxGenericDebitTransaction: models.TRANSACTION_TYPE_EXPENSE,
ofxDividendTransaction: models.TRANSACTION_TYPE_INCOME,
ofxFIFeeTransaction: models.TRANSACTION_TYPE_EXPENSE,
ofxServiceChargeTransaction: models.TRANSACTION_TYPE_EXPENSE,
ofxDepositTransaction: models.TRANSACTION_TYPE_INCOME,
ofxTransferTransaction: models.TRANSACTION_TYPE_TRANSFER,
ofxCheckTransaction: models.TRANSACTION_TYPE_EXPENSE,
ofxElectronicPaymentTransaction: models.TRANSACTION_TYPE_EXPENSE,
ofxCashWithdrawalTransaction: models.TRANSACTION_TYPE_EXPENSE,
ofxDirectDepositTransaction: models.TRANSACTION_TYPE_INCOME,
ofxMerchantInitiatedDebitTransaction: models.TRANSACTION_TYPE_EXPENSE,
ofxRepeatingPaymentTransaction: models.TRANSACTION_TYPE_EXPENSE,
}

// ofxFile represents the struct of open financial exchange (ofx) file
type ofxFile struct {
XMLName xml.Name `xml:"OFX"`
FileHeader *ofxFileHeader
BankMessageResponseV1 *ofxBankMessageResponseV1 `xml:"BANKMSGSRSV1"`
CreditCardMessageResponseV1 *ofxCreditCardMessageResponseV1 `xml:"CREDITCARDMSGSRSV1"`
}

// ofxFileHeader represents the struct of open financial exchange (ofx) file header
type ofxFileHeader struct {
OFXVersion string
OFXDataVersion string
Security string
OldFileUid string
NewFileUid string
}

// ofxBankMessageResponseV1 represents the struct of open financial exchange (ofx) bank message response v1
type ofxBankMessageResponseV1 struct {
StatementTransactionResponse *ofxBankStatementTransactionResponse `xml:"STMTTRNRS"`
}

// ofxCreditCardMessageResponseV1 represents the struct of open financial exchange (ofx) credit card message response v1
type ofxCreditCardMessageResponseV1 struct {
StatementTransactionResponse *ofxCreditCardStatementTransactionResponse `xml:"CCSTMTTRNRS"`
}

// ofxBankStatementTransactionResponse represents the struct of open financial exchange (ofx) bank statement transaction response
type ofxBankStatementTransactionResponse struct {
StatementResponse *ofxBankStatementResponse `xml:"STMTRS"`
}

// ofxCreditCardStatementTransactionResponse represents the struct of open financial exchange (ofx) credit card statement transaction response
type ofxCreditCardStatementTransactionResponse struct {
StatementResponse *ofxCreditCardStatementResponse `xml:"CCSTMTRS"`
}

// ofxBankStatementResponse represents the struct of open financial exchange (ofx) bank statement response
type ofxBankStatementResponse struct {
DefaultCurrency string `xml:"CURDEF"`
AccountFrom *ofxBankAccount `xml:"BANKACCTFROM"`
TransactionList *ofxBankTransactionList `xml:"BANKTRANLIST"`
}

// ofxCreditCardStatementResponse represents the struct of open financial exchange (ofx) credit card statement response
type ofxCreditCardStatementResponse struct {
DefaultCurrency string `xml:"CURDEF"`
AccountFrom *ofxCreditCardAccount `xml:"CCACCTFROM"`
TransactionList *ofxCreditCardTransactionList `xml:"BANKTRANLIST"`
}

// ofxBankAccount represents the struct of open financial exchange (ofx) bank account
type ofxBankAccount struct {
BankId string `xml:"BANKID"`
BranchId string `xml:"BRANCHID"`
AccountId string `xml:"ACCTID"`
AccountType ofxAccountType `xml:"ACCTTYPE"`
AccountKey string `xml:"ACCTKEY"`
}

// ofxCreditCardAccount represents the struct of open financial exchange (ofx) credit card account
type ofxCreditCardAccount struct {
AccountId string `xml:"ACCTID"`
AccountKey string `xml:"ACCTKEY"`
}

// ofxBankTransactionList represents the struct of open financial exchange (ofx) bank transaction list
type ofxBankTransactionList struct {
StartDate string `xml:"DTSTART"`
EndDate string `xml:"DTEND"`
StatementTransactions []*ofxBankStatementTransaction `xml:"STMTTRN"`
}

// ofxCreditCardTransactionList represents the struct of open financial exchange (ofx) credit card transaction list
type ofxCreditCardTransactionList struct {
StartDate string `xml:"DTSTART"`
EndDate string `xml:"DTEND"`
StatementTransactions []*ofxCreditCardStatementTransaction `xml:"STMTTRN"`
}

// ofxBasicStatementTransaction represents the struct of open financial exchange (ofx) basic statement transaction
type ofxBasicStatementTransaction struct {
TransactionId string `xml:"FITID"`
TransactionType ofxTransactionType `xml:"TRNTYPE"`
PostedDate string `xml:"DTPOSTED"`
Amount string `xml:"TRNAMT"`
Name string `xml:"NAME"`
Payee *ofxPayee `xml:"PAYEE"`
Memo string `xml:"MEMO"`
Currency string `xml:"CURRENCY"`
OriginalCurrency string `xml:"ORIGCURRENCY"`
}

// ofxBankStatementTransaction represents the struct of open financial exchange (ofx) bank statement transaction
type ofxBankStatementTransaction struct {
ofxBasicStatementTransaction
AccountTo *ofxCreditCardAccount `xml:"BANKACCTTO"`
}

// ofxCreditCardStatementTransaction represents the struct of open financial exchange (ofx) credit card statement transaction
type ofxCreditCardStatementTransaction struct {
ofxBasicStatementTransaction
AccountTo *ofxCreditCardAccount `xml:"CCACCTTO"`
}

// ofxPayee represents the struct of open financial exchange (ofx) payee info
type ofxPayee struct {
Name string `xml:"NAME"`
Address1 string `xml:"ADDR1"`
Address2 string `xml:"ADDR2"`
Address3 string `xml:"ADDR3"`
City string `xml:"CITY"`
State string `xml:"STATE"`
PostalCode string `xml:"POSTALCODE"`
Country string `xml:"COUNTRY"`
Phone string `xml:"PHONE"`
}
50 changes: 50 additions & 0 deletions pkg/converters/ofx/ofx_data_reader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package ofx

import (
"bytes"
"encoding/xml"

"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)

// ofxFileReader defines the structure of open financial exchange (ofx) file reader
type ofxFileReader struct {
xmlDecoder *xml.Decoder
}

// read returns the imported open financial exchange (ofx) file
func (r *ofxFileReader) read(ctx core.Context) (*ofxFile, error) {
file := &ofxFile{}

err := r.xmlDecoder.Decode(&file)

if err != nil {
return nil, err
}

return file, nil
}

func createNewOFXFileReader(data []byte) (*ofxFileReader, error) {
if len(data) > 5 && data[0] == 0x3C && data[1] == 0x3F && data[2] == 0x78 && data[3] == 0x6D && data[4] == 0x6C { // ofx 2.x starts with <?xml
xmlDecoder := xml.NewDecoder(bytes.NewReader(data))
xmlDecoder.CharsetReader = utils.IdentReader

return &ofxFileReader{
xmlDecoder: xmlDecoder,
}, nil
} else if len(data) > 13 && string(data[0:13]) == "OFXHEADER:100" { // ofx 1.x starts with OFXHEADER:100

} else if len(data) > 5 && string(data[0:5]) == "<OFX>" { // no ofx header
xmlDecoder := xml.NewDecoder(bytes.NewReader(data))
xmlDecoder.CharsetReader = utils.IdentReader

return &ofxFileReader{
xmlDecoder: xmlDecoder,
}, nil
}

return nil, errs.ErrInvalidOFXFile
}
48 changes: 48 additions & 0 deletions pkg/converters/ofx/ofx_transaction_data_file_importer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package ofx

import (
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)

var ofxTransactionTypeNameMapping = map[models.TransactionType]string{
models.TRANSACTION_TYPE_INCOME: utils.IntToString(int(models.TRANSACTION_TYPE_INCOME)),
models.TRANSACTION_TYPE_EXPENSE: utils.IntToString(int(models.TRANSACTION_TYPE_EXPENSE)),
models.TRANSACTION_TYPE_TRANSFER: utils.IntToString(int(models.TRANSACTION_TYPE_TRANSFER)),
}

// ofxTransactionDataImporter defines the structure of open financial exchange (ofx) file importer for transaction data
type ofxTransactionDataImporter struct {
}

// Initialize a open financial exchange (ofx) transaction data importer singleton instance
var (
OFXTransactionDataImporter = &ofxTransactionDataImporter{}
)

// ParseImportedData returns the imported data by parsing the open financial exchange (ofx) file transaction data
func (c *ofxTransactionDataImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, accountMap map[string]*models.Account, expenseCategoryMap map[string]*models.TransactionCategory, incomeCategoryMap map[string]*models.TransactionCategory, transferCategoryMap map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) {
ofxDataReader, err := createNewOFXFileReader(data)

if err != nil {
return nil, nil, nil, nil, nil, nil, err
}

ofxFile, err := ofxDataReader.read(ctx)

if err != nil {
return nil, nil, nil, nil, nil, nil, err
}

transactionDataTable, err := createNewOFXTransactionDataTable(ofxFile)

if err != nil {
return nil, nil, nil, nil, nil, nil, err
}

dataTableImporter := datatable.CreateNewSimpleImporter(ofxTransactionTypeNameMapping)

return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
}
Loading

0 comments on commit c372272

Please sign in to comment.