diff --git a/pkg/converters/iif/iif_data_reader.go b/pkg/converters/iif/iif_data_reader.go index b92eea5f..054a2c4d 100644 --- a/pkg/converters/iif/iif_data_reader.go +++ b/pkg/converters/iif/iif_data_reader.go @@ -129,11 +129,26 @@ func (r *iifDataReader) read(ctx core.Context) ([]*iifAccountDataset, []*iifTran } } else if lastLineSign == iifTransactionLineSignColumnName || lastLineSign == iifTransactionSplitLineSignColumnName { if items[0] == iifTransactionSplitLineSignColumnName { + if currentTransactionData == nil { + log.Errorf(ctx, "[iif_data_reader.read] expected current transaction data is not nil, but read \"%s\"", items[0]) + return nil, nil, errs.ErrInvalidIIFFile + } + currentTransactionData.splitData = append(currentTransactionData.splitData, &iifTransactionSplitData{ dataItems: items, }) lastLineSign = items[0] } else if items[0] == iifTransactionEndLineSignColumnName { + if currentTransactionData == nil { + log.Errorf(ctx, "[iif_data_reader.read] expected current transaction data is not nil, but read \"%s\"", items[0]) + return nil, nil, errs.ErrInvalidIIFFile + } + + if len(currentTransactionData.splitData) < 1 { + log.Errorf(ctx, "[iif_data_reader.read] expected reading transaction split line, but read \"%s\"", items[0]) + return nil, nil, errs.ErrInvalidIIFFile + } + currentTransactionDataset.transactions = append(currentTransactionDataset.transactions, currentTransactionData) lastLineSign = "" } else { diff --git a/pkg/converters/iif/iif_transaction_data_file_importer_test.go b/pkg/converters/iif/iif_transaction_data_file_importer_test.go index dab4115a..ae2639ab 100644 --- a/pkg/converters/iif/iif_transaction_data_file_importer_test.go +++ b/pkg/converters/iif/iif_transaction_data_file_importer_test.go @@ -517,7 +517,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseDescription(t *testing.T) assert.Equal(t, "Test", allNewTransactions[0].Comment) } -func TestIIFTransactionDataFileParseImportedData_NotSupportedToParseSplitTransaction(t *testing.T) { +func TestIIFTransactionDataFileParseImportedData_ParseSplitTransaction(t *testing.T) { converter := IifTransactionDataFileImporter context := core.NewNullContext() @@ -526,11 +526,218 @@ func TestIIFTransactionDataFileParseImportedData_NotSupportedToParseSplitTransac DefaultCurrency: "CNY", } + allNewTransactions, allNewAccounts, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( + "!ACCNT\tNAME\tACCNTTYPE\n"+ + "ACCNT\tTest Category\tINC\n"+ + "ACCNT\tTest Category2\tEXP\n"+ + "!TRNS\tDATE\tACCNT\tAMOUNT\n"+ + "!SPL\tDATE\tACCNT\tAMOUNT\n"+ + "!ENDTRNS\t\t\t\n"+ + "TRNS\t09/01/2024\tTest Account\t123.45\n"+ + "SPL\t09/01/2024\tTest Category\t-23.45\n"+ + "SPL\t09/01/2024\tTest Account2\t-100.00\n"+ + "ENDTRNS\t\t\t\n"+ + "TRNS\t09/02/2024\tTest Account\t-100.00\n"+ + "SPL\t09/02/2024\tTest Category2\t30.00\n"+ + "SPL\t09/02/2024\tTest Account3\t20.00\n"+ + "SPL\t09/02/2024\tTest Account4\t50.00\n"+ + "ENDTRNS\t\t\t\n"+ + "TRNS\t09/03/2024\tTest Account\t100.00\n"+ + "SPL\t09/03/2024\tTest Account2\t-100.00\n"+ + "ENDTRNS\t\t\t\n"+ + "TRNS\t09/04/2024\tTest Category\t-100.00\n"+ + "SPL\t09/04/2024\tTest Account\t40.00\n"+ + "SPL\t09/04/2024\tTest Account2\t60.00\n"+ + "ENDTRNS\t\t\t\n"+ + "TRNS\t09/05/2024\tTest Category2\t100.00\n"+ + "SPL\t09/05/2024\tTest Account3\t-40.00\n"+ + "SPL\t09/05/2024\tTest Account4\t-60.00\n"+ + "ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil) + + assert.Nil(t, err) + + assert.Equal(t, 10, len(allNewTransactions)) + assert.Equal(t, 4, len(allNewAccounts)) + + assert.Equal(t, int64(1234567890), allNewTransactions[0].Uid) + assert.Equal(t, models.TRANSACTION_DB_TYPE_INCOME, allNewTransactions[0].Type) + assert.Equal(t, int64(1725148800), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime)) + assert.Equal(t, int64(2345), allNewTransactions[0].Amount) + assert.Equal(t, "Test Account", allNewTransactions[0].OriginalSourceAccountName) + assert.Equal(t, "Test Category", allNewTransactions[0].OriginalCategoryName) + + assert.Equal(t, int64(1234567890), allNewTransactions[1].Uid) + assert.Equal(t, models.TRANSACTION_DB_TYPE_TRANSFER_OUT, allNewTransactions[1].Type) + assert.Equal(t, int64(1725148800), utils.GetUnixTimeFromTransactionTime(allNewTransactions[1].TransactionTime)) + assert.Equal(t, int64(10000), allNewTransactions[1].Amount) + assert.Equal(t, "Test Account2", allNewTransactions[1].OriginalSourceAccountName) + assert.Equal(t, "Test Account", allNewTransactions[1].OriginalDestinationAccountName) + assert.Equal(t, "", allNewTransactions[1].OriginalCategoryName) + + assert.Equal(t, int64(1234567890), allNewTransactions[2].Uid) + assert.Equal(t, models.TRANSACTION_DB_TYPE_EXPENSE, allNewTransactions[2].Type) + assert.Equal(t, int64(1725235200), utils.GetUnixTimeFromTransactionTime(allNewTransactions[2].TransactionTime)) + assert.Equal(t, int64(3000), allNewTransactions[2].Amount) + assert.Equal(t, "Test Account", allNewTransactions[2].OriginalSourceAccountName) + assert.Equal(t, "Test Category2", allNewTransactions[2].OriginalCategoryName) + + assert.Equal(t, int64(1234567890), allNewTransactions[3].Uid) + assert.Equal(t, models.TRANSACTION_DB_TYPE_TRANSFER_OUT, allNewTransactions[3].Type) + assert.Equal(t, int64(1725235200), utils.GetUnixTimeFromTransactionTime(allNewTransactions[3].TransactionTime)) + assert.Equal(t, int64(2000), allNewTransactions[3].Amount) + assert.Equal(t, "Test Account", allNewTransactions[3].OriginalSourceAccountName) + assert.Equal(t, "Test Account3", allNewTransactions[3].OriginalDestinationAccountName) + assert.Equal(t, "", allNewTransactions[3].OriginalCategoryName) + + assert.Equal(t, int64(1234567890), allNewTransactions[4].Uid) + assert.Equal(t, models.TRANSACTION_DB_TYPE_TRANSFER_OUT, allNewTransactions[4].Type) + assert.Equal(t, int64(1725235200), utils.GetUnixTimeFromTransactionTime(allNewTransactions[4].TransactionTime)) + assert.Equal(t, int64(5000), allNewTransactions[4].Amount) + assert.Equal(t, "Test Account", allNewTransactions[4].OriginalSourceAccountName) + assert.Equal(t, "Test Account4", allNewTransactions[4].OriginalDestinationAccountName) + assert.Equal(t, "", allNewTransactions[4].OriginalCategoryName) + + assert.Equal(t, int64(1234567890), allNewTransactions[5].Uid) + assert.Equal(t, models.TRANSACTION_DB_TYPE_TRANSFER_OUT, allNewTransactions[5].Type) + assert.Equal(t, int64(1725321600), utils.GetUnixTimeFromTransactionTime(allNewTransactions[5].TransactionTime)) + assert.Equal(t, int64(10000), allNewTransactions[5].Amount) + assert.Equal(t, "Test Account2", allNewTransactions[5].OriginalSourceAccountName) + assert.Equal(t, "Test Account", allNewTransactions[5].OriginalDestinationAccountName) + assert.Equal(t, "", allNewTransactions[5].OriginalCategoryName) + + assert.Equal(t, int64(1234567890), allNewTransactions[6].Uid) + assert.Equal(t, models.TRANSACTION_DB_TYPE_INCOME, allNewTransactions[6].Type) + assert.Equal(t, int64(1725408000), utils.GetUnixTimeFromTransactionTime(allNewTransactions[6].TransactionTime)) + assert.Equal(t, int64(4000), allNewTransactions[6].Amount) + assert.Equal(t, "Test Account", allNewTransactions[6].OriginalSourceAccountName) + assert.Equal(t, "Test Category", allNewTransactions[6].OriginalCategoryName) + + assert.Equal(t, int64(1234567890), allNewTransactions[7].Uid) + assert.Equal(t, models.TRANSACTION_DB_TYPE_INCOME, allNewTransactions[7].Type) + assert.Equal(t, int64(1725408000), utils.GetUnixTimeFromTransactionTime(allNewTransactions[7].TransactionTime)) + assert.Equal(t, int64(6000), allNewTransactions[7].Amount) + assert.Equal(t, "Test Account2", allNewTransactions[7].OriginalSourceAccountName) + assert.Equal(t, "Test Category", allNewTransactions[7].OriginalCategoryName) + + assert.Equal(t, int64(1234567890), allNewTransactions[8].Uid) + assert.Equal(t, models.TRANSACTION_DB_TYPE_EXPENSE, allNewTransactions[8].Type) + assert.Equal(t, int64(1725494400), utils.GetUnixTimeFromTransactionTime(allNewTransactions[8].TransactionTime)) + assert.Equal(t, int64(4000), allNewTransactions[8].Amount) + assert.Equal(t, "Test Account3", allNewTransactions[8].OriginalSourceAccountName) + assert.Equal(t, "Test Category2", allNewTransactions[8].OriginalCategoryName) + + assert.Equal(t, int64(1234567890), allNewTransactions[9].Uid) + assert.Equal(t, models.TRANSACTION_DB_TYPE_EXPENSE, allNewTransactions[9].Type) + assert.Equal(t, int64(1725494400), utils.GetUnixTimeFromTransactionTime(allNewTransactions[9].TransactionTime)) + assert.Equal(t, int64(6000), allNewTransactions[9].Amount) + assert.Equal(t, "Test Account4", allNewTransactions[9].OriginalSourceAccountName) + assert.Equal(t, "Test Category2", allNewTransactions[9].OriginalCategoryName) + + assert.Equal(t, int64(1234567890), allNewAccounts[0].Uid) + assert.Equal(t, "Test Account", allNewAccounts[0].Name) + assert.Equal(t, "CNY", allNewAccounts[0].Currency) + + assert.Equal(t, int64(1234567890), allNewAccounts[1].Uid) + assert.Equal(t, "Test Account2", allNewAccounts[1].Name) + assert.Equal(t, "CNY", allNewAccounts[1].Currency) + + assert.Equal(t, int64(1234567890), allNewAccounts[2].Uid) + assert.Equal(t, "Test Account3", allNewAccounts[2].Name) + assert.Equal(t, "CNY", allNewAccounts[2].Currency) + + assert.Equal(t, int64(1234567890), allNewAccounts[3].Uid) + assert.Equal(t, "Test Account4", allNewAccounts[3].Name) + assert.Equal(t, "CNY", allNewAccounts[3].Currency) +} + +func TestIIFTransactionDataFileParseImportedData_ParseSplitTransactionDescription(t *testing.T) { + converter := IifTransactionDataFileImporter + context := core.NewNullContext() + + user := &models.User{ + Uid: 1234567890, + DefaultCurrency: "CNY", + } + + allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( + "!TRNS\tDATE\tACCNT\tNAME\tAMOUNT\tMEMO\n"+ + "!SPL\tDATE\tACCNT\tNAME\tAMOUNT\tMEMO\n"+ + "!ENDTRNS\t\t\t\t\t\n"+ + "TRNS\t09/01/2024\tTest Account\t\"Test\"\t123.45\t\"foo bar\t#test\"\n"+ + "SPL\t09/01/2024\tTest Account2\t\t-100.00\t\"foo\ttest#bar\"\n"+ + "SPL\t09/01/2024\tTest Account3\t\t-23.45\t\n"+ + "ENDTRNS\t\t\t\t\t\n"), 0, nil, nil, nil, nil, nil) + + assert.Nil(t, err) + assert.Equal(t, 2, len(allNewTransactions)) + assert.Equal(t, "foo\ttest#bar", allNewTransactions[0].Comment) + assert.Equal(t, "foo bar\t#test", allNewTransactions[1].Comment) + + allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( + "!TRNS\tDATE\tACCNT\tNAME\tAMOUNT\tMEMO\n"+ + "!SPL\tDATE\tACCNT\tNAME\tAMOUNT\tMEMO\n"+ + "!ENDTRNS\t\t\t\t\t\n"+ + "TRNS\t09/01/2024\tTest Account\tTest\t123.45\t\n"+ + "SPL\t09/01/2024\tTest Account2\t\t-100.00\t\"test\"\n"+ + "SPL\t09/01/2024\tTest Account3\tfoo\t-12.34\t\n"+ + "SPL\t09/01/2024\tTest Account4\t\t-11.11\t\n"+ + "ENDTRNS\t\t\t\t\t\n"), 0, nil, nil, nil, nil, nil) + + assert.Nil(t, err) + assert.Equal(t, 3, len(allNewTransactions)) + assert.Equal(t, "test", allNewTransactions[0].Comment) + assert.Equal(t, "foo", allNewTransactions[1].Comment) + assert.Equal(t, "Test", allNewTransactions[2].Comment) +} + +func TestIIFTransactionDataFileParseImportedData_NotSupportedSplitTransaction(t *testing.T) { + converter := IifTransactionDataFileImporter + context := core.NewNullContext() + + user := &models.User{ + Uid: 1234567890, + DefaultCurrency: "CNY", + } + + // Opening balance transaction _, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( + "!TRNS\tTRNSTYPE\tDATE\tACCNT\tAMOUNT\n"+ + "!SPL\tTRNSTYPE\tDATE\tACCNT\tAMOUNT\n"+ + "!ENDTRNS\t\t\t\t\n"+ + "TRNS\tBEGINBALCHECK\t09/01/2024\tTest Account\t123.45\n"+ + "SPL\tBEGINBALCHECK\t09/01/2024\tTest Account2\t-100.00\n"+ + "SPL\tBEGINBALCHECK\t09/01/2024\tTest Account3\t-23.45\n"+ + "ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil) + assert.EqualError(t, err, errs.ErrNotSupportedSplitTransactions.Message) + + // Transaction with invalid amount + _, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( + "!TRNS\tDATE\tACCNT\tAMOUNT\n"+ + "!SPL\tDATE\tACCNT\tAMOUNT\n"+ + "!ENDTRNS\t\t\t\n"+ + "TRNS\t09/01/2024\tTest Account\t123 45\n"+ + "SPL\t09/01/2024\tTest Account2\t-100.00\n"+ + "SPL\t09/01/2024\tTest Account3\t-23.45\n"+ + "ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil) + assert.EqualError(t, err, errs.ErrAmountInvalid.Message) + + // Transaction split data with invalid amount + _, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( "!TRNS\tDATE\tACCNT\tAMOUNT\n"+ "!SPL\tDATE\tACCNT\tAMOUNT\n"+ "!ENDTRNS\t\t\t\n"+ "TRNS\t09/01/2024\tTest Account\t123.45\n"+ + "SPL\t09/01/2024\tTest Account2\t-100 00\n"+ + "SPL\t09/01/2024\tTest Account3\t-23.45\n"+ + "ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil) + assert.EqualError(t, err, errs.ErrAmountInvalid.Message) + + // Transaction amount not equal to sum of split data amount + _, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( + "!TRNS\tDATE\tACCNT\tAMOUNT\n"+ + "!SPL\tDATE\tACCNT\tAMOUNT\n"+ + "!ENDTRNS\t\t\t\n"+ + "TRNS\t09/01/2024\tTest Account\t123.00\n"+ "SPL\t09/01/2024\tTest Account2\t-100.00\n"+ "SPL\t09/01/2024\tTest Account3\t-23.45\n"+ "ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil) @@ -546,7 +753,7 @@ func TestIIFTransactionDataFileParseImportedData_InvalidDataLines(t *testing.T) DefaultCurrency: "CNY", } - // Missing Transaction Line + //Missing Transaction Line _, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( "!TRNS\tDATE\tACCNT\tAMOUNT\n"+ "!SPL\tDATE\tACCNT\tAMOUNT\n"+ @@ -555,6 +762,14 @@ func TestIIFTransactionDataFileParseImportedData_InvalidDataLines(t *testing.T) "ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message) + // Missing Transaction And Split Line + _, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( + "!TRNS\tDATE\tACCNT\tAMOUNT\n"+ + "!SPL\tDATE\tACCNT\tAMOUNT\n"+ + "!ENDTRNS\t\t\t\n"+ + "ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil) + assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message) + // Missing Split Line _, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( "!TRNS\tDATE\tACCNT\tAMOUNT\n"+ diff --git a/pkg/converters/iif/iif_transaction_data_table.go b/pkg/converters/iif/iif_transaction_data_table.go index c3d885a0..4f93f59b 100644 --- a/pkg/converters/iif/iif_transaction_data_table.go +++ b/pkg/converters/iif/iif_transaction_data_table.go @@ -60,6 +60,7 @@ type iifTransactionDataRowIterator struct { dataTable *iifTransactionDataTable currentDatasetIndex int currentIndexInDataset int + currentSplitDataIndex int } // HasColumn returns whether the transaction data table has specified column @@ -73,8 +74,15 @@ func (t *iifTransactionDataTable) TransactionRowCount() int { totalDataRowCount := 0 for i := 0; i < len(t.transactionDatasets); i++ { - transactions := t.transactionDatasets[i] - totalDataRowCount += len(transactions.transactions) + datasets := t.transactionDatasets[i] + + for j := 0; j < len(datasets.transactions); j++ { + transaction := datasets.transactions[j] + + if transaction.splitData != nil { + totalDataRowCount += len(transaction.splitData) + } + } } return totalDataRowCount @@ -85,7 +93,8 @@ func (t *iifTransactionDataTable) TransactionRowIterator() datatable.Transaction return &iifTransactionDataRowIterator{ dataTable: t, currentDatasetIndex: 0, - currentIndexInDataset: -1, + currentIndexInDataset: 0, + currentSplitDataIndex: -1, } } @@ -117,6 +126,9 @@ func (t *iifTransactionDataRowIterator) HasNext() bool { if t.currentIndexInDataset+1 < len(currentDataset.transactions) { return true + } else if t.currentIndexInDataset < len(currentDataset.transactions) && + t.currentSplitDataIndex+1 < len(currentDataset.transactions[t.currentIndexInDataset].splitData) { + return true } for i := t.currentDatasetIndex + 1; i < len(allDatasets); i++ { @@ -135,20 +147,29 @@ func (t *iifTransactionDataRowIterator) HasNext() bool { // Next returns the next imported data row func (t *iifTransactionDataRowIterator) Next(ctx core.Context, user *models.User) (daraRow datatable.TransactionDataRow, err error) { allDatasets := t.dataTable.transactionDatasets - currentIndexInDataset := t.currentIndexInDataset for i := t.currentDatasetIndex; i < len(allDatasets); i++ { + foundNextRow := false dataset := allDatasets[i] - if currentIndexInDataset+1 < len(dataset.transactions) { + for j := t.currentIndexInDataset; j < len(dataset.transactions); j++ { + if t.currentSplitDataIndex+1 < len(dataset.transactions[j].splitData) { + t.currentSplitDataIndex++ + foundNextRow = true + break + } + t.currentIndexInDataset++ - currentIndexInDataset = t.currentIndexInDataset + t.currentSplitDataIndex = -1 + } + + if foundNextRow { break } t.currentDatasetIndex++ - t.currentIndexInDataset = -1 - currentIndexInDataset = -1 + t.currentIndexInDataset = 0 + t.currentSplitDataIndex = -1 } if t.currentDatasetIndex >= len(allDatasets) { @@ -162,9 +183,28 @@ func (t *iifTransactionDataRowIterator) Next(ctx core.Context, user *models.User } data := currentDataset.transactions[t.currentIndexInDataset] - rowItems, err := t.parseTransaction(ctx, user, currentDataset, data) + + if len(data.splitData) < 1 { + log.Errorf(ctx, "[iif_transaction_data_table.Next] cannot parsing transaction in row#%d (dataset#%d), because split data is empty", t.currentIndexInDataset, t.currentDatasetIndex) + return nil, errs.ErrInvalidIIFFile + } + + if t.currentSplitDataIndex >= len(data.splitData) { + return nil, nil + } + + if len(data.splitData) > 1 { + _, err := t.isSplitTransactionSupported(ctx, currentDataset, data) + + if err != nil { + return nil, err + } + } + + rowItems, err := t.parseTransaction(ctx, user, currentDataset, data, t.currentSplitDataIndex) if err != nil { + log.Errorf(ctx, "[iif_transaction_data_table.Next] cannot parsing transaction in row#%d-split#%d (dataset#%d), because %s", t.currentIndexInDataset, t.currentSplitDataIndex, t.currentDatasetIndex, err.Error()) return nil, err } @@ -174,13 +214,7 @@ func (t *iifTransactionDataRowIterator) Next(ctx core.Context, user *models.User }, nil } -func (t *iifTransactionDataRowIterator) parseTransaction(ctx core.Context, user *models.User, dataset *iifTransactionDataset, transactionData *iifTransactionData) (map[datatable.TransactionDataTableColumn]string, error) { - if len(transactionData.splitData) < 1 { - return nil, errs.ErrInvalidIIFFile - } else if len(transactionData.splitData) > 1 { - return nil, errs.ErrNotSupportedSplitTransactions - } - +func (t *iifTransactionDataRowIterator) parseTransaction(ctx core.Context, user *models.User, dataset *iifTransactionDataset, transactionData *iifTransactionData, splitDataIndex int) (map[datatable.TransactionDataTableColumn]string, error) { var err error data := make(map[datatable.TransactionDataTableColumn]string, len(iifTransactionSupportedColumns)) @@ -190,18 +224,18 @@ func (t *iifTransactionDataRowIterator) parseTransaction(ctx core.Context, user return nil, err } - transactionType, _ := dataset.getTransactionDataItemValue(transactionData, iifTransactionTypeColumnName) - accountName1, _ := dataset.getTransactionDataItemValue(transactionData, iifTransactionAccountNameColumnName) - accountName2, _ := dataset.getSplitDataItemValue(transactionData.splitData[0], iifTransactionAccountNameColumnName) - amount1, _ := dataset.getTransactionDataItemValue(transactionData, iifTransactionAmountColumnName) - amount2, _ := dataset.getSplitDataItemValue(transactionData.splitData[0], iifTransactionAmountColumnName) - amountNum1, err := utils.ParseAmount(strings.ReplaceAll(amount1, ",", "")) + transactionType, _ := dataset.getSplitDataItemValue(transactionData.splitData[splitDataIndex], iifTransactionTypeColumnName) + mainAccountName, _ := dataset.getTransactionDataItemValue(transactionData, iifTransactionAccountNameColumnName) + splitAccountName, _ := dataset.getSplitDataItemValue(transactionData.splitData[splitDataIndex], iifTransactionAccountNameColumnName) + mainAmount, _ := dataset.getTransactionDataItemValue(transactionData, iifTransactionAmountColumnName) + splitAmount, _ := dataset.getSplitDataItemValue(transactionData.splitData[splitDataIndex], iifTransactionAmountColumnName) + mainAmountNum, err := parseAmount(mainAmount) if err != nil { return nil, errs.ErrAmountInvalid } - amountNum2, err := utils.ParseAmount(strings.ReplaceAll(amount2, ",", "")) + splitAmountNum, err := parseAmount(splitAmount) if err != nil { return nil, errs.ErrAmountInvalid @@ -209,24 +243,35 @@ func (t *iifTransactionDataRowIterator) parseTransaction(ctx core.Context, user if transactionType == iifTransactionTypeBeginningBalance { // balance modification data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = iifTransactionTypeNameMapping[models.TRANSACTION_TYPE_MODIFY_BALANCE] - data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = accountName1 - data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(amountNum1) - } else if t.dataTable.incomeAccountNames[accountName1] || t.dataTable.incomeAccountNames[accountName2] { // income + data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = mainAccountName + data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(mainAmountNum) + } else if (t.dataTable.incomeAccountNames[mainAccountName] && !t.dataTable.incomeAccountNames[splitAccountName] && !t.dataTable.expenseAccountNames[splitAccountName]) || + (t.dataTable.incomeAccountNames[splitAccountName] && !t.dataTable.incomeAccountNames[mainAccountName] && !t.dataTable.expenseAccountNames[mainAccountName]) { // income data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = iifTransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME] categoryName := "" accountName := "" amountNum := int64(0) - if t.dataTable.incomeAccountNames[accountName1] && !t.dataTable.incomeAccountNames[accountName2] { - categoryName = accountName1 - accountName = accountName2 - amountNum = amountNum2 - } else if t.dataTable.incomeAccountNames[accountName2] && !t.dataTable.incomeAccountNames[accountName1] { - categoryName = accountName2 - accountName = accountName1 - amountNum = amountNum1 + if t.dataTable.incomeAccountNames[mainAccountName] && !t.dataTable.incomeAccountNames[splitAccountName] { + categoryName = mainAccountName + accountName = splitAccountName + + if len(transactionData.splitData) > 1 { + amountNum = splitAmountNum + } else { + amountNum = -mainAmountNum + } + } else if t.dataTable.incomeAccountNames[splitAccountName] && !t.dataTable.incomeAccountNames[mainAccountName] { + categoryName = splitAccountName + accountName = mainAccountName + + if len(transactionData.splitData) > 1 { + amountNum = -splitAmountNum + } else { + amountNum = mainAmountNum + } } else { - log.Errorf(ctx, "[iif_transaction_data_table.parseTransaction] cannot parse transaction, because two accounts \"%s\" and \"%s\" are all income account", accountName1, accountName2) + log.Errorf(ctx, "[iif_transaction_data_table.parseTransaction] cannot parse transaction, because main account \"%s\" and split account \"%s\" are all income account", mainAccountName, splitAccountName) return nil, errs.ErrInvalidIIFFile } @@ -241,22 +286,33 @@ func (t *iifTransactionDataRowIterator) parseTransaction(ctx core.Context, user data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = accountName data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(amountNum) - } else if t.dataTable.expenseAccountNames[accountName1] || t.dataTable.expenseAccountNames[accountName2] { // expense + } else if (t.dataTable.expenseAccountNames[mainAccountName] && !t.dataTable.expenseAccountNames[splitAccountName] && !t.dataTable.incomeAccountNames[splitAccountName]) || + (t.dataTable.expenseAccountNames[splitAccountName] && !t.dataTable.expenseAccountNames[mainAccountName] && !t.dataTable.incomeAccountNames[mainAccountName]) { // expense data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = iifTransactionTypeNameMapping[models.TRANSACTION_TYPE_EXPENSE] categoryName := "" accountName := "" amountNum := int64(0) - if t.dataTable.expenseAccountNames[accountName1] && !t.dataTable.expenseAccountNames[accountName2] { - categoryName = accountName1 - accountName = accountName2 - amountNum = amountNum2 - } else if t.dataTable.expenseAccountNames[accountName2] && !t.dataTable.expenseAccountNames[accountName1] { - categoryName = accountName2 - accountName = accountName1 - amountNum = amountNum1 + if t.dataTable.expenseAccountNames[mainAccountName] && !t.dataTable.expenseAccountNames[splitAccountName] { + categoryName = mainAccountName + accountName = splitAccountName + + if len(transactionData.splitData) > 1 { + amountNum = -splitAmountNum + } else { + amountNum = mainAmountNum + } + } else if t.dataTable.expenseAccountNames[splitAccountName] && !t.dataTable.expenseAccountNames[mainAccountName] { + categoryName = splitAccountName + accountName = mainAccountName + + if len(transactionData.splitData) > 1 { + amountNum = splitAmountNum + } else { + amountNum = -mainAmountNum + } } else { - log.Errorf(ctx, "[iif_transaction_data_table.parseTransaction] cannot parse transaction, because two accounts \"%s\" and \"%s\" are all expense account", accountName1, accountName2) + log.Errorf(ctx, "[iif_transaction_data_table.parseTransaction] cannot parse transaction, because main account \"%s\" and split account \"%s\" are all expense account", mainAccountName, splitAccountName) return nil, errs.ErrInvalidIIFFile } @@ -270,26 +326,57 @@ func (t *iifTransactionDataRowIterator) parseTransaction(ctx core.Context, user } data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = accountName - data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(-amountNum) + data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(amountNum) } else { data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = iifTransactionTypeNameMapping[models.TRANSACTION_TYPE_TRANSFER] data[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = "" + amountNum := int64(0) + relatedAmountNum := int64(0) + mainAccountTransferToSplitAccount := false - if amountNum1 >= 0 { - data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = accountName2 - data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(-amountNum2) - data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = accountName1 - data[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT] = utils.FormatAmount(amountNum1) - } else if amountNum2 >= 0 { - data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = accountName1 - data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(-amountNum1) - data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = accountName2 - data[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT] = utils.FormatAmount(amountNum2) + if len(transactionData.splitData) > 1 { + amountNum = splitAmountNum + relatedAmountNum = splitAmountNum + mainAccountTransferToSplitAccount = amountNum >= 0 + } else { + if mainAmountNum >= 0 { + amountNum = splitAmountNum + relatedAmountNum = mainAmountNum + mainAccountTransferToSplitAccount = false + } else if splitAmountNum >= 0 { + amountNum = mainAmountNum + relatedAmountNum = splitAmountNum + mainAccountTransferToSplitAccount = true + } + } + + if mainAccountTransferToSplitAccount { + data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = mainAccountName + data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = splitAccountName + } else { + data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = splitAccountName + data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = mainAccountName + } + + if amountNum >= 0 { + data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(amountNum) + } else { + data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(-amountNum) + } + + if relatedAmountNum >= 0 { + data[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT] = utils.FormatAmount(relatedAmountNum) + } else { + data[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT] = utils.FormatAmount(-relatedAmountNum) } } - if memo, _ := dataset.getTransactionDataItemValue(transactionData, iifTransactionMemoColumnName); memo != "" { + if splitMemo, _ := dataset.getSplitDataItemValue(transactionData.splitData[splitDataIndex], iifTransactionMemoColumnName); splitMemo != "" { + data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = splitMemo + } else if memo, _ := dataset.getTransactionDataItemValue(transactionData, iifTransactionMemoColumnName); memo != "" { data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = memo + } else if splitName, _ := dataset.getSplitDataItemValue(transactionData.splitData[splitDataIndex], iifTransactionNameColumnName); splitName != "" { + data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = splitName } else if name, _ := dataset.getTransactionDataItemValue(transactionData, iifTransactionNameColumnName); name != "" { data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = name } else { @@ -299,6 +386,49 @@ func (t *iifTransactionDataRowIterator) parseTransaction(ctx core.Context, user return data, nil } +func (t *iifTransactionDataRowIterator) isSplitTransactionSupported(ctx core.Context, dataset *iifTransactionDataset, transactionData *iifTransactionData) (bool, error) { + supportSplitTransactions := true + transactionType, _ := dataset.getTransactionDataItemValue(transactionData, iifTransactionTypeColumnName) + + if transactionType == iifTransactionTypeBeginningBalance { // balance modification + supportSplitTransactions = false + log.Errorf(ctx, "[iif_transaction_data_table.isSplitTransactionSupported] cannot parse split balance modification transaction#%d (dataset#%d)", t.currentIndexInDataset, t.currentDatasetIndex) + } else { + transactionAmountStr, _ := dataset.getTransactionDataItemValue(transactionData, iifTransactionAmountColumnName) + transactionAmount, err := parseAmount(transactionAmountStr) + + if err != nil { + log.Errorf(ctx, "[iif_transaction_data_table.isSplitTransactionSupported] cannot parsing transaction in row#%d (dataset#%d), because transaction amount \"%s\" is invalid", t.currentIndexInDataset, t.currentDatasetIndex, transactionAmountStr) + return false, errs.ErrAmountInvalid + } + + splitTotalAmount := int64(0) + + for i := 0; i < len(transactionData.splitData); i++ { + splitAmountStr, _ := dataset.getSplitDataItemValue(transactionData.splitData[i], iifTransactionAmountColumnName) + splitAmount, err := parseAmount(splitAmountStr) + + if err != nil { + log.Errorf(ctx, "[iif_transaction_data_table.isSplitTransactionSupported] cannot parsing transaction in row#%d-split#%d (dataset#%d), because split amount \"%s\" is invalid", t.currentIndexInDataset, i, t.currentDatasetIndex, splitAmountStr) + return false, errs.ErrAmountInvalid + } + + splitTotalAmount += splitAmount + } + + if splitTotalAmount != -transactionAmount { + supportSplitTransactions = false + log.Errorf(ctx, "[iif_transaction_data_table.isSplitTransactionSupported] cannot parse split transaction#%d (dataset#%d), because the sum amount of each split data \"%d\" not equal to the transaction amount \"%d\"", t.currentIndexInDataset, t.currentDatasetIndex, splitTotalAmount, -transactionAmount) + } + } + + if len(transactionData.splitData) > 1 && !supportSplitTransactions { + return false, errs.ErrNotSupportedSplitTransactions + } + + return true, nil +} + func (t *iifTransactionDataRowIterator) parseTransactionTime(dataset *iifTransactionDataset, transactionData *iifTransactionData) (string, error) { date, _ := dataset.getTransactionDataItemValue(transactionData, iifTransactionDateColumnName) dateParts := strings.Split(date, "/") @@ -395,3 +525,7 @@ func getIncomeAndExpenseAccountNameMap(accountDatasets []*iifAccountDataset) (in return incomeAccountNames, expenseAccountNames } + +func parseAmount(amount string) (int64, error) { + return utils.ParseAmount(strings.ReplaceAll(amount, ",", "")) +}