315 lines
11 KiB
Go
315 lines
11 KiB
Go
package camt
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
|
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
|
"github.com/mayswind/ezbookkeeping/pkg/log"
|
|
"github.com/mayswind/ezbookkeeping/pkg/models"
|
|
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
|
)
|
|
|
|
var camtTransactionSupportedColumns = map[datatable.TransactionDataTableColumn]bool{
|
|
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME: true,
|
|
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE: true,
|
|
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: true,
|
|
datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY: true,
|
|
datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME: true,
|
|
datatable.TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY: true,
|
|
datatable.TRANSACTION_DATA_TABLE_AMOUNT: true,
|
|
datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME: true,
|
|
datatable.TRANSACTION_DATA_TABLE_DESCRIPTION: true,
|
|
}
|
|
|
|
// camtStatementTransactionDataTable defines the structure of camt statement transaction data table
|
|
type camtStatementTransactionDataTable struct {
|
|
allStatements []*camtStatement
|
|
}
|
|
|
|
// camtStatementTransactionDataRow defines the structure of camt statement transaction data row
|
|
type camtStatementTransactionDataRow struct {
|
|
dataTable *camtStatementTransactionDataTable
|
|
account *camtAccount
|
|
entry *camtEntry
|
|
transactionDetails *camtTransactionDetails
|
|
finalItems map[datatable.TransactionDataTableColumn]string
|
|
}
|
|
|
|
// camtStatementTransactionDataRowIterator defines the structure of camt statement transaction data row iterator
|
|
type camtStatementTransactionDataRowIterator struct {
|
|
dataTable *camtStatementTransactionDataTable
|
|
currentStatementIndex int
|
|
currentEntryIndex int
|
|
currentTransactionDetailsIndex int
|
|
}
|
|
|
|
// HasColumn returns whether the transaction data table has specified column
|
|
func (t *camtStatementTransactionDataTable) HasColumn(column datatable.TransactionDataTableColumn) bool {
|
|
_, exists := camtTransactionSupportedColumns[column]
|
|
return exists
|
|
}
|
|
|
|
// TransactionRowCount returns the total count of transaction data row
|
|
func (t *camtStatementTransactionDataTable) TransactionRowCount() int {
|
|
totalDataRowCount := 0
|
|
|
|
for i := 0; i < len(t.allStatements); i++ {
|
|
statement := t.allStatements[i]
|
|
|
|
for j := 0; j < len(statement.Entries); j++ {
|
|
entry := statement.Entries[j]
|
|
|
|
if entry.EntryDetails != nil {
|
|
totalDataRowCount += len(entry.EntryDetails.TransactionDetails)
|
|
} else {
|
|
totalDataRowCount++
|
|
}
|
|
}
|
|
}
|
|
|
|
return totalDataRowCount
|
|
}
|
|
|
|
// TransactionRowIterator returns the iterator of transaction data row
|
|
func (t *camtStatementTransactionDataTable) TransactionRowIterator() datatable.TransactionDataRowIterator {
|
|
return &camtStatementTransactionDataRowIterator{
|
|
dataTable: t,
|
|
currentStatementIndex: 0,
|
|
currentEntryIndex: 0,
|
|
currentTransactionDetailsIndex: -1,
|
|
}
|
|
}
|
|
|
|
// IsValid returns whether this row is valid data for importing
|
|
func (r *camtStatementTransactionDataRow) IsValid() bool {
|
|
return true
|
|
}
|
|
|
|
// GetData returns the data in the specified column type
|
|
func (r *camtStatementTransactionDataRow) GetData(column datatable.TransactionDataTableColumn) string {
|
|
_, exists := camtTransactionSupportedColumns[column]
|
|
|
|
if exists {
|
|
return r.finalItems[column]
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// HasNext returns whether the iterator does not reach the end
|
|
func (t *camtStatementTransactionDataRowIterator) HasNext() bool {
|
|
allStatements := t.dataTable.allStatements
|
|
|
|
if t.currentStatementIndex >= len(allStatements) {
|
|
return false
|
|
}
|
|
|
|
currentStatement := allStatements[t.currentStatementIndex]
|
|
|
|
if t.currentEntryIndex+1 < len(currentStatement.Entries) {
|
|
return true
|
|
} else if t.currentEntryIndex < len(currentStatement.Entries) {
|
|
currencyEntry := currentStatement.Entries[t.currentEntryIndex]
|
|
|
|
if currencyEntry.EntryDetails != nil {
|
|
if t.currentTransactionDetailsIndex+1 < len(currencyEntry.EntryDetails.TransactionDetails) {
|
|
return true
|
|
}
|
|
} else {
|
|
if t.currentTransactionDetailsIndex < 0 {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
for i := t.currentStatementIndex + 1; i < len(allStatements); i++ {
|
|
statement := allStatements[i]
|
|
|
|
if len(statement.Entries) < 1 {
|
|
continue
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// Next returns the next transaction data row
|
|
func (t *camtStatementTransactionDataRowIterator) Next(ctx core.Context, user *models.User) (daraRow datatable.TransactionDataRow, err error) {
|
|
allStatements := t.dataTable.allStatements
|
|
|
|
for i := t.currentStatementIndex; i < len(allStatements); i++ {
|
|
foundNextRow := false
|
|
statement := allStatements[i]
|
|
|
|
for j := t.currentEntryIndex; j < len(statement.Entries); j++ {
|
|
if statement.Entries[j].EntryDetails != nil {
|
|
if t.currentTransactionDetailsIndex+1 < len(statement.Entries[j].EntryDetails.TransactionDetails) {
|
|
t.currentTransactionDetailsIndex++
|
|
foundNextRow = true
|
|
break
|
|
}
|
|
} else {
|
|
if t.currentTransactionDetailsIndex < 0 {
|
|
t.currentTransactionDetailsIndex++
|
|
foundNextRow = true
|
|
break
|
|
}
|
|
}
|
|
|
|
t.currentEntryIndex++
|
|
t.currentTransactionDetailsIndex = -1
|
|
}
|
|
|
|
if foundNextRow {
|
|
break
|
|
}
|
|
|
|
t.currentStatementIndex++
|
|
t.currentEntryIndex = 0
|
|
t.currentTransactionDetailsIndex = -1
|
|
}
|
|
|
|
if t.currentStatementIndex >= len(allStatements) {
|
|
return nil, nil
|
|
}
|
|
|
|
currentStatement := allStatements[t.currentStatementIndex]
|
|
|
|
if t.currentEntryIndex >= len(currentStatement.Entries) {
|
|
return nil, nil
|
|
}
|
|
|
|
account := currentStatement.Account
|
|
entry := currentStatement.Entries[t.currentEntryIndex]
|
|
var transactionDetails *camtTransactionDetails
|
|
|
|
if entry.EntryDetails != nil {
|
|
if t.currentTransactionDetailsIndex >= len(entry.EntryDetails.TransactionDetails) {
|
|
return nil, nil
|
|
} else {
|
|
transactionDetails = entry.EntryDetails.TransactionDetails[t.currentTransactionDetailsIndex]
|
|
}
|
|
} else {
|
|
if t.currentTransactionDetailsIndex >= 1 {
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
rowItems, err := t.parseTransaction(ctx, user, account, entry, transactionDetails)
|
|
|
|
if err != nil {
|
|
log.Errorf(ctx, "[camt_statement_transaction_data_table.Next] cannot parsing transaction in entry#%d-transaction_detail#%d (statement#%d), because %s", t.currentEntryIndex, t.currentTransactionDetailsIndex, t.currentStatementIndex, err.Error())
|
|
return nil, err
|
|
}
|
|
|
|
return &camtStatementTransactionDataRow{
|
|
dataTable: t.dataTable,
|
|
account: account,
|
|
entry: entry,
|
|
transactionDetails: transactionDetails,
|
|
finalItems: rowItems,
|
|
}, nil
|
|
}
|
|
|
|
func (t *camtStatementTransactionDataRowIterator) parseTransaction(ctx core.Context, user *models.User, account *camtAccount, entry *camtEntry, transactionDetails *camtTransactionDetails) (map[datatable.TransactionDataTableColumn]string, error) {
|
|
data := make(map[datatable.TransactionDataTableColumn]string, len(camtTransactionSupportedColumns))
|
|
|
|
if account == nil {
|
|
return nil, errs.ErrMissingAccountData
|
|
}
|
|
|
|
if entry.BookingDate != nil && entry.BookingDate.DateTime != "" {
|
|
dateTime, err := utils.ParseFromLongDateTimeWithTimezoneRFC3339Format(entry.BookingDate.DateTime)
|
|
|
|
if err != nil {
|
|
return nil, errs.ErrTransactionTimeInvalid
|
|
}
|
|
|
|
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] = utils.FormatUnixTimeToLongDateTime(dateTime.Unix(), dateTime.Location())
|
|
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = utils.FormatTimezoneOffset(dateTime.Unix(), dateTime.Location())
|
|
} else if entry.BookingDate != nil && entry.BookingDate.Date != "" {
|
|
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] = fmt.Sprintf("%s 00:00:00", entry.BookingDate.Date)
|
|
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = datatable.TRANSACTION_DATA_TABLE_TIMEZONE_NOT_AVAILABLE
|
|
} else {
|
|
return nil, errs.ErrMissingTransactionTime
|
|
}
|
|
|
|
if account.IBAN != "" {
|
|
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = account.IBAN
|
|
} else if account.OtherIdentification != "" {
|
|
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = account.OtherIdentification
|
|
}
|
|
|
|
if transactionDetails != nil && transactionDetails.AmountDetails != nil && transactionDetails.AmountDetails.TransactionAmount != nil && transactionDetails.AmountDetails.TransactionAmount.Currency != "" {
|
|
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY] = transactionDetails.AmountDetails.TransactionAmount.Currency
|
|
} else if entry.Amount != nil && entry.Amount.Currency != "" {
|
|
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY] = entry.Amount.Currency
|
|
} else if account.Currency != "" {
|
|
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY] = account.Currency
|
|
} else {
|
|
return nil, errs.ErrAccountCurrencyInvalid
|
|
}
|
|
|
|
amountValue := ""
|
|
|
|
if entry.EntryDetails != nil && len(entry.EntryDetails.TransactionDetails) > 1 && transactionDetails != nil { // when there are multiple transaction details in one entry, only use the amount in the transaction details
|
|
if transactionDetails.AmountDetails != nil && transactionDetails.AmountDetails.InstructedAmount != nil && transactionDetails.AmountDetails.InstructedAmount.Value != "" {
|
|
amountValue = transactionDetails.AmountDetails.InstructedAmount.Value
|
|
} else if transactionDetails.AmountDetails != nil && transactionDetails.AmountDetails.TransactionAmount != nil && transactionDetails.AmountDetails.TransactionAmount.Value != "" {
|
|
amountValue = transactionDetails.AmountDetails.TransactionAmount.Value
|
|
} else {
|
|
return nil, errs.ErrAmountInvalid
|
|
}
|
|
} else if entry.Amount != nil && entry.Amount.Value != "" {
|
|
amountValue = entry.Amount.Value
|
|
}
|
|
|
|
if amountValue == "" {
|
|
return nil, errs.ErrAmountInvalid
|
|
}
|
|
|
|
amount, err := utils.ParseAmount(amountValue)
|
|
|
|
if err != nil {
|
|
log.Errorf(ctx, "[camt_statement_transaction_data_table.parseTransaction] cannot parsing transaction amount \"%s\", because %s", amountValue, err.Error())
|
|
return nil, errs.ErrAmountInvalid
|
|
}
|
|
|
|
data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(amount)
|
|
|
|
if entry.CreditDebitIndicator == CAMT_INDICATOR_CREDIT {
|
|
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = utils.IntToString(int(models.TRANSACTION_TYPE_INCOME))
|
|
} else if entry.CreditDebitIndicator == CAMT_INDICATOR_DEBIT {
|
|
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = utils.IntToString(int(models.TRANSACTION_TYPE_EXPENSE))
|
|
} else {
|
|
return nil, errs.ErrTransactionTypeInvalid
|
|
}
|
|
|
|
if transactionDetails != nil && transactionDetails.AdditionalTransactionInformation != "" {
|
|
data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = transactionDetails.AdditionalTransactionInformation
|
|
} else if transactionDetails != nil && transactionDetails.RemittanceInformation != nil && len(transactionDetails.RemittanceInformation.Unstructured) > 0 {
|
|
data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = strings.Join(transactionDetails.RemittanceInformation.Unstructured, "\n")
|
|
} else if entry.AdditionalEntryInformation != "" {
|
|
data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = entry.AdditionalEntryInformation
|
|
} else {
|
|
data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = ""
|
|
}
|
|
|
|
return data, nil
|
|
}
|
|
|
|
func createNewCamtStatementTransactionDataTable(camtStatements []*camtStatement) (*camtStatementTransactionDataTable, error) {
|
|
if len(camtStatements) == 0 {
|
|
return nil, errs.ErrNotFoundTransactionDataInFile
|
|
}
|
|
|
|
return &camtStatementTransactionDataTable{
|
|
allStatements: camtStatements,
|
|
}, nil
|
|
}
|