From 52b37c2a13e6e7b11d0e3640f8cacbd0977e099d Mon Sep 17 00:00:00 2001 From: MaysWind Date: Sun, 22 Sep 2024 19:27:20 +0800 Subject: [PATCH] support importing transaction data from alipay export file --- ...ipay_transaction_data_csv_file_importer.go | 323 +++++++++++++ ...transaction_data_csv_file_importer_test.go | 424 ++++++++++++++++++ .../data_table_transaction_data_converter.go | 26 +- ...nsaction_data_plain_text_converter_test.go | 18 - ...transaction_data_csv_file_importer_test.go | 27 -- pkg/converters/transaction_data_converters.go | 3 + pkg/core/context.go | 1 + pkg/core/context_cli.go | 5 + pkg/core/context_cron.go | 5 + pkg/core/context_null.go | 5 + pkg/errs/converter.go | 18 +- pkg/locales/base.go | 6 + pkg/locales/en.go | 3 + pkg/locales/zh_hans.go | 3 + pkg/utils/strings.go | 15 + pkg/utils/strings_test.go | 8 + src/consts/file.js | 5 + src/locales/en.json | 5 +- src/locales/zh_Hans.json | 5 +- 19 files changed, 826 insertions(+), 79 deletions(-) create mode 100644 pkg/converters/alipay/alipay_transaction_data_csv_file_importer.go create mode 100644 pkg/converters/alipay/alipay_transaction_data_csv_file_importer_test.go diff --git a/pkg/converters/alipay/alipay_transaction_data_csv_file_importer.go b/pkg/converters/alipay/alipay_transaction_data_csv_file_importer.go new file mode 100644 index 00000000..d3b4abbe --- /dev/null +++ b/pkg/converters/alipay/alipay_transaction_data_csv_file_importer.go @@ -0,0 +1,323 @@ +package alipay + +import ( + "bytes" + "encoding/csv" + "io" + "strings" + + "golang.org/x/text/encoding/simplifiedchinese" + "golang.org/x/text/transform" + + "github.com/mayswind/ezbookkeeping/pkg/converters/datatable" + "github.com/mayswind/ezbookkeeping/pkg/core" + "github.com/mayswind/ezbookkeeping/pkg/errs" + "github.com/mayswind/ezbookkeeping/pkg/locales" + "github.com/mayswind/ezbookkeeping/pkg/log" + "github.com/mayswind/ezbookkeeping/pkg/models" + "github.com/mayswind/ezbookkeeping/pkg/utils" +) + +const alipayTransactionDataCsvFileHeader = "支付宝交易记录明细查询" +const alipayTransactionDataCsvDataHeaderLineStartContent = "交易记录明细列表" +const alipayTransactionDataCsvDataHeaderLineEndLineRune = '-' + +const alipayTransactionDataStatusSuccessName = "交易成功" +const alipayTransactionDataStatusPaymentSuccessName = "支付成功" +const alipayTransactionDataStatusRepaymentSuccessName = "还款成功" +const alipayTransactionDataStatusClosedName = "交易关闭" +const alipayTransactionDataStatusRefundSuccessName = "退款成功" +const alipayTransactionDataStatusTaxRefundSuccessName = "退税成功" + +const alipayTransactionDataProductNameRechargePrefix = "充值-" +const alipayTransactionDataProductNameCashWithdrawalPrefix = "提现-" +const alipayTransactionDataProductNameTransferInText = "转入" +const alipayTransactionDataProductNameTransferOutText = "转出" +const alipayTransactionDataProductNameRepaymentText = "还款" + +var alipayTransactionTypeFundStatusNameMapping = map[models.TransactionType]string{ + models.TRANSACTION_TYPE_INCOME: "已收入", + models.TRANSACTION_TYPE_EXPENSE: "已支出", + models.TRANSACTION_TYPE_TRANSFER: "资金转移", +} + +// alipayTransactionDataCsvImporter defines the structure of alipay csv importer for transaction data +type alipayTransactionDataCsvImporter struct{} + +// Initialize a alipay transaction data csv file importer singleton instance +var ( + AlipayTransactionDataCsvImporter = &alipayTransactionDataCsvImporter{} +) + +// ParseImportedData returns the imported data by parsing the alipay transaction csv data +func (c *alipayTransactionDataCsvImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, accountMap map[string]*models.Account, categoryMap map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionTag, error) { + enc := simplifiedchinese.GB18030 + reader := transform.NewReader(bytes.NewReader(data), enc.NewDecoder()) + allLines, err := c.parseAllLinesFromCsvData(ctx, reader) + + if err != nil { + return nil, nil, nil, nil, err + } + + if len(allLines) <= 1 { + log.Errorf(ctx, "[alipayTransactionDataCsvImporter.ParseImportedData] cannot parse import data for user \"uid:%d\", because data table row count is less 1", user.Uid) + return nil, nil, nil, nil, errs.ErrNotFoundTransactionDataInFile + } + + headerLineItems := allLines[0] + headerItemMap := make(map[string]int) + + for i := 0; i < len(headerLineItems); i++ { + headerItemMap[headerLineItems[i]] = i + } + + timeColumnIdx, timeColumnExists := headerItemMap["交易创建时间"] + targetNameColumnIdx, targetNameColumnExists := headerItemMap["交易对方"] + productNameColumnIdx, productNameColumnExists := headerItemMap["商品名称"] + amountColumnIdx, amountColumnExists := headerItemMap["金额(元)"] + statusColumnIdx, statusColumnExists := headerItemMap["交易状态"] + descriptionColumnIdx, descriptionColumnExists := headerItemMap["备注"] + fundStatusColumnIdx, fundStatusColumnExists := headerItemMap["资金状态"] + + if !timeColumnExists || !amountColumnExists || !statusColumnExists || !fundStatusColumnExists { + log.Errorf(ctx, "[alipayTransactionDataCsvImporter.ParseImportedData] cannot parse import data for user \"uid:%d\", because missing essential columns in header row", user.Uid) + return nil, nil, nil, nil, errs.ErrMissingRequiredFieldInHeaderRow + } + + newColumns := make([]datatable.DataTableColumn, 0, 7) + newColumns = append(newColumns, datatable.DATA_TABLE_TRANSACTION_TYPE) + newColumns = append(newColumns, datatable.DATA_TABLE_TRANSACTION_TIME) + newColumns = append(newColumns, datatable.DATA_TABLE_SUB_CATEGORY) + newColumns = append(newColumns, datatable.DATA_TABLE_ACCOUNT_NAME) + newColumns = append(newColumns, datatable.DATA_TABLE_AMOUNT) + newColumns = append(newColumns, datatable.DATA_TABLE_RELATED_ACCOUNT_NAME) + newColumns = append(newColumns, datatable.DATA_TABLE_DESCRIPTION) + + dataTable := datatable.CreateNewWritableDataTable(newColumns) + + for i := 1; i < len(allLines); i++ { + items := allLines[i] + + if items[fundStatusColumnIdx] != alipayTransactionTypeFundStatusNameMapping[models.TRANSACTION_TYPE_INCOME] && + items[fundStatusColumnIdx] != alipayTransactionTypeFundStatusNameMapping[models.TRANSACTION_TYPE_EXPENSE] && + items[fundStatusColumnIdx] != alipayTransactionTypeFundStatusNameMapping[models.TRANSACTION_TYPE_TRANSFER] { + log.Warnf(ctx, "[alipayTransactionDataCsvImporter.ParseImportedData] skip parsing transaction in row \"index:%d\" for user \"uid:%d\", because fund status is \"%s\"", i, user.Uid, items[fundStatusColumnIdx]) + continue + } + + data := c.parseTransactionData(ctx, + user, + items, + timeColumnIdx, + timeColumnExists, + targetNameColumnIdx, + targetNameColumnExists, + productNameColumnIdx, + productNameColumnExists, + amountColumnIdx, + amountColumnExists, + statusColumnIdx, + statusColumnExists, + descriptionColumnIdx, + descriptionColumnExists, + fundStatusColumnIdx, + fundStatusColumnExists, + ) + + if len(items) < len(headerLineItems) { + log.Errorf(ctx, "[alipayTransactionDataCsvImporter.ParseImportedData] cannot parse 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)", i, user.Uid, len(items), len(headerLineItems)) + return nil, nil, nil, nil, errs.ErrFewerFieldsInDataRowThanInHeaderRow + } + + if items[statusColumnIdx] == alipayTransactionDataStatusSuccessName || items[statusColumnIdx] == alipayTransactionDataStatusPaymentSuccessName || items[statusColumnIdx] == alipayTransactionDataStatusRepaymentSuccessName { + dataTable.Add(data) + } else if items[statusColumnIdx] == alipayTransactionDataStatusClosedName { + dataTable.Add(data) + } else if items[statusColumnIdx] == alipayTransactionDataStatusRefundSuccessName || items[statusColumnIdx] == alipayTransactionDataStatusTaxRefundSuccessName { + data[datatable.DATA_TABLE_TRANSACTION_TYPE] = alipayTransactionTypeFundStatusNameMapping[models.TRANSACTION_TYPE_EXPENSE] + data[datatable.DATA_TABLE_AMOUNT] = "-" + data[datatable.DATA_TABLE_AMOUNT] + dataTable.Add(data) + } + } + + dataTableImporter := datatable.CreateNewSimpleImporterFromWritableDataTable( + dataTable, + alipayTransactionTypeFundStatusNameMapping, + ) + + return dataTableImporter.ParseImportedData(ctx, user, dataTable, defaultTimezoneOffset, accountMap, categoryMap, tagMap) +} + +func (c *alipayTransactionDataCsvImporter) parseAllLinesFromCsvData(ctx core.Context, reader io.Reader) ([][]string, error) { + csvReader := csv.NewReader(reader) + csvReader.FieldsPerRecord = -1 + + allLines := make([][]string, 0) + hasFileHeader := false + foundContentBeforeDataHeaderLine := false + + for { + items, err := csvReader.Read() + + if err == io.EOF { + break + } + + if err != nil { + log.Errorf(ctx, "[alipayTransactionDataCsvImporter.parseAllLinesFromCsvData] cannot parse alipay csv data, because %s", err.Error()) + return nil, errs.ErrInvalidCSVFile + } + + if !hasFileHeader { + if len(items) <= 0 { + continue + } else if strings.Index(items[0], alipayTransactionDataCsvFileHeader) == 0 { + hasFileHeader = true + continue + } else { + log.Warnf(ctx, "[alipayTransactionDataCsvImporter.parseAllLinesFromCsvData] read unexpected line before read file header, line content is %s", strings.Join(items, ",")) + } + } + + if !foundContentBeforeDataHeaderLine { + if len(items) <= 0 { + continue + } else if strings.Index(items[0], alipayTransactionDataCsvDataHeaderLineStartContent) >= 0 { + foundContentBeforeDataHeaderLine = true + continue + } else { + continue + } + } + + if foundContentBeforeDataHeaderLine { + if len(items) <= 0 { + continue + } else if len(items) == 1 && utils.ContainsOnlyOneRune(items[0], alipayTransactionDataCsvDataHeaderLineEndLineRune) { + break + } + + for i := 0; i < len(items); i++ { + items[i] = strings.Trim(items[i], " ") + } + + allLines = append(allLines, items) + } + } + + if !hasFileHeader || !foundContentBeforeDataHeaderLine { + return nil, errs.ErrInvalidFileHeader + } + + return allLines, nil +} + +func (c *alipayTransactionDataCsvImporter) parseTransactionData( + ctx core.Context, + user *models.User, + items []string, + timeColumnIdx int, + timeColumnExists bool, + targetNameColumnIdx int, + targetNameColumnExists bool, + productNameColumnIdx int, + productNameColumnExists bool, + amountColumnIdx int, + amountColumnExists bool, + statusColumnIdx int, + statusColumnExists bool, + descriptionColumnIdx int, + descriptionColumnExists bool, + fundStatusColumnIdx int, + fundStatusColumnExists bool, +) map[datatable.DataTableColumn]string { + data := make(map[datatable.DataTableColumn]string, 11) + + if timeColumnExists && timeColumnIdx < len(items) { + data[datatable.DATA_TABLE_TRANSACTION_TIME] = items[timeColumnIdx] + } + + if amountColumnExists && amountColumnIdx < len(items) { + data[datatable.DATA_TABLE_AMOUNT] = items[amountColumnIdx] + } + + data[datatable.DATA_TABLE_SUB_CATEGORY] = "" + + if descriptionColumnExists && descriptionColumnIdx < len(items) && items[descriptionColumnIdx] != "" { + data[datatable.DATA_TABLE_DESCRIPTION] = items[descriptionColumnIdx] + } else if productNameColumnExists && productNameColumnIdx < len(items) && items[productNameColumnIdx] != "" { + data[datatable.DATA_TABLE_DESCRIPTION] = items[productNameColumnIdx] + } else { + data[datatable.DATA_TABLE_DESCRIPTION] = "" + } + + if fundStatusColumnExists && fundStatusColumnIdx < len(items) { + data[datatable.DATA_TABLE_TRANSACTION_TYPE] = items[fundStatusColumnIdx] + + if items[fundStatusColumnIdx] == alipayTransactionTypeFundStatusNameMapping[models.TRANSACTION_TYPE_INCOME] { + locale := user.Language + + if locale == "" { + locale = ctx.GetClientLocale() + } + + localeTextItems := locales.GetLocaleTextItems(locale) + statusName := "" + + if statusColumnExists && statusColumnIdx < len(items) { + statusName = items[statusColumnIdx] + } + + if statusName == alipayTransactionDataStatusSuccessName { + data[datatable.DATA_TABLE_ACCOUNT_NAME] = localeTextItems.DataConverterTextItems.Alipay + data[datatable.DATA_TABLE_RELATED_ACCOUNT_NAME] = "" + } else { + data[datatable.DATA_TABLE_ACCOUNT_NAME] = "" + data[datatable.DATA_TABLE_RELATED_ACCOUNT_NAME] = "" + } + } else if items[fundStatusColumnIdx] == alipayTransactionTypeFundStatusNameMapping[models.TRANSACTION_TYPE_TRANSFER] { + locale := user.Language + + if locale == "" { + locale = ctx.GetClientLocale() + } + + localeTextItems := locales.GetLocaleTextItems(locale) + targetName := "" + productName := "" + + if targetNameColumnExists && targetNameColumnIdx < len(items) { + targetName = items[targetNameColumnIdx] + } + + if productNameColumnExists && productNameColumnIdx < len(items) { + productName = items[productNameColumnIdx] + } + + 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 + } 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 + } else if strings.Index(productName, alipayTransactionDataProductNameTransferInText) >= 0 { // transfer in + data[datatable.DATA_TABLE_ACCOUNT_NAME] = "" + data[datatable.DATA_TABLE_RELATED_ACCOUNT_NAME] = targetName + } else if strings.Index(productName, alipayTransactionDataProductNameTransferOutText) >= 0 { // transfer out + data[datatable.DATA_TABLE_ACCOUNT_NAME] = "" + data[datatable.DATA_TABLE_RELATED_ACCOUNT_NAME] = targetName + } else if strings.Index(productName, alipayTransactionDataProductNameRepaymentText) >= 0 { // repayment + data[datatable.DATA_TABLE_ACCOUNT_NAME] = "" + data[datatable.DATA_TABLE_RELATED_ACCOUNT_NAME] = targetName + } else { + data[datatable.DATA_TABLE_ACCOUNT_NAME] = "" + data[datatable.DATA_TABLE_RELATED_ACCOUNT_NAME] = "" + } + } else { + data[datatable.DATA_TABLE_ACCOUNT_NAME] = "" + data[datatable.DATA_TABLE_RELATED_ACCOUNT_NAME] = "" + } + } + + return data +} diff --git a/pkg/converters/alipay/alipay_transaction_data_csv_file_importer_test.go b/pkg/converters/alipay/alipay_transaction_data_csv_file_importer_test.go new file mode 100644 index 00000000..2052ce0c --- /dev/null +++ b/pkg/converters/alipay/alipay_transaction_data_csv_file_importer_test.go @@ -0,0 +1,424 @@ +package alipay + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "golang.org/x/text/encoding/simplifiedchinese" + + "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 TestAlipayCsvFileImporterParseImportedData_MinimumValidData(t *testing.T) { + converter := AlipayTransactionDataCsvImporter + context := core.NewNullContext() + + user := &models.User{ + Uid: 1234567890, + DefaultCurrency: "CNY", + } + + data, err := simplifiedchinese.GB18030.NewEncoder().String("支付宝交易记录明细查询\n" + + "账号:[xxx@xxx.xxx]\n" + + "起始日期:[2024-01-01 00:00:00] 终止日期:[2024-09-01 23:59:59]\n" + + "---------------------------------交易记录明细列表------------------------------------\n" + + "交易创建时间 ,金额(元),交易状态 ,资金状态 ,\n" + + "2024-09-01 01:23:45 ,0.12 ,交易成功 ,已收入 ,\n" + + "2024-09-01 12:34:56 ,123.45 ,交易成功 ,已支出 ,\n" + + "2024-09-01 23:59:59 ,0.05 ,交易成功 ,资金转移 ,\n" + + "------------------------------------------------------------------------------------\n") + assert.Nil(t, err) + + allNewTransactions, allNewAccounts, allNewSubCategories, allNewTags, err := converter.ParseImportedData(context, user, []byte(data), 0, nil, nil, nil) + assert.Nil(t, err) + + assert.Equal(t, 3, len(allNewTransactions)) + assert.Equal(t, 2, len(allNewAccounts)) + assert.Equal(t, 1, len(allNewSubCategories)) + 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, "2024-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, "Alipay", 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, "2024-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, "2024-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, "", allNewTransactions[2].OriginalDestinationAccountName) + assert.Equal(t, "", allNewTransactions[2].OriginalCategoryName) + + assert.Equal(t, int64(1234567890), allNewAccounts[0].Uid) + assert.Equal(t, "Alipay", 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), allNewSubCategories[0].Uid) + assert.Equal(t, "", allNewSubCategories[0].Name) +} + +func TestAlipayCsvFileImporterParseImportedData_ParseRefundTransaction(t *testing.T) { + converter := AlipayTransactionDataCsvImporter + context := core.NewNullContext() + + user := &models.User{ + Uid: 1234567890, + DefaultCurrency: "CNY", + } + + data1, err := simplifiedchinese.GB18030.NewEncoder().String("支付宝交易记录明细查询\n" + + "账号:[xxx@xxx.xxx]\n" + + "起始日期:[2024-01-01 00:00:00] 终止日期:[2024-09-01 23:59:59]\n" + + "---------------------------------交易记录明细列表------------------------------------\n" + + "交易创建时间 ,金额(元),交易状态 ,资金状态 ,\n" + + "2024-09-01 01:23:45 ,0.12 ,退款成功 ,已收入 ,\n" + + "------------------------------------------------------------------------------------\n") + assert.Nil(t, err) + + allNewTransactions, _, _, _, err := converter.ParseImportedData(context, user, []byte(data1), 0, 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, "2024-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) + + data2, err := simplifiedchinese.GB18030.NewEncoder().String("支付宝交易记录明细查询\n" + + "账号:[xxx@xxx.xxx]\n" + + "起始日期:[2024-01-01 00:00:00] 终止日期:[2024-09-01 23:59:59]\n" + + "---------------------------------交易记录明细列表------------------------------------\n" + + "交易创建时间 ,金额(元),交易状态 ,资金状态 ,\n" + + "2024-09-01 01:23:45 ,0.12 ,退税成功 ,已收入 ,\n" + + "------------------------------------------------------------------------------------\n") + assert.Nil(t, err) + + allNewTransactions, _, _, _, err = converter.ParseImportedData(context, user, []byte(data2), 0, 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, "2024-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) +} + +func TestAlipayCsvFileImporterParseImportedData_ParseInvalidTime(t *testing.T) { + converter := AlipayTransactionDataCsvImporter + context := core.NewNullContext() + + user := &models.User{ + Uid: 1234567890, + DefaultCurrency: "CNY", + } + + data1, err := simplifiedchinese.GB18030.NewEncoder().String("支付宝交易记录明细查询\n" + + "账号:[xxx@xxx.xxx]\n" + + "起始日期:[2024-01-01 00:00:00] 终止日期:[2024-09-01 23:59:59]\n" + + "---------------------------------交易记录明细列表------------------------------------\n" + + "交易创建时间 ,金额(元),交易状态 ,资金状态 ,\n" + + "2024-09-01T12:34:56 ,0.12 ,交易成功 ,已收入 ,\n" + + "------------------------------------------------------------------------------------\n") + assert.Nil(t, err) + + _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data1), 0, nil, nil, nil) + assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message) + + data2, err := simplifiedchinese.GB18030.NewEncoder().String("支付宝交易记录明细查询\n" + + "账号:[xxx@xxx.xxx]\n" + + "起始日期:[2024-01-01 00:00:00] 终止日期:[2024-09-01 23:59:59]\n" + + "---------------------------------交易记录明细列表------------------------------------\n" + + "交易创建时间 ,金额(元),交易状态 ,资金状态 ,\n" + + "09/01/2024 12:34:56 ,0.12 ,交易成功 ,已收入 ,\n" + + "------------------------------------------------------------------------------------\n") + assert.Nil(t, err) + + _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data2), 0, nil, nil, nil) + assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message) +} + +func TestAlipayCsvFileImporterParseImportedData_ParseInvalidType(t *testing.T) { + converter := AlipayTransactionDataCsvImporter + context := core.NewNullContext() + + user := &models.User{ + Uid: 1234567890, + DefaultCurrency: "CNY", + } + + data, err := simplifiedchinese.GB18030.NewEncoder().String("支付宝交易记录明细查询\n" + + "账号:[xxx@xxx.xxx]\n" + + "起始日期:[2024-01-01 00:00:00] 终止日期:[2024-09-01 23:59:59]\n" + + "---------------------------------交易记录明细列表------------------------------------\n" + + "交易创建时间 ,金额(元),交易状态 ,资金状态 ,\n" + + "2024-09-01 12:34:56 ,0.12 ,交易成功 , ,\n" + + "------------------------------------------------------------------------------------\n") + assert.Nil(t, err) + + _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data), 0, nil, nil, nil) + assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message) +} + +func TestAlipayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T) { + converter := AlipayTransactionDataCsvImporter + context := core.NewNullContext() + + user := &models.User{ + Uid: 1234567890, + DefaultCurrency: "CNY", + } + + // income to alipay wallet + data1, err := simplifiedchinese.GB18030.NewEncoder().String("支付宝交易记录明细查询\n" + + "账号:[xxx@xxx.xxx]\n" + + "起始日期:[2024-01-01 00:00:00] 终止日期:[2024-09-01 23:59:59]\n" + + "---------------------------------交易记录明细列表------------------------------------\n" + + "交易创建时间 ,交易对方 ,金额(元),交易状态 ,资金状态 ,\n" + + "2024-09-01 12:34:56 ,test ,0.12 ,交易成功 ,已收入 ,\n" + + "------------------------------------------------------------------------------------\n") + assert.Nil(t, err) + + allNewTransactions, _, _, _, err := converter.ParseImportedData(context, user, []byte(data1), 0, nil, nil, nil) + assert.Nil(t, err) + + assert.Equal(t, 1, len(allNewTransactions)) + assert.Equal(t, "Alipay", allNewTransactions[0].OriginalSourceAccountName) + + // income to other account + data2, err := simplifiedchinese.GB18030.NewEncoder().String("支付宝交易记录明细查询\n" + + "账号:[xxx@xxx.xxx]\n" + + "起始日期:[2024-01-01 00:00:00] 终止日期:[2024-09-01 23:59:59]\n" + + "---------------------------------交易记录明细列表------------------------------------\n" + + "交易创建时间 ,交易对方 ,金额(元),交易状态 ,资金状态 ,\n" + + "2024-09-01 12:34:56 ,test ,0.12 ,退款成功 ,已收入 ,\n" + + "------------------------------------------------------------------------------------\n") + assert.Nil(t, err) + + allNewTransactions, _, _, _, err = converter.ParseImportedData(context, user, []byte(data2), 0, nil, nil, nil) + assert.Nil(t, err) + + assert.Equal(t, 1, len(allNewTransactions)) + assert.Equal(t, "", allNewTransactions[0].OriginalSourceAccountName) + + // transfer to alipay wallet + data3, err := simplifiedchinese.GB18030.NewEncoder().String("支付宝交易记录明细查询\n" + + "账号:[xxx@xxx.xxx]\n" + + "起始日期:[2024-01-01 00:00:00] 终止日期:[2024-09-01 23:59:59]\n" + + "---------------------------------交易记录明细列表------------------------------------\n" + + "交易创建时间 ,交易对方 ,商品名称 ,金额(元),交易状态 ,资金状态 ,\n" + + "2024-09-01 12:34:56 ,test ,充值-普通充值 ,0.12 ,交易成功 ,资金转移 ,\n" + + "------------------------------------------------------------------------------------\n") + assert.Nil(t, err) + + allNewTransactions, _, _, _, err = converter.ParseImportedData(context, user, []byte(data3), 0, nil, nil, nil) + assert.Nil(t, err) + + assert.Equal(t, 1, len(allNewTransactions)) + assert.Equal(t, "", allNewTransactions[0].OriginalSourceAccountName) + assert.Equal(t, "Alipay", allNewTransactions[0].OriginalDestinationAccountName) + + // transfer from alipay wallet + data4, err := simplifiedchinese.GB18030.NewEncoder().String("支付宝交易记录明细查询\n" + + "账号:[xxx@xxx.xxx]\n" + + "起始日期:[2024-01-01 00:00:00] 终止日期:[2024-09-01 23:59:59]\n" + + "---------------------------------交易记录明细列表------------------------------------\n" + + "交易创建时间 ,交易对方 ,商品名称 ,金额(元),交易状态 ,资金状态 ,\n" + + "2024-09-01 12:34:56 ,test ,提现-实时提现 ,0.12 ,交易成功 ,资金转移 ,\n" + + "------------------------------------------------------------------------------------\n") + assert.Nil(t, err) + + allNewTransactions, _, _, _, err = converter.ParseImportedData(context, user, []byte(data4), 0, nil, nil, nil) + assert.Nil(t, err) + + assert.Equal(t, 1, len(allNewTransactions)) + assert.Equal(t, "Alipay", allNewTransactions[0].OriginalSourceAccountName) + assert.Equal(t, "test", allNewTransactions[0].OriginalDestinationAccountName) + + // transfer in + data5, err := simplifiedchinese.GB18030.NewEncoder().String("支付宝交易记录明细查询\n" + + "账号:[xxx@xxx.xxx]\n" + + "起始日期:[2024-01-01 00:00:00] 终止日期:[2024-09-01 23:59:59]\n" + + "---------------------------------交易记录明细列表------------------------------------\n" + + "交易创建时间 ,交易对方 ,商品名称 ,金额(元),交易状态 ,资金状态 ,\n" + + "2024-09-01 12:34:56 ,test ,xx-转入 ,0.12 ,交易成功 ,资金转移 ,\n" + + "------------------------------------------------------------------------------------\n") + assert.Nil(t, err) + + allNewTransactions, _, _, _, err = converter.ParseImportedData(context, user, []byte(data5), 0, nil, nil, nil) + assert.Nil(t, err) + + assert.Equal(t, 1, len(allNewTransactions)) + assert.Equal(t, "", allNewTransactions[0].OriginalSourceAccountName) + assert.Equal(t, "test", allNewTransactions[0].OriginalDestinationAccountName) + + // transfer out + data6, err := simplifiedchinese.GB18030.NewEncoder().String("支付宝交易记录明细查询\n" + + "账号:[xxx@xxx.xxx]\n" + + "起始日期:[2024-01-01 00:00:00] 终止日期:[2024-09-01 23:59:59]\n" + + "---------------------------------交易记录明细列表------------------------------------\n" + + "交易创建时间 ,交易对方 ,商品名称 ,金额(元),交易状态 ,资金状态 ,\n" + + "2024-09-01 12:34:56 ,test ,xx-转出 ,0.12 ,交易成功 ,资金转移 ,\n" + + "------------------------------------------------------------------------------------\n") + assert.Nil(t, err) + + allNewTransactions, _, _, _, err = converter.ParseImportedData(context, user, []byte(data6), 0, nil, nil, nil) + assert.Nil(t, err) + + assert.Equal(t, 1, len(allNewTransactions)) + assert.Equal(t, "", allNewTransactions[0].OriginalSourceAccountName) + assert.Equal(t, "test", allNewTransactions[0].OriginalDestinationAccountName) + + // repayment + data7, err := simplifiedchinese.GB18030.NewEncoder().String("支付宝交易记录明细查询\n" + + "账号:[xxx@xxx.xxx]\n" + + "起始日期:[2024-01-01 00:00:00] 终止日期:[2024-09-01 23:59:59]\n" + + "---------------------------------交易记录明细列表------------------------------------\n" + + "交易创建时间 ,交易对方 ,商品名称 ,金额(元),交易状态 ,资金状态 ,\n" + + "2024-09-01 12:34:56 ,test ,xx还款 ,0.12 ,交易成功 ,资金转移 ,\n" + + "------------------------------------------------------------------------------------\n") + assert.Nil(t, err) + + allNewTransactions, _, _, _, err = converter.ParseImportedData(context, user, []byte(data7), 0, nil, nil, nil) + assert.Nil(t, err) + + assert.Equal(t, 1, len(allNewTransactions)) + assert.Equal(t, "", allNewTransactions[0].OriginalSourceAccountName) + assert.Equal(t, "test", allNewTransactions[0].OriginalDestinationAccountName) +} + +func TestAlipayCsvFileImporterParseImportedData_ParseDescription(t *testing.T) { + converter := AlipayTransactionDataCsvImporter + context := core.NewNullContext() + + user := &models.User{ + Uid: 1234567890, + DefaultCurrency: "CNY", + } + + data1, err := simplifiedchinese.GB18030.NewEncoder().String("支付宝交易记录明细查询\n" + + "账号:[xxx@xxx.xxx]\n" + + "起始日期:[2024-01-01 00:00:00] 终止日期:[2024-09-01 23:59:59]\n" + + "---------------------------------交易记录明细列表------------------------------------\n" + + "交易创建时间 ,商品名称 ,金额(元),交易状态 ,备注 ,资金状态 ,\n" + + "2024-09-01 12:34:56 ,test ,0.12 ,交易成功 ,test2 ,已收入 ,\n" + + "------------------------------------------------------------------------------------\n") + assert.Nil(t, err) + + allNewTransactions, _, _, _, err := converter.ParseImportedData(context, user, []byte(data1), 0, nil, nil, nil) + assert.Nil(t, err) + + assert.Equal(t, 1, len(allNewTransactions)) + assert.Equal(t, "test2", allNewTransactions[0].Comment) + + data2, err := simplifiedchinese.GB18030.NewEncoder().String("支付宝交易记录明细查询\n" + + "账号:[xxx@xxx.xxx]\n" + + "起始日期:[2024-01-01 00:00:00] 终止日期:[2024-09-01 23:59:59]\n" + + "---------------------------------交易记录明细列表------------------------------------\n" + + "交易创建时间 ,商品名称 ,金额(元),交易状态 ,备注 ,资金状态 ,\n" + + "2024-09-01 12:34:56 ,test ,0.12 ,交易成功 , ,已收入 ,\n" + + "------------------------------------------------------------------------------------\n") + assert.Nil(t, err) + + allNewTransactions, _, _, _, err = converter.ParseImportedData(context, user, []byte(data2), 0, nil, nil, nil) + assert.Nil(t, err) + + assert.Equal(t, 1, len(allNewTransactions)) + assert.Equal(t, "test", allNewTransactions[0].Comment) +} + +func TestAlipayCsvFileImporterParseImportedData_MissingFileHeader(t *testing.T) { + converter := AlipayTransactionDataCsvImporter + context := core.NewNullContext() + + user := &models.User{ + Uid: 1, + DefaultCurrency: "CNY", + } + + data, err := simplifiedchinese.GB18030.NewEncoder().String( + "交易创建时间 ,金额(元),交易状态 ,资金状态 ,\n" + + "2024-09-01 12:34:56 ,0.12 ,交易成功 ,Type ,\n" + + "------------------------------------------------------------------------------------\n") + assert.Nil(t, err) + + _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data), 0, nil, nil, nil) + assert.EqualError(t, err, errs.ErrInvalidFileHeader.Message) + + _, _, _, _, err = converter.ParseImportedData(context, user, []byte(""), 0, nil, nil, nil) + assert.EqualError(t, err, errs.ErrInvalidFileHeader.Message) +} + +func TestAlipayCsvFileImporterParseImportedData_MissingRequiredColumn(t *testing.T) { + converter := AlipayTransactionDataCsvImporter + context := core.NewNullContext() + + user := &models.User{ + Uid: 1, + DefaultCurrency: "CNY", + } + + // Missing Time Column + data1, err := simplifiedchinese.GB18030.NewEncoder().String("支付宝交易记录明细查询\n" + + "账号:[xxx@xxx.xxx]\n" + + "起始日期:[2024-01-01 00:00:00] 终止日期:[2024-09-01 23:59:59]\n" + + "---------------------------------交易记录明细列表------------------------------------\n" + + "金额(元),交易状态 ,资金状态 ,\n" + + "0.12 ,交易成功 ,已收入 ,\n" + + "------------------------------------------------------------------------------------\n") + _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data1), 0, nil, nil, nil) + assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) + + // Missing Amount Column + data2, err := simplifiedchinese.GB18030.NewEncoder().String("支付宝交易记录明细查询\n" + + "账号:[xxx@xxx.xxx]\n" + + "起始日期:[2024-01-01 00:00:00] 终止日期:[2024-09-01 23:59:59]\n" + + "---------------------------------交易记录明细列表------------------------------------\n" + + "交易创建时间 ,交易状态 ,资金状态 ,\n" + + "2024-09-01 12:34:56 ,交易成功 ,已收入 ,\n" + + "------------------------------------------------------------------------------------\n") + _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data2), 0, nil, nil, nil) + assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) + + // Missing Status Column + data3, err := simplifiedchinese.GB18030.NewEncoder().String("支付宝交易记录明细查询\n" + + "账号:[xxx@xxx.xxx]\n" + + "起始日期:[2024-01-01 00:00:00] 终止日期:[2024-09-01 23:59:59]\n" + + "---------------------------------交易记录明细列表------------------------------------\n" + + "交易创建时间 ,金额(元),资金状态 ,\n" + + "2024-09-01 12:34:56 ,0.12 ,已收入 ,\n" + + "------------------------------------------------------------------------------------\n") + _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data3), 0, nil, nil, nil) + assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) + + // Missing Fund Status Column + data4, err := simplifiedchinese.GB18030.NewEncoder().String("支付宝交易记录明细查询\n" + + "账号:[xxx@xxx.xxx]\n" + + "起始日期:[2024-01-01 00:00:00] 终止日期:[2024-09-01 23:59:59]\n" + + "---------------------------------交易记录明细列表------------------------------------\n" + + "交易创建时间 ,金额(元),交易状态 ,\n" + + "2024-09-01 12:34:56 ,0.12 ,交易成功 ,\n" + + "------------------------------------------------------------------------------------\n") + _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data4), 0, nil, nil, nil) + assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) +} diff --git a/pkg/converters/datatable/data_table_transaction_data_converter.go b/pkg/converters/datatable/data_table_transaction_data_converter.go index 53df0d2d..a6a04a92 100644 --- a/pkg/converters/datatable/data_table_transaction_data_converter.go +++ b/pkg/converters/datatable/data_table_transaction_data_converter.go @@ -84,6 +84,14 @@ func CreateNewSimpleImporterWithPostProcessFunc(dataColumnMapping map[DataTableC } } +// 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{ @@ -93,7 +101,7 @@ func CreateNewSimpleImporterFromWritableDataTableWithPostProcessFunc(writableDat } } -// BuildExportedContent writes the exported transaction data to the data table builder +// 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 { for i := 0; i < len(transactions); i++ { transaction := transactions[i] @@ -356,12 +364,6 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u } accountName := dataRow.GetData(accountColumnIdx) - - if accountName == "" { - log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] account name is empty in data row \"index:%d\" for user \"uid:%d\"", dataRowIndex, user.Uid) - return nil, nil, nil, nil, errs.ErrAccountNameCannotBeBlank - } - accountCurrency := user.DefaultCurrency if accountCurrencyColumnExists { @@ -382,7 +384,7 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u } if accountCurrencyColumnExists { - if account.Currency != accountCurrency { + 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, errs.ErrAccountCurrencyInvalid } @@ -404,12 +406,6 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u if transactionDbType == models.TRANSACTION_DB_TYPE_TRANSFER_OUT { account2Name = dataRow.GetData(account2ColumnIdx) - - if account2Name == "" { - log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] account2 name is empty in data row \"index:%d\" for user \"uid:%d\"", dataRowIndex, user.Uid) - return nil, nil, nil, nil, errs.ErrDestinationAccountNameCannotBeBlank - } - account2Currency = user.DefaultCurrency if account2CurrencyColumnExists { @@ -430,7 +426,7 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u } if account2CurrencyColumnExists { - if account2.Currency != account2Currency { + 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, errs.ErrAccountCurrencyInvalid } diff --git a/pkg/converters/default/ezbookkeeping_transaction_data_plain_text_converter_test.go b/pkg/converters/default/ezbookkeeping_transaction_data_plain_text_converter_test.go index 923b3d24..6cb891cd 100644 --- a/pkg/converters/default/ezbookkeeping_transaction_data_plain_text_converter_test.go +++ b/pkg/converters/default/ezbookkeeping_transaction_data_plain_text_converter_test.go @@ -255,24 +255,6 @@ func TestEzBookKeepingPlainFileConverterParseImportedData_ParseInvalidTimezone(t assert.NotNil(t, err) } -func TestEzBookKeepingPlainFileConverterParseImportedData_ParseInvalidAccountName(t *testing.T) { - converter := EzBookKeepingTransactionDataCSVFileConverter - context := core.NewNullContext() - - user := &models.User{ - Uid: 1234567890, - DefaultCurrency: "CNY", - } - - _, _, _, _, err := converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+ - "2024-09-01 12:34:56,Expense,Test Category,,123.45,,"), 0, nil, nil, nil) - assert.NotNil(t, err) - - _, _, _, _, err = converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+ - "2024-09-01 12:34:56,Transfer,Test Category,Test Account,123.45,,123.45"), 0, nil, nil, nil) - assert.NotNil(t, err) -} - func TestEzBookKeepingPlainFileConverterParseImportedData_ParseValidAccountCurrency(t *testing.T) { converter := EzBookKeepingTransactionDataCSVFileConverter context := core.NewNullContext() diff --git a/pkg/converters/feidee/feidee_mymoney_transaction_data_csv_file_importer_test.go b/pkg/converters/feidee/feidee_mymoney_transaction_data_csv_file_importer_test.go index c4e3d8c3..d01aa727 100644 --- a/pkg/converters/feidee/feidee_mymoney_transaction_data_csv_file_importer_test.go +++ b/pkg/converters/feidee/feidee_mymoney_transaction_data_csv_file_importer_test.go @@ -135,33 +135,6 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseInvalidType(t *testi assert.NotNil(t, err) } -func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseInvalidAccountName(t *testing.T) { - converter := FeideeMymoneyTransactionDataCsvImporter - context := core.NewNullContext() - - user := &models.User{ - Uid: 1234567890, - DefaultCurrency: "CNY", - } - - _, _, _, _, err := converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+ - "\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+ - "\"支出\",\"2024-09-01 12:34:56\",\"Test Category\",\"\",\"123.45\",\"\",\"\""), 0, nil, nil, nil) - assert.NotNil(t, err) - - _, _, _, _, err = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+ - "\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+ - "\"转出\",\"2024-09-01 12:34:56\",\"Test Category\",\"\",\"123.45\",\"\",\"00000000-0000-0000-0000-000000000001\"\n"+ - "\"转入\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account2\",\"123.45\",\"\",\"00000000-0000-0000-0000-000000000001\""), 0, nil, nil, nil) - assert.NotNil(t, err) - - _, _, _, _, err = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+ - "\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+ - "\"转出\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account\",\"123.45\",\"\",\"00000000-0000-0000-0000-000000000001\"\n"+ - "\"转入\",\"2024-09-01 12:34:56\",\"Test Category\",\"\",\"123.45\",\"\",\"00000000-0000-0000-0000-000000000001\""), 0, nil, nil, nil) - assert.NotNil(t, err) -} - func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseValidAccountCurrency(t *testing.T) { converter := FeideeMymoneyTransactionDataCsvImporter context := core.NewNullContext() diff --git a/pkg/converters/transaction_data_converters.go b/pkg/converters/transaction_data_converters.go index d02fcde9..10c04111 100644 --- a/pkg/converters/transaction_data_converters.go +++ b/pkg/converters/transaction_data_converters.go @@ -1,6 +1,7 @@ package converters import ( + "github.com/mayswind/ezbookkeeping/pkg/converters/alipay" "github.com/mayswind/ezbookkeeping/pkg/converters/base" "github.com/mayswind/ezbookkeeping/pkg/converters/default" "github.com/mayswind/ezbookkeeping/pkg/converters/feidee" @@ -28,6 +29,8 @@ func GetTransactionDataImporter(fileType string) (base.TransactionDataImporter, return feidee.FeideeMymoneyTransactionDataCsvImporter, nil } else if fileType == "feidee_mymoney_xls" { return feidee.FeideeMymoneyTransactionDataXlsImporter, nil + } else if fileType == "alipay_csv" { + return alipay.AlipayTransactionDataCsvImporter, nil } else { return nil, errs.ErrImportFileTypeNotSupported } diff --git a/pkg/core/context.go b/pkg/core/context.go index 7e5420c1..e4c142e9 100644 --- a/pkg/core/context.go +++ b/pkg/core/context.go @@ -3,4 +3,5 @@ package core // Context is the base context of ezBookkeeping type Context interface { GetContextId() string + GetClientLocale() string } diff --git a/pkg/core/context_cli.go b/pkg/core/context_cli.go index e6c6009e..2509ab9c 100644 --- a/pkg/core/context_cli.go +++ b/pkg/core/context_cli.go @@ -14,6 +14,11 @@ func (c *CliContext) GetContextId() string { return "" } +// GetClientLocale returns the client locale name +func (c *CliContext) GetClientLocale() string { + return "" +} + // WrapCliContext returns a context wrapped by this file func WrapCilContext(cliCtx *cli.Context) *CliContext { return &CliContext{ diff --git a/pkg/core/context_cron.go b/pkg/core/context_cron.go index 0e4c3c58..8dc5eba8 100644 --- a/pkg/core/context_cron.go +++ b/pkg/core/context_cron.go @@ -19,6 +19,11 @@ func (c *CronContext) GetContextId() string { return c.contextId } +// GetClientLocale returns the client locale name +func (c *CronContext) GetClientLocale() string { + return "" +} + // GetInterval returns the current cron job interval func (c *CronContext) GetInterval() time.Duration { return c.cronJobInterval diff --git a/pkg/core/context_null.go b/pkg/core/context_null.go index 1f6e5fa9..b749ef79 100644 --- a/pkg/core/context_null.go +++ b/pkg/core/context_null.go @@ -14,6 +14,11 @@ func (c *NullContext) GetContextId() string { return nullContextId } +// GetClientLocale returns the client locale name +func (c *NullContext) GetClientLocale() string { + return "" +} + // NewCronJobContext returns a new null context func NewNullContext() *NullContext { return &NullContext{ diff --git a/pkg/errs/converter.go b/pkg/errs/converter.go index 63f12617..c681f09f 100644 --- a/pkg/errs/converter.go +++ b/pkg/errs/converter.go @@ -9,15 +9,11 @@ var ( ErrFewerFieldsInDataRowThanInHeaderRow = NewNormalError(NormalSubcategoryConverter, 2, http.StatusBadRequest, "fewer fields in data row than in header row") ErrTransactionTimeInvalid = NewNormalError(NormalSubcategoryConverter, 3, http.StatusBadRequest, "transaction time is invalid") ErrTransactionTimeZoneInvalid = NewNormalError(NormalSubcategoryConverter, 4, http.StatusBadRequest, "transaction time zone is invalid") - ErrCategoryNameCannotBeBlank = NewNormalError(NormalSubcategoryConverter, 5, http.StatusBadRequest, "category name cannot be blank") - ErrSubCategoryNameCannotBeBlank = NewNormalError(NormalSubcategoryConverter, 6, http.StatusBadRequest, "secondary category name cannot be blank") - ErrAccountNameCannotBeBlank = NewNormalError(NormalSubcategoryConverter, 7, http.StatusBadRequest, "account name cannot be blank") - ErrDestinationAccountNameCannotBeBlank = NewNormalError(NormalSubcategoryConverter, 8, http.StatusBadRequest, "destination account name cannot be blank") - ErrAmountInvalid = NewNormalError(NormalSubcategoryConverter, 9, http.StatusBadRequest, "transaction amount is invalid") - ErrGeographicLocationInvalid = NewNormalError(NormalSubcategoryConverter, 10, http.StatusBadRequest, "geographic location is invalid") - ErrFieldsInMultiTableAreDifferent = NewNormalError(NormalSubcategoryConverter, 11, http.StatusBadRequest, "fields in multiple table headers are different") - ErrInvalidFileHeader = NewNormalError(NormalSubcategoryConverter, 12, http.StatusBadRequest, "invalid file header") - ErrInvalidCSVFile = NewNormalError(NormalSubcategoryConverter, 13, http.StatusBadRequest, "invalid csv file") - ErrRelatedIdCannotBeBlank = NewNormalError(NormalSubcategoryConverter, 14, http.StatusBadRequest, "related id cannot be blank") - ErrFoundRecordNotHasRelatedRecord = NewNormalError(NormalSubcategoryConverter, 15, http.StatusBadRequest, "found some transactions without related records") + ErrAmountInvalid = NewNormalError(NormalSubcategoryConverter, 5, http.StatusBadRequest, "transaction amount is invalid") + ErrGeographicLocationInvalid = NewNormalError(NormalSubcategoryConverter, 6, http.StatusBadRequest, "geographic location is invalid") + ErrFieldsInMultiTableAreDifferent = NewNormalError(NormalSubcategoryConverter, 7, http.StatusBadRequest, "fields in multiple table headers are different") + ErrInvalidFileHeader = NewNormalError(NormalSubcategoryConverter, 8, http.StatusBadRequest, "invalid file header") + ErrInvalidCSVFile = NewNormalError(NormalSubcategoryConverter, 9, http.StatusBadRequest, "invalid csv file") + ErrRelatedIdCannotBeBlank = NewNormalError(NormalSubcategoryConverter, 10, http.StatusBadRequest, "related id cannot be blank") + ErrFoundRecordNotHasRelatedRecord = NewNormalError(NormalSubcategoryConverter, 11, http.StatusBadRequest, "found some transactions without related records") ) diff --git a/pkg/locales/base.go b/pkg/locales/base.go index 9f2e7d28..a6d789c7 100644 --- a/pkg/locales/base.go +++ b/pkg/locales/base.go @@ -7,6 +7,7 @@ import ( // LocaleTextItems represents all text items need to be translated type LocaleTextItems struct { DefaultTypes *DefaultTypes + DataConverterTextItems *DataConverterTextItems VerifyEmailTextItems *VerifyEmailTextItems ForgetPasswordMailTextItems *ForgetPasswordMailTextItems } @@ -17,6 +18,11 @@ type DefaultTypes struct { DigitGroupingSymbol core.DigitGroupingSymbol } +// DataConverterTextItems represents text items need to be translated in data converter +type DataConverterTextItems struct { + Alipay string +} + // VerifyEmailTextItems represents text items need to be translated in verify mail type VerifyEmailTextItems struct { Title string diff --git a/pkg/locales/en.go b/pkg/locales/en.go index 69e596b3..cbe2e53f 100644 --- a/pkg/locales/en.go +++ b/pkg/locales/en.go @@ -9,6 +9,9 @@ var en = &LocaleTextItems{ DecimalSeparator: core.DECIMAL_SEPARATOR_DOT, DigitGroupingSymbol: core.DIGIT_GROUPING_SYMBOL_COMMA, }, + DataConverterTextItems: &DataConverterTextItems{ + Alipay: "Alipay", + }, VerifyEmailTextItems: &VerifyEmailTextItems{ Title: "Verify Email", SalutationFormat: "Hi %s,", diff --git a/pkg/locales/zh_hans.go b/pkg/locales/zh_hans.go index bfffa9f1..4b61cb39 100644 --- a/pkg/locales/zh_hans.go +++ b/pkg/locales/zh_hans.go @@ -9,6 +9,9 @@ var zhHans = &LocaleTextItems{ DecimalSeparator: core.DECIMAL_SEPARATOR_DOT, DigitGroupingSymbol: core.DIGIT_GROUPING_SYMBOL_COMMA, }, + DataConverterTextItems: &DataConverterTextItems{ + Alipay: "支付宝", + }, VerifyEmailTextItems: &VerifyEmailTextItems{ Title: "验证邮箱", SalutationFormat: "%s 您好,", diff --git a/pkg/utils/strings.go b/pkg/utils/strings.go index 5674d06f..91cb5fa5 100644 --- a/pkg/utils/strings.go +++ b/pkg/utils/strings.go @@ -76,6 +76,21 @@ func GetFirstLowerCharString(s string) string { return string(chars) } +// ContainsOnlyOneRune returns the source string only contains one character +func ContainsOnlyOneRune(s string, r rune) bool { + if len(s) < 1 { + return false + } + + for i := 0; i < len(s); i++ { + if rune(s[i]) != r { + return false + } + } + + return true +} + // GetRandomString returns a random string of which length is n func GetRandomString(n int) (string, error) { var result = make([]byte, n) diff --git a/pkg/utils/strings_test.go b/pkg/utils/strings_test.go index d9eca959..a845e66b 100644 --- a/pkg/utils/strings_test.go +++ b/pkg/utils/strings_test.go @@ -82,6 +82,14 @@ func TestGetFirstLowerCharString(t *testing.T) { assert.Equal(t, expectedValue, actualValue) } +func TestContainsOnlyOneRune(t *testing.T) { + actualValue := ContainsOnlyOneRune("-------", '-') + assert.Equal(t, true, actualValue) + + actualValue = ContainsOnlyOneRune(" -------", '-') + assert.Equal(t, false, actualValue) +} + func TestGetRandomString(t *testing.T) { actualValue, err := GetRandomString(10) assert.Equal(t, nil, err) diff --git a/src/consts/file.js b/src/consts/file.js index 45214e92..aea82dd2 100644 --- a/src/consts/file.js +++ b/src/consts/file.js @@ -20,6 +20,11 @@ const supportedImportFileTypes = [ type: 'feidee_mymoney_xls', name: 'Feidee MyMoney (Web) Data Export File', extensions: '.xls' + }, + { + type: 'alipay_csv', + name: 'Alipay Data Export File', + extensions: '.csv' } ]; diff --git a/src/locales/en.json b/src/locales/en.json index fe288bda..2069fa62 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1112,10 +1112,6 @@ "fewer fields in data row than in header row": "There are fewer fields in the data row than in the header row", "transaction time is invalid": "Transaction time is invalid", "transaction time zone is invalid": "Transaction time zone is invalid", - "category name cannot be blank": "Category name cannot be blank", - "secondary category name cannot be blank": "secondary category name cannot be blank", - "account name cannot be blank": "Account name cannot be blank", - "destination account name cannot be blank": "Destination account name cannot be blank", "transaction amount is invalid": "Transaction amount is invalid", "geographic location is invalid": "Geographic location is invalid", "fields in multiple table headers are different": "Fields in multiple table headers are different", @@ -1505,6 +1501,7 @@ "ezbookkeeping Data Export File (TSV)": "ezbookkeeping Data Export File (TSV)", "Feidee MyMoney (Web) Data Export File": "Feidee MyMoney (Web) Data Export File", "Feidee MyMoney (App) Data Export File": "Feidee MyMoney (App) Data Export File", + "Alipay Data Export File": "Alipay Data Export File", "Data File": "Data File", "No data to import": "No data to import", "Unable to parse import file": "Unable to parse import file", diff --git a/src/locales/zh_Hans.json b/src/locales/zh_Hans.json index 10f594a7..4f7eef69 100644 --- a/src/locales/zh_Hans.json +++ b/src/locales/zh_Hans.json @@ -1112,10 +1112,6 @@ "fewer fields in data row than in header row": "数据行中的字段少于比标题行中的字段", "transaction time is invalid": "交易时间无效", "transaction time zone is invalid": "交易时区无效", - "category name cannot be blank": "分类名称不能为空", - "secondary category name cannot be blank": "二级分类名称不能为空", - "account name cannot be blank": "账户名不能为空", - "destination account name cannot be blank": "目标账户名不能为空", "transaction amount is invalid": "交易金额无效", "geographic location is invalid": "地理位置无效", "fields in multiple table headers are different": "多个表头中的字段不同", @@ -1505,6 +1501,7 @@ "ezbookkeeping Data Export File (TSV)": "ezbookkeeping 数据导出文件 (TSV)", "Feidee MyMoney (Web) Data Export File": "金蝶随手记 (Web版) 数据导出文件", "Feidee MyMoney (App) Data Export File": "金蝶随手记 (App) 数据导出文件", + "Alipay Data Export File": "支付宝数据导出文件", "Data File": "数据文件", "No data to import": "没有可以导入的数据", "Unable to parse import file": "无法解析导入的文件",