From e52c7037c7018c822273812173aa06ec3344c084 Mon Sep 17 00:00:00 2001 From: MaysWind Date: Sat, 13 Sep 2025 01:52:43 +0800 Subject: [PATCH] import transactions from JD.com finance statement file (#240) --- ...ance_transaction_data_csv_file_importer.go | 64 +++ ...transaction_data_csv_file_importer_test.go | 508 ++++++++++++++++++ ...jdcom_finance_transaction_data_extrator.go | 74 +++ ...com_finance_transaction_data_row_parser.go | 148 +++++ pkg/converters/transaction_data_converters.go | 3 + src/consts/file.ts | 9 + src/locales/de.json | 1 + src/locales/en.json | 1 + src/locales/es.json | 1 + src/locales/it.json | 1 + src/locales/ja.json | 1 + src/locales/nl.json | 1 + src/locales/pt_BR.json | 1 + src/locales/ru.json | 1 + src/locales/uk.json | 1 + src/locales/vi.json | 1 + src/locales/zh_Hans.json | 1 + src/locales/zh_Hant.json | 1 + 18 files changed, 818 insertions(+) create mode 100644 pkg/converters/jdcom/jdcom_finance_transaction_data_csv_file_importer.go create mode 100644 pkg/converters/jdcom/jdcom_finance_transaction_data_csv_file_importer_test.go create mode 100644 pkg/converters/jdcom/jdcom_finance_transaction_data_extrator.go create mode 100644 pkg/converters/jdcom/jdcom_finance_transaction_data_row_parser.go diff --git a/pkg/converters/jdcom/jdcom_finance_transaction_data_csv_file_importer.go b/pkg/converters/jdcom/jdcom_finance_transaction_data_csv_file_importer.go new file mode 100644 index 00000000..272be036 --- /dev/null +++ b/pkg/converters/jdcom/jdcom_finance_transaction_data_csv_file_importer.go @@ -0,0 +1,64 @@ +package jdcom + +import ( + "bytes" + + "golang.org/x/text/encoding/unicode" + "golang.org/x/text/transform" + + "github.com/mayswind/ezbookkeeping/pkg/converters/converter" + "github.com/mayswind/ezbookkeeping/pkg/converters/csv" + "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" +) + +// jdComFinanceTransactionDataCsvFileImporter defines the structure of jd.com finance csv importer for transaction data +type jdComFinanceTransactionDataCsvFileImporter struct { + fileHeaderLineBeginning string + dataHeaderStartContentBeginning string +} + +// Initialize a jd.com finance transaction data csv file importer singleton instance +var ( + JDComFinanceTransactionDataCsvFileImporter = &jdComFinanceTransactionDataCsvFileImporter{} +) + +// ParseImportedData returns the imported data by parsing the jd.com finance transaction csv data +func (c *jdComFinanceTransactionDataCsvFileImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) { + fallback := unicode.UTF8.NewDecoder() + reader := transform.NewReader(bytes.NewReader(data), unicode.BOMOverride(fallback)) + + csvDataTable, err := csv.CreateNewCsvBasicDataTable(ctx, reader, false) + + if err != nil { + return nil, nil, nil, nil, nil, nil, err + } + + dataTable, err := createNewJDComFinanceTransactionBasicDataTable(ctx, csvDataTable) + + if err != nil { + return nil, nil, nil, nil, nil, nil, err + } + + commonDataTable := datatable.CreateNewCommonDataTableFromBasicDataTable(dataTable) + + if !commonDataTable.HasColumn(jdComFinanceTransactionTimeColumnName) || + !commonDataTable.HasColumn(jdComFinanceTransactionMerchantNameColumnName) || + !commonDataTable.HasColumn(jdComFinanceTransactionMemoColumnName) || + !commonDataTable.HasColumn(jdComFinanceTransactionAmountColumnName) || + !commonDataTable.HasColumn(jdComFinanceTransactionRelatedAccountColumnName) || + !commonDataTable.HasColumn(jdComFinanceTransactionStatusColumnName) || + !commonDataTable.HasColumn(jdComFinanceTransactionTypeColumnName) { + log.Errorf(ctx, "[jdcom_finance_transaction_data_csv_file_importer.ParseImportedData] cannot parse jd.com finance csv data, because missing essential columns in header row") + return nil, nil, nil, nil, nil, nil, errs.ErrMissingRequiredFieldInHeaderRow + } + + transactionRowParser := createJDComFinanceTransactionDataRowParser(dataTable.HeaderColumnNames()) + transactionDataTable := datatable.CreateNewTransactionDataTableFromCommonDataTable(commonDataTable, jdComFinanceTransactionSupportedColumns, transactionRowParser) + dataTableImporter := converter.CreateNewSimpleImporterWithTypeNameMapping(jdComFinanceTransactionTypeNameMapping) + + return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap) +} diff --git a/pkg/converters/jdcom/jdcom_finance_transaction_data_csv_file_importer_test.go b/pkg/converters/jdcom/jdcom_finance_transaction_data_csv_file_importer_test.go new file mode 100644 index 00000000..5e2c53d5 --- /dev/null +++ b/pkg/converters/jdcom/jdcom_finance_transaction_data_csv_file_importer_test.go @@ -0,0 +1,508 @@ +package jdcom + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/mayswind/ezbookkeeping/pkg/core" + "github.com/mayswind/ezbookkeeping/pkg/errs" + "github.com/mayswind/ezbookkeeping/pkg/models" + "github.com/mayswind/ezbookkeeping/pkg/utils" +) + +func TestJDComFinanceCsvFileImporterParseImportedData_MinimumValidData(t *testing.T) { + converter := JDComFinanceTransactionDataCsvFileImporter + context := core.NewNullContext() + + user := &models.User{ + Uid: 1234567890, + DefaultCurrency: "CNY", + } + + data := "导出信息:\n" + + "京东账号名:xxxxxx\n" + + "日期区间:2025-01-01 至 2025-09-01\n" + + "\n" + + "交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支,交易分类\n" + + "2025-09-01 01:23:45,xxx,xxx,0.12,余额,交易成功,收入,其他\n" + + "2025-09-01 12:34:56,xxx,xxx,123.45,银行卡,交易成功,支出,其他网购\n" + + "2025-09-01 23:59:59,xxx,京东钱包余额充值,0.05,银行卡,交易成功,不计收支,余额\n" + + "2025-09-02 23:59:59,xxx,京东余额提现,0.03,银行卡,交易成功,不计收支,余额\n" + allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := converter.ParseImportedData(context, user, []byte(data), 0, nil, nil, nil, nil, nil) + assert.Nil(t, err) + + assert.Equal(t, 4, len(allNewTransactions)) + assert.Equal(t, 3, len(allNewAccounts)) + assert.Equal(t, 1, len(allNewSubExpenseCategories)) + assert.Equal(t, 1, len(allNewSubIncomeCategories)) + assert.Equal(t, 1, len(allNewSubTransferCategories)) + assert.Equal(t, 0, len(allNewTags)) + + assert.Equal(t, int64(1234567890), allNewTransactions[0].Uid) + assert.Equal(t, models.TRANSACTION_DB_TYPE_INCOME, allNewTransactions[0].Type) + assert.Equal(t, "2025-09-01 01:23:45", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime), time.UTC)) + assert.Equal(t, int64(12), allNewTransactions[0].Amount) + assert.Equal(t, "余额", allNewTransactions[0].OriginalSourceAccountName) + assert.Equal(t, "其他", allNewTransactions[0].OriginalCategoryName) + + assert.Equal(t, int64(1234567890), allNewTransactions[1].Uid) + assert.Equal(t, models.TRANSACTION_DB_TYPE_EXPENSE, allNewTransactions[1].Type) + assert.Equal(t, "2025-09-01 12:34:56", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[1].TransactionTime), time.UTC)) + assert.Equal(t, int64(12345), allNewTransactions[1].Amount) + assert.Equal(t, "银行卡", allNewTransactions[1].OriginalSourceAccountName) + assert.Equal(t, "其他网购", allNewTransactions[1].OriginalCategoryName) + + assert.Equal(t, int64(1234567890), allNewTransactions[2].Uid) + assert.Equal(t, models.TRANSACTION_DB_TYPE_TRANSFER_OUT, allNewTransactions[2].Type) + assert.Equal(t, "2025-09-01 23:59:59", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[2].TransactionTime), time.UTC)) + assert.Equal(t, int64(5), allNewTransactions[2].Amount) + assert.Equal(t, "银行卡", allNewTransactions[2].OriginalSourceAccountName) + assert.Equal(t, "xxx", allNewTransactions[2].OriginalDestinationAccountName) + assert.Equal(t, "余额", 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, "2025-09-02 23:59:59", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[3].TransactionTime), time.UTC)) + assert.Equal(t, int64(3), allNewTransactions[3].Amount) + assert.Equal(t, "xxx", allNewTransactions[3].OriginalSourceAccountName) + assert.Equal(t, "银行卡", allNewTransactions[3].OriginalDestinationAccountName) + assert.Equal(t, "余额", allNewTransactions[3].OriginalCategoryName) + + assert.Equal(t, int64(1234567890), allNewAccounts[0].Uid) + assert.Equal(t, "余额", allNewAccounts[0].Name) + assert.Equal(t, "CNY", allNewAccounts[0].Currency) + + assert.Equal(t, int64(1234567890), allNewAccounts[1].Uid) + assert.Equal(t, "银行卡", allNewAccounts[1].Name) + assert.Equal(t, "CNY", allNewAccounts[1].Currency) + + assert.Equal(t, int64(1234567890), allNewAccounts[2].Uid) + assert.Equal(t, "xxx", allNewAccounts[2].Name) + assert.Equal(t, "CNY", allNewAccounts[2].Currency) + + assert.Equal(t, int64(1234567890), allNewSubExpenseCategories[0].Uid) + assert.Equal(t, "其他网购", allNewSubExpenseCategories[0].Name) + + assert.Equal(t, int64(1234567890), allNewSubIncomeCategories[0].Uid) + assert.Equal(t, "其他", allNewSubIncomeCategories[0].Name) + + assert.Equal(t, int64(1234567890), allNewSubTransferCategories[0].Uid) + assert.Equal(t, "余额", allNewSubTransferCategories[0].Name) +} + +func TestJDComFinanceCsvFileImporterParseImportedData_ParseRefundTransaction(t *testing.T) { + converter := JDComFinanceTransactionDataCsvFileImporter + context := core.NewNullContext() + + user := &models.User{ + Uid: 1234567890, + DefaultCurrency: "CNY", + } + + data1 := "导出信息:\n" + + "京东账号名:xxxxxx\n" + + "日期区间:2025-01-01 至 2025-09-01\n" + + "\n" + + "交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" + + "2025-09-01 01:23:45,xxx,xxx,0.12,银行卡,退款成功,不计收支\n" + + "2025-09-01 02:34:56,xxx,xxx,0.12(已全额退款),银行卡,交易成功,不计收支\n" + + "2025-09-02 01:23:45,xxx,xxx,3.45,银行卡,退款成功,不计收支\n" + + "2025-09-02 02:34:56,xxx,xxx,123.45(已退款3.45),银行卡,交易成功,支出\n" + allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(data1), 0, nil, nil, nil, nil, nil) + assert.Nil(t, err) + + assert.Equal(t, int64(1234567890), allNewTransactions[0].Uid) + assert.Equal(t, models.TRANSACTION_DB_TYPE_EXPENSE, allNewTransactions[0].Type) + assert.Equal(t, "2025-09-01 01:23:45", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime), time.UTC)) + assert.Equal(t, int64(-12), allNewTransactions[0].Amount) + assert.Equal(t, "银行卡", allNewTransactions[0].OriginalSourceAccountName) + + assert.Equal(t, int64(1234567890), allNewTransactions[1].Uid) + assert.Equal(t, models.TRANSACTION_DB_TYPE_EXPENSE, allNewTransactions[1].Type) + assert.Equal(t, "2025-09-01 02:34:56", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[1].TransactionTime), time.UTC)) + assert.Equal(t, int64(12), allNewTransactions[1].Amount) + assert.Equal(t, "银行卡", allNewTransactions[1].OriginalSourceAccountName) + + assert.Equal(t, int64(1234567890), allNewTransactions[2].Uid) + assert.Equal(t, models.TRANSACTION_DB_TYPE_EXPENSE, allNewTransactions[2].Type) + assert.Equal(t, "2025-09-02 01:23:45", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[2].TransactionTime), time.UTC)) + assert.Equal(t, int64(-345), allNewTransactions[2].Amount) + assert.Equal(t, "银行卡", allNewTransactions[2].OriginalSourceAccountName) + + assert.Equal(t, int64(1234567890), allNewTransactions[3].Uid) + assert.Equal(t, models.TRANSACTION_DB_TYPE_EXPENSE, allNewTransactions[3].Type) + assert.Equal(t, "2025-09-02 02:34:56", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[3].TransactionTime), time.UTC)) + assert.Equal(t, int64(12345), allNewTransactions[3].Amount) + assert.Equal(t, "银行卡", allNewTransactions[3].OriginalSourceAccountName) +} + +func TestJDComFinanceCsvFileImporterParseImportedData_ParseInvalidTime(t *testing.T) { + converter := JDComFinanceTransactionDataCsvFileImporter + context := core.NewNullContext() + + user := &models.User{ + Uid: 1234567890, + DefaultCurrency: "CNY", + } + + data1 := "导出信息:\n" + + "京东账号名:xxxxxx\n" + + "日期区间:2025-01-01 至 2025-09-01\n" + + "\n" + + "交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" + + "2025-09-01T01:23:45,xxx,xxx,0.12,银行卡,交易成功,支出\n" + _, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(data1), 0, nil, nil, nil, nil, nil) + assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message) + + data2 := "导出信息:\n" + + "京东账号名:xxxxxx\n" + + "日期区间:2025-01-01 至 2025-09-01\n" + + "\n" + + "交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" + + "09/01/2025 01:23:45,xxx,xxx,0.12,银行卡,交易成功,支出\n" + _, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data2), 0, nil, nil, nil, nil, nil) + assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message) +} + +func TestJDComFinanceCsvFileImporterParseImportedData_ParseInvalidType(t *testing.T) { + converter := JDComFinanceTransactionDataCsvFileImporter + context := core.NewNullContext() + + user := &models.User{ + Uid: 1234567890, + DefaultCurrency: "CNY", + } + + data := "导出信息:\n" + + "京东账号名:xxxxxx\n" + + "日期区间:2025-01-01 至 2025-09-01\n" + + "\n" + + "交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" + + "2025-09-01 01:23:45,xxx,xxx,0.12,银行卡,交易成功,转账\n" + _, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(data), 0, nil, nil, nil, nil, nil) + assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message) +} + +func TestJDComFinanceCsvFileImporterParseImportedData_ParseInvalidAmount(t *testing.T) { + converter := JDComFinanceTransactionDataCsvFileImporter + context := core.NewNullContext() + + user := &models.User{ + Uid: 1234567890, + DefaultCurrency: "CNY", + } + + data := "导出信息:\n" + + "京东账号名:xxxxxx\n" + + "日期区间:2025-01-01 至 2025-09-01\n" + + "\n" + + "交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" + + "2025-09-01 01:23:45,xxx,xxx,¥0.12,银行卡,交易成功,支出\n" + _, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(data), 0, nil, nil, nil, nil, nil) + assert.EqualError(t, err, errs.ErrAmountInvalid.Message) +} + +func TestJDComFinanceCsvFileImporterParseImportedData_ParseAccountName(t *testing.T) { + converter := JDComFinanceTransactionDataCsvFileImporter + context := core.NewNullContext() + + user := &models.User{ + Uid: 1234567890, + DefaultCurrency: "CNY", + } + + // transfer to jd.com finance wallet + data1 := "导出信息:\n" + + "京东账号名:xxxxxx\n" + + "日期区间:2025-01-01 至 2025-09-01\n" + + "\n" + + "交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支,交易分类\n" + + "2025-09-01 01:23:45,xxx,京东钱包余额充值,0.05,银行卡,交易成功,不计收支,余额\n" + allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(data1), 0, nil, nil, nil, nil, nil) + assert.Nil(t, err) + + assert.Equal(t, 1, len(allNewTransactions)) + assert.Equal(t, "银行卡", allNewTransactions[0].OriginalSourceAccountName) + assert.Equal(t, "xxx", allNewTransactions[0].OriginalDestinationAccountName) + assert.Equal(t, models.TRANSACTION_DB_TYPE_TRANSFER_OUT, allNewTransactions[0].Type) + + // transfer from jd.com finance wallet + data2 := "导出信息:\n" + + "京东账号名:xxxxxx\n" + + "日期区间:2025-01-01 至 2025-09-01\n" + + "\n" + + "交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支,交易分类\n" + + "2025-09-01 01:23:45,xxx,京东余额提现,0.05,银行卡,交易成功,不计收支,余额\n" + allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data2), 0, nil, nil, nil, nil, nil) + assert.Nil(t, err) + + assert.Equal(t, 1, len(allNewTransactions)) + assert.Equal(t, "xxx", allNewTransactions[0].OriginalSourceAccountName) + assert.Equal(t, "银行卡", allNewTransactions[0].OriginalDestinationAccountName) + assert.Equal(t, models.TRANSACTION_DB_TYPE_TRANSFER_OUT, allNewTransactions[0].Type) + + // transfer from other account + data3 := "导出信息:\n" + + "京东账号名:xxxxxx\n" + + "日期区间:2025-01-01 至 2025-09-01\n" + + "\n" + + "交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支,交易分类\n" + + "2025-09-01 01:23:45,xxx,京东小金库-转入,0.05,余额,交易成功,不计收支,小金库\n" + assert.Nil(t, err) + + allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data3), 0, nil, nil, nil, nil, nil) + assert.Nil(t, err) + + assert.Equal(t, 1, len(allNewTransactions)) + assert.Equal(t, "余额", allNewTransactions[0].OriginalSourceAccountName) + assert.Equal(t, "xxx", allNewTransactions[0].OriginalDestinationAccountName) + assert.Equal(t, models.TRANSACTION_DB_TYPE_TRANSFER_OUT, allNewTransactions[0].Type) + + // transfer to other account + data4 := "导出信息:\n" + + "京东账号名:xxxxxx\n" + + "日期区间:2025-01-01 至 2025-09-01\n" + + "\n" + + "交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支,交易分类\n" + + "2025-09-01 01:23:45,xxx,京东小金库-转出,0.05,余额,交易成功,不计收支,小金库\n" + assert.Nil(t, err) + + allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data4), 0, nil, nil, nil, nil, nil) + assert.Nil(t, err) + + assert.Equal(t, 1, len(allNewTransactions)) + assert.Equal(t, "xxx", allNewTransactions[0].OriginalSourceAccountName) + assert.Equal(t, "余额", allNewTransactions[0].OriginalDestinationAccountName) + assert.Equal(t, models.TRANSACTION_DB_TYPE_TRANSFER_OUT, allNewTransactions[0].Type) + + // refund + data5 := "导出信息:\n" + + "京东账号名:xxxxxx\n" + + "日期区间:2025-01-01 至 2025-09-01\n" + + "\n" + + "交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支,交易分类\n" + + "2025-09-01 01:23:45,xxx,价保退款,0.05,银行卡,交易成功,不计收支,其他\n" + assert.Nil(t, err) + + allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data5), 0, nil, nil, nil, nil, nil) + assert.Nil(t, err) + + assert.Equal(t, 1, len(allNewTransactions)) + assert.Equal(t, "银行卡", allNewTransactions[0].OriginalSourceAccountName) + assert.Equal(t, models.TRANSACTION_DB_TYPE_EXPENSE, allNewTransactions[0].Type) + + // repayment + data6 := "导出信息:\n" + + "京东账号名:xxxxxx\n" + + "日期区间:2025-01-01 至 2025-09-01\n" + + "\n" + + "交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支,交易分类\n" + + "2025-09-01 01:23:45,xxx,白条主动还款,0.05,银行卡,交易成功,不计收支,白条\n" + assert.Nil(t, err) + + allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data6), 0, nil, nil, nil, nil, nil) + assert.Nil(t, err) + + assert.Equal(t, 1, len(allNewTransactions)) + assert.Equal(t, "银行卡", allNewTransactions[0].OriginalSourceAccountName) + assert.Equal(t, "xxx", allNewTransactions[0].OriginalDestinationAccountName) + assert.Equal(t, models.TRANSACTION_DB_TYPE_TRANSFER_OUT, allNewTransactions[0].Type) +} + +func TestJDComFinanceCsvFileImporterParseImportedData_ParseDescription(t *testing.T) { + converter := JDComFinanceTransactionDataCsvFileImporter + context := core.NewNullContext() + + user := &models.User{ + Uid: 1234567890, + DefaultCurrency: "CNY", + } + + data1 := "导出信息:\n" + + "京东账号名:xxxxxx\n" + + "日期区间:2025-01-01 至 2025-09-01\n" + + "\n" + + "交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" + + "2025-09-01 01:23:45,xxx,,0.12,银行卡,交易成功,支出\n" + allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(data1), 0, nil, nil, nil, nil, nil) + assert.Nil(t, err) + + assert.Equal(t, 1, len(allNewTransactions)) + assert.Equal(t, "", allNewTransactions[0].Comment) + + data2 := "导出信息:\n" + + "京东账号名:xxxxxx\n" + + "日期区间:2025-01-01 至 2025-09-01\n" + + "\n" + + "交易时间,商户名称,交易说明,交易说明,金额,收/付款方式,交易状态,收/支,备注\n" + + "2025-09-01 01:23:45,xxx,xxx,Test,0.12,银行卡,交易成功,支出,\"foo\"\"bar,\ntest\"\n" + allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data2), 0, nil, nil, nil, nil, nil) + assert.Equal(t, 1, len(allNewTransactions)) + assert.Equal(t, "foo\"bar,\ntest", allNewTransactions[0].Comment) + + data3 := "导出信息:\n" + + "京东账号名:xxxxxx\n" + + "日期区间:2025-01-01 至 2025-09-01\n" + + "\n" + + "交易时间,商户名称,交易说明,交易说明,金额,收/付款方式,交易状态,收/支,备注\n" + + "2025-09-01 01:23:45,xxx,xxx,Test,0.12,银行卡,交易成功,支出,\n" + allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data3), 0, nil, nil, nil, nil, nil) + assert.Equal(t, 1, len(allNewTransactions)) + assert.Equal(t, "Test", allNewTransactions[0].Comment) +} + +func TestJDComFinanceCsvFileImporterParseImportedData_SkipUnknownStatusTransaction(t *testing.T) { + converter := JDComFinanceTransactionDataCsvFileImporter + context := core.NewNullContext() + + user := &models.User{ + Uid: 1234567890, + DefaultCurrency: "CNY", + } + + data := "导出信息:\n" + + "京东账号名:xxxxxx\n" + + "日期区间:2025-01-01 至 2025-09-01\n" + + "\n" + + "交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" + + "2025-09-01 01:23:45,xxx,xxx,0.12,银行卡,xxxx,支出\n" + _, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(data), 0, nil, nil, nil, nil, nil) + assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message) +} + +func TestJDComFinanceCsvFileImporterParseImportedData_SkipUnknownMemoTransferTransaction(t *testing.T) { + converter := JDComFinanceTransactionDataCsvFileImporter + context := core.NewNullContext() + + user := &models.User{ + Uid: 1234567890, + DefaultCurrency: "CNY", + } + + data := "导出信息:\n" + + "京东账号名:xxxxxx\n" + + "日期区间:2025-01-01 至 2025-09-01\n" + + "\n" + + "交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" + + "2025-09-01 01:23:45,xxx,xxx,0.12,银行卡,交易成功,不计收支\n" + _, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(data), 0, nil, nil, nil, nil, nil) + assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message) +} + +func TestJDComFinanceCsvFileImporterParseImportedData_MissingFileHeader(t *testing.T) { + converter := JDComFinanceTransactionDataCsvFileImporter + context := core.NewNullContext() + + user := &models.User{ + Uid: 1, + DefaultCurrency: "CNY", + } + + data := "交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" + + "2025-09-01 01:23:45,xxx,xxx,0.12,银行卡,交易成功,支出\n" + _, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(data), 0, nil, nil, nil, nil, nil) + assert.EqualError(t, err, errs.ErrInvalidFileHeader.Message) + + _, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(""), 0, nil, nil, nil, nil, nil) + assert.EqualError(t, err, errs.ErrInvalidFileHeader.Message) +} + +func TestJDComFinanceCsvFileImporterParseImportedData_MissingRequiredColumn(t *testing.T) { + converter := JDComFinanceTransactionDataCsvFileImporter + context := core.NewNullContext() + + user := &models.User{ + Uid: 1, + DefaultCurrency: "CNY", + } + + // Missing Time Column + data1 := "导出信息:\n" + + "京东账号名:xxxxxx\n" + + "日期区间:2025-01-01 至 2025-09-01\n" + + "\n" + + "商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" + + "xxx,xxx,0.12,银行卡,交易成功,支出\n" + _, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(data1), 0, nil, nil, nil, nil, nil) + assert.EqualError(t, err, errs.ErrInvalidFileHeader.Message) + + // Missing Merchant Name Column + data2 := "导出信息:\n" + + "京东账号名:xxxxxx\n" + + "日期区间:2025-01-01 至 2025-09-01\n" + + "\n" + + "交易时间,交易说明,金额,收/付款方式,交易状态,收/支\n" + + "2025-09-01 01:23:45,xxx,0.12,银行卡,交易成功,支出\n" + _, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data2), 0, nil, nil, nil, nil, nil) + assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) + + // Missing Transaction Memo Column + data3 := "导出信息:\n" + + "京东账号名:xxxxxx\n" + + "日期区间:2025-01-01 至 2025-09-01\n" + + "\n" + + "交易时间,商户名称,金额,收/付款方式,交易状态,收/支\n" + + "2025-09-01 01:23:45,xxx,0.12,银行卡,交易成功,支出\n" + _, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data3), 0, nil, nil, nil, nil, nil) + assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) + + // Missing Amount Column + data4 := "导出信息:\n" + + "京东账号名:xxxxxx\n" + + "日期区间:2025-01-01 至 2025-09-01\n" + + "\n" + + "交易时间,商户名称,交易说明,收/付款方式,交易状态,收/支\n" + + "2025-09-01 01:23:45,xxx,xxx,银行卡,交易成功,支出\n" + _, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data4), 0, nil, nil, nil, nil, nil) + assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) + + // Missing Related Account Column + data5 := "导出信息:\n" + + "京东账号名:xxxxxx\n" + + "日期区间:2025-01-01 至 2025-09-01\n" + + "\n" + + "交易时间,商户名称,交易说明,金额,交易状态,收/支\n" + + "2025-09-01 01:23:45,xxx,xxx,0.12,交易成功,支出\n" + _, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data5), 0, nil, nil, nil, nil, nil) + assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) + + // Missing Status Column + data6 := "导出信息:\n" + + "京东账号名:xxxxxx\n" + + "日期区间:2025-01-01 至 2025-09-01\n" + + "\n" + + "交易时间,商户名称,交易说明,金额,收/付款方式,收/支\n" + + "2025-09-01 01:23:45,xxx,xxx,0.12,银行卡,支出\n" + _, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data6), 0, nil, nil, nil, nil, nil) + assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) + + // Missing Type Column + data7 := "导出信息:\n" + + "京东账号名:xxxxxx\n" + + "日期区间:2025-01-01 至 2025-09-01\n" + + "\n" + + "交易时间,商户名称,交易说明,金额,收/付款方式,交易状态\n" + + "2025-09-01 01:23:45,xxx,xxx,0.12,银行卡,交易成功\n" + _, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data7), 0, nil, nil, nil, nil, nil) + assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) +} + +func TestJDComFinanceCsvFileImporterParseImportedData_NoTransactionData(t *testing.T) { + converter := JDComFinanceTransactionDataCsvFileImporter + context := core.NewNullContext() + + user := &models.User{ + Uid: 1234567890, + DefaultCurrency: "CNY", + } + + data := "导出信息:\n" + + "京东账号名:xxxxxx\n" + + "日期区间:2025-01-01 至 2025-09-01\n" + + "\n" + + "交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" + _, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(data), 0, nil, nil, nil, nil, nil) + assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message) +} diff --git a/pkg/converters/jdcom/jdcom_finance_transaction_data_extrator.go b/pkg/converters/jdcom/jdcom_finance_transaction_data_extrator.go new file mode 100644 index 00000000..827a3a78 --- /dev/null +++ b/pkg/converters/jdcom/jdcom_finance_transaction_data_extrator.go @@ -0,0 +1,74 @@ +package jdcom + +import ( + "strings" + + "github.com/mayswind/ezbookkeeping/pkg/converters/csv" + "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" +) + +func createNewJDComFinanceTransactionBasicDataTable(ctx core.Context, originalDataTable datatable.BasicDataTable) (datatable.BasicDataTable, error) { + iterator := originalDataTable.DataRowIterator() + allOriginalLines := make([][]string, 0) + hasFileHeader := false + foundDataHeaderLine := false + + for iterator.HasNext() { + row := iterator.Next() + + if !hasFileHeader { + if row.ColumnCount() <= 0 { + continue + } else if strings.Index(row.GetData(0), jdComFinanceTransactionDataCsvFileHeader) == 0 { + hasFileHeader = true + continue + } else { + log.Warnf(ctx, "[jdcom_finance_transaction_data_extrator.createNewJDComFinanceTransactionBasicDataTable] read unexpected line in row \"%s\" before read file header", iterator.CurrentRowId()) + continue + } + } + + if !foundDataHeaderLine { + if row.ColumnCount() <= 0 { + continue + } else if row.GetData(0) == jdComFinanceTransactionTimeColumnName { + foundDataHeaderLine = true + } else { + continue + } + } + + if foundDataHeaderLine { + if row.ColumnCount() <= 0 { + continue + } + + items := make([]string, row.ColumnCount()) + + for i := 0; i < row.ColumnCount(); i++ { + items[i] = strings.TrimRight(strings.Trim(row.GetData(i), " "), "\t") + } + + if len(allOriginalLines) > 0 && len(items) < len(allOriginalLines[0]) { + log.Errorf(ctx, "[jdcom_finance_transaction_data_extrator.createNewJDComFinanceTransactionBasicDataTable] cannot parse row \"%s\", because may missing some columns (column count %d in data row is less than header column count %d)", iterator.CurrentRowId(), len(items), len(allOriginalLines[0])) + return nil, errs.ErrFewerFieldsInDataRowThanInHeaderRow + } + + allOriginalLines = append(allOriginalLines, items) + } + } + + if !hasFileHeader || !foundDataHeaderLine { + return nil, errs.ErrInvalidFileHeader + } + + if len(allOriginalLines) < 2 { + log.Errorf(ctx, "[jdcom_finance_transaction_data_extrator.createNewJDComFinanceTransactionBasicDataTable] cannot parse import data, because data table row count is less 1") + return nil, errs.ErrNotFoundTransactionDataInFile + } + + return csv.CreateNewCustomCsvBasicDataTable(allOriginalLines, true), nil +} diff --git a/pkg/converters/jdcom/jdcom_finance_transaction_data_row_parser.go b/pkg/converters/jdcom/jdcom_finance_transaction_data_row_parser.go new file mode 100644 index 00000000..6a5e8c33 --- /dev/null +++ b/pkg/converters/jdcom/jdcom_finance_transaction_data_row_parser.go @@ -0,0 +1,148 @@ +package jdcom + +import ( + "strings" + + "github.com/mayswind/ezbookkeeping/pkg/converters/datatable" + "github.com/mayswind/ezbookkeeping/pkg/core" + "github.com/mayswind/ezbookkeeping/pkg/log" + "github.com/mayswind/ezbookkeeping/pkg/models" + "github.com/mayswind/ezbookkeeping/pkg/utils" +) + +const jdComFinanceTransactionDataCsvFileHeader = "导出信息:" + +const jdComFinanceTransactionTimeColumnName = "交易时间" +const jdComFinanceTransactionMerchantNameColumnName = "商户名称" +const jdComFinanceTransactionMemoColumnName = "交易说明" +const jdComFinanceTransactionAmountColumnName = "金额" +const jdComFinanceTransactionRelatedAccountColumnName = "收/付款方式" +const jdComFinanceTransactionStatusColumnName = "交易状态" +const jdComFinanceTransactionTypeColumnName = "收/支" +const jdComFinanceTransactionCategoryColumnName = "交易分类" +const jdComFinanceTransactionDescriptionColumnName = "备注" + +const jdComFinanceTransactionAmountRefundAll = "(已全额退款)" + +const jdComFinanceTransactionMemoTransferToWalletPrefix = "充值" +const jdComFinanceTransactionMemoTransferFromWalletPrefix = "提现" +const jdComFinanceTransactionMemoTransferInText = "转入" +const jdComFinanceTransactionMemoTransferOutText = "转出" +const jdComFinanceTransactionMemoRepaymentText = "还款" +const jdComFinanceTransactionMemoRefundText = "退款" + +const jdComFinanceTransactionDataStatusSuccessName = "交易成功" +const jdComFinanceTransactionDataStatusRefundSuccessName = "退款成功" + +var jdComFinanceTransactionSupportedColumns = map[datatable.TransactionDataTableColumn]bool{ + 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, +} + +var jdComFinanceTransactionTypeNameMapping = map[models.TransactionType]string{ + models.TRANSACTION_TYPE_INCOME: "收入", + models.TRANSACTION_TYPE_EXPENSE: "支出", + models.TRANSACTION_TYPE_TRANSFER: "不计收支", +} + +// jdComFinanceTransactionDataRowParser defines the structure of jd.com finance transaction data row parser +type jdComFinanceTransactionDataRowParser struct { + existedOriginalDataColumns map[string]bool +} + +// Parse returns the converted transaction data row +func (p *jdComFinanceTransactionDataRowParser) Parse(ctx core.Context, user *models.User, dataRow datatable.CommonDataTableRow, rowId string) (rowData map[datatable.TransactionDataTableColumn]string, rowDataValid bool, err error) { + if dataRow.GetData(jdComFinanceTransactionTypeColumnName) != jdComFinanceTransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME] && + dataRow.GetData(jdComFinanceTransactionTypeColumnName) != jdComFinanceTransactionTypeNameMapping[models.TRANSACTION_TYPE_EXPENSE] && + dataRow.GetData(jdComFinanceTransactionTypeColumnName) != jdComFinanceTransactionTypeNameMapping[models.TRANSACTION_TYPE_TRANSFER] { + log.Warnf(ctx, "[jdcom_finance_transaction_data_row_parser.Parse] skip parsing transaction in row \"%s\", because type is \"%s\"", rowId, dataRow.GetData(jdComFinanceTransactionTypeColumnName)) + return nil, false, nil + } + + statusName := dataRow.GetData(jdComFinanceTransactionStatusColumnName) + + if statusName != jdComFinanceTransactionDataStatusSuccessName && + statusName != jdComFinanceTransactionDataStatusRefundSuccessName { + log.Warnf(ctx, "[jdcom_finance_transaction_data_row_parser.Parse] skip parsing transaction in row \"%s\", because status is \"%s\"", rowId, statusName) + return nil, false, nil + } + + data := make(map[datatable.TransactionDataTableColumn]string, len(jdComFinanceTransactionSupportedColumns)) + data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] = dataRow.GetData(jdComFinanceTransactionTimeColumnName) + data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = dataRow.GetData(jdComFinanceTransactionTypeColumnName) + data[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = dataRow.GetData(jdComFinanceTransactionCategoryColumnName) + data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = dataRow.GetData(jdComFinanceTransactionRelatedAccountColumnName) + data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = "" + + if strings.Index(dataRow.GetData(jdComFinanceTransactionAmountColumnName), "(") >= 0 { + // If a transaction includes a refund, the original transaction amount will like "-xx.xx(已全额退款)" or "-xx.xx(已退款yy.yy)", along with another refund transaction + data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = strings.Split(dataRow.GetData(jdComFinanceTransactionAmountColumnName), "(")[0] + } else { + data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = dataRow.GetData(jdComFinanceTransactionAmountColumnName) + } + + if p.hasOriginalColumn(jdComFinanceTransactionDescriptionColumnName) && dataRow.GetData(jdComFinanceTransactionDescriptionColumnName) != "" { + data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = dataRow.GetData(jdComFinanceTransactionDescriptionColumnName) + } else if p.hasOriginalColumn(jdComFinanceTransactionMemoColumnName) && dataRow.GetData(jdComFinanceTransactionMemoColumnName) != "" { + data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = dataRow.GetData(jdComFinanceTransactionMemoColumnName) + } else { + data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = "" + } + + if data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] == jdComFinanceTransactionTypeNameMapping[models.TRANSACTION_TYPE_TRANSFER] { + memo := dataRow.GetData(jdComFinanceTransactionMemoColumnName) + + if statusName == jdComFinanceTransactionDataStatusRefundSuccessName || strings.Index(memo, jdComFinanceTransactionMemoRefundText) >= 0 { // refund + amount, err := utils.ParseAmount(data[datatable.TRANSACTION_DATA_TABLE_AMOUNT]) + + if err == nil { + data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = jdComFinanceTransactionTypeNameMapping[models.TRANSACTION_TYPE_EXPENSE] + data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(-amount) + } + } else if strings.Index(dataRow.GetData(jdComFinanceTransactionAmountColumnName), jdComFinanceTransactionAmountRefundAll) > 0 { // expense transaction (but include a full refund) + data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = jdComFinanceTransactionTypeNameMapping[models.TRANSACTION_TYPE_EXPENSE] + } else { // transfer + if strings.Index(memo, jdComFinanceTransactionMemoTransferToWalletPrefix) >= 0 { // transfer to jd.com finance wallet + data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = dataRow.GetData(jdComFinanceTransactionMerchantNameColumnName) + } else if strings.Index(memo, jdComFinanceTransactionMemoTransferFromWalletPrefix) >= 0 { // transfer from jd.com finance wallet + data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] + data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = dataRow.GetData(jdComFinanceTransactionMerchantNameColumnName) + } else if strings.Index(memo, jdComFinanceTransactionMemoTransferInText) >= 0 { // transfer in + data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = dataRow.GetData(jdComFinanceTransactionMerchantNameColumnName) + } else if strings.Index(memo, jdComFinanceTransactionMemoTransferOutText) >= 0 { // transfer out + data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] + data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = dataRow.GetData(jdComFinanceTransactionMerchantNameColumnName) + } else if strings.Index(memo, jdComFinanceTransactionMemoRepaymentText) >= 0 { // repayment + data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = dataRow.GetData(jdComFinanceTransactionMerchantNameColumnName) + } else { + log.Warnf(ctx, "[jdcom_finance_transaction_data_row_parser.Parse] skip parsing transaction in row \"%s\", because memo (\"%s\") of this transfer transaction is unknown", rowId, memo) + return nil, false, nil + } + } + } + + return data, true, nil +} + +func (p *jdComFinanceTransactionDataRowParser) hasOriginalColumn(columnName string) bool { + _, exists := p.existedOriginalDataColumns[columnName] + return exists +} + +// createJDComFinanceTransactionDataRowParser returns jd.com finance transaction data row parser +func createJDComFinanceTransactionDataRowParser(headerColumnNames []string) datatable.CommonTransactionDataRowParser { + existedOriginalDataColumns := make(map[string]bool, len(headerColumnNames)) + + for i := 0; i < len(headerColumnNames); i++ { + existedOriginalDataColumns[headerColumnNames[i]] = true + } + + return &jdComFinanceTransactionDataRowParser{ + existedOriginalDataColumns: existedOriginalDataColumns, + } +} diff --git a/pkg/converters/transaction_data_converters.go b/pkg/converters/transaction_data_converters.go index c3e064e0..0c90115c 100644 --- a/pkg/converters/transaction_data_converters.go +++ b/pkg/converters/transaction_data_converters.go @@ -12,6 +12,7 @@ import ( "github.com/mayswind/ezbookkeeping/pkg/converters/fireflyIII" "github.com/mayswind/ezbookkeeping/pkg/converters/gnucash" "github.com/mayswind/ezbookkeeping/pkg/converters/iif" + "github.com/mayswind/ezbookkeeping/pkg/converters/jdcom" "github.com/mayswind/ezbookkeeping/pkg/converters/mt" "github.com/mayswind/ezbookkeeping/pkg/converters/ofx" "github.com/mayswind/ezbookkeeping/pkg/converters/qif" @@ -73,6 +74,8 @@ func GetTransactionDataImporter(fileType string) (converter.TransactionDataImpor return wechat.WeChatPayTransactionDataXlsxFileImporter, nil } else if fileType == "wechat_pay_app_csv" { return wechat.WeChatPayTransactionDataCsvFileImporter, nil + } else if fileType == "jdcom_finance_app_csv" { + return jdcom.JDComFinanceTransactionDataCsvFileImporter, nil } else { return nil, errs.ErrImportFileTypeNotSupported } diff --git a/src/consts/file.ts b/src/consts/file.ts index 73e2d289..55953715 100644 --- a/src/consts/file.ts +++ b/src/consts/file.ts @@ -233,6 +233,15 @@ export const SUPPORTED_IMPORT_FILE_CATEGORY_AND_TYPES: ImportFileCategoryAndType supportMultiLanguages: 'zh-Hans', anchor: '如何获取微信支付账单文件' } + }, + { + type: 'jdcom_finance_app_csv', + name: 'JD.com Finance Statement File', + extensions: '.csv', + document: { + supportMultiLanguages: 'zh-Hans', + anchor: '如何获取京东金融账单文件' + } } ] }, diff --git a/src/locales/de.json b/src/locales/de.json index 71c27532..4abff6da 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -1776,6 +1776,7 @@ "Alipay (App) Statement File": "Alipay (App) Statement File", "Alipay (Web) Statement File": "Alipay (Web) Statement File", "WeChat Pay Statement File": "WeChat Pay Statement File", + "JD.com Finance Statement File": "JD.com Finance Statement File", "Data File": "Datendatei", "Data to import": "Data to import", "Please select a file to import": "Bitte wählen Sie eine Datei zum Importieren aus", diff --git a/src/locales/en.json b/src/locales/en.json index c698667a..86220899 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1776,6 +1776,7 @@ "Alipay (App) Statement File": "Alipay (App) Statement File", "Alipay (Web) Statement File": "Alipay (Web) Statement File", "WeChat Pay Statement File": "WeChat Pay Statement File", + "JD.com Finance Statement File": "JD.com Finance Statement File", "Data File": "Data File", "Data to import": "Data to import", "Please select a file to import": "Please select a file to import", diff --git a/src/locales/es.json b/src/locales/es.json index 3cc96ef9..5846b3e5 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -1776,6 +1776,7 @@ "Alipay (App) Statement File": "Alipay (App) Statement File", "Alipay (Web) Statement File": "Alipay (Web) Statement File", "WeChat Pay Statement File": "WeChat Pay Statement File", + "JD.com Finance Statement File": "JD.com Finance Statement File", "Data File": "Archivo de datos", "Data to import": "Data to import", "Please select a file to import": "Please select a file to import", diff --git a/src/locales/it.json b/src/locales/it.json index 33b1011c..da3df653 100644 --- a/src/locales/it.json +++ b/src/locales/it.json @@ -1776,6 +1776,7 @@ "Alipay (App) Statement File": "Alipay (App) Statement File", "Alipay (Web) Statement File": "Alipay (Web) Statement File", "WeChat Pay Statement File": "WeChat Pay Statement File", + "JD.com Finance Statement File": "JD.com Finance Statement File", "Data File": "File dati", "Data to import": "Dati da importare", "Please select a file to import": "Seleziona un file da importare", diff --git a/src/locales/ja.json b/src/locales/ja.json index 57b1c636..5b2b4bdf 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -1776,6 +1776,7 @@ "Alipay (App) Statement File": "Alipay (App) Statement File", "Alipay (Web) Statement File": "Alipay (Web) Statement File", "WeChat Pay Statement File": "WeChat Pay Statement File", + "JD.com Finance Statement File": "JD.com Finance Statement File", "Data File": "データファイル", "Data to import": "インポートするデータ", "Please select a file to import": "インポートするファイルを選択してください", diff --git a/src/locales/nl.json b/src/locales/nl.json index 0256368f..dc7c694f 100644 --- a/src/locales/nl.json +++ b/src/locales/nl.json @@ -1776,6 +1776,7 @@ "Alipay (App) Statement File": "Alipay (App) Statement File", "Alipay (Web) Statement File": "Alipay (Web) Statement File", "WeChat Pay Statement File": "WeChat Pay Statement File", + "JD.com Finance Statement File": "JD.com Finance Statement File", "Data File": "Gegevensbestand", "Data to import": "Te importeren gegevens", "Please select a file to import": "Selecteer een bestand om te importeren", diff --git a/src/locales/pt_BR.json b/src/locales/pt_BR.json index d9be2e84..4e30e77a 100644 --- a/src/locales/pt_BR.json +++ b/src/locales/pt_BR.json @@ -1776,6 +1776,7 @@ "Alipay (App) Statement File": "Alipay (App) Statement File", "Alipay (Web) Statement File": "Alipay (Web) Statement File", "WeChat Pay Statement File": "WeChat Pay Statement File", + "JD.com Finance Statement File": "JD.com Finance Statement File", "Data File": "Arquivo de Dados", "Data to import": "Dados para importar", "Please select a file to import": "Por favor, selecione um arquivo para importar", diff --git a/src/locales/ru.json b/src/locales/ru.json index cb7fd994..8d6b869b 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -1776,6 +1776,7 @@ "Alipay (App) Statement File": "Alipay (App) Statement File", "Alipay (Web) Statement File": "Alipay (Web) Statement File", "WeChat Pay Statement File": "WeChat Pay Statement File", + "JD.com Finance Statement File": "JD.com Finance Statement File", "Data File": "Файл данных", "Data to import": "Data to import", "Please select a file to import": "Please select a file to import", diff --git a/src/locales/uk.json b/src/locales/uk.json index c207f1df..7d9ffec0 100644 --- a/src/locales/uk.json +++ b/src/locales/uk.json @@ -1776,6 +1776,7 @@ "Alipay (App) Statement File": "Alipay (App) Statement File", "Alipay (Web) Statement File": "Alipay (Web) Statement File", "WeChat Pay Statement File": "WeChat Pay Statement File", + "JD.com Finance Statement File": "JD.com Finance Statement File", "Data File": "Файл даних", "Data to import": "Дані для імпорту", "Please select a file to import": "Будь ласка, виберіть файл для імпорту", diff --git a/src/locales/vi.json b/src/locales/vi.json index ec935c5f..932915d1 100644 --- a/src/locales/vi.json +++ b/src/locales/vi.json @@ -1776,6 +1776,7 @@ "Alipay (App) Statement File": "Alipay (App) Statement File", "Alipay (Web) Statement File": "Alipay (Web) Statement File", "WeChat Pay Statement File": "WeChat Pay Statement File", + "JD.com Finance Statement File": "JD.com Finance Statement File", "Data File": "Tệp dữ liệu", "Data to import": "Data to import", "Please select a file to import": "Please select a file to import", diff --git a/src/locales/zh_Hans.json b/src/locales/zh_Hans.json index 77b11d98..5021ceb0 100644 --- a/src/locales/zh_Hans.json +++ b/src/locales/zh_Hans.json @@ -1776,6 +1776,7 @@ "Alipay (App) Statement File": "支付宝 (App) 交易流水文件", "Alipay (Web) Statement File": "支付宝 (网页版) 交易流水文件", "WeChat Pay Statement File": "微信支付账单文件", + "JD.com Finance Statement File": "京东金融账单文件", "Data File": "数据文件", "Data to import": "要导入的数据", "Please select a file to import": "请选择要导入的文件", diff --git a/src/locales/zh_Hant.json b/src/locales/zh_Hant.json index 58f6548d..956a1d51 100644 --- a/src/locales/zh_Hant.json +++ b/src/locales/zh_Hant.json @@ -1776,6 +1776,7 @@ "Alipay (App) Statement File": "支付寶 (App) 交易流水檔案", "Alipay (Web) Statement File": "支付寶 (網頁版) 交易流水檔案", "WeChat Pay Statement File": "微信支付帳單檔案", + "JD.com Finance Statement File": "京東金融帳單檔案", "Data File": "資料檔案", "Data to import": "要匯入的資料", "Please select a file to import": "請選擇要匯入的檔案",