code refactor

This commit is contained in:
MaysWind
2024-10-13 19:44:29 +08:00
parent 5ac9eb5d5c
commit d9b819d1a1
26 changed files with 1331 additions and 1226 deletions
@@ -30,7 +30,7 @@ func (c *alipayTransactionDataCsvImporter) ParseImportedData(ctx core.Context, u
enc := simplifiedchinese.GB18030
reader := transform.NewReader(bytes.NewReader(data), enc.NewDecoder())
dataTable, err := createNewAlipayTransactionPlainTextDataTable(
transactionDataTable, err := createNewAlipayTransactionDataTable(
ctx,
reader,
c.fileHeaderLine,
@@ -43,10 +43,7 @@ func (c *alipayTransactionDataCsvImporter) ParseImportedData(ctx core.Context, u
return nil, nil, nil, nil, nil, nil, err
}
dataTableImporter := datatable.CreateNewSimpleImporter(
dataTable.GetDataColumnMapping(),
alipayTransactionTypeNameMapping,
)
dataTableImporter := datatable.CreateNewSimpleImporter(alipayTransactionTypeNameMapping)
return dataTableImporter.ParseImportedData(ctx, user, dataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
}
@@ -5,7 +5,6 @@ import (
"fmt"
"io"
"strings"
"time"
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
"github.com/mayswind/ezbookkeeping/pkg/core"
@@ -29,14 +28,14 @@ const alipayTransactionDataProductNameTransferInText = "转入"
const alipayTransactionDataProductNameTransferOutText = "转出"
const alipayTransactionDataProductNameRepaymentText = "还款"
var alipayTransactionSupportedColumns = []datatable.DataTableColumn{
datatable.DATA_TABLE_TRANSACTION_TIME,
datatable.DATA_TABLE_TRANSACTION_TYPE,
datatable.DATA_TABLE_SUB_CATEGORY,
datatable.DATA_TABLE_ACCOUNT_NAME,
datatable.DATA_TABLE_AMOUNT,
datatable.DATA_TABLE_RELATED_ACCOUNT_NAME,
datatable.DATA_TABLE_DESCRIPTION,
var alipayTransactionSupportedColumns = map[datatable.TransactionDataTableColumn]any{
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME: 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_AMOUNT: true,
datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME: true,
datatable.TRANSACTION_DATA_TABLE_DESCRIPTION: true,
}
// alipayTransactionColumnNames defines the structure of alipay transaction plain text header names
@@ -52,8 +51,8 @@ type alipayTransactionColumnNames struct {
descriptionColumnName string
}
// alipayTransactionPlainTextDataTable defines the structure of alipay transaction plain text data table
type alipayTransactionPlainTextDataTable struct {
// alipayTransactionDataTable defines the structure of alipay transaction plain text data table
type alipayTransactionDataTable struct {
allOriginalLines [][]string
originalHeaderLineColumnNames []string
originalTimeColumnIndex int
@@ -67,22 +66,28 @@ type alipayTransactionPlainTextDataTable struct {
originalDescriptionColumnIndex int
}
// alipayTransactionPlainTextDataRow defines the structure of alipay transaction plain text data row
type alipayTransactionPlainTextDataRow struct {
dataTable *alipayTransactionPlainTextDataTable
// alipayTransactionDataRow defines the structure of alipay transaction plain text data row
type alipayTransactionDataRow struct {
dataTable *alipayTransactionDataTable
isValid bool
originalItems []string
finalItems map[datatable.DataTableColumn]string
finalItems map[datatable.TransactionDataTableColumn]string
}
// alipayTransactionPlainTextDataRowIterator defines the structure of alipay transaction plain text data row iterator
type alipayTransactionPlainTextDataRowIterator struct {
dataTable *alipayTransactionPlainTextDataTable
// alipayTransactionDataRowIterator defines the structure of alipay transaction plain text data row iterator
type alipayTransactionDataRowIterator struct {
dataTable *alipayTransactionDataTable
currentIndex int
}
// DataRowCount returns the total count of data row
func (t *alipayTransactionPlainTextDataTable) DataRowCount() int {
// HasColumn returns whether the transaction data table has specified column
func (t *alipayTransactionDataTable) HasColumn(column datatable.TransactionDataTableColumn) bool {
_, exists := alipayTransactionSupportedColumns[column]
return exists
}
// TransactionRowCount returns the total count of transaction data row
func (t *alipayTransactionDataTable) TransactionRowCount() int {
if len(t.allOriginalLines) < 1 {
return 0
}
@@ -90,77 +95,39 @@ func (t *alipayTransactionPlainTextDataTable) DataRowCount() int {
return len(t.allOriginalLines) - 1
}
// GetDataColumnMapping returns data column map for data importer
func (t *alipayTransactionPlainTextDataTable) GetDataColumnMapping() map[datatable.DataTableColumn]string {
dataColumnMapping := make(map[datatable.DataTableColumn]string, len(alipayTransactionSupportedColumns))
for i := 0; i < len(alipayTransactionSupportedColumns); i++ {
column := alipayTransactionSupportedColumns[i]
dataColumnMapping[column] = utils.IntToString(int(column))
}
return dataColumnMapping
}
// HeaderLineColumnNames returns the header column name list
func (t *alipayTransactionPlainTextDataTable) HeaderLineColumnNames() []string {
columnIndexes := make([]string, len(alipayTransactionSupportedColumns))
for i := 0; i < len(alipayTransactionSupportedColumns); i++ {
columnIndexes[i] = utils.IntToString(int(alipayTransactionSupportedColumns[i]))
}
return columnIndexes
}
// DataRowIterator returns the iterator of data row
func (t *alipayTransactionPlainTextDataTable) DataRowIterator() datatable.ImportedDataRowIterator {
return &alipayTransactionPlainTextDataRowIterator{
// TransactionRowIterator returns the iterator of transaction data row
func (t *alipayTransactionDataTable) TransactionRowIterator() datatable.TransactionDataRowIterator {
return &alipayTransactionDataRowIterator{
dataTable: t,
currentIndex: 0,
}
}
// IsValid returns whether this row contains valid data for importing
func (r *alipayTransactionPlainTextDataRow) IsValid() bool {
// IsValid returns whether this row is valid data for importing
func (r *alipayTransactionDataRow) IsValid() bool {
return r.isValid
}
// ColumnCount returns the total count of column in this data row
func (r *alipayTransactionPlainTextDataRow) ColumnCount() int {
return len(alipayTransactionSupportedColumns)
}
// GetData returns the data in the specified column type
func (r *alipayTransactionDataRow) GetData(column datatable.TransactionDataTableColumn) string {
_, exists := alipayTransactionSupportedColumns[column]
// GetData returns the data in the specified column index
func (r *alipayTransactionPlainTextDataRow) GetData(columnIndex int) string {
if columnIndex >= len(alipayTransactionSupportedColumns) {
if !exists {
return ""
}
dataColumn := alipayTransactionSupportedColumns[columnIndex]
return r.finalItems[dataColumn]
}
// GetTime returns the time in the specified column index
func (r *alipayTransactionPlainTextDataRow) GetTime(columnIndex int, timezoneOffset int16) (time.Time, error) {
return utils.ParseFromLongDateTime(r.GetData(columnIndex), timezoneOffset)
}
// GetTimezoneOffset returns the time zone offset in the specified column index
func (r *alipayTransactionPlainTextDataRow) GetTimezoneOffset(columnIndex int) (*time.Location, error) {
return nil, errs.ErrNotSupported
return r.finalItems[column]
}
// HasNext returns whether the iterator does not reach the end
func (t *alipayTransactionPlainTextDataRowIterator) HasNext() bool {
func (t *alipayTransactionDataRowIterator) HasNext() bool {
return t.currentIndex+1 < len(t.dataTable.allOriginalLines)
}
// Next returns the next imported data row
func (t *alipayTransactionPlainTextDataRowIterator) Next(ctx core.Context, user *models.User) datatable.ImportedDataRow {
func (t *alipayTransactionDataRowIterator) Next(ctx core.Context, user *models.User) (daraRow datatable.TransactionDataRow, err error) {
if t.currentIndex+1 >= len(t.dataTable.allOriginalLines) {
return nil
return nil, nil
}
t.currentIndex++
@@ -187,7 +154,7 @@ func (t *alipayTransactionPlainTextDataRowIterator) Next(ctx core.Context, user
isValid = false
}
var finalItems map[datatable.DataTableColumn]string
var finalItems map[datatable.TransactionDataTableColumn]string
var errMsg string
if isValid {
@@ -199,37 +166,37 @@ func (t *alipayTransactionPlainTextDataRowIterator) Next(ctx core.Context, user
}
}
return &alipayTransactionPlainTextDataRow{
return &alipayTransactionDataRow{
dataTable: t.dataTable,
isValid: isValid,
originalItems: rowItems,
finalItems: finalItems,
}
}, nil
}
func (t *alipayTransactionPlainTextDataTable) parseTransactionData(ctx core.Context, user *models.User, items []string) (map[datatable.DataTableColumn]string, string) {
data := make(map[datatable.DataTableColumn]string, 7)
func (t *alipayTransactionDataTable) parseTransactionData(ctx core.Context, user *models.User, items []string) (map[datatable.TransactionDataTableColumn]string, string) {
data := make(map[datatable.TransactionDataTableColumn]string, len(alipayTransactionSupportedColumns))
if t.originalTimeColumnIndex >= 0 && t.originalTimeColumnIndex < len(items) {
data[datatable.DATA_TABLE_TRANSACTION_TIME] = items[t.originalTimeColumnIndex]
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] = items[t.originalTimeColumnIndex]
}
if t.originalCategoryColumnIndex >= 0 && t.originalCategoryColumnIndex < len(items) {
data[datatable.DATA_TABLE_SUB_CATEGORY] = items[t.originalCategoryColumnIndex]
data[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = items[t.originalCategoryColumnIndex]
} else {
data[datatable.DATA_TABLE_SUB_CATEGORY] = ""
data[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = ""
}
if t.originalAmountColumnIndex >= 0 && t.originalAmountColumnIndex < len(items) {
data[datatable.DATA_TABLE_AMOUNT] = items[t.originalAmountColumnIndex]
data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = items[t.originalAmountColumnIndex]
}
if t.originalDescriptionColumnIndex >= 0 && t.originalDescriptionColumnIndex < len(items) && items[t.originalDescriptionColumnIndex] != "" {
data[datatable.DATA_TABLE_DESCRIPTION] = items[t.originalDescriptionColumnIndex]
data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = items[t.originalDescriptionColumnIndex]
} else if t.originalProductNameColumnIndex >= 0 && t.originalProductNameColumnIndex < len(items) && items[t.originalProductNameColumnIndex] != "" {
data[datatable.DATA_TABLE_DESCRIPTION] = items[t.originalProductNameColumnIndex]
data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = items[t.originalProductNameColumnIndex]
} else {
data[datatable.DATA_TABLE_DESCRIPTION] = ""
data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = ""
}
relatedAccountName := ""
@@ -253,7 +220,7 @@ func (t *alipayTransactionPlainTextDataTable) parseTransactionData(ctx core.Cont
localeTextItems := locales.GetLocaleTextItems(locale)
if t.originalTypeColumnIndex >= 0 && t.originalTypeColumnIndex < len(items) {
data[datatable.DATA_TABLE_TRANSACTION_TYPE] = items[t.originalTypeColumnIndex]
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = items[t.originalTypeColumnIndex]
if items[t.originalTypeColumnIndex] == alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME] {
if statusName == alipayTransactionDataStatusClosedName {
@@ -261,11 +228,11 @@ func (t *alipayTransactionPlainTextDataTable) parseTransactionData(ctx core.Cont
}
if statusName == alipayTransactionDataStatusSuccessName {
data[datatable.DATA_TABLE_ACCOUNT_NAME] = localeTextItems.DataConverterTextItems.Alipay
data[datatable.DATA_TABLE_RELATED_ACCOUNT_NAME] = ""
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = localeTextItems.DataConverterTextItems.Alipay
data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = ""
} else {
data[datatable.DATA_TABLE_ACCOUNT_NAME] = ""
data[datatable.DATA_TABLE_RELATED_ACCOUNT_NAME] = ""
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = ""
data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = ""
}
} else if items[t.originalTypeColumnIndex] == alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_TRANSFER] {
if statusName == alipayTransactionDataStatusClosedName {
@@ -284,42 +251,42 @@ func (t *alipayTransactionPlainTextDataTable) parseTransactionData(ctx core.Cont
}
if statusName == alipayTransactionDataStatusRefundSuccessName {
data[datatable.DATA_TABLE_TRANSACTION_TYPE] = alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME]
data[datatable.DATA_TABLE_ACCOUNT_NAME] = relatedAccountName
data[datatable.DATA_TABLE_RELATED_ACCOUNT_NAME] = ""
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME]
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = relatedAccountName
data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = ""
} else {
if strings.Index(productName, alipayTransactionDataProductNameRechargePrefix) == 0 { // transfer to alipay wallet
data[datatable.DATA_TABLE_ACCOUNT_NAME] = ""
data[datatable.DATA_TABLE_RELATED_ACCOUNT_NAME] = localeTextItems.DataConverterTextItems.Alipay
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = ""
data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = localeTextItems.DataConverterTextItems.Alipay
} else if strings.Index(productName, alipayTransactionDataProductNameCashWithdrawalPrefix) == 0 { // transfer from alipay wallet
data[datatable.DATA_TABLE_ACCOUNT_NAME] = localeTextItems.DataConverterTextItems.Alipay
data[datatable.DATA_TABLE_RELATED_ACCOUNT_NAME] = targetName
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = localeTextItems.DataConverterTextItems.Alipay
data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = targetName
} else if strings.Index(productName, alipayTransactionDataProductNameTransferInText) >= 0 { // transfer in
data[datatable.DATA_TABLE_ACCOUNT_NAME] = relatedAccountName
data[datatable.DATA_TABLE_RELATED_ACCOUNT_NAME] = targetName
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = relatedAccountName
data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = targetName
} else if strings.Index(productName, alipayTransactionDataProductNameTransferOutText) >= 0 { // transfer out
data[datatable.DATA_TABLE_ACCOUNT_NAME] = relatedAccountName
data[datatable.DATA_TABLE_RELATED_ACCOUNT_NAME] = targetName
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = relatedAccountName
data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = targetName
} else if strings.Index(productName, alipayTransactionDataProductNameRepaymentText) >= 0 { // repayment
data[datatable.DATA_TABLE_ACCOUNT_NAME] = relatedAccountName
data[datatable.DATA_TABLE_RELATED_ACCOUNT_NAME] = targetName
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = relatedAccountName
data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = targetName
} else {
return nil, fmt.Sprintf("product name (\"%s\") is unknown", productName)
}
}
} else {
data[datatable.DATA_TABLE_ACCOUNT_NAME] = relatedAccountName
data[datatable.DATA_TABLE_RELATED_ACCOUNT_NAME] = ""
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = relatedAccountName
data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = ""
}
}
if data[datatable.DATA_TABLE_TRANSACTION_TYPE] == alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME] && statusName != "" {
if data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] == alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME] && statusName != "" {
if statusName == alipayTransactionDataStatusRefundSuccessName || statusName == alipayTransactionDataStatusTaxRefundSuccessName {
amount, err := utils.ParseAmount(data[datatable.DATA_TABLE_AMOUNT])
amount, err := utils.ParseAmount(data[datatable.TRANSACTION_DATA_TABLE_AMOUNT])
if err == nil {
data[datatable.DATA_TABLE_TRANSACTION_TYPE] = alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_EXPENSE]
data[datatable.DATA_TABLE_AMOUNT] = utils.FormatAmount(-amount)
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_EXPENSE]
data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(-amount)
}
}
}
@@ -327,7 +294,7 @@ func (t *alipayTransactionPlainTextDataTable) parseTransactionData(ctx core.Cont
return data, ""
}
func createNewAlipayTransactionPlainTextDataTable(ctx core.Context, reader io.Reader, fileHeaderLine string, dataHeaderStartContent string, dataBottomEndLineRune rune, originalColumnNames alipayTransactionColumnNames) (*alipayTransactionPlainTextDataTable, error) {
func createNewAlipayTransactionDataTable(ctx core.Context, reader io.Reader, fileHeaderLine string, dataHeaderStartContent string, dataBottomEndLineRune rune, originalColumnNames alipayTransactionColumnNames) (*alipayTransactionDataTable, error) {
allOriginalLines, err := parseAllLinesFromAlipayTransactionPlainText(ctx, reader, fileHeaderLine, dataHeaderStartContent, dataBottomEndLineRune)
if err != nil {
@@ -381,7 +348,7 @@ func createNewAlipayTransactionPlainTextDataTable(ctx core.Context, reader io.Re
descriptionColumnIdx = -1
}
return &alipayTransactionPlainTextDataTable{
return &alipayTransactionDataTable{
allOriginalLines: allOriginalLines,
originalHeaderLineColumnNames: originalHeaderItems,
originalTimeColumnIndex: timeColumnIdx,
-56
View File
@@ -1,56 +0,0 @@
package datatable
import (
"time"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/models"
)
// ImportedDataTable defines the structure of imported data table
type ImportedDataTable interface {
// DataRowCount returns the total count of data row
DataRowCount() int
// HeaderLineColumnNames returns the header column name list
HeaderLineColumnNames() []string
// DataRowIterator returns the iterator of data row
DataRowIterator() ImportedDataRowIterator
}
// ImportedDataRow defines the structure of imported data row
type ImportedDataRow interface {
// IsValid returns whether this row contains valid data for importing
IsValid() bool
// ColumnCount returns the total count of column in this data row
ColumnCount() int
// GetData returns the data in the specified column index
GetData(columnIndex int) string
// GetTime returns the time in the specified column index
GetTime(columnIndex int, timezoneOffset int16) (time.Time, error)
// GetTimezoneOffset returns the time zone offset in the specified column index
GetTimezoneOffset(columnIndex int) (*time.Location, error)
}
// ImportedDataRowIterator defines the structure of imported data row iterator
type ImportedDataRowIterator interface {
// HasNext returns whether the iterator does not reach the end
HasNext() bool
// Next returns the next imported data row
Next(ctx core.Context, user *models.User) ImportedDataRow
}
// DataTableBuilder defines the structure of data table builder
type DataTableBuilder interface {
// AppendTransaction appends the specified transaction to data builder
AppendTransaction(data map[DataTableColumn]string)
// ReplaceDelimiters returns the text after removing the delimiters
ReplaceDelimiters(text string) string
}
@@ -14,30 +14,8 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/validators"
)
// DataTableColumn represents the data column type of data table
type DataTableColumn byte
// Data table columns
const (
DATA_TABLE_TRANSACTION_TIME DataTableColumn = 1
DATA_TABLE_TRANSACTION_TIMEZONE DataTableColumn = 2
DATA_TABLE_TRANSACTION_TYPE DataTableColumn = 3
DATA_TABLE_CATEGORY DataTableColumn = 4
DATA_TABLE_SUB_CATEGORY DataTableColumn = 5
DATA_TABLE_ACCOUNT_NAME DataTableColumn = 6
DATA_TABLE_ACCOUNT_CURRENCY DataTableColumn = 7
DATA_TABLE_AMOUNT DataTableColumn = 8
DATA_TABLE_RELATED_ACCOUNT_NAME DataTableColumn = 9
DATA_TABLE_RELATED_ACCOUNT_CURRENCY DataTableColumn = 10
DATA_TABLE_RELATED_AMOUNT DataTableColumn = 11
DATA_TABLE_GEOGRAPHIC_LOCATION DataTableColumn = 12
DATA_TABLE_TAGS DataTableColumn = 13
DATA_TABLE_DESCRIPTION DataTableColumn = 14
)
// DataTableTransactionDataExporter defines the structure of plain text data table exporter for transaction data
type DataTableTransactionDataExporter struct {
dataColumnMapping map[DataTableColumn]string
transactionTypeMapping map[models.TransactionType]string
geoLocationSeparator string
transactionTagSeparator string
@@ -45,20 +23,14 @@ type DataTableTransactionDataExporter struct {
// DataTableTransactionDataImporter defines the structure of plain text data table importer for transaction data
type DataTableTransactionDataImporter struct {
dataColumnMapping map[DataTableColumn]string
transactionTypeMapping map[models.TransactionType]string
geoLocationSeparator string
transactionTagSeparator string
postProcessFunc DataTableTransactionDataImporterPostProcessFunc
}
// DataTableTransactionDataImporterPostProcessFunc represents item post process function of DataTableTransactionDataImporter
type DataTableTransactionDataImporterPostProcessFunc func(core.Context, *models.ImportTransaction) error
// CreateNewExporter returns a new data table transaction data exporter according to the specified arguments
func CreateNewExporter(dataColumnMapping map[DataTableColumn]string, transactionTypeMapping map[models.TransactionType]string, geoLocationSeparator string, transactionTagSeparator string) *DataTableTransactionDataExporter {
func CreateNewExporter(transactionTypeMapping map[models.TransactionType]string, geoLocationSeparator string, transactionTagSeparator string) *DataTableTransactionDataExporter {
return &DataTableTransactionDataExporter{
dataColumnMapping: dataColumnMapping,
transactionTypeMapping: transactionTypeMapping,
geoLocationSeparator: geoLocationSeparator,
transactionTagSeparator: transactionTagSeparator,
@@ -66,9 +38,8 @@ func CreateNewExporter(dataColumnMapping map[DataTableColumn]string, transaction
}
// CreateNewImporter returns a new data table transaction data importer according to the specified arguments
func CreateNewImporter(dataColumnMapping map[DataTableColumn]string, transactionTypeMapping map[models.TransactionType]string, geoLocationSeparator string, transactionTagSeparator string) *DataTableTransactionDataImporter {
func CreateNewImporter(transactionTypeMapping map[models.TransactionType]string, geoLocationSeparator string, transactionTagSeparator string) *DataTableTransactionDataImporter {
return &DataTableTransactionDataImporter{
dataColumnMapping: dataColumnMapping,
transactionTypeMapping: transactionTypeMapping,
geoLocationSeparator: geoLocationSeparator,
transactionTagSeparator: transactionTagSeparator,
@@ -76,41 +47,14 @@ func CreateNewImporter(dataColumnMapping map[DataTableColumn]string, transaction
}
// CreateNewSimpleImporter returns a new data table transaction data importer according to the specified arguments
func CreateNewSimpleImporter(dataColumnMapping map[DataTableColumn]string, transactionTypeMapping map[models.TransactionType]string) *DataTableTransactionDataImporter {
func CreateNewSimpleImporter(transactionTypeMapping map[models.TransactionType]string) *DataTableTransactionDataImporter {
return &DataTableTransactionDataImporter{
dataColumnMapping: dataColumnMapping,
transactionTypeMapping: transactionTypeMapping,
}
}
// CreateNewSimpleImporterWithPostProcessFunc returns a new data table transaction data importer according to the specified arguments
func CreateNewSimpleImporterWithPostProcessFunc(dataColumnMapping map[DataTableColumn]string, transactionTypeMapping map[models.TransactionType]string, postProcessFunc DataTableTransactionDataImporterPostProcessFunc) *DataTableTransactionDataImporter {
return &DataTableTransactionDataImporter{
dataColumnMapping: dataColumnMapping,
transactionTypeMapping: transactionTypeMapping,
postProcessFunc: postProcessFunc,
}
}
// CreateNewSimpleImporterFromWritableDataTable returns a new data table transaction data importer according to the specified arguments
func CreateNewSimpleImporterFromWritableDataTable(writableDataTable *WritableDataTable, transactionTypeMapping map[models.TransactionType]string) *DataTableTransactionDataImporter {
return &DataTableTransactionDataImporter{
dataColumnMapping: writableDataTable.GetDataColumnMapping(),
transactionTypeMapping: transactionTypeMapping,
}
}
// CreateNewSimpleImporterFromWritableDataTableWithPostProcessFunc returns a new data table transaction data importer according to the specified arguments
func CreateNewSimpleImporterFromWritableDataTableWithPostProcessFunc(writableDataTable *WritableDataTable, transactionTypeMapping map[models.TransactionType]string, postProcessFunc DataTableTransactionDataImporterPostProcessFunc) *DataTableTransactionDataImporter {
return &DataTableTransactionDataImporter{
dataColumnMapping: writableDataTable.GetDataColumnMapping(),
transactionTypeMapping: transactionTypeMapping,
postProcessFunc: postProcessFunc,
}
}
// BuildExportedContent writes the exported transaction data to the data table builder
func (c *DataTableTransactionDataExporter) BuildExportedContent(ctx core.Context, dataTableBuilder DataTableBuilder, uid int64, transactions []*models.Transaction, accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTagIndexes map[int64][]int64) error {
func (c *DataTableTransactionDataExporter) BuildExportedContent(ctx core.Context, dataTableBuilder TransactionDataTableBuilder, uid int64, transactions []*models.Transaction, accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTagIndexes map[int64][]int64) error {
for i := 0; i < len(transactions); i++ {
transaction := transactions[i]
@@ -118,27 +62,27 @@ func (c *DataTableTransactionDataExporter) BuildExportedContent(ctx core.Context
continue
}
dataRowMap := make(map[DataTableColumn]string, 15)
dataRowMap := make(map[TransactionDataTableColumn]string, 15)
transactionTimeZone := time.FixedZone("Transaction Timezone", int(transaction.TimezoneUtcOffset)*60)
dataRowMap[DATA_TABLE_TRANSACTION_TIME] = utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime), transactionTimeZone)
dataRowMap[DATA_TABLE_TRANSACTION_TIMEZONE] = utils.FormatTimezoneOffset(transactionTimeZone)
dataRowMap[DATA_TABLE_TRANSACTION_TYPE] = dataTableBuilder.ReplaceDelimiters(c.getDisplayTransactionTypeName(transaction.Type))
dataRowMap[DATA_TABLE_CATEGORY] = c.getExportedTransactionCategoryName(dataTableBuilder, transaction.CategoryId, categoryMap)
dataRowMap[DATA_TABLE_SUB_CATEGORY] = c.getExportedTransactionSubCategoryName(dataTableBuilder, transaction.CategoryId, categoryMap)
dataRowMap[DATA_TABLE_ACCOUNT_NAME] = c.getExportedAccountName(dataTableBuilder, transaction.AccountId, accountMap)
dataRowMap[DATA_TABLE_ACCOUNT_CURRENCY] = c.getAccountCurrency(dataTableBuilder, transaction.AccountId, accountMap)
dataRowMap[DATA_TABLE_AMOUNT] = utils.FormatAmount(transaction.Amount)
dataRowMap[TRANSACTION_DATA_TABLE_TRANSACTION_TIME] = utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime), transactionTimeZone)
dataRowMap[TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = utils.FormatTimezoneOffset(transactionTimeZone)
dataRowMap[TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = dataTableBuilder.ReplaceDelimiters(c.getDisplayTransactionTypeName(transaction.Type))
dataRowMap[TRANSACTION_DATA_TABLE_CATEGORY] = c.getExportedTransactionCategoryName(dataTableBuilder, transaction.CategoryId, categoryMap)
dataRowMap[TRANSACTION_DATA_TABLE_SUB_CATEGORY] = c.getExportedTransactionSubCategoryName(dataTableBuilder, transaction.CategoryId, categoryMap)
dataRowMap[TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = c.getExportedAccountName(dataTableBuilder, transaction.AccountId, accountMap)
dataRowMap[TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY] = c.getAccountCurrency(dataTableBuilder, transaction.AccountId, accountMap)
dataRowMap[TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(transaction.Amount)
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
dataRowMap[DATA_TABLE_RELATED_ACCOUNT_NAME] = c.getExportedAccountName(dataTableBuilder, transaction.RelatedAccountId, accountMap)
dataRowMap[DATA_TABLE_RELATED_ACCOUNT_CURRENCY] = c.getAccountCurrency(dataTableBuilder, transaction.RelatedAccountId, accountMap)
dataRowMap[DATA_TABLE_RELATED_AMOUNT] = utils.FormatAmount(transaction.RelatedAccountAmount)
dataRowMap[TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = c.getExportedAccountName(dataTableBuilder, transaction.RelatedAccountId, accountMap)
dataRowMap[TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_CURRENCY] = c.getAccountCurrency(dataTableBuilder, transaction.RelatedAccountId, accountMap)
dataRowMap[TRANSACTION_DATA_TABLE_RELATED_AMOUNT] = utils.FormatAmount(transaction.RelatedAccountAmount)
}
dataRowMap[DATA_TABLE_GEOGRAPHIC_LOCATION] = c.getExportedGeographicLocation(transaction)
dataRowMap[DATA_TABLE_TAGS] = c.getExportedTags(dataTableBuilder, transaction.TransactionId, allTagIndexes, tagMap)
dataRowMap[DATA_TABLE_DESCRIPTION] = dataTableBuilder.ReplaceDelimiters(transaction.Comment)
dataRowMap[TRANSACTION_DATA_TABLE_GEOGRAPHIC_LOCATION] = c.getExportedGeographicLocation(transaction)
dataRowMap[TRANSACTION_DATA_TABLE_TAGS] = c.getExportedTags(dataTableBuilder, transaction.TransactionId, allTagIndexes, tagMap)
dataRowMap[TRANSACTION_DATA_TABLE_DESCRIPTION] = dataTableBuilder.ReplaceDelimiters(transaction.Comment)
dataTableBuilder.AppendTransaction(dataRowMap)
}
@@ -162,7 +106,7 @@ func (c *DataTableTransactionDataExporter) getDisplayTransactionTypeName(transac
return transactionTypeName
}
func (c *DataTableTransactionDataExporter) getExportedTransactionCategoryName(dataTableBuilder DataTableBuilder, categoryId int64, categoryMap map[int64]*models.TransactionCategory) string {
func (c *DataTableTransactionDataExporter) getExportedTransactionCategoryName(dataTableBuilder TransactionDataTableBuilder, categoryId int64, categoryMap map[int64]*models.TransactionCategory) string {
category, exists := categoryMap[categoryId]
if !exists {
@@ -182,7 +126,7 @@ func (c *DataTableTransactionDataExporter) getExportedTransactionCategoryName(da
return dataTableBuilder.ReplaceDelimiters(parentCategory.Name)
}
func (c *DataTableTransactionDataExporter) getExportedTransactionSubCategoryName(dataTableBuilder DataTableBuilder, categoryId int64, categoryMap map[int64]*models.TransactionCategory) string {
func (c *DataTableTransactionDataExporter) getExportedTransactionSubCategoryName(dataTableBuilder TransactionDataTableBuilder, categoryId int64, categoryMap map[int64]*models.TransactionCategory) string {
category, exists := categoryMap[categoryId]
if exists {
@@ -192,7 +136,7 @@ func (c *DataTableTransactionDataExporter) getExportedTransactionSubCategoryName
}
}
func (c *DataTableTransactionDataExporter) getExportedAccountName(dataTableBuilder DataTableBuilder, accountId int64, accountMap map[int64]*models.Account) string {
func (c *DataTableTransactionDataExporter) getExportedAccountName(dataTableBuilder TransactionDataTableBuilder, accountId int64, accountMap map[int64]*models.Account) string {
account, exists := accountMap[accountId]
if exists {
@@ -202,7 +146,7 @@ func (c *DataTableTransactionDataExporter) getExportedAccountName(dataTableBuild
}
}
func (c *DataTableTransactionDataExporter) getAccountCurrency(dataTableBuilder DataTableBuilder, accountId int64, accountMap map[int64]*models.Account) string {
func (c *DataTableTransactionDataExporter) getAccountCurrency(dataTableBuilder TransactionDataTableBuilder, accountId int64, accountMap map[int64]*models.Account) string {
account, exists := accountMap[accountId]
if exists {
@@ -220,7 +164,7 @@ func (c *DataTableTransactionDataExporter) getExportedGeographicLocation(transac
return ""
}
func (c *DataTableTransactionDataExporter) getExportedTags(dataTableBuilder DataTableBuilder, transactionId int64, allTagIndexes map[int64][]int64, tagMap map[int64]*models.TransactionTag) string {
func (c *DataTableTransactionDataExporter) getExportedTags(dataTableBuilder TransactionDataTableBuilder, transactionId int64, allTagIndexes map[int64][]int64, tagMap map[int64]*models.TransactionTag) string {
tagIndexes, exists := allTagIndexes[transactionId]
if !exists {
@@ -248,8 +192,8 @@ func (c *DataTableTransactionDataExporter) getExportedTags(dataTableBuilder Data
}
// ParseImportedData returns the imported transaction data
func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, user *models.User, dataTable ImportedDataTable, 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) {
if dataTable.DataRowCount() < 1 {
func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, user *models.User, dataTable TransactionDataTable, 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) {
if dataTable.TransactionRowCount() < 1 {
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse import data for user \"uid:%d\", because data table row count is less 1", user.Uid)
return nil, nil, nil, nil, nil, nil, errs.ErrNotFoundTransactionDataInFile
}
@@ -260,29 +204,12 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u
return nil, nil, nil, nil, nil, nil, err
}
headerLineItems := dataTable.HeaderLineColumnNames()
headerItemMap := make(map[string]int)
for i := 0; i < len(headerLineItems); i++ {
headerItemMap[headerLineItems[i]] = i
}
timeColumnIdx, timeColumnExists := headerItemMap[c.dataColumnMapping[DATA_TABLE_TRANSACTION_TIME]]
timezoneColumnIdx, timezoneColumnExists := headerItemMap[c.dataColumnMapping[DATA_TABLE_TRANSACTION_TIMEZONE]]
typeColumnIdx, typeColumnExists := headerItemMap[c.dataColumnMapping[DATA_TABLE_TRANSACTION_TYPE]]
subCategoryColumnIdx, subCategoryColumnExists := headerItemMap[c.dataColumnMapping[DATA_TABLE_SUB_CATEGORY]]
accountColumnIdx, accountColumnExists := headerItemMap[c.dataColumnMapping[DATA_TABLE_ACCOUNT_NAME]]
accountCurrencyColumnIdx, accountCurrencyColumnExists := headerItemMap[c.dataColumnMapping[DATA_TABLE_ACCOUNT_CURRENCY]]
amountColumnIdx, amountColumnExists := headerItemMap[c.dataColumnMapping[DATA_TABLE_AMOUNT]]
account2ColumnIdx, account2ColumnExists := headerItemMap[c.dataColumnMapping[DATA_TABLE_RELATED_ACCOUNT_NAME]]
account2CurrencyColumnIdx, account2CurrencyColumnExists := headerItemMap[c.dataColumnMapping[DATA_TABLE_RELATED_ACCOUNT_CURRENCY]]
amount2ColumnIdx, amount2ColumnExists := headerItemMap[c.dataColumnMapping[DATA_TABLE_RELATED_AMOUNT]]
geoLocationIdx, geoLocationExists := headerItemMap[c.dataColumnMapping[DATA_TABLE_GEOGRAPHIC_LOCATION]]
tagsColumnIdx, tagsColumnExists := headerItemMap[c.dataColumnMapping[DATA_TABLE_TAGS]]
descriptionColumnIdx, descriptionColumnExists := headerItemMap[c.dataColumnMapping[DATA_TABLE_DESCRIPTION]]
if !timeColumnExists || !typeColumnExists || !subCategoryColumnExists ||
!accountColumnExists || !amountColumnExists || !account2ColumnExists {
if !dataTable.HasColumn(TRANSACTION_DATA_TABLE_TRANSACTION_TIME) ||
!dataTable.HasColumn(TRANSACTION_DATA_TABLE_TRANSACTION_TYPE) ||
!dataTable.HasColumn(TRANSACTION_DATA_TABLE_SUB_CATEGORY) ||
!dataTable.HasColumn(TRANSACTION_DATA_TABLE_ACCOUNT_NAME) ||
!dataTable.HasColumn(TRANSACTION_DATA_TABLE_AMOUNT) ||
!dataTable.HasColumn(TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME) {
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse import data for user \"uid:%d\", because missing essential columns in header row", user.Uid)
return nil, nil, nil, nil, nil, nil, errs.ErrMissingRequiredFieldInHeaderRow
}
@@ -307,59 +234,53 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u
tagMap = make(map[string]*models.TransactionTag)
}
allNewTransactions := make(models.ImportedTransactionSlice, 0, dataTable.DataRowCount())
allNewTransactions := make(models.ImportedTransactionSlice, 0, dataTable.TransactionRowCount())
allNewAccounts := make([]*models.Account, 0)
allNewSubExpenseCategories := make([]*models.TransactionCategory, 0)
allNewSubIncomeCategories := make([]*models.TransactionCategory, 0)
allNewSubTransferCategories := make([]*models.TransactionCategory, 0)
allNewTags := make([]*models.TransactionTag, 0)
dataRowIterator := dataTable.DataRowIterator()
dataRowIterator := dataTable.TransactionRowIterator()
dataRowIndex := 0
for dataRowIterator.HasNext() {
dataRowIndex++
dataRow := dataRowIterator.Next(ctx, user)
dataRow, err := dataRowIterator.Next(ctx, user)
if err != nil {
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse data row \"index:%d\" for user \"uid:%d\", because %s", dataRowIndex, user.Uid, err.Error())
return nil, nil, nil, nil, nil, nil, err
}
if !dataRow.IsValid() {
continue
}
columnCount := dataRow.ColumnCount()
if columnCount < 1 || (columnCount == 1 && dataRow.GetData(0) == "") {
continue
}
if columnCount < len(headerLineItems) {
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse data row \"index:%d\" for user \"uid:%d\", because may missing some columns (column count %d in data row is less than header column count %d)", dataRowIndex, user.Uid, columnCount, len(headerLineItems))
return nil, nil, nil, nil, nil, nil, errs.ErrFewerFieldsInDataRowThanInHeaderRow
}
timezoneOffset := defaultTimezoneOffset
if timezoneColumnExists {
transactionTimezone, err := dataRow.GetTimezoneOffset(timezoneColumnIdx)
if dataTable.HasColumn(TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE) {
transactionTimezone, err := utils.ParseFromTimezoneOffset(dataRow.GetData(TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE))
if err != nil {
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse time zone \"%s\" in data row \"index:%d\" for user \"uid:%d\", because %s", dataRow.GetData(timezoneColumnIdx), dataRowIndex, user.Uid, err.Error())
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse time zone \"%s\" in data row \"index:%d\" for user \"uid:%d\", because %s", dataRow.GetData(TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE), dataRowIndex, user.Uid, err.Error())
return nil, nil, nil, nil, nil, nil, errs.ErrTransactionTimeZoneInvalid
}
timezoneOffset = utils.GetTimezoneOffsetMinutes(transactionTimezone)
}
transactionTime, err := dataRow.GetTime(timeColumnIdx, timezoneOffset)
transactionTime, err := utils.ParseFromLongDateTime(dataRow.GetData(TRANSACTION_DATA_TABLE_TRANSACTION_TIME), timezoneOffset)
if err != nil {
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse time \"%s\" in data row \"index:%d\" for user \"uid:%d\", because %s", dataRow.GetData(timeColumnIdx), dataRowIndex, user.Uid, err.Error())
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse time \"%s\" in data row \"index:%d\" for user \"uid:%d\", because %s", dataRow.GetData(TRANSACTION_DATA_TABLE_TRANSACTION_TIME), dataRowIndex, user.Uid, err.Error())
return nil, nil, nil, nil, nil, nil, errs.ErrTransactionTimeInvalid
}
transactionDbType, err := c.getTransactionDbType(nameDbTypeMap, dataRow.GetData(typeColumnIdx))
transactionDbType, err := c.getTransactionDbType(nameDbTypeMap, dataRow.GetData(TRANSACTION_DATA_TABLE_TRANSACTION_TYPE))
if err != nil {
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse transaction type \"%s\" in data row \"index:%d\" for user \"uid:%d\", because %s", dataRow.GetData(typeColumnIdx), dataRowIndex, user.Uid, err.Error())
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse transaction type \"%s\" in data row \"index:%d\" for user \"uid:%d\", because %s", dataRow.GetData(TRANSACTION_DATA_TABLE_TRANSACTION_TYPE), dataRowIndex, user.Uid, err.Error())
return nil, nil, nil, nil, nil, nil, errs.Or(err, errs.ErrTransactionTypeInvalid)
}
@@ -374,7 +295,7 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u
return nil, nil, nil, nil, nil, nil, errs.Or(err, errs.ErrTransactionTypeInvalid)
}
subCategoryName = dataRow.GetData(subCategoryColumnIdx)
subCategoryName = dataRow.GetData(TRANSACTION_DATA_TABLE_SUB_CATEGORY)
if transactionDbType == models.TRANSACTION_DB_TYPE_EXPENSE {
subCategory, exists := expenseCategoryMap[subCategoryName]
@@ -409,11 +330,11 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u
}
}
accountName := dataRow.GetData(accountColumnIdx)
accountName := dataRow.GetData(TRANSACTION_DATA_TABLE_ACCOUNT_NAME)
accountCurrency := user.DefaultCurrency
if accountCurrencyColumnExists {
accountCurrency = dataRow.GetData(accountCurrencyColumnIdx)
if dataTable.HasColumn(TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY) {
accountCurrency = dataRow.GetData(TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY)
if _, ok := validators.AllCurrencyNames[accountCurrency]; !ok {
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] account currency \"%s\" is not supported in data row \"index:%d\" for user \"uid:%d\"", accountCurrency, dataRowIndex, user.Uid)
@@ -429,7 +350,7 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u
accountMap[accountName] = account
}
if accountCurrencyColumnExists {
if dataTable.HasColumn(TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY) {
if account.Name != "" && account.Currency != accountCurrency {
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] currency \"%s\" in data row \"index:%d\" not equals currency \"%s\" of the account for user \"uid:%d\"", accountCurrency, dataRowIndex, account.Currency, user.Uid)
return nil, nil, nil, nil, nil, nil, errs.ErrAccountCurrencyInvalid
@@ -438,10 +359,10 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u
accountCurrency = account.Currency
}
amount, err := utils.ParseAmount(dataRow.GetData(amountColumnIdx))
amount, err := utils.ParseAmount(dataRow.GetData(TRANSACTION_DATA_TABLE_AMOUNT))
if err != nil {
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse acmount \"%s\" in data row \"index:%d\" for user \"uid:%d\", because %s", dataRow.GetData(amountColumnIdx), dataRowIndex, user.Uid, err.Error())
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse acmount \"%s\" in data row \"index:%d\" for user \"uid:%d\", because %s", dataRow.GetData(TRANSACTION_DATA_TABLE_AMOUNT), dataRowIndex, user.Uid, err.Error())
return nil, nil, nil, nil, nil, nil, errs.ErrAmountInvalid
}
@@ -451,11 +372,11 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u
account2Currency := ""
if transactionDbType == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
account2Name = dataRow.GetData(account2ColumnIdx)
account2Name = dataRow.GetData(TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME)
account2Currency = user.DefaultCurrency
if account2CurrencyColumnExists {
account2Currency = dataRow.GetData(account2CurrencyColumnIdx)
if dataTable.HasColumn(TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_CURRENCY) {
account2Currency = dataRow.GetData(TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_CURRENCY)
if _, ok := validators.AllCurrencyNames[account2Currency]; !ok {
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] account2 currency \"%s\" is not supported in data row \"index:%d\" for user \"uid:%d\"", account2Currency, dataRowIndex, user.Uid)
@@ -471,7 +392,7 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u
accountMap[account2Name] = account2
}
if account2CurrencyColumnExists {
if dataTable.HasColumn(TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_CURRENCY) {
if account2.Name != "" && account2.Currency != account2Currency {
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] currency \"%s\" in data row \"index:%d\" not equals currency \"%s\" of the account2 for user \"uid:%d\"", account2Currency, dataRowIndex, account2.Currency, user.Uid)
return nil, nil, nil, nil, nil, nil, errs.ErrAccountCurrencyInvalid
@@ -482,11 +403,11 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u
relatedAccountId = account2.AccountId
if amount2ColumnExists {
relatedAccountAmount, err = utils.ParseAmount(dataRow.GetData(amount2ColumnIdx))
if dataTable.HasColumn(TRANSACTION_DATA_TABLE_RELATED_AMOUNT) {
relatedAccountAmount, err = utils.ParseAmount(dataRow.GetData(TRANSACTION_DATA_TABLE_RELATED_AMOUNT))
if err != nil {
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse acmount2 \"%s\" in data row \"index:%d\" for user \"uid:%d\", because %s", dataRow.GetData(amount2ColumnIdx), dataRowIndex, user.Uid, err.Error())
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse acmount2 \"%s\" in data row \"index:%d\" for user \"uid:%d\", because %s", dataRow.GetData(TRANSACTION_DATA_TABLE_RELATED_AMOUNT), dataRowIndex, user.Uid, err.Error())
return nil, nil, nil, nil, nil, nil, errs.ErrAmountInvalid
}
} else if transactionDbType == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
@@ -497,21 +418,21 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u
geoLongitude := float64(0)
geoLatitude := float64(0)
if geoLocationExists {
geoLocationItems := strings.Split(dataRow.GetData(geoLocationIdx), c.geoLocationSeparator)
if dataTable.HasColumn(TRANSACTION_DATA_TABLE_GEOGRAPHIC_LOCATION) {
geoLocationItems := strings.Split(dataRow.GetData(TRANSACTION_DATA_TABLE_GEOGRAPHIC_LOCATION), c.geoLocationSeparator)
if len(geoLocationItems) == 2 {
geoLongitude, err = utils.StringToFloat64(geoLocationItems[0])
if err != nil {
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse geographic location \"%s\" in data row \"index:%d\" for user \"uid:%d\", because %s", dataRow.GetData(geoLocationIdx), dataRowIndex, user.Uid, err.Error())
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse geographic location \"%s\" in data row \"index:%d\" for user \"uid:%d\", because %s", dataRow.GetData(TRANSACTION_DATA_TABLE_GEOGRAPHIC_LOCATION), dataRowIndex, user.Uid, err.Error())
return nil, nil, nil, nil, nil, nil, errs.ErrGeographicLocationInvalid
}
geoLatitude, err = utils.StringToFloat64(geoLocationItems[1])
if err != nil {
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse geographic location \"%s\" in data row \"index:%d\" for user \"uid:%d\", because %s", dataRow.GetData(geoLocationIdx), dataRowIndex, user.Uid, err.Error())
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse geographic location \"%s\" in data row \"index:%d\" for user \"uid:%d\", because %s", dataRow.GetData(TRANSACTION_DATA_TABLE_GEOGRAPHIC_LOCATION), dataRowIndex, user.Uid, err.Error())
return nil, nil, nil, nil, nil, nil, errs.ErrGeographicLocationInvalid
}
}
@@ -520,8 +441,8 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u
var tagIds []string
var tagNames []string
if tagsColumnExists {
tagNameItems := strings.Split(dataRow.GetData(tagsColumnIdx), c.transactionTagSeparator)
if dataTable.HasColumn(TRANSACTION_DATA_TABLE_TAGS) {
tagNameItems := strings.Split(dataRow.GetData(TRANSACTION_DATA_TABLE_TAGS), c.transactionTagSeparator)
for i := 0; i < len(tagNameItems); i++ {
tagName := tagNameItems[i]
@@ -548,8 +469,8 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u
description := ""
if descriptionColumnExists {
description = dataRow.GetData(descriptionColumnIdx)
if dataTable.HasColumn(TRANSACTION_DATA_TABLE_DESCRIPTION) {
description = dataRow.GetData(TRANSACTION_DATA_TABLE_DESCRIPTION)
}
transaction := &models.ImportTransaction{
@@ -578,15 +499,6 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u
OriginalTagNames: tagNames,
}
if c.postProcessFunc != nil {
err = c.postProcessFunc(ctx, transaction)
if err != nil {
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot post process data row \"index:%d\" for user \"uid:%d\", because %s", dataRowIndex, user.Uid, err.Error())
return nil, nil, nil, nil, nil, nil, err
}
}
allNewTransactions = append(allNewTransactions, transaction)
}
@@ -1,39 +1,34 @@
package feidee
package datatable
import (
"bytes"
"time"
"github.com/shakinm/xlsReader/xls"
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
// feideeMymoneyTransactionExcelFileDataTable defines the structure of feidee mymoney transaction plain text data table
type feideeMymoneyTransactionExcelFileDataTable struct {
// DefaultExcelFileImportedDataTable defines the structure of default excel file data table
type DefaultExcelFileImportedDataTable struct {
workbook *xls.Workbook
headerLineColumnNames []string
}
// feideeMymoneyTransactionExcelFileDataRow defines the structure of feidee mymoney transaction plain text data row
type feideeMymoneyTransactionExcelFileDataRow struct {
// DefaultExcelFileDataRow defines the structure of default excel file data table row
type DefaultExcelFileDataRow struct {
sheet *xls.Sheet
rowIndex int
}
// feideeMymoneyTransactionExcelFileDataRowIterator defines the structure of feidee mymoney transaction plain text data row iterator
type feideeMymoneyTransactionExcelFileDataRowIterator struct {
dataTable *feideeMymoneyTransactionExcelFileDataTable
// DefaultExcelFileDataRowIterator defines the structure of default excel file data table row iterator
type DefaultExcelFileDataRowIterator struct {
dataTable *DefaultExcelFileImportedDataTable
currentTableIndex int
currentRowIndexInTable int
}
// DataRowCount returns the total count of data row
func (t *feideeMymoneyTransactionExcelFileDataTable) DataRowCount() int {
func (t *DefaultExcelFileImportedDataTable) DataRowCount() int {
allSheets := t.workbook.GetSheets()
totalDataRowCount := 0
@@ -50,27 +45,22 @@ func (t *feideeMymoneyTransactionExcelFileDataTable) DataRowCount() int {
return totalDataRowCount
}
// HeaderLineColumnNames returns the header column name list
func (t *feideeMymoneyTransactionExcelFileDataTable) HeaderLineColumnNames() []string {
// HeaderColumnNames returns the header column name list
func (t *DefaultExcelFileImportedDataTable) HeaderColumnNames() []string {
return t.headerLineColumnNames
}
// DataRowIterator returns the iterator of data row
func (t *feideeMymoneyTransactionExcelFileDataTable) DataRowIterator() datatable.ImportedDataRowIterator {
return &feideeMymoneyTransactionExcelFileDataRowIterator{
func (t *DefaultExcelFileImportedDataTable) DataRowIterator() ImportedDataRowIterator {
return &DefaultExcelFileDataRowIterator{
dataTable: t,
currentTableIndex: 0,
currentRowIndexInTable: 0,
}
}
// IsValid returns whether this row contains valid data for importing
func (r *feideeMymoneyTransactionExcelFileDataRow) IsValid() bool {
return true
}
// ColumnCount returns the total count of column in this data row
func (r *feideeMymoneyTransactionExcelFileDataRow) ColumnCount() int {
func (r *DefaultExcelFileDataRow) ColumnCount() int {
row, err := r.sheet.GetRow(r.rowIndex)
if err != nil {
@@ -81,7 +71,7 @@ func (r *feideeMymoneyTransactionExcelFileDataRow) ColumnCount() int {
}
// GetData returns the data in the specified column index
func (r *feideeMymoneyTransactionExcelFileDataRow) GetData(columnIndex int) string {
func (r *DefaultExcelFileDataRow) GetData(columnIndex int) string {
row, err := r.sheet.GetRow(r.rowIndex)
if err != nil {
@@ -97,32 +87,8 @@ func (r *feideeMymoneyTransactionExcelFileDataRow) GetData(columnIndex int) stri
return cell.GetString()
}
// GetTime returns the time in the specified column index
func (r *feideeMymoneyTransactionExcelFileDataRow) GetTime(columnIndex int, timezoneOffset int16) (time.Time, error) {
str := r.GetData(columnIndex)
if utils.IsValidLongDateTimeFormat(str) {
return utils.ParseFromLongDateTime(str, timezoneOffset)
}
if utils.IsValidLongDateTimeWithoutSecondFormat(str) {
return utils.ParseFromLongDateTimeWithoutSecond(str, timezoneOffset)
}
if utils.IsValidLongDateFormat(str) {
return utils.ParseFromLongDateTimeWithoutSecond(str+" 00:00", timezoneOffset)
}
return time.Unix(0, 0), errs.ErrTransactionTimeInvalid
}
// GetTimezoneOffset returns the time zone offset in the specified column index
func (r *feideeMymoneyTransactionExcelFileDataRow) GetTimezoneOffset(columnIndex int) (*time.Location, error) {
return nil, errs.ErrNotSupported
}
// HasNext returns whether the iterator does not reach the end
func (t *feideeMymoneyTransactionExcelFileDataRowIterator) HasNext() bool {
func (t *DefaultExcelFileDataRowIterator) HasNext() bool {
allSheets := t.dataTable.workbook.GetSheets()
if t.currentTableIndex >= len(allSheets) {
@@ -149,7 +115,7 @@ func (t *feideeMymoneyTransactionExcelFileDataRowIterator) HasNext() bool {
}
// Next returns the next imported data row
func (t *feideeMymoneyTransactionExcelFileDataRowIterator) Next(ctx core.Context, user *models.User) datatable.ImportedDataRow {
func (t *DefaultExcelFileDataRowIterator) Next() ImportedDataRow {
allSheets := t.dataTable.workbook.GetSheets()
currentRowIndexInTable := t.currentRowIndexInTable
@@ -177,13 +143,14 @@ func (t *feideeMymoneyTransactionExcelFileDataRowIterator) Next(ctx core.Context
return nil
}
return &feideeMymoneyTransactionExcelFileDataRow{
return &DefaultExcelFileDataRow{
sheet: &currentSheet,
rowIndex: t.currentRowIndexInTable,
}
}
func createNewFeideeMymoneyTransactionExcelFileDataTable(data []byte) (*feideeMymoneyTransactionExcelFileDataTable, error) {
// CreateNewDefaultExcelFileImportedDataTable returns default excel xls data table by file binary data
func CreateNewDefaultExcelFileImportedDataTable(data []byte) (*DefaultExcelFileImportedDataTable, error) {
reader := bytes.NewReader(data)
workbook, err := xls.OpenReader(reader)
@@ -230,7 +197,7 @@ func createNewFeideeMymoneyTransactionExcelFileDataTable(data []byte) (*feideeMy
}
}
return &feideeMymoneyTransactionExcelFileDataTable{
return &DefaultExcelFileImportedDataTable{
workbook: &workbook,
headerLineColumnNames: headerRowItems,
}, nil
@@ -0,0 +1,124 @@
package datatable
import (
"encoding/csv"
"io"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/log"
)
// DefaultPlainTextImportedDataTable defines the structure of default plain text data table
type DefaultPlainTextImportedDataTable struct {
allLines [][]string
}
// DefaultPlainTextImportedDataRow defines the structure of default plain text data table row
type DefaultPlainTextImportedDataRow struct {
dataTable *DefaultPlainTextImportedDataTable
allItems []string
}
// DefaultPlainTextImportedDataRowIterator defines the structure of default plain text data table row iterator
type DefaultPlainTextImportedDataRowIterator struct {
dataTable *DefaultPlainTextImportedDataTable
currentIndex int
}
// DataRowCount returns the total count of data row
func (t *DefaultPlainTextImportedDataTable) DataRowCount() int {
if len(t.allLines) < 1 {
return 0
}
return len(t.allLines) - 1
}
// HeaderColumnNames returns the header column name list
func (t *DefaultPlainTextImportedDataTable) HeaderColumnNames() []string {
if len(t.allLines) < 1 {
return nil
}
return t.allLines[0]
}
// DataRowIterator returns the iterator of data row
func (t *DefaultPlainTextImportedDataTable) DataRowIterator() ImportedDataRowIterator {
return &DefaultPlainTextImportedDataRowIterator{
dataTable: t,
currentIndex: 0,
}
}
// ColumnCount returns the total count of column in this data row
func (r *DefaultPlainTextImportedDataRow) ColumnCount() int {
return len(r.allItems)
}
// GetData returns the data in the specified column index
func (r *DefaultPlainTextImportedDataRow) GetData(columnIndex int) string {
if columnIndex >= len(r.allItems) {
return ""
}
return r.allItems[columnIndex]
}
// HasNext returns whether the iterator does not reach the end
func (t *DefaultPlainTextImportedDataRowIterator) HasNext() bool {
return t.currentIndex+1 < len(t.dataTable.allLines)
}
// Next returns the next imported data row
func (t *DefaultPlainTextImportedDataRowIterator) Next() ImportedDataRow {
if t.currentIndex+1 >= len(t.dataTable.allLines) {
return nil
}
t.currentIndex++
rowItems := t.dataTable.allLines[t.currentIndex]
return &DefaultPlainTextImportedDataRow{
dataTable: t.dataTable,
allItems: rowItems,
}
}
// CreateNewDefaultCsvDataTable returns default csv data table by io readers
func CreateNewDefaultCsvDataTable(ctx core.Context, reader io.Reader) (*DefaultPlainTextImportedDataTable, error) {
return createNewDefaultPlainTextDataTable(ctx, reader, ',')
}
func createNewDefaultPlainTextDataTable(ctx core.Context, reader io.Reader, comma rune) (*DefaultPlainTextImportedDataTable, error) {
csvReader := csv.NewReader(reader)
csvReader.Comma = comma
csvReader.FieldsPerRecord = -1
allLines := make([][]string, 0)
for {
items, err := csvReader.Read()
if err == io.EOF {
break
}
if err != nil {
log.Errorf(ctx, "[Default_plain_text_imported_data_table.createNewDefaultPlainTextDataTable] cannot parse plain text data, because %s", err.Error())
return nil, errs.ErrInvalidCSVFile
}
if len(items) == 0 && items[0] == "" {
continue
}
allLines = append(allLines, items)
}
return &DefaultPlainTextImportedDataTable{
allLines: allLines,
}, nil
}
@@ -0,0 +1,31 @@
package datatable
// ImportedDataTable defines the structure of imported data table
type ImportedDataTable interface {
// DataRowCount returns the total count of data row
DataRowCount() int
// HeaderColumnNames returns the header column name list
HeaderColumnNames() []string
// DataRowIterator returns the iterator of data row
DataRowIterator() ImportedDataRowIterator
}
// ImportedDataRow defines the structure of imported data row
type ImportedDataRow interface {
// ColumnCount returns the total count of column in this data row
ColumnCount() int
// GetData returns the data in the specified column index
GetData(columnIndex int) string
}
// ImportedDataRowIterator defines the structure of imported data row iterator
type ImportedDataRowIterator interface {
// HasNext returns whether the iterator does not reach the end
HasNext() bool
// Next returns the next imported data row
Next() ImportedDataRow
}
@@ -0,0 +1,188 @@
package datatable
import (
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/models"
)
// ImportedTransactionDataTable defines the structure of imported transaction data table
type ImportedTransactionDataTable struct {
innerDataTable ImportedDataTable
dataColumnMapping map[TransactionDataTableColumn]string
dataColumnIndexes map[TransactionDataTableColumn]int
rowParser TransactionDataRowParser
addedColumns map[TransactionDataTableColumn]any
}
// ImportedTransactionDataRow defines the structure of imported transaction data row
type ImportedTransactionDataRow struct {
transactionDataTable *ImportedTransactionDataTable
rowData map[TransactionDataTableColumn]string
rowDataValid bool
}
// ImportedTransactionDataRowIterator defines the structure of imported transaction data row iterator
type ImportedTransactionDataRowIterator struct {
transactionDataTable *ImportedTransactionDataTable
innerIterator ImportedDataRowIterator
}
// HasColumn returns whether the data table has specified column
func (t *ImportedTransactionDataTable) HasColumn(column TransactionDataTableColumn) bool {
index, exists := t.dataColumnIndexes[column]
if exists && index >= 0 {
return exists
}
if t.addedColumns != nil {
_, exists = t.addedColumns[column]
if exists {
return exists
}
}
return false
}
// TransactionRowCount returns the total count of transaction data row
func (t *ImportedTransactionDataTable) TransactionRowCount() int {
return t.innerDataTable.DataRowCount()
}
// TransactionRowIterator returns the iterator of transaction data row
func (t *ImportedTransactionDataTable) TransactionRowIterator() TransactionDataRowIterator {
return &ImportedTransactionDataRowIterator{
transactionDataTable: t,
innerIterator: t.innerDataTable.DataRowIterator(),
}
}
// IsValid returns whether this row is valid data for importing
func (r *ImportedTransactionDataRow) IsValid() bool {
return r.rowDataValid
}
// GetData returns the data in the specified column type
func (r *ImportedTransactionDataRow) GetData(column TransactionDataTableColumn) string {
if !r.rowDataValid {
return ""
}
_, exists := r.transactionDataTable.dataColumnIndexes[column]
if exists {
return r.rowData[column]
}
if r.transactionDataTable.addedColumns != nil {
_, exists = r.transactionDataTable.addedColumns[column]
if exists {
return r.rowData[column]
}
}
return ""
}
// HasNext returns whether the iterator does not reach the end
func (t *ImportedTransactionDataRowIterator) HasNext() bool {
return t.innerIterator.HasNext()
}
// Next returns the next transaction data row
func (t *ImportedTransactionDataRowIterator) Next(ctx core.Context, user *models.User) (daraRow TransactionDataRow, err error) {
importedRow := t.innerIterator.Next()
if importedRow == nil {
return nil, nil
}
if importedRow.ColumnCount() == 1 && importedRow.GetData(0) == "" {
return &ImportedTransactionDataRow{
transactionDataTable: t.transactionDataTable,
rowData: nil,
rowDataValid: false,
}, nil
}
if importedRow.ColumnCount() < len(t.transactionDataTable.dataColumnIndexes) {
log.Errorf(ctx, "[imported_transaction_data_table.Next] cannot parse data row, because may missing some columns (column count %d in data row is less than header column count %d)", importedRow.ColumnCount(), len(t.transactionDataTable.dataColumnIndexes))
return nil, errs.ErrFewerFieldsInDataRowThanInHeaderRow
}
rowData := make(map[TransactionDataTableColumn]string, len(t.transactionDataTable.dataColumnIndexes))
rowDataValid := true
for column, columnIndex := range t.transactionDataTable.dataColumnIndexes {
if columnIndex < 0 || columnIndex >= importedRow.ColumnCount() {
continue
}
value := importedRow.GetData(columnIndex)
rowData[column] = value
}
if t.transactionDataTable.rowParser != nil {
rowData, rowDataValid, err = t.transactionDataTable.rowParser.Parse(rowData)
if err != nil {
log.Errorf(ctx, "[imported_transaction_data_table.Next] cannot parse data row, because %s", err.Error())
return nil, err
}
}
return &ImportedTransactionDataRow{
transactionDataTable: t.transactionDataTable,
rowData: rowData,
rowDataValid: rowDataValid,
}, nil
}
// CreateImportedTransactionDataTable returns transaction data table from imported data table
func CreateImportedTransactionDataTable(dataTable ImportedDataTable, dataColumnMapping map[TransactionDataTableColumn]string) *ImportedTransactionDataTable {
return CreateImportedTransactionDataTableWithRowParser(dataTable, dataColumnMapping, nil)
}
// CreateImportedTransactionDataTableWithRowParser returns transaction data table from imported data table
func CreateImportedTransactionDataTableWithRowParser(dataTable ImportedDataTable, dataColumnMapping map[TransactionDataTableColumn]string, rowParser TransactionDataRowParser) *ImportedTransactionDataTable {
headerLineItems := dataTable.HeaderColumnNames()
headerItemMap := make(map[string]int, len(headerLineItems))
for i := 0; i < len(headerLineItems); i++ {
headerItemMap[headerLineItems[i]] = i
}
dataColumnIndexes := make(map[TransactionDataTableColumn]int, len(headerLineItems))
for column, columnName := range dataColumnMapping {
columnIndex, exists := headerItemMap[columnName]
if exists {
dataColumnIndexes[column] = columnIndex
}
}
var addedColumns map[TransactionDataTableColumn]any
if rowParser != nil {
addedColumnsByParser := rowParser.GetAddedColumns()
addedColumns = make(map[TransactionDataTableColumn]any, len(addedColumnsByParser))
for i := 0; i < len(addedColumnsByParser); i++ {
addedColumns[addedColumnsByParser[i]] = true
}
}
return &ImportedTransactionDataTable{
innerDataTable: dataTable,
dataColumnMapping: dataColumnMapping,
dataColumnIndexes: dataColumnIndexes,
rowParser: rowParser,
addedColumns: addedColumns,
}
}
@@ -0,0 +1,75 @@
package datatable
import (
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/models"
)
// TransactionDataTable defines the structure of transaction data table
type TransactionDataTable interface {
// HasColumn returns whether the transaction data table has specified column
HasColumn(column TransactionDataTableColumn) bool
// TransactionRowCount returns the total count of transaction data row
TransactionRowCount() int
// TransactionRowIterator returns the iterator of transaction data row
TransactionRowIterator() TransactionDataRowIterator
}
// TransactionDataRow defines the structure of transaction data row
type TransactionDataRow interface {
// IsValid returns whether this row is valid data for importing
IsValid() bool
// GetData returns the data in the specified column type
GetData(column TransactionDataTableColumn) string
}
// TransactionDataRowIterator defines the structure of transaction data row iterator
type TransactionDataRowIterator interface {
// HasNext returns whether the iterator does not reach the end
HasNext() bool
// Next returns the next transaction data row
Next(ctx core.Context, user *models.User) (daraRow TransactionDataRow, err error)
}
// TransactionDataRowParser defines the structure of transaction data row parser
type TransactionDataRowParser interface {
// GetAddedColumns returns the added columns after converting the data row
GetAddedColumns() []TransactionDataTableColumn
// Parse returns the converted transaction data row
Parse(data map[TransactionDataTableColumn]string) (rowData map[TransactionDataTableColumn]string, rowDataValid bool, err error)
}
// TransactionDataTableBuilder defines the structure of data table builder
type TransactionDataTableBuilder interface {
// AppendTransaction appends the specified transaction to data builder
AppendTransaction(data map[TransactionDataTableColumn]string)
// ReplaceDelimiters returns the text after removing the delimiters
ReplaceDelimiters(text string) string
}
// TransactionDataTableColumn represents the data column type of data table
type TransactionDataTableColumn byte
// Transaction data table columns
const (
TRANSACTION_DATA_TABLE_TRANSACTION_TIME TransactionDataTableColumn = 1
TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE TransactionDataTableColumn = 2
TRANSACTION_DATA_TABLE_TRANSACTION_TYPE TransactionDataTableColumn = 3
TRANSACTION_DATA_TABLE_CATEGORY TransactionDataTableColumn = 4
TRANSACTION_DATA_TABLE_SUB_CATEGORY TransactionDataTableColumn = 5
TRANSACTION_DATA_TABLE_ACCOUNT_NAME TransactionDataTableColumn = 6
TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY TransactionDataTableColumn = 7
TRANSACTION_DATA_TABLE_AMOUNT TransactionDataTableColumn = 8
TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME TransactionDataTableColumn = 9
TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_CURRENCY TransactionDataTableColumn = 10
TRANSACTION_DATA_TABLE_RELATED_AMOUNT TransactionDataTableColumn = 11
TRANSACTION_DATA_TABLE_GEOGRAPHIC_LOCATION TransactionDataTableColumn = 12
TRANSACTION_DATA_TABLE_TAGS TransactionDataTableColumn = 13
TRANSACTION_DATA_TABLE_DESCRIPTION TransactionDataTableColumn = 14
)
@@ -1,152 +0,0 @@
package datatable
import (
"time"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
// WritableDataTable defines the structure of writable data table
type WritableDataTable struct {
allData []map[DataTableColumn]string
columns []DataTableColumn
}
// WritableDataRow defines the structure of data row of writable data table
type WritableDataRow struct {
dataTable *WritableDataTable
rowData map[DataTableColumn]string
}
// WritableDataRowIterator defines the structure of data row iterator of writable data table
type WritableDataRowIterator struct {
dataTable *WritableDataTable
nextIndex int
}
// Add appends a new record to data table
func (t *WritableDataTable) Add(data map[DataTableColumn]string) {
finalData := make(map[DataTableColumn]string, len(data))
for i := 0; i < len(t.columns); i++ {
column := t.columns[i]
if value, exists := data[column]; exists {
finalData[column] = value
}
}
t.allData = append(t.allData, finalData)
}
// Get returns the record in the specified index
func (t *WritableDataTable) Get(index int) ImportedDataRow {
if index >= len(t.allData) {
return nil
}
rowData := t.allData[index]
return &WritableDataRow{
dataTable: t,
rowData: rowData,
}
}
// DataRowCount returns the total count of data row
func (t *WritableDataTable) DataRowCount() int {
return len(t.allData)
}
// GetDataColumnMapping returns data column map for data importer
func (t *WritableDataTable) GetDataColumnMapping() map[DataTableColumn]string {
dataColumnMapping := make(map[DataTableColumn]string, len(t.columns))
for i := 0; i < len(t.columns); i++ {
column := t.columns[i]
dataColumnMapping[column] = utils.IntToString(int(column))
}
return dataColumnMapping
}
// HeaderLineColumnNames returns the header column name list
func (t *WritableDataTable) HeaderLineColumnNames() []string {
columnIndexes := make([]string, len(t.columns))
for i := 0; i < len(t.columns); i++ {
columnIndexes[i] = utils.IntToString(int(t.columns[i]))
}
return columnIndexes
}
// DataRowIterator returns the iterator of data row
func (t *WritableDataTable) DataRowIterator() ImportedDataRowIterator {
return &WritableDataRowIterator{
dataTable: t,
nextIndex: 0,
}
}
// IsValid returns whether this row contains valid data for importing
func (r *WritableDataRow) IsValid() bool {
return true
}
// ColumnCount returns the total count of column in this data row
func (r *WritableDataRow) ColumnCount() int {
return len(r.rowData)
}
// GetData returns the data in the specified column index
func (r *WritableDataRow) GetData(columnIndex int) string {
if columnIndex >= len(r.dataTable.columns) {
return ""
}
dataColumn := r.dataTable.columns[columnIndex]
return r.rowData[dataColumn]
}
// GetTime returns the time in the specified column index
func (r *WritableDataRow) GetTime(columnIndex int, timezoneOffset int16) (time.Time, error) {
return utils.ParseFromLongDateTime(r.GetData(columnIndex), timezoneOffset)
}
// GetTimezoneOffset returns the time zone offset in the specified column index
func (r *WritableDataRow) GetTimezoneOffset(columnIndex int) (*time.Location, error) {
return utils.ParseFromTimezoneOffset(r.GetData(columnIndex))
}
// HasNext returns whether the iterator does not reach the end
func (t *WritableDataRowIterator) HasNext() bool {
return t.nextIndex < len(t.dataTable.allData)
}
// Next returns the next imported data row
func (t *WritableDataRowIterator) Next(ctx core.Context, user *models.User) ImportedDataRow {
if t.nextIndex >= len(t.dataTable.allData) {
return nil
}
rowData := t.dataTable.allData[t.nextIndex]
t.nextIndex++
return &WritableDataRow{
dataTable: t.dataTable,
rowData: rowData,
}
}
// CreateNewWritableDataTable returns a new writable data table according to the specified columns
func CreateNewWritableDataTable(columns []DataTableColumn) *WritableDataTable {
return &WritableDataTable{
allData: make([]map[DataTableColumn]string, 0),
columns: columns,
}
}
@@ -1,208 +0,0 @@
package datatable
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
func TestWritableDataTableAdd(t *testing.T) {
columns := make([]DataTableColumn, 5)
columns[0] = DATA_TABLE_TRANSACTION_TIME
columns[1] = DATA_TABLE_TRANSACTION_TYPE
columns[2] = DATA_TABLE_SUB_CATEGORY
columns[3] = DATA_TABLE_ACCOUNT_NAME
columns[4] = DATA_TABLE_AMOUNT
writableDataTable := CreateNewWritableDataTable(columns)
assert.Equal(t, 0, writableDataTable.DataRowCount())
expectedTransactionUnixTime := time.Now().Unix()
expectedTextualTransactionTime := utils.FormatUnixTimeToLongDateTime(expectedTransactionUnixTime, time.Local)
expectedTransactionType := "Expense"
expectedSubCategory := "Test Category"
expectedAccountName := "Test Account"
expectedAmount := "123.45"
writableDataTable.Add(map[DataTableColumn]string{
DATA_TABLE_TRANSACTION_TIME: expectedTextualTransactionTime,
DATA_TABLE_TRANSACTION_TYPE: expectedTransactionType,
DATA_TABLE_SUB_CATEGORY: expectedSubCategory,
DATA_TABLE_ACCOUNT_NAME: expectedAccountName,
DATA_TABLE_AMOUNT: expectedAmount,
})
assert.Equal(t, 1, writableDataTable.DataRowCount())
dataRow := writableDataTable.Get(0)
actualTransactionTime, err := dataRow.GetTime(0, utils.GetTimezoneOffsetMinutes(time.Local))
assert.Nil(t, err)
actualTransactionUnixTime := actualTransactionTime.Unix()
assert.Equal(t, expectedTransactionUnixTime, actualTransactionUnixTime)
actualTextualTransactionTime := dataRow.GetData(0)
assert.Equal(t, expectedTextualTransactionTime, actualTextualTransactionTime)
actualTransactionType := dataRow.GetData(1)
assert.Equal(t, expectedTransactionType, actualTransactionType)
actualSubCategory := dataRow.GetData(2)
assert.Equal(t, expectedSubCategory, actualSubCategory)
actualAccountName := dataRow.GetData(3)
assert.Equal(t, expectedAccountName, actualAccountName)
actualAmount := dataRow.GetData(4)
assert.Equal(t, expectedAmount, actualAmount)
}
func TestWritableDataTableAdd_NotExistsColumn(t *testing.T) {
columns := make([]DataTableColumn, 1)
columns[0] = DATA_TABLE_TRANSACTION_TIME
writableDataTable := CreateNewWritableDataTable(columns)
expectedTransactionUnixTime := time.Now().Unix()
expectedTextualTransactionTime := utils.FormatUnixTimeToLongDateTime(expectedTransactionUnixTime, time.Local)
expectedTransactionType := "Expense"
writableDataTable.Add(map[DataTableColumn]string{
DATA_TABLE_TRANSACTION_TIME: expectedTextualTransactionTime,
DATA_TABLE_TRANSACTION_TYPE: expectedTransactionType,
})
assert.Equal(t, 1, writableDataTable.DataRowCount())
dataRow := writableDataTable.Get(0)
assert.Equal(t, 1, dataRow.ColumnCount())
}
func TestWritableDataTableGet_NotExistsRow(t *testing.T) {
columns := make([]DataTableColumn, 1)
columns[0] = DATA_TABLE_TRANSACTION_TIME
writableDataTable := CreateNewWritableDataTable(columns)
assert.Equal(t, 0, writableDataTable.DataRowCount())
dataRow := writableDataTable.Get(0)
assert.Nil(t, dataRow)
}
func TestWritableDataRowGetData_NotExistsColumn(t *testing.T) {
columns := make([]DataTableColumn, 1)
columns[0] = DATA_TABLE_TRANSACTION_TIME
writableDataTable := CreateNewWritableDataTable(columns)
expectedTransactionUnixTime := time.Now().Unix()
expectedTextualTransactionTime := utils.FormatUnixTimeToLongDateTime(expectedTransactionUnixTime, time.Local)
writableDataTable.Add(map[DataTableColumn]string{
DATA_TABLE_TRANSACTION_TIME: expectedTextualTransactionTime,
})
assert.Equal(t, 1, writableDataTable.DataRowCount())
dataRow := writableDataTable.Get(0)
assert.Equal(t, 1, dataRow.ColumnCount())
assert.Equal(t, "", dataRow.GetData(1))
}
func TestWritableDataTableDataRowIterator(t *testing.T) {
columns := make([]DataTableColumn, 5)
columns[0] = DATA_TABLE_TRANSACTION_TIME
columns[1] = DATA_TABLE_TRANSACTION_TYPE
columns[2] = DATA_TABLE_SUB_CATEGORY
columns[3] = DATA_TABLE_ACCOUNT_NAME
columns[4] = DATA_TABLE_AMOUNT
writableDataTable := CreateNewWritableDataTable(columns)
assert.Equal(t, 0, writableDataTable.DataRowCount())
expectedTransactionUnixTimes := make([]int64, 3)
expectedTextualTransactionTimes := make([]string, 3)
expectedTransactionTypes := make([]string, 3)
expectedSubCategories := make([]string, 3)
expectedAccountNames := make([]string, 3)
expectedAmounts := make([]string, 3)
expectedTransactionUnixTimes[0] = time.Now().Add(-5 * time.Hour).Unix()
expectedTextualTransactionTimes[0] = utils.FormatUnixTimeToLongDateTime(expectedTransactionUnixTimes[0], time.Local)
expectedTransactionTypes[0] = "Balance Modification"
expectedSubCategories[0] = ""
expectedAccountNames[0] = "Test Account"
expectedAmounts[0] = "123.45"
writableDataTable.Add(map[DataTableColumn]string{
DATA_TABLE_TRANSACTION_TIME: expectedTextualTransactionTimes[0],
DATA_TABLE_TRANSACTION_TYPE: expectedTransactionTypes[0],
DATA_TABLE_SUB_CATEGORY: expectedSubCategories[0],
DATA_TABLE_ACCOUNT_NAME: expectedAccountNames[0],
DATA_TABLE_AMOUNT: expectedAmounts[0],
})
expectedTransactionUnixTimes[1] = time.Now().Add(-45 * time.Minute).Unix()
expectedTextualTransactionTimes[1] = utils.FormatUnixTimeToLongDateTime(expectedTransactionUnixTimes[1], time.Local)
expectedTransactionTypes[1] = "Expense"
expectedSubCategories[1] = "Test Category2"
expectedAccountNames[1] = "Test Account"
expectedAmounts[1] = "-23.4"
writableDataTable.Add(map[DataTableColumn]string{
DATA_TABLE_TRANSACTION_TIME: expectedTextualTransactionTimes[1],
DATA_TABLE_TRANSACTION_TYPE: expectedTransactionTypes[1],
DATA_TABLE_SUB_CATEGORY: expectedSubCategories[1],
DATA_TABLE_ACCOUNT_NAME: expectedAccountNames[1],
DATA_TABLE_AMOUNT: expectedAmounts[1],
})
expectedTransactionUnixTimes[2] = time.Now().Unix()
expectedTextualTransactionTimes[2] = utils.FormatUnixTimeToLongDateTime(expectedTransactionUnixTimes[2], time.Local)
expectedTransactionTypes[2] = "Income"
expectedSubCategories[2] = "Test Category3"
expectedAccountNames[2] = "Test Account2"
expectedAmounts[2] = "123"
writableDataTable.Add(map[DataTableColumn]string{
DATA_TABLE_TRANSACTION_TIME: expectedTextualTransactionTimes[2],
DATA_TABLE_TRANSACTION_TYPE: expectedTransactionTypes[2],
DATA_TABLE_SUB_CATEGORY: expectedSubCategories[2],
DATA_TABLE_ACCOUNT_NAME: expectedAccountNames[2],
DATA_TABLE_AMOUNT: expectedAmounts[2],
})
assert.Equal(t, 3, writableDataTable.DataRowCount())
index := 0
iterator := writableDataTable.DataRowIterator()
for iterator.HasNext() {
dataRow := iterator.Next(core.NewNullContext(), &models.User{})
actualTransactionTime, err := dataRow.GetTime(0, utils.GetTimezoneOffsetMinutes(time.Local))
assert.Nil(t, err)
actualTransactionUnixTime := actualTransactionTime.Unix()
assert.Equal(t, expectedTransactionUnixTimes[index], actualTransactionUnixTime)
actualTextualTransactionTime := dataRow.GetData(0)
assert.Equal(t, expectedTextualTransactionTimes[index], actualTextualTransactionTime)
actualTransactionType := dataRow.GetData(1)
assert.Equal(t, expectedTransactionTypes[index], actualTransactionType)
actualSubCategory := dataRow.GetData(2)
assert.Equal(t, expectedSubCategories[index], actualSubCategory)
actualAccountName := dataRow.GetData(3)
assert.Equal(t, expectedAccountNames[index], actualAccountName)
actualAmount := dataRow.GetData(4)
assert.Equal(t, expectedAmounts[index], actualAmount)
index++
}
assert.Equal(t, 3, index)
}
@@ -0,0 +1,169 @@
package datatable
import (
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/models"
)
// WritableTransactionDataTable defines the structure of writable transaction data table
type WritableTransactionDataTable struct {
allData []map[TransactionDataTableColumn]string
supportedColumns map[TransactionDataTableColumn]any
rowParser TransactionDataRowParser
addedColumns map[TransactionDataTableColumn]any
}
// WritableTransactionDataRow defines the structure of transaction data row of writable data table
type WritableTransactionDataRow struct {
dataTable *WritableTransactionDataTable
rowData map[TransactionDataTableColumn]string
rowDataValid bool
}
// WritableTransactionDataRowIterator defines the structure of transaction data row iterator of writable data table
type WritableTransactionDataRowIterator struct {
dataTable *WritableTransactionDataTable
nextIndex int
}
// Add appends a new record to data table
func (t *WritableTransactionDataTable) Add(data map[TransactionDataTableColumn]string) {
finalData := make(map[TransactionDataTableColumn]string, len(data))
for column, value := range data {
_, exists := t.supportedColumns[column]
if exists {
finalData[column] = value
}
}
t.allData = append(t.allData, finalData)
}
// Get returns the record in the specified index
func (t *WritableTransactionDataTable) Get(index int) *WritableTransactionDataRow {
if index >= len(t.allData) {
return nil
}
rowData := t.allData[index]
return &WritableTransactionDataRow{
dataTable: t,
rowData: rowData,
}
}
// HasColumn returns whether the data table has specified column
func (t *WritableTransactionDataTable) HasColumn(column TransactionDataTableColumn) bool {
_, exists := t.supportedColumns[column]
if exists {
return exists
}
if t.addedColumns != nil {
_, exists = t.addedColumns[column]
if exists {
return exists
}
}
return false
}
// TransactionRowCount returns the total count of transaction data row
func (t *WritableTransactionDataTable) TransactionRowCount() int {
return len(t.allData)
}
// TransactionRowIterator returns the iterator of transaction data row
func (t *WritableTransactionDataTable) TransactionRowIterator() TransactionDataRowIterator {
return &WritableTransactionDataRowIterator{
dataTable: t,
nextIndex: 0,
}
}
// ColumnCount returns the total count of column in this data row
func (r *WritableTransactionDataRow) ColumnCount() int {
return len(r.rowData)
}
// IsValid returns whether this row is valid data for importing
func (r *WritableTransactionDataRow) IsValid() bool {
return r.rowDataValid
}
// GetData returns the data in the specified column type
func (r *WritableTransactionDataRow) GetData(column TransactionDataTableColumn) string {
return r.rowData[column]
}
// HasNext returns whether the iterator does not reach the end
func (t *WritableTransactionDataRowIterator) HasNext() bool {
return t.nextIndex < len(t.dataTable.allData)
}
// Next returns the next transaction data row
func (t *WritableTransactionDataRowIterator) Next(ctx core.Context, user *models.User) (daraRow TransactionDataRow, err error) {
if t.nextIndex >= len(t.dataTable.allData) {
return nil, nil
}
rowData := t.dataTable.allData[t.nextIndex]
rowDataValid := true
if t.dataTable.rowParser != nil {
rowData, rowDataValid, err = t.dataTable.rowParser.Parse(rowData)
if err != nil {
log.Errorf(ctx, "[writable_transaction_data_table.Next] cannot parse data row, because %s", err.Error())
return nil, err
}
}
t.nextIndex++
return &WritableTransactionDataRow{
dataTable: t.dataTable,
rowData: rowData,
rowDataValid: rowDataValid,
}, nil
}
// CreateNewWritableTransactionDataTable returns a new writable transaction data table according to the specified columns
func CreateNewWritableTransactionDataTable(columns []TransactionDataTableColumn) *WritableTransactionDataTable {
return CreateNewWritableTransactionDataTableWithRowParser(columns, nil)
}
// CreateNewWritableTransactionDataTableWithRowParser returns a new writable transaction data table according to the specified columns
func CreateNewWritableTransactionDataTableWithRowParser(columns []TransactionDataTableColumn, rowParser TransactionDataRowParser) *WritableTransactionDataTable {
supportedColumns := make(map[TransactionDataTableColumn]any, len(columns))
for i := 0; i < len(columns); i++ {
column := columns[i]
supportedColumns[column] = true
}
var addedColumns map[TransactionDataTableColumn]any
if rowParser != nil {
addedColumnsByParser := rowParser.GetAddedColumns()
addedColumns = make(map[TransactionDataTableColumn]any, len(addedColumnsByParser))
for i := 0; i < len(addedColumnsByParser); i++ {
addedColumns[addedColumnsByParser[i]] = true
}
}
return &WritableTransactionDataTable{
allData: make([]map[TransactionDataTableColumn]string, 0),
supportedColumns: supportedColumns,
rowParser: rowParser,
addedColumns: addedColumns,
}
}
@@ -0,0 +1,196 @@
package datatable
import (
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/models"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
func TestWritableDataTableAdd(t *testing.T) {
columns := make([]TransactionDataTableColumn, 5)
columns[0] = TRANSACTION_DATA_TABLE_TRANSACTION_TIME
columns[1] = TRANSACTION_DATA_TABLE_TRANSACTION_TYPE
columns[2] = TRANSACTION_DATA_TABLE_SUB_CATEGORY
columns[3] = TRANSACTION_DATA_TABLE_ACCOUNT_NAME
columns[4] = TRANSACTION_DATA_TABLE_AMOUNT
writableDataTable := CreateNewWritableTransactionDataTable(columns)
assert.Equal(t, 0, writableDataTable.TransactionRowCount())
expectedTransactionTime := "2024-09-01 01:23:45"
expectedTransactionType := "Expense"
expectedSubCategory := "Test Category"
expectedAccountName := "Test Account"
expectedAmount := "123.45"
writableDataTable.Add(map[TransactionDataTableColumn]string{
TRANSACTION_DATA_TABLE_TRANSACTION_TIME: expectedTransactionTime,
TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: expectedTransactionType,
TRANSACTION_DATA_TABLE_SUB_CATEGORY: expectedSubCategory,
TRANSACTION_DATA_TABLE_ACCOUNT_NAME: expectedAccountName,
TRANSACTION_DATA_TABLE_AMOUNT: expectedAmount,
})
assert.Equal(t, 1, writableDataTable.TransactionRowCount())
dataRow := writableDataTable.Get(0)
actualTransactionTime := dataRow.GetData(TRANSACTION_DATA_TABLE_TRANSACTION_TIME)
assert.Equal(t, expectedTransactionTime, actualTransactionTime)
actualTransactionType := dataRow.GetData(TRANSACTION_DATA_TABLE_TRANSACTION_TYPE)
assert.Equal(t, expectedTransactionType, actualTransactionType)
actualSubCategory := dataRow.GetData(TRANSACTION_DATA_TABLE_SUB_CATEGORY)
assert.Equal(t, expectedSubCategory, actualSubCategory)
actualAccountName := dataRow.GetData(TRANSACTION_DATA_TABLE_ACCOUNT_NAME)
assert.Equal(t, expectedAccountName, actualAccountName)
actualAmount := dataRow.GetData(TRANSACTION_DATA_TABLE_AMOUNT)
assert.Equal(t, expectedAmount, actualAmount)
}
func TestWritableDataTableAdd_NotExistsColumn(t *testing.T) {
columns := make([]TransactionDataTableColumn, 1)
columns[0] = TRANSACTION_DATA_TABLE_TRANSACTION_TIME
writableDataTable := CreateNewWritableTransactionDataTable(columns)
expectedTransactionUnixTime := time.Now().Unix()
expectedTextualTransactionTime := utils.FormatUnixTimeToLongDateTime(expectedTransactionUnixTime, time.Local)
expectedTransactionType := "Expense"
writableDataTable.Add(map[TransactionDataTableColumn]string{
TRANSACTION_DATA_TABLE_TRANSACTION_TIME: expectedTextualTransactionTime,
TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: expectedTransactionType,
})
assert.Equal(t, 1, writableDataTable.TransactionRowCount())
dataRow := writableDataTable.Get(0)
assert.Equal(t, 1, dataRow.ColumnCount())
}
func TestWritableDataTableGet_NotExistsRow(t *testing.T) {
columns := make([]TransactionDataTableColumn, 1)
columns[0] = TRANSACTION_DATA_TABLE_TRANSACTION_TIME
writableDataTable := CreateNewWritableTransactionDataTable(columns)
assert.Equal(t, 0, writableDataTable.TransactionRowCount())
dataRow := writableDataTable.Get(0)
assert.Nil(t, dataRow)
}
func TestWritableDataRowGetData_NotExistsColumn(t *testing.T) {
columns := make([]TransactionDataTableColumn, 1)
columns[0] = TRANSACTION_DATA_TABLE_TRANSACTION_TIME
writableDataTable := CreateNewWritableTransactionDataTable(columns)
expectedTransactionUnixTime := time.Now().Unix()
expectedTextualTransactionTime := utils.FormatUnixTimeToLongDateTime(expectedTransactionUnixTime, time.Local)
writableDataTable.Add(map[TransactionDataTableColumn]string{
TRANSACTION_DATA_TABLE_TRANSACTION_TIME: expectedTextualTransactionTime,
})
assert.Equal(t, 1, writableDataTable.TransactionRowCount())
dataRow := writableDataTable.Get(0)
assert.Equal(t, 1, dataRow.ColumnCount())
assert.Equal(t, "", dataRow.GetData(TRANSACTION_DATA_TABLE_TRANSACTION_TYPE))
}
func TestWritableDataTableDataRowIterator(t *testing.T) {
columns := make([]TransactionDataTableColumn, 5)
columns[0] = TRANSACTION_DATA_TABLE_TRANSACTION_TIME
columns[1] = TRANSACTION_DATA_TABLE_TRANSACTION_TYPE
columns[2] = TRANSACTION_DATA_TABLE_SUB_CATEGORY
columns[3] = TRANSACTION_DATA_TABLE_ACCOUNT_NAME
columns[4] = TRANSACTION_DATA_TABLE_AMOUNT
writableDataTable := CreateNewWritableTransactionDataTable(columns)
assert.Equal(t, 0, writableDataTable.TransactionRowCount())
expectedTransactionUnixTimes := make([]int64, 3)
expectedTransactionTimes := make([]string, 3)
expectedTransactionTypes := make([]string, 3)
expectedSubCategories := make([]string, 3)
expectedAccountNames := make([]string, 3)
expectedAmounts := make([]string, 3)
expectedTransactionUnixTimes[0] = time.Now().Add(-5 * time.Hour).Unix()
expectedTransactionTimes[0] = utils.FormatUnixTimeToLongDateTime(expectedTransactionUnixTimes[0], time.Local)
expectedTransactionTypes[0] = "Balance Modification"
expectedSubCategories[0] = ""
expectedAccountNames[0] = "Test Account"
expectedAmounts[0] = "123.45"
writableDataTable.Add(map[TransactionDataTableColumn]string{
TRANSACTION_DATA_TABLE_TRANSACTION_TIME: expectedTransactionTimes[0],
TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: expectedTransactionTypes[0],
TRANSACTION_DATA_TABLE_SUB_CATEGORY: expectedSubCategories[0],
TRANSACTION_DATA_TABLE_ACCOUNT_NAME: expectedAccountNames[0],
TRANSACTION_DATA_TABLE_AMOUNT: expectedAmounts[0],
})
expectedTransactionUnixTimes[1] = time.Now().Add(-45 * time.Minute).Unix()
expectedTransactionTimes[1] = utils.FormatUnixTimeToLongDateTime(expectedTransactionUnixTimes[1], time.Local)
expectedTransactionTypes[1] = "Expense"
expectedSubCategories[1] = "Test Category2"
expectedAccountNames[1] = "Test Account"
expectedAmounts[1] = "-23.4"
writableDataTable.Add(map[TransactionDataTableColumn]string{
TRANSACTION_DATA_TABLE_TRANSACTION_TIME: expectedTransactionTimes[1],
TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: expectedTransactionTypes[1],
TRANSACTION_DATA_TABLE_SUB_CATEGORY: expectedSubCategories[1],
TRANSACTION_DATA_TABLE_ACCOUNT_NAME: expectedAccountNames[1],
TRANSACTION_DATA_TABLE_AMOUNT: expectedAmounts[1],
})
expectedTransactionUnixTimes[2] = time.Now().Unix()
expectedTransactionTimes[2] = utils.FormatUnixTimeToLongDateTime(expectedTransactionUnixTimes[2], time.Local)
expectedTransactionTypes[2] = "Income"
expectedSubCategories[2] = "Test Category3"
expectedAccountNames[2] = "Test Account2"
expectedAmounts[2] = "123"
writableDataTable.Add(map[TransactionDataTableColumn]string{
TRANSACTION_DATA_TABLE_TRANSACTION_TIME: expectedTransactionTimes[2],
TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: expectedTransactionTypes[2],
TRANSACTION_DATA_TABLE_SUB_CATEGORY: expectedSubCategories[2],
TRANSACTION_DATA_TABLE_ACCOUNT_NAME: expectedAccountNames[2],
TRANSACTION_DATA_TABLE_AMOUNT: expectedAmounts[2],
})
assert.Equal(t, 3, writableDataTable.TransactionRowCount())
index := 0
iterator := writableDataTable.TransactionRowIterator()
for iterator.HasNext() {
dataRow, err := iterator.Next(core.NewNullContext(), &models.User{})
assert.Nil(t, err)
actualTransactionTime := dataRow.GetData(TRANSACTION_DATA_TABLE_TRANSACTION_TIME)
assert.Equal(t, expectedTransactionTimes[index], actualTransactionTime)
actualTransactionType := dataRow.GetData(TRANSACTION_DATA_TABLE_TRANSACTION_TYPE)
assert.Equal(t, expectedTransactionTypes[index], actualTransactionType)
actualSubCategory := dataRow.GetData(TRANSACTION_DATA_TABLE_SUB_CATEGORY)
assert.Equal(t, expectedSubCategories[index], actualSubCategory)
actualAccountName := dataRow.GetData(TRANSACTION_DATA_TABLE_ACCOUNT_NAME)
assert.Equal(t, expectedAccountNames[index], actualAccountName)
actualAmount := dataRow.GetData(TRANSACTION_DATA_TABLE_AMOUNT)
assert.Equal(t, expectedAmounts[index], actualAmount)
index++
}
assert.Equal(t, 3, index)
}
@@ -15,21 +15,21 @@ const ezbookkeepingLineSeparator = "\n"
const ezbookkeepingGeoLocationSeparator = " "
const ezbookkeepingTagSeparator = ";"
var ezbookkeepingDataColumnNameMapping = map[datatable.DataTableColumn]string{
datatable.DATA_TABLE_TRANSACTION_TIME: "Time",
datatable.DATA_TABLE_TRANSACTION_TIMEZONE: "Timezone",
datatable.DATA_TABLE_TRANSACTION_TYPE: "Type",
datatable.DATA_TABLE_CATEGORY: "Category",
datatable.DATA_TABLE_SUB_CATEGORY: "Sub Category",
datatable.DATA_TABLE_ACCOUNT_NAME: "Account",
datatable.DATA_TABLE_ACCOUNT_CURRENCY: "Account Currency",
datatable.DATA_TABLE_AMOUNT: "Amount",
datatable.DATA_TABLE_RELATED_ACCOUNT_NAME: "Account2",
datatable.DATA_TABLE_RELATED_ACCOUNT_CURRENCY: "Account2 Currency",
datatable.DATA_TABLE_RELATED_AMOUNT: "Account2 Amount",
datatable.DATA_TABLE_GEOGRAPHIC_LOCATION: "Geographic Location",
datatable.DATA_TABLE_TAGS: "Tags",
datatable.DATA_TABLE_DESCRIPTION: "Description",
var ezbookkeepingDataColumnNameMapping = map[datatable.TransactionDataTableColumn]string{
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME: "Time",
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE: "Timezone",
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: "Type",
datatable.TRANSACTION_DATA_TABLE_CATEGORY: "Category",
datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY: "Sub Category",
datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME: "Account",
datatable.TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY: "Account Currency",
datatable.TRANSACTION_DATA_TABLE_AMOUNT: "Amount",
datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME: "Account2",
datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_CURRENCY: "Account2 Currency",
datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT: "Account2 Amount",
datatable.TRANSACTION_DATA_TABLE_GEOGRAPHIC_LOCATION: "Geographic Location",
datatable.TRANSACTION_DATA_TABLE_TAGS: "Tags",
datatable.TRANSACTION_DATA_TABLE_DESCRIPTION: "Description",
}
var ezbookkeepingTransactionTypeNameMapping = map[models.TransactionType]string{
@@ -39,21 +39,21 @@ var ezbookkeepingTransactionTypeNameMapping = map[models.TransactionType]string{
models.TRANSACTION_TYPE_TRANSFER: "Transfer",
}
var ezbookkeepingDataColumns = []datatable.DataTableColumn{
datatable.DATA_TABLE_TRANSACTION_TIME,
datatable.DATA_TABLE_TRANSACTION_TIMEZONE,
datatable.DATA_TABLE_TRANSACTION_TYPE,
datatable.DATA_TABLE_CATEGORY,
datatable.DATA_TABLE_SUB_CATEGORY,
datatable.DATA_TABLE_ACCOUNT_NAME,
datatable.DATA_TABLE_ACCOUNT_CURRENCY,
datatable.DATA_TABLE_AMOUNT,
datatable.DATA_TABLE_RELATED_ACCOUNT_NAME,
datatable.DATA_TABLE_RELATED_ACCOUNT_CURRENCY,
datatable.DATA_TABLE_RELATED_AMOUNT,
datatable.DATA_TABLE_GEOGRAPHIC_LOCATION,
datatable.DATA_TABLE_TAGS,
datatable.DATA_TABLE_DESCRIPTION,
var ezbookkeepingDataColumns = []datatable.TransactionDataTableColumn{
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME,
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE,
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE,
datatable.TRANSACTION_DATA_TABLE_CATEGORY,
datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY,
datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME,
datatable.TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY,
datatable.TRANSACTION_DATA_TABLE_AMOUNT,
datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME,
datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_CURRENCY,
datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT,
datatable.TRANSACTION_DATA_TABLE_GEOGRAPHIC_LOCATION,
datatable.TRANSACTION_DATA_TABLE_TAGS,
datatable.TRANSACTION_DATA_TABLE_DESCRIPTION,
}
// ToExportedContent returns the exported transaction plain text data
@@ -67,7 +67,6 @@ func (c *ezBookKeepingTransactionDataPlainTextConverter) ToExportedContent(ctx c
)
dataTableExporter := datatable.CreateNewExporter(
ezbookkeepingDataColumnNameMapping,
ezbookkeepingTransactionTypeNameMapping,
ezbookkeepingGeoLocationSeparator,
ezbookkeepingTagSeparator,
@@ -84,7 +83,7 @@ func (c *ezBookKeepingTransactionDataPlainTextConverter) ToExportedContent(ctx c
// ParseImportedData returns the imported data by parsing the transaction plain text data
func (c *ezBookKeepingTransactionDataPlainTextConverter) 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) {
dataTable, err := createNewezbookkeepingTransactionPlainTextDataTable(
dataTable, err := createNewezbookkeepingPlainTextDataTable(
string(data),
c.columnSeparator,
ezbookkeepingLineSeparator,
@@ -94,12 +93,13 @@ func (c *ezBookKeepingTransactionDataPlainTextConverter) ParseImportedData(ctx c
return nil, nil, nil, nil, nil, nil, err
}
transactionDataTable := datatable.CreateImportedTransactionDataTable(dataTable, ezbookkeepingDataColumnNameMapping)
dataTableImporter := datatable.CreateNewImporter(
ezbookkeepingDataColumnNameMapping,
ezbookkeepingTransactionTypeNameMapping,
ezbookkeepingGeoLocationSeparator,
ezbookkeepingTagSeparator,
)
return dataTableImporter.ParseImportedData(ctx, user, dataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
}
@@ -238,10 +238,22 @@ func TestEzBookKeepingPlainFileConverterParseImportedData_ParseValidTimezone(t *
}
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("Time,Timezone,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
"2024-09-01 12:34:56,+08:00,Expense,Test Category,Test Account,123.45,,"), 0, nil, nil, nil, nil, nil)
"2024-09-01 12:34:56,-10:00,Expense,Test Category,Test Account,123.45,,"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725165296), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
assert.Equal(t, int64(1725230096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("Time,Timezone,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
"2024-09-01 12:34:56,+00:00,Expense,Test Category,Test Account,123.45,,"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725194096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("Time,Timezone,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
"2024-09-01 12:34:56,+12:45,Expense,Test Category,Test Account,123.45,,"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725148196), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
}
func TestEzBookKeepingPlainFileConverterParseImportedData_ParseInvalidTimezone(t *testing.T) {
@@ -3,31 +3,27 @@ package _default
import (
"fmt"
"strings"
"time"
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
// ezBookKeepingTransactionPlainTextDataTable defines the structure of ezbookkeeping transaction plain text data table
type ezBookKeepingTransactionPlainTextDataTable struct {
// ezBookKeepingPlainTextDataTable defines the structure of ezbookkeeping plain text data table
type ezBookKeepingPlainTextDataTable struct {
columnSeparator string
lineSeparator string
allLines []string
headerLineColumnNames []string
}
// ezBookKeepingTransactionPlainTextDataRow defines the structure of ezbookkeeping transaction plain text data row
type ezBookKeepingTransactionPlainTextDataRow struct {
// ezBookKeepingPlainTextDataRow defines the structure of ezbookkeeping plain text data row
type ezBookKeepingPlainTextDataRow struct {
allItems []string
}
// ezBookKeepingTransactionPlainTextDataRowIterator defines the structure of ezbookkeeping transaction plain text data row iterator
type ezBookKeepingTransactionPlainTextDataRowIterator struct {
dataTable *ezBookKeepingTransactionPlainTextDataTable
// ezBookKeepingPlainTextDataRowIterator defines the structure of ezbookkeeping plain text data row iterator
type ezBookKeepingPlainTextDataRowIterator struct {
dataTable *ezBookKeepingPlainTextDataTable
currentIndex int
}
@@ -35,14 +31,14 @@ type ezBookKeepingTransactionPlainTextDataRowIterator struct {
type ezBookKeepingTransactionPlainTextDataTableBuilder struct {
columnSeparator string
lineSeparator string
columns []datatable.DataTableColumn
dataColumnNameMapping map[datatable.DataTableColumn]string
columns []datatable.TransactionDataTableColumn
dataColumnNameMapping map[datatable.TransactionDataTableColumn]string
dataLineFormat string
builder *strings.Builder
}
// DataRowCount returns the total count of data row
func (t *ezBookKeepingTransactionPlainTextDataTable) DataRowCount() int {
func (t *ezBookKeepingPlainTextDataTable) DataRowCount() int {
if len(t.allLines) < 1 {
return 0
}
@@ -50,31 +46,26 @@ func (t *ezBookKeepingTransactionPlainTextDataTable) DataRowCount() int {
return len(t.allLines) - 1
}
// HeaderLineColumnNames returns the header column name list
func (t *ezBookKeepingTransactionPlainTextDataTable) HeaderLineColumnNames() []string {
// HeaderColumnNames returns the header column name list
func (t *ezBookKeepingPlainTextDataTable) HeaderColumnNames() []string {
return t.headerLineColumnNames
}
// DataRowIterator returns the iterator of data row
func (t *ezBookKeepingTransactionPlainTextDataTable) DataRowIterator() datatable.ImportedDataRowIterator {
return &ezBookKeepingTransactionPlainTextDataRowIterator{
func (t *ezBookKeepingPlainTextDataTable) DataRowIterator() datatable.ImportedDataRowIterator {
return &ezBookKeepingPlainTextDataRowIterator{
dataTable: t,
currentIndex: 0,
}
}
// IsValid returns whether this row contains valid data for importing
func (r *ezBookKeepingTransactionPlainTextDataRow) IsValid() bool {
return true
}
// ColumnCount returns the total count of column in this data row
func (r *ezBookKeepingTransactionPlainTextDataRow) ColumnCount() int {
func (r *ezBookKeepingPlainTextDataRow) ColumnCount() int {
return len(r.allItems)
}
// GetData returns the data in the specified column index
func (r *ezBookKeepingTransactionPlainTextDataRow) GetData(columnIndex int) string {
func (r *ezBookKeepingPlainTextDataRow) GetData(columnIndex int) string {
if columnIndex >= len(r.allItems) {
return ""
}
@@ -82,23 +73,13 @@ func (r *ezBookKeepingTransactionPlainTextDataRow) GetData(columnIndex int) stri
return r.allItems[columnIndex]
}
// GetTime returns the time in the specified column index
func (r *ezBookKeepingTransactionPlainTextDataRow) GetTime(columnIndex int, timezoneOffset int16) (time.Time, error) {
return utils.ParseFromLongDateTime(r.GetData(columnIndex), timezoneOffset)
}
// GetTimezoneOffset returns the time zone offset in the specified column index
func (r *ezBookKeepingTransactionPlainTextDataRow) GetTimezoneOffset(columnIndex int) (*time.Location, error) {
return utils.ParseFromTimezoneOffset(r.GetData(columnIndex))
}
// HasNext returns whether the iterator does not reach the end
func (t *ezBookKeepingTransactionPlainTextDataRowIterator) HasNext() bool {
func (t *ezBookKeepingPlainTextDataRowIterator) HasNext() bool {
return t.currentIndex+1 < len(t.dataTable.allLines)
}
// Next returns the next imported data row
func (t *ezBookKeepingTransactionPlainTextDataRowIterator) Next(ctx core.Context, user *models.User) datatable.ImportedDataRow {
func (t *ezBookKeepingPlainTextDataRowIterator) Next() datatable.ImportedDataRow {
if t.currentIndex+1 >= len(t.dataTable.allLines) {
return nil
}
@@ -108,13 +89,13 @@ func (t *ezBookKeepingTransactionPlainTextDataRowIterator) Next(ctx core.Context
rowContent := t.dataTable.allLines[t.currentIndex]
rowItems := strings.Split(rowContent, t.dataTable.columnSeparator)
return &ezBookKeepingTransactionPlainTextDataRow{
return &ezBookKeepingPlainTextDataRow{
allItems: rowItems,
}
}
// AppendTransaction appends the specified transaction to data builder
func (b *ezBookKeepingTransactionPlainTextDataTableBuilder) AppendTransaction(data map[datatable.DataTableColumn]string) {
func (b *ezBookKeepingTransactionPlainTextDataTableBuilder) AppendTransaction(data map[datatable.TransactionDataTableColumn]string) {
dataRowParams := make([]any, len(b.columns))
for i := 0; i < len(b.columns); i++ {
@@ -175,7 +156,7 @@ func (b *ezBookKeepingTransactionPlainTextDataTableBuilder) generateDataLineForm
return ret.String()
}
func createNewezbookkeepingTransactionPlainTextDataTable(content string, columnSeparator string, lineSeparator string) (*ezBookKeepingTransactionPlainTextDataTable, error) {
func createNewezbookkeepingPlainTextDataTable(content string, columnSeparator string, lineSeparator string) (*ezBookKeepingPlainTextDataTable, error) {
allLines := strings.Split(content, lineSeparator)
if len(allLines) < 2 {
@@ -186,7 +167,7 @@ func createNewezbookkeepingTransactionPlainTextDataTable(content string, columnS
headerLine = strings.ReplaceAll(headerLine, "\r", "")
headerLineItems := strings.Split(headerLine, columnSeparator)
return &ezBookKeepingTransactionPlainTextDataTable{
return &ezBookKeepingPlainTextDataTable{
columnSeparator: columnSeparator,
lineSeparator: lineSeparator,
allLines: allLines,
@@ -194,7 +175,7 @@ func createNewezbookkeepingTransactionPlainTextDataTable(content string, columnS
}, nil
}
func createNewezbookkeepingTransactionPlainTextDataTableBuilder(transactionCount int, columns []datatable.DataTableColumn, dataColumnNameMapping map[datatable.DataTableColumn]string, columnSeparator string, lineSeparator string) *ezBookKeepingTransactionPlainTextDataTableBuilder {
func createNewezbookkeepingTransactionPlainTextDataTableBuilder(transactionCount int, columns []datatable.TransactionDataTableColumn, dataColumnNameMapping map[datatable.TransactionDataTableColumn]string, columnSeparator string, lineSeparator string) *ezBookKeepingTransactionPlainTextDataTableBuilder {
var builder strings.Builder
builder.Grow(transactionCount * 100)
@@ -43,7 +43,7 @@ func (c *feideeMymoneyTransactionDataCsvImporter) ParseImportedData(ctx core.Con
return nil, nil, nil, nil, nil, nil, err
}
if len(allLines) <= 1 {
if len(allLines) < 2 {
log.Errorf(ctx, "[feidee_mymoney_transaction_data_csv_file_importer.ParseImportedData] cannot parse import data for user \"uid:%d\", because data table row count is less 1", user.Uid)
return nil, nil, nil, nil, nil, nil, errs.ErrNotFoundTransactionDataInFile
}
@@ -71,36 +71,37 @@ func (c *feideeMymoneyTransactionDataCsvImporter) ParseImportedData(ctx core.Con
return nil, nil, nil, nil, nil, nil, errs.ErrMissingRequiredFieldInHeaderRow
}
newColumns := make([]datatable.DataTableColumn, 0, 11)
newColumns = append(newColumns, datatable.DATA_TABLE_TRANSACTION_TYPE)
newColumns = append(newColumns, datatable.DATA_TABLE_TRANSACTION_TIME)
newColumns := make([]datatable.TransactionDataTableColumn, 0, 11)
newColumns = append(newColumns, datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE)
newColumns = append(newColumns, datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME)
if categoryColumnExists {
newColumns = append(newColumns, datatable.DATA_TABLE_CATEGORY)
newColumns = append(newColumns, datatable.TRANSACTION_DATA_TABLE_CATEGORY)
}
newColumns = append(newColumns, datatable.DATA_TABLE_SUB_CATEGORY)
newColumns = append(newColumns, datatable.DATA_TABLE_ACCOUNT_NAME)
newColumns = append(newColumns, datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY)
newColumns = append(newColumns, datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME)
if accountCurrencyColumnExists {
newColumns = append(newColumns, datatable.DATA_TABLE_ACCOUNT_CURRENCY)
newColumns = append(newColumns, datatable.TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY)
}
newColumns = append(newColumns, datatable.DATA_TABLE_AMOUNT)
newColumns = append(newColumns, datatable.DATA_TABLE_RELATED_ACCOUNT_NAME)
newColumns = append(newColumns, datatable.TRANSACTION_DATA_TABLE_AMOUNT)
newColumns = append(newColumns, datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME)
if accountCurrencyColumnExists {
newColumns = append(newColumns, datatable.DATA_TABLE_RELATED_ACCOUNT_CURRENCY)
newColumns = append(newColumns, datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_CURRENCY)
}
newColumns = append(newColumns, datatable.DATA_TABLE_RELATED_AMOUNT)
newColumns = append(newColumns, datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT)
if descriptionColumnExists {
newColumns = append(newColumns, datatable.DATA_TABLE_DESCRIPTION)
newColumns = append(newColumns, datatable.TRANSACTION_DATA_TABLE_DESCRIPTION)
}
dataTable := datatable.CreateNewWritableDataTable(newColumns)
transferTransactionsMap := make(map[string]map[datatable.DataTableColumn]string, 0)
transactionRowParser := createFeideeMymoneyTransactionDataRowParser()
dataTable := datatable.CreateNewWritableTransactionDataTableWithRowParser(newColumns, transactionRowParser)
transferTransactionsMap := make(map[string]map[datatable.TransactionDataTableColumn]string, 0)
for i := 1; i < len(allLines); i++ {
items := allLines[i]
@@ -131,20 +132,20 @@ func (c *feideeMymoneyTransactionDataCsvImporter) ParseImportedData(ctx core.Con
relatedIdColumnExists,
)
transactionType := data[datatable.DATA_TABLE_TRANSACTION_TYPE]
transactionType := data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE]
if transactionType == feideeMymoneyCsvFileTransactionTypeModifyBalanceText || transactionType == feideeMymoneyCsvFileTransactionTypeIncomeText || transactionType == feideeMymoneyCsvFileTransactionTypeExpenseText {
if transactionType == feideeMymoneyCsvFileTransactionTypeModifyBalanceText {
data[datatable.DATA_TABLE_TRANSACTION_TYPE] = feideeMymoneyTransactionTypeNameMapping[models.TRANSACTION_TYPE_MODIFY_BALANCE]
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = feideeMymoneyTransactionTypeNameMapping[models.TRANSACTION_TYPE_MODIFY_BALANCE]
} else if transactionType == feideeMymoneyCsvFileTransactionTypeIncomeText {
data[datatable.DATA_TABLE_TRANSACTION_TYPE] = feideeMymoneyTransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME]
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = feideeMymoneyTransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME]
} else if transactionType == feideeMymoneyCsvFileTransactionTypeExpenseText {
data[datatable.DATA_TABLE_TRANSACTION_TYPE] = feideeMymoneyTransactionTypeNameMapping[models.TRANSACTION_TYPE_EXPENSE]
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = feideeMymoneyTransactionTypeNameMapping[models.TRANSACTION_TYPE_EXPENSE]
}
data[datatable.DATA_TABLE_RELATED_ACCOUNT_NAME] = ""
data[datatable.DATA_TABLE_RELATED_ACCOUNT_CURRENCY] = ""
data[datatable.DATA_TABLE_RELATED_AMOUNT] = ""
data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = ""
data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_CURRENCY] = ""
data[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT] = ""
dataTable.Add(data)
} else if transactionType == feideeMymoneyCsvFileTransactionTypeTransferInText || transactionType == feideeMymoneyCsvFileTransactionTypeTransferOutText {
if relatedId == "" {
@@ -159,18 +160,18 @@ func (c *feideeMymoneyTransactionDataCsvImporter) ParseImportedData(ctx core.Con
continue
}
if transactionType == feideeMymoneyCsvFileTransactionTypeTransferInText && relatedData[datatable.DATA_TABLE_TRANSACTION_TYPE] == feideeMymoneyCsvFileTransactionTypeTransferOutText {
relatedData[datatable.DATA_TABLE_TRANSACTION_TYPE] = feideeMymoneyTransactionTypeNameMapping[models.TRANSACTION_TYPE_TRANSFER]
relatedData[datatable.DATA_TABLE_RELATED_ACCOUNT_NAME] = data[datatable.DATA_TABLE_ACCOUNT_NAME]
relatedData[datatable.DATA_TABLE_RELATED_ACCOUNT_CURRENCY] = data[datatable.DATA_TABLE_ACCOUNT_CURRENCY]
relatedData[datatable.DATA_TABLE_RELATED_AMOUNT] = data[datatable.DATA_TABLE_AMOUNT]
if transactionType == feideeMymoneyCsvFileTransactionTypeTransferInText && relatedData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] == feideeMymoneyCsvFileTransactionTypeTransferOutText {
relatedData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = feideeMymoneyTransactionTypeNameMapping[models.TRANSACTION_TYPE_TRANSFER]
relatedData[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME]
relatedData[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_CURRENCY] = data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY]
relatedData[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT] = data[datatable.TRANSACTION_DATA_TABLE_AMOUNT]
dataTable.Add(relatedData)
delete(transferTransactionsMap, relatedId)
} else if transactionType == feideeMymoneyCsvFileTransactionTypeTransferOutText && relatedData[datatable.DATA_TABLE_TRANSACTION_TYPE] == feideeMymoneyCsvFileTransactionTypeTransferInText {
data[datatable.DATA_TABLE_TRANSACTION_TYPE] = feideeMymoneyTransactionTypeNameMapping[models.TRANSACTION_TYPE_TRANSFER]
data[datatable.DATA_TABLE_RELATED_ACCOUNT_NAME] = relatedData[datatable.DATA_TABLE_ACCOUNT_NAME]
data[datatable.DATA_TABLE_RELATED_ACCOUNT_CURRENCY] = relatedData[datatable.DATA_TABLE_ACCOUNT_CURRENCY]
data[datatable.DATA_TABLE_RELATED_AMOUNT] = relatedData[datatable.DATA_TABLE_AMOUNT]
} else if transactionType == feideeMymoneyCsvFileTransactionTypeTransferOutText && relatedData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] == feideeMymoneyCsvFileTransactionTypeTransferInText {
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = feideeMymoneyTransactionTypeNameMapping[models.TRANSACTION_TYPE_TRANSFER]
data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = relatedData[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME]
data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_CURRENCY] = relatedData[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY]
data[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT] = relatedData[datatable.TRANSACTION_DATA_TABLE_AMOUNT]
dataTable.Add(data)
delete(transferTransactionsMap, relatedId)
} else {
@@ -188,11 +189,7 @@ func (c *feideeMymoneyTransactionDataCsvImporter) ParseImportedData(ctx core.Con
return nil, nil, nil, nil, nil, nil, errs.ErrFoundRecordNotHasRelatedRecord
}
dataTableImporter := datatable.CreateNewSimpleImporterFromWritableDataTableWithPostProcessFunc(
dataTable,
feideeMymoneyTransactionTypeNameMapping,
feideeMymoneyTransactionDataImporterPostProcess,
)
dataTableImporter := datatable.CreateNewSimpleImporter(feideeMymoneyTransactionTypeNameMapping)
return dataTableImporter.ParseImportedData(ctx, user, dataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
}
@@ -253,40 +250,40 @@ func (c *feideeMymoneyTransactionDataCsvImporter) parseTransactionData(
descriptionColumnExists bool,
relatedIdColumnIdx int,
relatedIdColumnExists bool,
) (map[datatable.DataTableColumn]string, string) {
data := make(map[datatable.DataTableColumn]string, 11)
) (map[datatable.TransactionDataTableColumn]string, string) {
data := make(map[datatable.TransactionDataTableColumn]string, 11)
relatedId := ""
if timeColumnExists && timeColumnIdx < len(items) {
data[datatable.DATA_TABLE_TRANSACTION_TIME] = items[timeColumnIdx]
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] = items[timeColumnIdx]
}
if typeColumnExists && typeColumnIdx < len(items) {
data[datatable.DATA_TABLE_TRANSACTION_TYPE] = items[typeColumnIdx]
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = items[typeColumnIdx]
}
if categoryColumnExists && categoryColumnIdx < len(items) {
data[datatable.DATA_TABLE_CATEGORY] = items[categoryColumnIdx]
data[datatable.TRANSACTION_DATA_TABLE_CATEGORY] = items[categoryColumnIdx]
}
if subCategoryColumnExists && subCategoryColumnIdx < len(items) {
data[datatable.DATA_TABLE_SUB_CATEGORY] = items[subCategoryColumnIdx]
data[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = items[subCategoryColumnIdx]
}
if accountColumnExists && accountColumnIdx < len(items) {
data[datatable.DATA_TABLE_ACCOUNT_NAME] = items[accountColumnIdx]
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = items[accountColumnIdx]
}
if accountCurrencyColumnExists && accountCurrencyColumnIdx < len(items) {
data[datatable.DATA_TABLE_ACCOUNT_CURRENCY] = items[accountCurrencyColumnIdx]
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY] = items[accountCurrencyColumnIdx]
}
if amountColumnExists && amountColumnIdx < len(items) {
data[datatable.DATA_TABLE_AMOUNT] = items[amountColumnIdx]
data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = items[amountColumnIdx]
}
if descriptionColumnExists && descriptionColumnIdx < len(items) {
data[datatable.DATA_TABLE_DESCRIPTION] = items[descriptionColumnIdx]
data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = items[descriptionColumnIdx]
}
if relatedIdColumnExists && relatedIdColumnIdx < len(items) {
@@ -296,7 +293,7 @@ func (c *feideeMymoneyTransactionDataCsvImporter) parseTransactionData(
return data, relatedId
}
func (c *feideeMymoneyTransactionDataCsvImporter) getRelatedIds(transferTransactionsMap map[string]map[datatable.DataTableColumn]string) string {
func (c *feideeMymoneyTransactionDataCsvImporter) getRelatedIds(transferTransactionsMap map[string]map[datatable.TransactionDataTableColumn]string) string {
builder := strings.Builder{}
for relatedId := range transferTransactionsMap {
@@ -36,8 +36,8 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_MinimumValidData(t *testi
assert.Equal(t, 6, len(allNewTransactions))
assert.Equal(t, 2, len(allNewAccounts))
assert.Equal(t, 1, len(allNewSubExpenseCategories))
assert.Equal(t, 1, len(allNewSubIncomeCategories))
assert.Equal(t, 2, len(allNewSubExpenseCategories))
assert.Equal(t, 2, len(allNewSubIncomeCategories))
assert.Equal(t, 1, len(allNewSubTransferCategories))
assert.Equal(t, 0, len(allNewTags))
@@ -94,10 +94,16 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_MinimumValidData(t *testi
assert.Equal(t, "CNY", allNewAccounts[1].Currency)
assert.Equal(t, int64(1234567890), allNewSubExpenseCategories[0].Uid)
assert.Equal(t, "Test Category2", allNewSubExpenseCategories[0].Name)
assert.Equal(t, "", allNewSubExpenseCategories[0].Name)
assert.Equal(t, int64(1234567890), allNewSubExpenseCategories[1].Uid)
assert.Equal(t, "Test Category2", allNewSubExpenseCategories[1].Name)
assert.Equal(t, int64(1234567890), allNewSubIncomeCategories[0].Uid)
assert.Equal(t, "Test Category", allNewSubIncomeCategories[0].Name)
assert.Equal(t, "", allNewSubIncomeCategories[0].Name)
assert.Equal(t, int64(1234567890), allNewSubIncomeCategories[1].Uid)
assert.Equal(t, "Test Category", allNewSubIncomeCategories[1].Name)
assert.Equal(t, int64(1234567890), allNewSubTransferCategories[0].Uid)
assert.Equal(t, "Test Category3", allNewSubTransferCategories[0].Name)
@@ -2,19 +2,18 @@ package feidee
import (
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/models"
)
var feideeMymoneyDataColumnNameMapping = map[datatable.DataTableColumn]string{
datatable.DATA_TABLE_TRANSACTION_TIME: "日期",
datatable.DATA_TABLE_TRANSACTION_TYPE: "交易类型",
datatable.DATA_TABLE_CATEGORY: "分类",
datatable.DATA_TABLE_SUB_CATEGORY: "子分类",
datatable.DATA_TABLE_ACCOUNT_NAME: "账户1",
datatable.DATA_TABLE_AMOUNT: "金额",
datatable.DATA_TABLE_RELATED_ACCOUNT_NAME: "账户2",
datatable.DATA_TABLE_DESCRIPTION: "备注",
var feideeMymoneyDataColumnNameMapping = map[datatable.TransactionDataTableColumn]string{
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME: "日期",
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: "交易类型",
datatable.TRANSACTION_DATA_TABLE_CATEGORY: "分类",
datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY: "子分类",
datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME: "账户1",
datatable.TRANSACTION_DATA_TABLE_AMOUNT: "金额",
datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME: "账户2",
datatable.TRANSACTION_DATA_TABLE_DESCRIPTION: "备注",
}
var feideeMymoneyTransactionTypeNameMapping = map[models.TransactionType]string{
@@ -23,16 +22,3 @@ var feideeMymoneyTransactionTypeNameMapping = map[models.TransactionType]string{
models.TRANSACTION_TYPE_EXPENSE: "支出",
models.TRANSACTION_TYPE_TRANSFER: "转账",
}
func feideeMymoneyTransactionDataImporterPostProcess(ctx core.Context, transaction *models.ImportTransaction) error {
if transaction.Type == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE {
if transaction.Amount >= 0 {
transaction.Type = models.TRANSACTION_DB_TYPE_INCOME
} else if transaction.Amount < 0 {
transaction.Amount = -transaction.Amount
transaction.Type = models.TRANSACTION_DB_TYPE_EXPENSE
}
}
return nil
}
@@ -0,0 +1,82 @@
package feidee
import (
"time"
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
// feideeMymoneyTransactionDataRowParser defines the structure of feidee mymoney transaction data row parser
type feideeMymoneyTransactionDataRowParser struct {
}
// GetAddedColumns returns the added columns after converting the data row
func (p *feideeMymoneyTransactionDataRowParser) GetAddedColumns() []datatable.TransactionDataTableColumn {
return nil
}
// Parse returns the converted transaction data row
func (p *feideeMymoneyTransactionDataRowParser) Parse(data map[datatable.TransactionDataTableColumn]string) (rowData map[datatable.TransactionDataTableColumn]string, rowDataValid bool, err error) {
rowData = make(map[datatable.TransactionDataTableColumn]string, len(data))
for column, value := range data {
rowData[column] = value
}
if rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] != "" {
rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] = p.getLongDateTime(rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME])
}
if rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] == feideeMymoneyTransactionTypeNameMapping[models.TRANSACTION_TYPE_MODIFY_BALANCE] {
amount, err := utils.ParseAmount(rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT])
if err != nil {
return nil, false, errs.ErrAmountInvalid
}
if amount >= 0 {
rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = feideeMymoneyTransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME]
} else {
rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = feideeMymoneyTransactionTypeNameMapping[models.TRANSACTION_TYPE_EXPENSE]
rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(-amount)
}
}
return rowData, true, nil
}
// Parse returns the converted transaction data row
func (p *feideeMymoneyTransactionDataRowParser) getLongDateTime(str string) string {
if utils.IsValidLongDateTimeFormat(str) {
return str
}
utcTimezone := time.UTC
utcTimezoneOffsetMinutes := utils.GetTimezoneOffsetMinutes(utcTimezone)
if utils.IsValidLongDateTimeWithoutSecondFormat(str) {
dateTime, err := utils.ParseFromLongDateTimeWithoutSecond(str, utcTimezoneOffsetMinutes)
if err == nil {
return utils.FormatUnixTimeToLongDateTime(dateTime.Unix(), utcTimezone)
}
}
if utils.IsValidLongDateFormat(str) {
dateTime, err := utils.ParseFromLongDateTimeWithoutSecond(str+" 00:00", utcTimezoneOffsetMinutes)
if err == nil {
return utils.FormatUnixTimeToLongDateTime(dateTime.Unix(), utcTimezone)
}
}
return str
}
// createFeideeMymoneyTransactionDataRowParser returns feidee mymoney transaction data row parser
func createFeideeMymoneyTransactionDataRowParser() datatable.TransactionDataRowParser {
return &feideeMymoneyTransactionDataRowParser{}
}
@@ -18,17 +18,15 @@ var (
// ParseImportedData returns the imported data by parsing the feidee mymoney transaction xls data
func (c *feideeMymoneyTransactionDataXlsImporter) 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) {
dataTable, err := createNewFeideeMymoneyTransactionExcelFileDataTable(data)
dataTable, err := datatable.CreateNewDefaultExcelFileImportedDataTable(data)
if err != nil {
return nil, nil, nil, nil, nil, nil, err
}
dataTableImporter := datatable.CreateNewSimpleImporterWithPostProcessFunc(
feideeMymoneyDataColumnNameMapping,
feideeMymoneyTransactionTypeNameMapping,
feideeMymoneyTransactionDataImporterPostProcess,
)
transactionRowParser := createFeideeMymoneyTransactionDataRowParser()
transactionDataTable := datatable.CreateImportedTransactionDataTableWithRowParser(dataTable, feideeMymoneyDataColumnNameMapping, transactionRowParser)
dataTableImporter := datatable.CreateNewSimpleImporter(feideeMymoneyTransactionTypeNameMapping)
return dataTableImporter.ParseImportedData(ctx, user, dataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
}
@@ -29,8 +29,8 @@ func TestFeideeMymoneyTransactionDataXlsImporterParseImportedData_MinimumValidDa
assert.Equal(t, 7, len(allNewTransactions))
assert.Equal(t, 2, len(allNewAccounts))
assert.Equal(t, 2, len(allNewSubExpenseCategories))
assert.Equal(t, 2, len(allNewSubIncomeCategories))
assert.Equal(t, 3, len(allNewSubExpenseCategories))
assert.Equal(t, 3, len(allNewSubIncomeCategories))
assert.Equal(t, 1, len(allNewSubTransferCategories))
assert.Equal(t, 0, len(allNewTags))
@@ -95,16 +95,22 @@ func TestFeideeMymoneyTransactionDataXlsImporterParseImportedData_MinimumValidDa
assert.Equal(t, "CNY", allNewAccounts[1].Currency)
assert.Equal(t, int64(1234567890), allNewSubExpenseCategories[0].Uid)
assert.Equal(t, "Test Category2", allNewSubExpenseCategories[0].Name)
assert.Equal(t, "", allNewSubExpenseCategories[0].Name)
assert.Equal(t, int64(1234567890), allNewSubExpenseCategories[1].Uid)
assert.Equal(t, "Test Category4", allNewSubExpenseCategories[1].Name)
assert.Equal(t, "Test Category2", allNewSubExpenseCategories[1].Name)
assert.Equal(t, int64(1234567890), allNewSubExpenseCategories[2].Uid)
assert.Equal(t, "Test Category4", allNewSubExpenseCategories[2].Name)
assert.Equal(t, int64(1234567890), allNewSubIncomeCategories[0].Uid)
assert.Equal(t, "Test Category", allNewSubIncomeCategories[0].Name)
assert.Equal(t, "", allNewSubIncomeCategories[0].Name)
assert.Equal(t, int64(1234567890), allNewSubIncomeCategories[1].Uid)
assert.Equal(t, "Test Category5", allNewSubIncomeCategories[1].Name)
assert.Equal(t, "Test Category", allNewSubIncomeCategories[1].Name)
assert.Equal(t, int64(1234567890), allNewSubIncomeCategories[2].Uid)
assert.Equal(t, "Test Category5", allNewSubIncomeCategories[2].Name)
assert.Equal(t, int64(1234567890), allNewSubTransferCategories[0].Uid)
assert.Equal(t, "Test Category3", allNewSubTransferCategories[0].Name)
@@ -8,7 +8,28 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/models"
)
// fireflyiiiTransactionDataCsvImporter defines the structure of firefly III csv importer for transaction data
var fireflyIIITransactionDataColumnNameMapping = map[datatable.TransactionDataTableColumn]string{
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME: "date",
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: "type",
datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY: "category",
datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME: "source_name",
datatable.TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY: "currency_code",
datatable.TRANSACTION_DATA_TABLE_AMOUNT: "amount",
datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME: "destination_name",
datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_CURRENCY: "foreign_currency_code",
datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT: "foreign_amount",
datatable.TRANSACTION_DATA_TABLE_TAGS: "tags",
datatable.TRANSACTION_DATA_TABLE_DESCRIPTION: "description",
}
var fireflyIIITransactionTypeNameMapping = map[models.TransactionType]string{
models.TRANSACTION_TYPE_MODIFY_BALANCE: "Opening balance",
models.TRANSACTION_TYPE_INCOME: "Deposit",
models.TRANSACTION_TYPE_EXPENSE: "Withdrawal",
models.TRANSACTION_TYPE_TRANSFER: "Transfer",
}
// fireflyIIITransactionDataCsvImporter defines the structure of firefly III csv importer for transaction data
type fireflyIIITransactionDataCsvImporter struct{}
// Initialize a firefly III transaction data csv file importer singleton instance
@@ -16,25 +37,18 @@ var (
FireflyIIITransactionDataCsvImporter = &fireflyIIITransactionDataCsvImporter{}
)
// ParseImportedData returns the imported data by parsing the firefly iii transaction csv data
// ParseImportedData returns the imported data by parsing the firefly III transaction csv data
func (c *fireflyIIITransactionDataCsvImporter) 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) {
reader := bytes.NewReader(data)
dataTable, err := createNewFireflyIIITransactionPlainTextDataTable(
ctx,
reader,
)
dataTable, err := datatable.CreateNewDefaultCsvDataTable(ctx, reader)
if err != nil {
return nil, nil, nil, nil, nil, nil, err
}
dataTableImporter := datatable.CreateNewImporter(
dataTable.GetDataColumnMapping(),
fireflyIIITransactionTypeNameMapping,
"",
",",
)
transactionRowParser := createFireflyIIITransactionDataRowParser()
transactionDataTable := datatable.CreateImportedTransactionDataTableWithRowParser(dataTable, fireflyIIITransactionDataColumnNameMapping, transactionRowParser)
dataTableImporter := datatable.CreateNewImporter(fireflyIIITransactionTypeNameMapping, "", ",")
return dataTableImporter.ParseImportedData(ctx, user, dataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
}
@@ -114,6 +114,34 @@ func TestFireFlyIIICsvFileConverterParseImportedData_ParseInvalidType(t *testing
assert.EqualError(t, err, errs.ErrTransactionTypeInvalid.Message)
}
func TestFireFlyIIICsvFileConverterParseImportedData_ParseValidTimezone(t *testing.T) {
converter := FireflyIIITransactionDataCsvImporter
context := core.NewNullContext()
user := &models.User{
Uid: 1234567890,
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
"Withdrawal,-1.00,2024-09-01T12:34:56-10:00,\"Test Account\",\"A expense account\",\"Test Category\""), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725230096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
"Withdrawal,-1.00,2024-09-01T12:34:56+00:00,\"Test Account\",\"A expense account\",\"Test Category\""), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725194096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
"Withdrawal,-1.00,2024-09-01T12:34:56+12:45,\"Test Account\",\"A expense account\",\"Test Category\""), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725148196), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
}
func TestFireFlyIIICsvFileConverterParseImportedData_ParseValidAccountCurrency(t *testing.T) {
converter := FireflyIIITransactionDataCsvImporter
context := core.NewNullContext()
@@ -1,304 +0,0 @@
package fireflyIII
import (
"encoding/csv"
"io"
"time"
"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 fireflyIIITransactionSupportedColumns = []datatable.DataTableColumn{
datatable.DATA_TABLE_TRANSACTION_TIME,
datatable.DATA_TABLE_TRANSACTION_TYPE,
datatable.DATA_TABLE_SUB_CATEGORY,
datatable.DATA_TABLE_ACCOUNT_NAME,
datatable.DATA_TABLE_ACCOUNT_CURRENCY,
datatable.DATA_TABLE_AMOUNT,
datatable.DATA_TABLE_RELATED_ACCOUNT_NAME,
datatable.DATA_TABLE_RELATED_ACCOUNT_CURRENCY,
datatable.DATA_TABLE_RELATED_AMOUNT,
datatable.DATA_TABLE_TAGS,
datatable.DATA_TABLE_DESCRIPTION,
}
var fireflyIIITransactionTypeNameMapping = map[models.TransactionType]string{
models.TRANSACTION_TYPE_MODIFY_BALANCE: "Opening balance",
models.TRANSACTION_TYPE_INCOME: "Deposit",
models.TRANSACTION_TYPE_EXPENSE: "Withdrawal",
models.TRANSACTION_TYPE_TRANSFER: "Transfer",
}
// fireflyIIITransactionPlainTextDataTable defines the structure of firefly III transaction plain text data table
type fireflyIIITransactionPlainTextDataTable struct {
allOriginalLines [][]string
originalHeaderLineColumnNames []string
originalColumnIndex map[datatable.DataTableColumn]int
}
// fireflyIIITransactionPlainTextDataRow defines the structure of firefly III transaction plain text data row
type fireflyIIITransactionPlainTextDataRow struct {
dataTable *fireflyIIITransactionPlainTextDataTable
originalItems []string
finalItems map[datatable.DataTableColumn]string
}
// fireflyIIITransactionPlainTextDataRowIterator defines the structure of firefly III transaction plain text data row iterator
type fireflyIIITransactionPlainTextDataRowIterator struct {
dataTable *fireflyIIITransactionPlainTextDataTable
currentIndex int
}
// DataRowCount returns the total count of data row
func (t *fireflyIIITransactionPlainTextDataTable) DataRowCount() int {
if len(t.allOriginalLines) < 1 {
return 0
}
return len(t.allOriginalLines) - 1
}
// GetDataColumnMapping returns data column map for data importer
func (t *fireflyIIITransactionPlainTextDataTable) GetDataColumnMapping() map[datatable.DataTableColumn]string {
dataColumnMapping := make(map[datatable.DataTableColumn]string, len(fireflyIIITransactionSupportedColumns))
for i := 0; i < len(fireflyIIITransactionSupportedColumns); i++ {
column := fireflyIIITransactionSupportedColumns[i]
dataColumnMapping[column] = utils.IntToString(int(column))
}
return dataColumnMapping
}
// HeaderLineColumnNames returns the header column name list
func (t *fireflyIIITransactionPlainTextDataTable) HeaderLineColumnNames() []string {
columnIndexes := make([]string, len(fireflyIIITransactionSupportedColumns))
for i := 0; i < len(fireflyIIITransactionSupportedColumns); i++ {
column := fireflyIIITransactionSupportedColumns[i]
if t.originalColumnIndex[column] >= 0 {
columnIndexes[i] = utils.IntToString(int(column))
} else {
columnIndexes[i] = "-1"
}
}
return columnIndexes
}
// DataRowIterator returns the iterator of data row
func (t *fireflyIIITransactionPlainTextDataTable) DataRowIterator() datatable.ImportedDataRowIterator {
return &fireflyIIITransactionPlainTextDataRowIterator{
dataTable: t,
currentIndex: 0,
}
}
// IsValid returns whether this row contains valid data for importing
func (r *fireflyIIITransactionPlainTextDataRow) IsValid() bool {
return true
}
// ColumnCount returns the total count of column in this data row
func (r *fireflyIIITransactionPlainTextDataRow) ColumnCount() int {
return len(fireflyIIITransactionSupportedColumns)
}
// GetData returns the data in the specified column index
func (r *fireflyIIITransactionPlainTextDataRow) GetData(columnIndex int) string {
if columnIndex >= len(fireflyIIITransactionSupportedColumns) {
return ""
}
dataColumn := fireflyIIITransactionSupportedColumns[columnIndex]
return r.finalItems[dataColumn]
}
// GetTime returns the time in the specified column index
func (r *fireflyIIITransactionPlainTextDataRow) GetTime(columnIndex int, timezoneOffset int16) (time.Time, error) {
return utils.ParseFromLongDateTimeWithTimezone(r.GetData(columnIndex))
}
// GetTimezoneOffset returns the time zone offset in the specified column index
func (r *fireflyIIITransactionPlainTextDataRow) GetTimezoneOffset(columnIndex int) (*time.Location, error) {
return nil, errs.ErrNotSupported
}
// HasNext returns whether the iterator does not reach the end
func (t *fireflyIIITransactionPlainTextDataRowIterator) HasNext() bool {
return t.currentIndex+1 < len(t.dataTable.allOriginalLines)
}
// Next returns the next imported data row
func (t *fireflyIIITransactionPlainTextDataRowIterator) Next(ctx core.Context, user *models.User) datatable.ImportedDataRow {
if t.currentIndex+1 >= len(t.dataTable.allOriginalLines) {
return nil
}
t.currentIndex++
rowItems := t.dataTable.allOriginalLines[t.currentIndex]
finalItems := t.dataTable.parseTransactionData(rowItems)
return &fireflyIIITransactionPlainTextDataRow{
dataTable: t.dataTable,
originalItems: rowItems,
finalItems: finalItems,
}
}
func (t *fireflyIIITransactionPlainTextDataTable) parseTransactionData(items []string) map[datatable.DataTableColumn]string {
data := make(map[datatable.DataTableColumn]string, 12)
data[datatable.DATA_TABLE_SUB_CATEGORY] = ""
for column, index := range t.originalColumnIndex {
if index >= 0 && index < len(items) {
data[column] = items[index]
}
}
// trim trailing zero in decimal
if data[datatable.DATA_TABLE_AMOUNT] != "" {
data[datatable.DATA_TABLE_AMOUNT] = utils.TrimTrailingZerosInDecimal(data[datatable.DATA_TABLE_AMOUNT])
amount, err := utils.ParseAmount(data[datatable.DATA_TABLE_AMOUNT])
if err == nil {
data[datatable.DATA_TABLE_AMOUNT] = utils.FormatAmount(-amount)
}
}
if data[datatable.DATA_TABLE_RELATED_AMOUNT] != "" {
data[datatable.DATA_TABLE_RELATED_AMOUNT] = utils.TrimTrailingZerosInDecimal(data[datatable.DATA_TABLE_RELATED_AMOUNT])
amount, err := utils.ParseAmount(data[datatable.DATA_TABLE_RELATED_AMOUNT])
if err == nil {
data[datatable.DATA_TABLE_RELATED_AMOUNT] = utils.FormatAmount(-amount)
}
} else {
data[datatable.DATA_TABLE_RELATED_AMOUNT] = data[datatable.DATA_TABLE_AMOUNT]
}
// the related account currency field is foreign currency in firefly iii actually
if data[datatable.DATA_TABLE_RELATED_ACCOUNT_CURRENCY] == "" {
data[datatable.DATA_TABLE_RELATED_ACCOUNT_CURRENCY] = data[datatable.DATA_TABLE_ACCOUNT_CURRENCY]
}
// the destination account of modify balance transaction in firefly iii is the asset account
if data[datatable.DATA_TABLE_TRANSACTION_TYPE] == fireflyIIITransactionTypeNameMapping[models.TRANSACTION_TYPE_MODIFY_BALANCE] {
data[datatable.DATA_TABLE_ACCOUNT_NAME] = data[datatable.DATA_TABLE_RELATED_ACCOUNT_NAME]
}
// the destination account of income transaction in firefly iii is the asset account
if data[datatable.DATA_TABLE_TRANSACTION_TYPE] == fireflyIIITransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME] {
data[datatable.DATA_TABLE_ACCOUNT_NAME] = data[datatable.DATA_TABLE_RELATED_ACCOUNT_NAME]
}
return data
}
func createNewFireflyIIITransactionPlainTextDataTable(ctx core.Context, reader io.Reader) (*fireflyIIITransactionPlainTextDataTable, error) {
allOriginalLines, err := parseAllLinesFromFireflyIIITransactionPlainText(ctx, reader)
if err != nil {
return nil, err
}
if len(allOriginalLines) < 2 {
log.Errorf(ctx, "[fireflyiii_transaction_data_plain_text_data_table.createNewFireflyIIITransactionPlainTextDataTable] cannot parse import data, because data table row count is less 1")
return nil, errs.ErrNotFoundTransactionDataInFile
}
originalHeaderItems := allOriginalLines[0]
originalHeaderItemMap := make(map[string]int)
for i := 0; i < len(originalHeaderItems); i++ {
originalHeaderItemMap[originalHeaderItems[i]] = i
}
typeColumnIdx, typeColumnExists := originalHeaderItemMap["type"]
amountColumnIdx, amountColumnExists := originalHeaderItemMap["amount"]
foreignAmountColumnIdx, foreignAmountColumnExists := originalHeaderItemMap["foreign_amount"]
currencyColumnIdx, currencyColumnExists := originalHeaderItemMap["currency_code"]
foreignCurrencyColumnIdx, foreignCurrencyColumnExists := originalHeaderItemMap["foreign_currency_code"]
descriptionColumnIdx, descriptionColumnExists := originalHeaderItemMap["description"]
dateColumnIdx, dateColumnExists := originalHeaderItemMap["date"]
sourceNameColumnIdx, sourceNameColumnExists := originalHeaderItemMap["source_name"]
destinationNameColumnIdx, destinationNameColumnExists := originalHeaderItemMap["destination_name"]
categoryColumnIdx, categoryColumnExists := originalHeaderItemMap["category"]
tagsColumnIdx, tagsColumnExists := originalHeaderItemMap["tags"]
if !typeColumnExists || !amountColumnExists || !dateColumnExists || !sourceNameColumnExists || !destinationNameColumnExists || !categoryColumnExists {
log.Errorf(ctx, "[fireflyiii_transaction_data_plain_text_data_table.createNewFireflyIIITransactionPlainTextDataTable] cannot parse firefly III csv data, because missing essential columns in header row")
return nil, errs.ErrMissingRequiredFieldInHeaderRow
}
if !foreignAmountColumnExists {
foreignAmountColumnIdx = -1
}
if !currencyColumnExists {
currencyColumnIdx = -1
}
if !foreignCurrencyColumnExists {
foreignCurrencyColumnIdx = -1
}
if !descriptionColumnExists {
descriptionColumnIdx = -1
}
if !tagsColumnExists {
tagsColumnIdx = -1
}
return &fireflyIIITransactionPlainTextDataTable{
allOriginalLines: allOriginalLines,
originalHeaderLineColumnNames: originalHeaderItems,
originalColumnIndex: map[datatable.DataTableColumn]int{
datatable.DATA_TABLE_TRANSACTION_TIME: dateColumnIdx,
datatable.DATA_TABLE_TRANSACTION_TYPE: typeColumnIdx,
datatable.DATA_TABLE_SUB_CATEGORY: categoryColumnIdx,
datatable.DATA_TABLE_ACCOUNT_NAME: sourceNameColumnIdx,
datatable.DATA_TABLE_ACCOUNT_CURRENCY: currencyColumnIdx,
datatable.DATA_TABLE_AMOUNT: amountColumnIdx,
datatable.DATA_TABLE_RELATED_ACCOUNT_NAME: destinationNameColumnIdx,
datatable.DATA_TABLE_RELATED_ACCOUNT_CURRENCY: foreignCurrencyColumnIdx,
datatable.DATA_TABLE_RELATED_AMOUNT: foreignAmountColumnIdx,
datatable.DATA_TABLE_TAGS: tagsColumnIdx,
datatable.DATA_TABLE_DESCRIPTION: descriptionColumnIdx,
},
}, nil
}
func parseAllLinesFromFireflyIIITransactionPlainText(ctx core.Context, reader io.Reader) ([][]string, error) {
csvReader := csv.NewReader(reader)
csvReader.FieldsPerRecord = -1
allOriginalLines := make([][]string, 0)
for {
items, err := csvReader.Read()
if err == io.EOF {
break
}
if err != nil {
log.Errorf(ctx, "[fireflyiii_transaction_data_plain_text_data_table.parseAllLinesFromFireflyIIITransactionPlainText] cannot parse firefly III csv data, because %s", err.Error())
return nil, errs.ErrInvalidCSVFile
}
allOriginalLines = append(allOriginalLines, items)
}
return allOriginalLines, nil
}
@@ -0,0 +1,89 @@
package fireflyIII
import (
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
// fireflyIIITransactionDataRowParser defines the structure of firefly III transaction data row parser
type fireflyIIITransactionDataRowParser struct {
}
// GetAddedColumns returns the added columns after converting the data row
func (p *fireflyIIITransactionDataRowParser) GetAddedColumns() []datatable.TransactionDataTableColumn {
return []datatable.TransactionDataTableColumn{
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE,
}
}
// Parse returns the converted transaction data row
func (p *fireflyIIITransactionDataRowParser) Parse(data map[datatable.TransactionDataTableColumn]string) (rowData map[datatable.TransactionDataTableColumn]string, rowDataValid bool, err error) {
rowData = make(map[datatable.TransactionDataTableColumn]string, len(data))
rowData[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = ""
for column, value := range data {
rowData[column] = value
}
// parse long date time and timezone
if rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] != "" {
dateTime, err := utils.ParseFromLongDateTimeWithTimezone(rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME])
if err != nil {
return nil, false, errs.ErrTransactionTimeInvalid
}
rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] = utils.FormatUnixTimeToLongDateTime(dateTime.Unix(), dateTime.Location())
rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = utils.FormatTimezoneOffset(dateTime.Location())
}
// trim trailing zero in decimal
if rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT] != "" {
rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.TrimTrailingZerosInDecimal(rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT])
amount, err := utils.ParseAmount(rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT])
if err != nil {
return nil, false, errs.ErrAmountInvalid
}
rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(-amount)
}
if rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT] != "" {
rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT] = utils.TrimTrailingZerosInDecimal(rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT])
amount, err := utils.ParseAmount(rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT])
if err != nil {
return nil, false, errs.ErrAmountInvalid
}
rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT] = utils.FormatAmount(-amount)
} else {
rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT] = rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT]
}
// the related account currency field is foreign currency in firefly III actually
if rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_CURRENCY] == "" {
rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_CURRENCY] = rowData[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY]
}
// the destination account of modify balance transaction in firefly III is the asset account
if rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] == fireflyIIITransactionTypeNameMapping[models.TRANSACTION_TYPE_MODIFY_BALANCE] {
rowData[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME]
}
// the destination account of income transaction in firefly III is the asset account
if rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] == fireflyIIITransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME] {
rowData[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME]
}
return rowData, true, nil
}
// createFireflyIIITransactionDataRowParser returns firefly III transaction data row parser
func createFireflyIIITransactionDataRowParser() datatable.TransactionDataRowParser {
return &fireflyIIITransactionDataRowParser{}
}