248 lines
9.2 KiB
Go
248 lines
9.2 KiB
Go
package qif
|
|
|
|
import (
|
|
"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"
|
|
)
|
|
|
|
const qifOpeningBalancePayeeText = "Opening Balance"
|
|
|
|
var qifTransactionSupportedColumns = map[datatable.TransactionDataTableColumn]bool{
|
|
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME: true,
|
|
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: true,
|
|
datatable.TRANSACTION_DATA_TABLE_CATEGORY: true,
|
|
datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY: true,
|
|
datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME: true,
|
|
datatable.TRANSACTION_DATA_TABLE_AMOUNT: true,
|
|
datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME: true,
|
|
datatable.TRANSACTION_DATA_TABLE_DESCRIPTION: true,
|
|
datatable.TRANSACTION_DATA_TABLE_PAYEE: true,
|
|
}
|
|
|
|
// qifDateFormatType represents the quicken interchange format (qif) date format type
|
|
type qifDateFormatType byte
|
|
|
|
const (
|
|
qifYearMonthDayDateFormat qifDateFormatType = 0
|
|
qifMonthDayYearDateFormat qifDateFormatType = 1
|
|
qifDayMonthYearDateFormat qifDateFormatType = 2
|
|
)
|
|
|
|
// qifTransactionDataTable defines the structure of quicken interchange format (qif) transaction data table
|
|
type qifTransactionDataTable struct {
|
|
dateFormatType qifDateFormatType
|
|
allData []*qifTransactionData
|
|
}
|
|
|
|
// qifTransactionDataRow defines the structure of quicken interchange format (qif) transaction data row
|
|
type qifTransactionDataRow struct {
|
|
dataTable *qifTransactionDataTable
|
|
data *qifTransactionData
|
|
finalItems map[datatable.TransactionDataTableColumn]string
|
|
}
|
|
|
|
// qifTransactionDataRowIterator defines the structure of quicken interchange format (qif) transaction data row iterator
|
|
type qifTransactionDataRowIterator struct {
|
|
dataTable *qifTransactionDataTable
|
|
currentIndex int
|
|
}
|
|
|
|
// HasColumn returns whether the transaction data table has specified column
|
|
func (t *qifTransactionDataTable) HasColumn(column datatable.TransactionDataTableColumn) bool {
|
|
_, exists := qifTransactionSupportedColumns[column]
|
|
return exists
|
|
}
|
|
|
|
// TransactionRowCount returns the total count of transaction data row
|
|
func (t *qifTransactionDataTable) TransactionRowCount() int {
|
|
return len(t.allData)
|
|
}
|
|
|
|
// TransactionRowIterator returns the iterator of transaction data row
|
|
func (t *qifTransactionDataTable) TransactionRowIterator() datatable.TransactionDataRowIterator {
|
|
return &qifTransactionDataRowIterator{
|
|
dataTable: t,
|
|
currentIndex: -1,
|
|
}
|
|
}
|
|
|
|
// IsValid returns whether this row is valid data for importing
|
|
func (r *qifTransactionDataRow) IsValid() bool {
|
|
return true
|
|
}
|
|
|
|
// GetData returns the data in the specified column type
|
|
func (r *qifTransactionDataRow) GetData(column datatable.TransactionDataTableColumn) string {
|
|
_, exists := qifTransactionSupportedColumns[column]
|
|
|
|
if exists {
|
|
return r.finalItems[column]
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// HasNext returns whether the iterator does not reach the end
|
|
func (t *qifTransactionDataRowIterator) HasNext() bool {
|
|
return t.currentIndex+1 < len(t.dataTable.allData)
|
|
}
|
|
|
|
// Next returns the next transaction data row
|
|
func (t *qifTransactionDataRowIterator) Next(ctx core.Context, user *models.User) (daraRow datatable.TransactionDataRow, err error) {
|
|
if t.currentIndex+1 >= len(t.dataTable.allData) {
|
|
return nil, nil
|
|
}
|
|
|
|
t.currentIndex++
|
|
|
|
data := t.dataTable.allData[t.currentIndex]
|
|
rowItems, err := t.parseTransaction(ctx, user, data)
|
|
|
|
if err != nil {
|
|
log.Errorf(ctx, "[qif_transaction_data_table.Next] cannot parsing transaction in row#%d, because %s", t.currentIndex, err.Error())
|
|
return nil, err
|
|
}
|
|
|
|
return &qifTransactionDataRow{
|
|
dataTable: t.dataTable,
|
|
data: data,
|
|
finalItems: rowItems,
|
|
}, nil
|
|
}
|
|
|
|
func (t *qifTransactionDataRowIterator) parseTransaction(ctx core.Context, user *models.User, qifTransaction *qifTransactionData) (map[datatable.TransactionDataTableColumn]string, error) {
|
|
data := make(map[datatable.TransactionDataTableColumn]string, len(qifTransactionSupportedColumns))
|
|
|
|
if qifTransaction.Date == "" {
|
|
return nil, errs.ErrMissingTransactionTime
|
|
}
|
|
|
|
transactionTime, err := t.parseTransactionTime(ctx, qifTransaction.Date)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] = transactionTime
|
|
|
|
if qifTransaction.Amount == "" {
|
|
return nil, errs.ErrAmountInvalid
|
|
}
|
|
|
|
amount, err := utils.ParseAmount(strings.ReplaceAll(qifTransaction.Amount, ",", "")) // trim thousands separator
|
|
|
|
if err != nil {
|
|
return nil, errs.ErrAmountInvalid
|
|
}
|
|
|
|
if qifTransaction.Account != nil {
|
|
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = qifTransaction.Account.Name
|
|
} else {
|
|
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = ""
|
|
}
|
|
|
|
if len(qifTransaction.Category) > 0 && qifTransaction.Category[0] == '[' && qifTransaction.Category[len(qifTransaction.Category)-1] == ']' {
|
|
if qifTransaction.Payee == qifOpeningBalancePayeeText { // balance modification
|
|
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = qifTransactionTypeNameMapping[models.TRANSACTION_TYPE_MODIFY_BALANCE]
|
|
data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(amount)
|
|
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = qifTransaction.Category[1 : len(qifTransaction.Category)-1]
|
|
} else { // transfer
|
|
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = qifTransactionTypeNameMapping[models.TRANSACTION_TYPE_TRANSFER]
|
|
|
|
if amount >= 0 { // transfer from [account name]
|
|
data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(amount)
|
|
data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME]
|
|
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = qifTransaction.Category[1 : len(qifTransaction.Category)-1]
|
|
} else { // transfer to [account name]
|
|
data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(-amount)
|
|
data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = qifTransaction.Category[1 : len(qifTransaction.Category)-1]
|
|
}
|
|
}
|
|
} else { // income/expense
|
|
if amount >= 0 {
|
|
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = qifTransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME]
|
|
data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(amount)
|
|
} else {
|
|
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = qifTransactionTypeNameMapping[models.TRANSACTION_TYPE_EXPENSE]
|
|
data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(-amount)
|
|
}
|
|
|
|
if strings.Index(qifTransaction.Category, ":") > 0 { // category:subcategory
|
|
categories := strings.Split(qifTransaction.Category, ":")
|
|
data[datatable.TRANSACTION_DATA_TABLE_CATEGORY] = categories[0]
|
|
data[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = categories[len(categories)-1]
|
|
} else {
|
|
data[datatable.TRANSACTION_DATA_TABLE_CATEGORY] = ""
|
|
data[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = qifTransaction.Category
|
|
}
|
|
}
|
|
|
|
if qifTransaction.Memo != "" {
|
|
data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = qifTransaction.Memo
|
|
}
|
|
|
|
if qifTransaction.Payee != "" && qifTransaction.Payee != qifOpeningBalancePayeeText {
|
|
data[datatable.TRANSACTION_DATA_TABLE_PAYEE] = qifTransaction.Payee
|
|
}
|
|
|
|
return data, nil
|
|
}
|
|
|
|
func (t *qifTransactionDataRowIterator) parseTransactionTime(ctx core.Context, date string) (string, error) {
|
|
var year, month, day string
|
|
|
|
if (t.dataTable.dateFormatType == qifYearMonthDayDateFormat && utils.IsValidYearMonthDayLongOrShortDateFormat(date)) ||
|
|
(t.dataTable.dateFormatType == qifMonthDayYearDateFormat && utils.IsValidMonthDayYearLongOrShortDateFormat(date)) ||
|
|
(t.dataTable.dateFormatType == qifDayMonthYearDateFormat && utils.IsValidDayMonthYearLongOrShortDateFormat(date)) {
|
|
date = strings.ReplaceAll(date, ".", "-")
|
|
date = strings.ReplaceAll(date, "/", "-")
|
|
date = strings.ReplaceAll(date, "'", "-")
|
|
items := strings.Split(date, "-")
|
|
|
|
if t.dataTable.dateFormatType == qifYearMonthDayDateFormat {
|
|
year = items[0]
|
|
month = items[1]
|
|
day = items[2]
|
|
} else if t.dataTable.dateFormatType == qifMonthDayYearDateFormat {
|
|
month = items[0]
|
|
day = items[1]
|
|
year = items[2]
|
|
} else if t.dataTable.dateFormatType == qifDayMonthYearDateFormat {
|
|
day = items[0]
|
|
month = items[1]
|
|
year = items[2]
|
|
}
|
|
}
|
|
|
|
if year == "" || month == "" || day == "" {
|
|
log.Errorf(ctx, "[qif_transaction_data_table.parseTransactionTime] cannot parse date \"%s\"", date)
|
|
return "", errs.ErrTransactionTimeInvalid
|
|
}
|
|
|
|
return utils.FormatYearMonthDayToLongDateTime(year, month, day)
|
|
}
|
|
|
|
func createNewQifTransactionDataTable(dateFormatType qifDateFormatType, qifData *qifData) (*qifTransactionDataTable, error) {
|
|
if qifData == nil {
|
|
return nil, errs.ErrNotFoundTransactionDataInFile
|
|
}
|
|
|
|
allData := make([]*qifTransactionData, 0)
|
|
allData = append(allData, qifData.BankAccountTransactions...)
|
|
allData = append(allData, qifData.CashAccountTransactions...)
|
|
allData = append(allData, qifData.CreditCardAccountTransactions...)
|
|
allData = append(allData, qifData.AssetAccountTransactions...)
|
|
allData = append(allData, qifData.LiabilityAccountTransactions...)
|
|
|
|
return &qifTransactionDataTable{
|
|
dateFormatType: dateFormatType,
|
|
allData: allData,
|
|
}, nil
|
|
}
|