From 8bc763be9bcb574962bb7ad821d0eef8389eae9f Mon Sep 17 00:00:00 2001 From: MaysWind Date: Fri, 18 Oct 2024 00:53:10 +0800 Subject: [PATCH] code refactor --- .../alipay_transaction_csv_data_table.go | 374 ------------------ ...ipay_transaction_data_csv_file_importer.go | 128 +++++- .../alipay_transaction_data_row_parser.go | 178 +++++++++ .../common_transaction_data_table.go | 114 ++++++ ..._app_transaction_data_csv_file_importer.go | 12 +- .../wechat_pay_transaction_csv_data_table.go | 321 --------------- ..._pay_transaction_data_csv_file_importer.go | 107 ++++- .../wechat_pay_transaction_data_row_parser.go | 139 +++++++ 8 files changed, 664 insertions(+), 709 deletions(-) delete mode 100644 pkg/converters/alipay/alipay_transaction_csv_data_table.go create mode 100644 pkg/converters/alipay/alipay_transaction_data_row_parser.go create mode 100644 pkg/converters/datatable/common_transaction_data_table.go delete mode 100644 pkg/converters/wechat/wechat_pay_transaction_csv_data_table.go create mode 100644 pkg/converters/wechat/wechat_pay_transaction_data_row_parser.go diff --git a/pkg/converters/alipay/alipay_transaction_csv_data_table.go b/pkg/converters/alipay/alipay_transaction_csv_data_table.go deleted file mode 100644 index 64c199ee..00000000 --- a/pkg/converters/alipay/alipay_transaction_csv_data_table.go +++ /dev/null @@ -1,374 +0,0 @@ -package alipay - -import ( - "encoding/csv" - "io" - "strings" - - csvdatatable "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/locales" - "github.com/mayswind/ezbookkeeping/pkg/log" - "github.com/mayswind/ezbookkeeping/pkg/models" - "github.com/mayswind/ezbookkeeping/pkg/utils" -) - -const alipayTransactionDataStatusSuccessName = "交易成功" -const alipayTransactionDataStatusPaymentSuccessName = "支付成功" -const alipayTransactionDataStatusRepaymentSuccessName = "还款成功" -const alipayTransactionDataStatusClosedName = "交易关闭" -const alipayTransactionDataStatusRefundSuccessName = "退款成功" -const alipayTransactionDataStatusTaxRefundSuccessName = "退税成功" - -const alipayTransactionDataProductNameTransferToAlipayPrefix = "充值-" -const alipayTransactionDataProductNameTransferFromAlipayPrefix = "提现-" -const alipayTransactionDataProductNameTransferInText = "转入" -const alipayTransactionDataProductNameTransferOutText = "转出" -const alipayTransactionDataProductNameRepaymentText = "还款" - -var alipayTransactionSupportedColumns = 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, -} - -// alipayTransactionColumnNames defines the structure of alipay transaction plain text header names -type alipayTransactionColumnNames struct { - timeColumnName string - categoryColumnName string - targetNameColumnName string - productNameColumnName string - amountColumnName string - typeColumnName string - relatedAccountColumnName string - statusColumnName string - descriptionColumnName string -} - -// alipayTransactionDataTable defines the structure of alipay transaction plain text data table -type alipayTransactionDataTable struct { - innerDataTable datatable.CommonDataTable - columns alipayTransactionColumnNames -} - -// alipayTransactionDataRow defines the structure of alipay transaction plain text data row -type alipayTransactionDataRow struct { - isValid bool - finalItems map[datatable.TransactionDataTableColumn]string -} - -// alipayTransactionDataRowIterator defines the structure of alipay transaction plain text data row iterator -type alipayTransactionDataRowIterator struct { - dataTable *alipayTransactionDataTable - innerIterator datatable.CommonDataRowIterator -} - -// HasColumn returns whether the transaction data table has specified column -func (t *alipayTransactionDataTable) HasColumn(column datatable.TransactionDataTableColumn) bool { - _, exists := alipayTransactionSupportedColumns[column] - return exists -} - -// TransactionRowCount returns the total count of transaction data row -func (t *alipayTransactionDataTable) TransactionRowCount() int { - return t.innerDataTable.DataRowCount() -} - -// TransactionRowIterator returns the iterator of transaction data row -func (t *alipayTransactionDataTable) TransactionRowIterator() datatable.TransactionDataRowIterator { - return &alipayTransactionDataRowIterator{ - dataTable: t, - innerIterator: t.innerDataTable.DataRowIterator(), - } -} - -// IsValid returns whether this row is valid data for importing -func (r *alipayTransactionDataRow) IsValid() bool { - return r.isValid -} - -// GetData returns the data in the specified column type -func (r *alipayTransactionDataRow) GetData(column datatable.TransactionDataTableColumn) string { - _, exists := alipayTransactionSupportedColumns[column] - - if !exists { - return "" - } - - return r.finalItems[column] -} - -// HasNext returns whether the iterator does not reach the end -func (t *alipayTransactionDataRowIterator) HasNext() bool { - return t.innerIterator.HasNext() -} - -// Next returns the next imported data row -func (t *alipayTransactionDataRowIterator) Next(ctx core.Context, user *models.User) (daraRow datatable.TransactionDataRow, err error) { - importedRow := t.innerIterator.Next() - - if importedRow == nil { - return nil, nil - } - - finalItems, isValid, err := t.dataTable.parseTransactionData(ctx, user, importedRow, t.innerIterator.CurrentRowId()) - - if err != nil { - return nil, err - } - - return &alipayTransactionDataRow{ - isValid: isValid, - finalItems: finalItems, - }, nil -} - -func (t *alipayTransactionDataTable) hasOriginalColumn(columnName string) bool { - return columnName != "" && t.innerDataTable.HasColumn(columnName) -} - -func (t *alipayTransactionDataTable) parseTransactionData(ctx core.Context, user *models.User, dataRow datatable.CommonDataRow, rowId string) (map[datatable.TransactionDataTableColumn]string, bool, error) { - if dataRow.GetData(t.columns.typeColumnName) != alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME] && - dataRow.GetData(t.columns.typeColumnName) != alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_EXPENSE] && - dataRow.GetData(t.columns.typeColumnName) != alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_TRANSFER] { - log.Warnf(ctx, "[alipay_transaction_csv_data_table.parseTransactionData] skip parsing transaction in row \"%s\", because type is \"%s\"", rowId, dataRow.GetData(t.columns.typeColumnName)) - return nil, false, nil - } - - if dataRow.GetData(t.columns.statusColumnName) != alipayTransactionDataStatusSuccessName && - dataRow.GetData(t.columns.statusColumnName) != alipayTransactionDataStatusPaymentSuccessName && - dataRow.GetData(t.columns.statusColumnName) != alipayTransactionDataStatusRepaymentSuccessName && - dataRow.GetData(t.columns.statusColumnName) != alipayTransactionDataStatusClosedName && - dataRow.GetData(t.columns.statusColumnName) != alipayTransactionDataStatusRefundSuccessName && - dataRow.GetData(t.columns.statusColumnName) != alipayTransactionDataStatusTaxRefundSuccessName { - log.Warnf(ctx, "[alipay_transaction_csv_data_table.parseTransactionData] skip parsing transaction in row \"%s\", because status is \"%s\"", rowId, dataRow.GetData(t.columns.statusColumnName)) - return nil, false, nil - } - - data := make(map[datatable.TransactionDataTableColumn]string, len(alipayTransactionSupportedColumns)) - - if t.hasOriginalColumn(t.columns.timeColumnName) { - data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] = dataRow.GetData(t.columns.timeColumnName) - } - - if t.hasOriginalColumn(t.columns.categoryColumnName) { - data[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = dataRow.GetData(t.columns.categoryColumnName) - } else { - data[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = "" - } - - if t.hasOriginalColumn(t.columns.amountColumnName) { - data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = dataRow.GetData(t.columns.amountColumnName) - } - - if t.hasOriginalColumn(t.columns.descriptionColumnName) && dataRow.GetData(t.columns.descriptionColumnName) != "" { - data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = dataRow.GetData(t.columns.descriptionColumnName) - } else if t.hasOriginalColumn(t.columns.productNameColumnName) && dataRow.GetData(t.columns.productNameColumnName) != "" { - data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = dataRow.GetData(t.columns.productNameColumnName) - } else { - data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = "" - } - - relatedAccountName := "" - - if t.hasOriginalColumn(t.columns.relatedAccountColumnName) { - relatedAccountName = dataRow.GetData(t.columns.relatedAccountColumnName) - } - - statusName := "" - - if t.hasOriginalColumn(t.columns.statusColumnName) { - statusName = dataRow.GetData(t.columns.statusColumnName) - } - - locale := user.Language - - if locale == "" { - locale = ctx.GetClientLocale() - } - - localeTextItems := locales.GetLocaleTextItems(locale) - - if t.hasOriginalColumn(t.columns.typeColumnName) { - data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = dataRow.GetData(t.columns.typeColumnName) - - if dataRow.GetData(t.columns.typeColumnName) == alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME] { - if statusName == alipayTransactionDataStatusClosedName { - log.Warnf(ctx, "[alipay_transaction_csv_data_table.parseTransactionData] skip parsing transaction in row \"%s\", because income transaction is closed", rowId) - return nil, false, nil - } - - if statusName == alipayTransactionDataStatusSuccessName { - data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = localeTextItems.DataConverterTextItems.Alipay - data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = "" - } else { - data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = "" - data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = "" - } - } else if dataRow.GetData(t.columns.typeColumnName) == alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_TRANSFER] { - if statusName == alipayTransactionDataStatusClosedName { - log.Warnf(ctx, "[alipay_transaction_csv_data_table.parseTransactionData] skip parsing transaction in row \"%s\", because non-income/expense transaction is closed", rowId) - return nil, false, nil - } - - targetName := "" - productName := "" - - if t.hasOriginalColumn(t.columns.targetNameColumnName) { - targetName = dataRow.GetData(t.columns.targetNameColumnName) - } - - if t.hasOriginalColumn(t.columns.productNameColumnName) { - productName = dataRow.GetData(t.columns.productNameColumnName) - } - - if statusName == alipayTransactionDataStatusRefundSuccessName { - data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME] - data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = relatedAccountName - data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = "" - } else { - if strings.Index(productName, alipayTransactionDataProductNameTransferToAlipayPrefix) == 0 { // transfer to alipay wallet - data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = "" - data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = localeTextItems.DataConverterTextItems.Alipay - } else if strings.Index(productName, alipayTransactionDataProductNameTransferFromAlipayPrefix) == 0 { // transfer from alipay wallet - data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = localeTextItems.DataConverterTextItems.Alipay - data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = targetName - } else if strings.Index(productName, alipayTransactionDataProductNameTransferInText) >= 0 { // transfer in - data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = relatedAccountName - data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = targetName - } else if strings.Index(productName, alipayTransactionDataProductNameTransferOutText) >= 0 { // transfer out - data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = relatedAccountName - data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = targetName - } else if strings.Index(productName, alipayTransactionDataProductNameRepaymentText) >= 0 { // repayment - data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = relatedAccountName - data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = targetName - } else { - log.Warnf(ctx, "[alipay_transaction_csv_data_table.parseTransactionData] skip parsing transaction in row \"%s\", because product name (\"%s\") is unknown", rowId, productName) - return nil, false, nil - } - } - } else { - data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = relatedAccountName - data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = "" - } - } - - if data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] == alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME] && statusName != "" { - if statusName == alipayTransactionDataStatusRefundSuccessName || statusName == alipayTransactionDataStatusTaxRefundSuccessName { - amount, err := utils.ParseAmount(data[datatable.TRANSACTION_DATA_TABLE_AMOUNT]) - - if err == nil { - data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_EXPENSE] - data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(-amount) - } - } - } - - return data, true, nil -} - -func createNewAlipayTransactionDataTable(ctx core.Context, reader io.Reader, fileHeaderLine string, dataHeaderStartContent string, dataBottomEndLineRune rune, originalColumnNames alipayTransactionColumnNames) (*alipayTransactionDataTable, error) { - dataTable, err := createNewAlipayImportedDataTable(ctx, reader, fileHeaderLine, dataHeaderStartContent, dataBottomEndLineRune) - - if err != nil { - return nil, err - } - - commonDataTable := datatable.CreateNewImportedCommonDataTable(dataTable) - - if !commonDataTable.HasColumn(originalColumnNames.timeColumnName) || - !commonDataTable.HasColumn(originalColumnNames.amountColumnName) || - !commonDataTable.HasColumn(originalColumnNames.typeColumnName) || - !commonDataTable.HasColumn(originalColumnNames.statusColumnName) { - log.Errorf(ctx, "[alipay_transaction_csv_data_table.createNewAlipayTransactionDataTable] cannot parse alipay csv data, because missing essential columns in header row") - return nil, errs.ErrMissingRequiredFieldInHeaderRow - } - - return &alipayTransactionDataTable{ - innerDataTable: commonDataTable, - columns: originalColumnNames, - }, nil -} - -func createNewAlipayImportedDataTable(ctx core.Context, reader io.Reader, fileHeaderLine string, dataHeaderStartContent string, dataBottomEndLineRune rune) (datatable.ImportedDataTable, error) { - csvReader := csv.NewReader(reader) - csvReader.FieldsPerRecord = -1 - - allOriginalLines := make([][]string, 0) - hasFileHeader := false - foundContentBeforeDataHeaderLine := false - - for { - items, err := csvReader.Read() - - if err == io.EOF { - break - } - - if err != nil { - log.Errorf(ctx, "[alipay_transaction_csv_data_table.createNewAlipayImportedDataTable] 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], fileHeaderLine) == 0 { - hasFileHeader = true - continue - } else { - log.Warnf(ctx, "[alipay_transaction_csv_data_table.createNewAlipayImportedDataTable] read unexpected line before read file header, line content is %s", strings.Join(items, ",")) - continue - } - } - - if !foundContentBeforeDataHeaderLine { - if len(items) <= 0 { - continue - } else if strings.Index(items[0], dataHeaderStartContent) >= 0 { - foundContentBeforeDataHeaderLine = true - continue - } else { - continue - } - } - - if foundContentBeforeDataHeaderLine { - if len(items) <= 0 { - continue - } else if len(items) == 1 && dataBottomEndLineRune > 0 && utils.ContainsOnlyOneRune(items[0], dataBottomEndLineRune) { - break - } - - for i := 0; i < len(items); i++ { - items[i] = strings.Trim(items[i], " ") - } - - if len(allOriginalLines) > 0 && len(items) < len(allOriginalLines[0]) { - log.Errorf(ctx, "[alipay_transaction_csv_data_table.createNewAlipayImportedDataTable] cannot parse row \"index:%d\", because may missing some columns (column count %d in data row is less than header column count %d)", len(allOriginalLines), len(items), len(allOriginalLines[0])) - return nil, errs.ErrFewerFieldsInDataRowThanInHeaderRow - } - - allOriginalLines = append(allOriginalLines, items) - } - } - - if !hasFileHeader || !foundContentBeforeDataHeaderLine { - return nil, errs.ErrInvalidFileHeader - } - - if len(allOriginalLines) < 2 { - log.Errorf(ctx, "[alipay_transaction_csv_data_table.createNewAlipayImportedDataTable] cannot parse import data, because data table row count is less 1") - return nil, errs.ErrNotFoundTransactionDataInFile - } - - dataTable := csvdatatable.CreateNewCustomCsvImportedDataTable(allOriginalLines) - - return dataTable, nil -} diff --git a/pkg/converters/alipay/alipay_transaction_data_csv_file_importer.go b/pkg/converters/alipay/alipay_transaction_data_csv_file_importer.go index b4353b21..f86cf76a 100644 --- a/pkg/converters/alipay/alipay_transaction_data_csv_file_importer.go +++ b/pkg/converters/alipay/alipay_transaction_data_csv_file_importer.go @@ -2,13 +2,20 @@ package alipay import ( "bytes" + "encoding/csv" + "io" + "strings" "golang.org/x/text/encoding/simplifiedchinese" "golang.org/x/text/transform" + csvdatatable "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" + "github.com/mayswind/ezbookkeeping/pkg/utils" ) var alipayTransactionTypeNameMapping = map[models.TransactionType]string{ @@ -17,6 +24,29 @@ var alipayTransactionTypeNameMapping = map[models.TransactionType]string{ models.TRANSACTION_TYPE_TRANSFER: "不计收支", } +var alipayTransactionSupportedColumns = 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, +} + +// alipayTransactionColumnNames defines the structure of alipay transaction plain text header names +type alipayTransactionColumnNames struct { + timeColumnName string + categoryColumnName string + targetNameColumnName string + productNameColumnName string + amountColumnName string + typeColumnName string + relatedAccountColumnName string + statusColumnName string + descriptionColumnName string +} + // alipayTransactionDataCsvFileImporter defines the structure of alipay csv importer for transaction data type alipayTransactionDataCsvFileImporter struct { fileHeaderLine string @@ -30,20 +60,102 @@ func (c *alipayTransactionDataCsvFileImporter) ParseImportedData(ctx core.Contex enc := simplifiedchinese.GB18030 reader := transform.NewReader(bytes.NewReader(data), enc.NewDecoder()) - transactionDataTable, err := createNewAlipayTransactionDataTable( - ctx, - reader, - c.fileHeaderLine, - c.dataHeaderStartContent, - c.dataBottomEndLineRune, - c.originalColumnNames, - ) + dataTable, err := c.createNewAlipayImportedDataTable(ctx, reader, c.fileHeaderLine, c.dataHeaderStartContent, c.dataBottomEndLineRune) if err != nil { return nil, nil, nil, nil, nil, nil, err } + commonDataTable := datatable.CreateNewImportedCommonDataTable(dataTable) + + if !commonDataTable.HasColumn(c.originalColumnNames.timeColumnName) || + !commonDataTable.HasColumn(c.originalColumnNames.amountColumnName) || + !commonDataTable.HasColumn(c.originalColumnNames.typeColumnName) || + !commonDataTable.HasColumn(c.originalColumnNames.statusColumnName) { + log.Errorf(ctx, "[alipay_transaction_data_csv_file_importer.ParseImportedData] cannot parse alipay csv data, because missing essential columns in header row") + return nil, nil, nil, nil, nil, nil, errs.ErrMissingRequiredFieldInHeaderRow + } + + transactionRowParser := createAlipayTransactionDataRowParser(c.originalColumnNames) + transactionDataTable := datatable.CreateNewCommonTransactionDataTable(commonDataTable, alipayTransactionSupportedColumns, transactionRowParser) dataTableImporter := datatable.CreateNewSimpleImporter(alipayTransactionTypeNameMapping) return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap) } + +func (c *alipayTransactionDataCsvFileImporter) createNewAlipayImportedDataTable(ctx core.Context, reader io.Reader, fileHeaderLine string, dataHeaderStartContent string, dataBottomEndLineRune rune) (datatable.ImportedDataTable, error) { + csvReader := csv.NewReader(reader) + csvReader.FieldsPerRecord = -1 + + allOriginalLines := make([][]string, 0) + hasFileHeader := false + foundContentBeforeDataHeaderLine := false + + for { + items, err := csvReader.Read() + + if err == io.EOF { + break + } + + if err != nil { + log.Errorf(ctx, "[alipay_transaction_csv_data_table.createNewAlipayImportedDataTable] 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], fileHeaderLine) == 0 { + hasFileHeader = true + continue + } else { + log.Warnf(ctx, "[alipay_transaction_csv_data_table.createNewAlipayImportedDataTable] read unexpected line before read file header, line content is %s", strings.Join(items, ",")) + continue + } + } + + if !foundContentBeforeDataHeaderLine { + if len(items) <= 0 { + continue + } else if strings.Index(items[0], dataHeaderStartContent) >= 0 { + foundContentBeforeDataHeaderLine = true + continue + } else { + continue + } + } + + if foundContentBeforeDataHeaderLine { + if len(items) <= 0 { + continue + } else if len(items) == 1 && dataBottomEndLineRune > 0 && utils.ContainsOnlyOneRune(items[0], dataBottomEndLineRune) { + break + } + + for i := 0; i < len(items); i++ { + items[i] = strings.Trim(items[i], " ") + } + + if len(allOriginalLines) > 0 && len(items) < len(allOriginalLines[0]) { + log.Errorf(ctx, "[alipay_transaction_csv_data_table.createNewAlipayImportedDataTable] cannot parse row \"index:%d\", because may missing some columns (column count %d in data row is less than header column count %d)", len(allOriginalLines), len(items), len(allOriginalLines[0])) + return nil, errs.ErrFewerFieldsInDataRowThanInHeaderRow + } + + allOriginalLines = append(allOriginalLines, items) + } + } + + if !hasFileHeader || !foundContentBeforeDataHeaderLine { + return nil, errs.ErrInvalidFileHeader + } + + if len(allOriginalLines) < 2 { + log.Errorf(ctx, "[alipay_transaction_csv_data_table.createNewAlipayImportedDataTable] cannot parse import data, because data table row count is less 1") + return nil, errs.ErrNotFoundTransactionDataInFile + } + + dataTable := csvdatatable.CreateNewCustomCsvImportedDataTable(allOriginalLines) + + return dataTable, nil +} diff --git a/pkg/converters/alipay/alipay_transaction_data_row_parser.go b/pkg/converters/alipay/alipay_transaction_data_row_parser.go new file mode 100644 index 00000000..8c36362c --- /dev/null +++ b/pkg/converters/alipay/alipay_transaction_data_row_parser.go @@ -0,0 +1,178 @@ +package alipay + +import ( + "strings" + + "github.com/mayswind/ezbookkeeping/pkg/converters/datatable" + "github.com/mayswind/ezbookkeeping/pkg/core" + "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 alipayTransactionDataStatusSuccessName = "交易成功" +const alipayTransactionDataStatusPaymentSuccessName = "支付成功" +const alipayTransactionDataStatusRepaymentSuccessName = "还款成功" +const alipayTransactionDataStatusClosedName = "交易关闭" +const alipayTransactionDataStatusRefundSuccessName = "退款成功" +const alipayTransactionDataStatusTaxRefundSuccessName = "退税成功" + +const alipayTransactionDataProductNameTransferToAlipayPrefix = "充值-" +const alipayTransactionDataProductNameTransferFromAlipayPrefix = "提现-" +const alipayTransactionDataProductNameTransferInText = "转入" +const alipayTransactionDataProductNameTransferOutText = "转出" +const alipayTransactionDataProductNameRepaymentText = "还款" + +// alipayTransactionDataRowParser defines the structure of alipay transaction data row parser +type alipayTransactionDataRowParser struct { + columns alipayTransactionColumnNames +} + +// Parse returns the converted transaction data row +func (p *alipayTransactionDataRowParser) Parse(ctx core.Context, user *models.User, dataTable *datatable.CommonTransactionDataTable, dataRow datatable.CommonDataRow, rowId string) (rowData map[datatable.TransactionDataTableColumn]string, rowDataValid bool, err error) { + if dataRow.GetData(p.columns.typeColumnName) != alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME] && + dataRow.GetData(p.columns.typeColumnName) != alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_EXPENSE] && + dataRow.GetData(p.columns.typeColumnName) != alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_TRANSFER] { + log.Warnf(ctx, "[alipay_transaction_data_row_parser.Parse] skip parsing transaction in row \"%s\", because type is \"%s\"", rowId, dataRow.GetData(p.columns.typeColumnName)) + return nil, false, nil + } + + if dataRow.GetData(p.columns.statusColumnName) != alipayTransactionDataStatusSuccessName && + dataRow.GetData(p.columns.statusColumnName) != alipayTransactionDataStatusPaymentSuccessName && + dataRow.GetData(p.columns.statusColumnName) != alipayTransactionDataStatusRepaymentSuccessName && + dataRow.GetData(p.columns.statusColumnName) != alipayTransactionDataStatusClosedName && + dataRow.GetData(p.columns.statusColumnName) != alipayTransactionDataStatusRefundSuccessName && + dataRow.GetData(p.columns.statusColumnName) != alipayTransactionDataStatusTaxRefundSuccessName { + log.Warnf(ctx, "[alipay_transaction_data_row_parser.Parse] skip parsing transaction in row \"%s\", because status is \"%s\"", rowId, dataRow.GetData(p.columns.statusColumnName)) + return nil, false, nil + } + + data := make(map[datatable.TransactionDataTableColumn]string, len(alipayTransactionSupportedColumns)) + + if dataTable.HasOriginalColumn(p.columns.timeColumnName) { + data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] = dataRow.GetData(p.columns.timeColumnName) + } + + if dataTable.HasOriginalColumn(p.columns.categoryColumnName) { + data[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = dataRow.GetData(p.columns.categoryColumnName) + } else { + data[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = "" + } + + if dataTable.HasOriginalColumn(p.columns.amountColumnName) { + data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = dataRow.GetData(p.columns.amountColumnName) + } + + if dataTable.HasOriginalColumn(p.columns.descriptionColumnName) && dataRow.GetData(p.columns.descriptionColumnName) != "" { + data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = dataRow.GetData(p.columns.descriptionColumnName) + } else if dataTable.HasOriginalColumn(p.columns.productNameColumnName) && dataRow.GetData(p.columns.productNameColumnName) != "" { + data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = dataRow.GetData(p.columns.productNameColumnName) + } else { + data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = "" + } + + relatedAccountName := "" + + if dataTable.HasOriginalColumn(p.columns.relatedAccountColumnName) { + relatedAccountName = dataRow.GetData(p.columns.relatedAccountColumnName) + } + + statusName := "" + + if dataTable.HasOriginalColumn(p.columns.statusColumnName) { + statusName = dataRow.GetData(p.columns.statusColumnName) + } + + locale := user.Language + + if locale == "" { + locale = ctx.GetClientLocale() + } + + localeTextItems := locales.GetLocaleTextItems(locale) + + if dataTable.HasOriginalColumn(p.columns.typeColumnName) { + data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = dataRow.GetData(p.columns.typeColumnName) + + if dataRow.GetData(p.columns.typeColumnName) == alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME] { + if statusName == alipayTransactionDataStatusClosedName { + log.Warnf(ctx, "[alipay_transaction_data_row_parser.Parse] skip parsing transaction in row \"%s\", because income transaction is closed", rowId) + return nil, false, nil + } + + if statusName == alipayTransactionDataStatusSuccessName { + data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = localeTextItems.DataConverterTextItems.Alipay + data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = "" + } else { + data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = "" + data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = "" + } + } else if dataRow.GetData(p.columns.typeColumnName) == alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_TRANSFER] { + if statusName == alipayTransactionDataStatusClosedName { + log.Warnf(ctx, "[alipay_transaction_data_row_parser.Parse] skip parsing transaction in row \"%s\", because non-income/expense transaction is closed", rowId) + return nil, false, nil + } + + targetName := "" + productName := "" + + if dataTable.HasOriginalColumn(p.columns.targetNameColumnName) { + targetName = dataRow.GetData(p.columns.targetNameColumnName) + } + + if dataTable.HasOriginalColumn(p.columns.productNameColumnName) { + productName = dataRow.GetData(p.columns.productNameColumnName) + } + + if statusName == alipayTransactionDataStatusRefundSuccessName { + data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME] + data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = relatedAccountName + data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = "" + } else { + if strings.Index(productName, alipayTransactionDataProductNameTransferToAlipayPrefix) == 0 { // transfer to alipay wallet + data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = "" + data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = localeTextItems.DataConverterTextItems.Alipay + } else if strings.Index(productName, alipayTransactionDataProductNameTransferFromAlipayPrefix) == 0 { // transfer from alipay wallet + data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = localeTextItems.DataConverterTextItems.Alipay + data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = targetName + } else if strings.Index(productName, alipayTransactionDataProductNameTransferInText) >= 0 { // transfer in + data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = relatedAccountName + data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = targetName + } else if strings.Index(productName, alipayTransactionDataProductNameTransferOutText) >= 0 { // transfer out + data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = relatedAccountName + data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = targetName + } else if strings.Index(productName, alipayTransactionDataProductNameRepaymentText) >= 0 { // repayment + data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = relatedAccountName + data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = targetName + } else { + log.Warnf(ctx, "[alipay_transaction_data_row_parser.Parse] skip parsing transaction in row \"%s\", because product name (\"%s\") is unknown", rowId, productName) + return nil, false, nil + } + } + } else { + data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = relatedAccountName + data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = "" + } + } + + if data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] == alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME] && statusName != "" { + if statusName == alipayTransactionDataStatusRefundSuccessName || statusName == alipayTransactionDataStatusTaxRefundSuccessName { + amount, err := utils.ParseAmount(data[datatable.TRANSACTION_DATA_TABLE_AMOUNT]) + + if err == nil { + data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_EXPENSE] + data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(-amount) + } + } + } + + return data, true, nil +} + +// createAlipayTransactionDataRowParser returns alipay transaction data row parser +func createAlipayTransactionDataRowParser(originalColumnNames alipayTransactionColumnNames) datatable.CommonTransactionDataRowParser { + return &alipayTransactionDataRowParser{ + columns: originalColumnNames, + } +} diff --git a/pkg/converters/datatable/common_transaction_data_table.go b/pkg/converters/datatable/common_transaction_data_table.go new file mode 100644 index 00000000..57399de3 --- /dev/null +++ b/pkg/converters/datatable/common_transaction_data_table.go @@ -0,0 +1,114 @@ +package datatable + +import ( + "github.com/mayswind/ezbookkeeping/pkg/core" + "github.com/mayswind/ezbookkeeping/pkg/log" + "github.com/mayswind/ezbookkeeping/pkg/models" +) + +// CommonTransactionDataTable defines the structure of common transaction data table +type CommonTransactionDataTable struct { + innerDataTable CommonDataTable + supportedDataColumns map[TransactionDataTableColumn]bool + rowParser CommonTransactionDataRowParser +} + +// CommonTransactionDataRow defines the structure of common transaction data row +type CommonTransactionDataRow struct { + transactionDataTable *CommonTransactionDataTable + rowData map[TransactionDataTableColumn]string + rowDataValid bool +} + +// CommonTransactionDataRowIterator defines the structure of common transaction data row iterator +type CommonTransactionDataRowIterator struct { + transactionDataTable *CommonTransactionDataTable + innerIterator CommonDataRowIterator +} + +// CommonTransactionDataRowParser defines the structure of common transaction data row parser +type CommonTransactionDataRowParser interface { + // Parse returns the converted transaction data row + Parse(ctx core.Context, user *models.User, dataTable *CommonTransactionDataTable, dataRow CommonDataRow, rowId string) (rowData map[TransactionDataTableColumn]string, rowDataValid bool, err error) +} + +// HasColumn returns whether the data table has specified column +func (t *CommonTransactionDataTable) HasColumn(column TransactionDataTableColumn) bool { + _, exists := t.supportedDataColumns[column] + return exists +} + +// HasOriginalColumn returns whether the original data table has specified column name +func (t *CommonTransactionDataTable) HasOriginalColumn(columnName string) bool { + return columnName != "" && t.innerDataTable.HasColumn(columnName) +} + +// TransactionRowCount returns the total count of transaction data row +func (t *CommonTransactionDataTable) TransactionRowCount() int { + return t.innerDataTable.DataRowCount() +} + +// TransactionRowIterator returns the iterator of transaction data row +func (t *CommonTransactionDataTable) TransactionRowIterator() TransactionDataRowIterator { + return &CommonTransactionDataRowIterator{ + transactionDataTable: t, + innerIterator: t.innerDataTable.DataRowIterator(), + } +} + +// IsValid returns whether this row is valid data for importing +func (r *CommonTransactionDataRow) IsValid() bool { + return r.rowDataValid +} + +// GetData returns the data in the specified column type +func (r *CommonTransactionDataRow) GetData(column TransactionDataTableColumn) string { + if !r.rowDataValid { + return "" + } + + _, exists := r.transactionDataTable.supportedDataColumns[column] + + if !exists { + return "" + } + + return r.rowData[column] +} + +// HasNext returns whether the iterator does not reach the end +func (t *CommonTransactionDataRowIterator) HasNext() bool { + return t.innerIterator.HasNext() +} + +// Next returns the next transaction data row +func (t *CommonTransactionDataRowIterator) Next(ctx core.Context, user *models.User) (daraRow TransactionDataRow, err error) { + commonRow := t.innerIterator.Next() + + if commonRow == nil { + return nil, nil + } + + rowId := t.innerIterator.CurrentRowId() + rowData, rowDataValid, err := t.transactionDataTable.rowParser.Parse(ctx, user, t.transactionDataTable, commonRow, rowId) + + if err != nil { + log.Errorf(ctx, "[common_transaction_data_table.Next] cannot parse data row, because %s", err.Error()) + return nil, err + } + + return &CommonTransactionDataRow{ + transactionDataTable: t.transactionDataTable, + rowData: rowData, + rowDataValid: rowDataValid, + }, nil +} + +// CreateNewCommonTransactionDataTable returns transaction data table from Common data table +func CreateNewCommonTransactionDataTable(dataTable CommonDataTable, supportedDataColumns map[TransactionDataTableColumn]bool, rowParser CommonTransactionDataRowParser) *CommonTransactionDataTable { + return &CommonTransactionDataTable{ + innerDataTable: dataTable, + supportedDataColumns: supportedDataColumns, + rowParser: rowParser, + } +} diff --git a/pkg/converters/feidee/feidee_mymoney_app_transaction_data_csv_file_importer.go b/pkg/converters/feidee/feidee_mymoney_app_transaction_data_csv_file_importer.go index d5a83f68..a8db7f65 100644 --- a/pkg/converters/feidee/feidee_mymoney_app_transaction_data_csv_file_importer.go +++ b/pkg/converters/feidee/feidee_mymoney_app_transaction_data_csv_file_importer.go @@ -114,6 +114,7 @@ func (c *feideeMymoneyAppTransactionDataCsvFileImporter) createNewFeideeMymoneyA continue } else { log.Warnf(ctx, "[feidee_mymoney_app_transaction_data_csv_file_importer.createNewFeideeMymoneyAppTransactionDataTable] read unexpected line before read file header, line content is %s", strings.Join(items, ",")) + continue } } @@ -179,7 +180,6 @@ func (c *feideeMymoneyAppTransactionDataCsvFileImporter) createNewFeideeMymoneyA } data := make(map[datatable.TransactionDataTableColumn]string, 11) - relatedId := "" for columnType, columnName := range feideeMymoneyAppDataColumnNameMapping { if dataRow.HasData(columnName) { @@ -187,10 +187,6 @@ func (c *feideeMymoneyAppTransactionDataCsvFileImporter) createNewFeideeMymoneyA } } - if dataRow.HasData(feideeMymoneyAppTransactionRelatedIdColumnName) { - relatedId = dataRow.GetData(feideeMymoneyAppTransactionRelatedIdColumnName) - } - transactionType := data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] if transactionType == feideeMymoneyAppTransactionTypeModifyBalanceText || transactionType == feideeMymoneyAppTransactionTypeIncomeText || transactionType == feideeMymoneyAppTransactionTypeExpenseText { @@ -207,6 +203,12 @@ func (c *feideeMymoneyAppTransactionDataCsvFileImporter) createNewFeideeMymoneyA data[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT] = "" transactionDataTable.Add(data) } else if transactionType == feideeMymoneyAppTransactionTypeTransferInText || transactionType == feideeMymoneyAppTransactionTypeTransferOutText { + relatedId := "" + + if dataRow.HasData(feideeMymoneyAppTransactionRelatedIdColumnName) { + relatedId = dataRow.GetData(feideeMymoneyAppTransactionRelatedIdColumnName) + } + if relatedId == "" { log.Errorf(ctx, "[feidee_mymoney_app_transaction_data_csv_file_importer.createNewFeideeMymoneyAppTransactionDataTable] transfer transaction has blank related id in row \"%s\"", rowId) return nil, errs.ErrRelatedIdCannotBeBlank diff --git a/pkg/converters/wechat/wechat_pay_transaction_csv_data_table.go b/pkg/converters/wechat/wechat_pay_transaction_csv_data_table.go deleted file mode 100644 index ec299534..00000000 --- a/pkg/converters/wechat/wechat_pay_transaction_csv_data_table.go +++ /dev/null @@ -1,321 +0,0 @@ -package wechat - -import ( - "encoding/csv" - "io" - "strings" - - csvdatatable "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/locales" - "github.com/mayswind/ezbookkeeping/pkg/log" - "github.com/mayswind/ezbookkeeping/pkg/models" - "github.com/mayswind/ezbookkeeping/pkg/utils" -) - -const wechatPayTransactionDataCsvFileHeader = "微信支付账单明细" -const wechatPayTransactionDataCsvFileHeaderWithUtf8Bom = "\xEF\xBB\xBF" + wechatPayTransactionDataCsvFileHeader -const wechatPayTransactionDataHeaderStartContentBeginning = "----------------------微信支付账单明细列表--------------------" - -const wechatPayTransactionTimeColumnName = "交易时间" -const wechatPayTransactionCategoryColumnName = "交易类型" -const wechatPayTransactionProductNameColumnName = "商品" -const wechatPayTransactionTypeColumnName = "收/支" -const wechatPayTransactionAmountColumnName = "金额(元)" -const wechatPayTransactionRelatedAccountColumnName = "支付方式" -const wechatPayTransactionStatusColumnName = "当前状态" -const wechatPayTransactionDescriptionColumnName = "备注" - -const wechatPayTransactionDataCategoryTransferToWeChatWallet = "零钱充值" -const wechatPayTransactionDataCategoryTransferFromWeChatWallet = "零钱提现" - -const wechatPayTransactionDataStatusRefundName = "退款" - -var wechatPayTransactionSupportedColumns = 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, -} - -// wechatPayTransactionDataTable defines the structure of wechat pay transaction plain text data table -type wechatPayTransactionDataTable struct { - innerDataTable datatable.CommonDataTable -} - -// wechatPayTransactionDataRow defines the structure of wechat pay transaction plain text data row -type wechatPayTransactionDataRow struct { - isValid bool - finalItems map[datatable.TransactionDataTableColumn]string -} - -// wechatPayTransactionDataRowIterator defines the structure of wechat pay transaction plain text data row iterator -type wechatPayTransactionDataRowIterator struct { - dataTable *wechatPayTransactionDataTable - innerIterator datatable.CommonDataRowIterator -} - -// HasColumn returns whether the transaction data table has specified column -func (t *wechatPayTransactionDataTable) HasColumn(column datatable.TransactionDataTableColumn) bool { - _, exists := wechatPayTransactionSupportedColumns[column] - return exists -} - -// TransactionRowCount returns the total count of transaction data row -func (t *wechatPayTransactionDataTable) TransactionRowCount() int { - return t.innerDataTable.DataRowCount() -} - -// TransactionRowIterator returns the iterator of transaction data row -func (t *wechatPayTransactionDataTable) TransactionRowIterator() datatable.TransactionDataRowIterator { - return &wechatPayTransactionDataRowIterator{ - dataTable: t, - innerIterator: t.innerDataTable.DataRowIterator(), - } -} - -// IsValid returns whether this row is valid data for importing -func (r *wechatPayTransactionDataRow) IsValid() bool { - return r.isValid -} - -// GetData returns the data in the specified column type -func (r *wechatPayTransactionDataRow) GetData(column datatable.TransactionDataTableColumn) string { - _, exists := wechatPayTransactionSupportedColumns[column] - - if !exists { - return "" - } - - return r.finalItems[column] -} - -// HasNext returns whether the iterator does not reach the end -func (t *wechatPayTransactionDataRowIterator) HasNext() bool { - return t.innerIterator.HasNext() -} - -// Next returns the next imported data row -func (t *wechatPayTransactionDataRowIterator) Next(ctx core.Context, user *models.User) (daraRow datatable.TransactionDataRow, err error) { - importedRow := t.innerIterator.Next() - - if importedRow == nil { - return nil, nil - } - - finalItems, isValid, err := t.dataTable.parseTransactionData(ctx, user, importedRow, t.innerIterator.CurrentRowId()) - - if err != nil { - return nil, err - } - - return &wechatPayTransactionDataRow{ - isValid: isValid, - finalItems: finalItems, - }, nil -} - -func (t *wechatPayTransactionDataTable) hasOriginalColumn(columnName string) bool { - return columnName != "" && t.innerDataTable.HasColumn(columnName) -} - -func (t *wechatPayTransactionDataTable) parseTransactionData(ctx core.Context, user *models.User, dataRow datatable.CommonDataRow, rowId string) (map[datatable.TransactionDataTableColumn]string, bool, error) { - if dataRow.GetData(wechatPayTransactionTypeColumnName) != wechatPayTransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME] && - dataRow.GetData(wechatPayTransactionTypeColumnName) != wechatPayTransactionTypeNameMapping[models.TRANSACTION_TYPE_EXPENSE] && - dataRow.GetData(wechatPayTransactionTypeColumnName) != wechatPayTransactionTypeNameMapping[models.TRANSACTION_TYPE_TRANSFER] { - log.Warnf(ctx, "[wechat_pay_transaction_csv_data_table.parseTransactionData] skip parsing transaction in row \"%s\", because type is \"%s\"", rowId, dataRow.GetData(wechatPayTransactionTypeColumnName)) - return nil, false, nil - } - - data := make(map[datatable.TransactionDataTableColumn]string, len(wechatPayTransactionSupportedColumns)) - - if t.hasOriginalColumn(wechatPayTransactionTimeColumnName) { - data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] = dataRow.GetData(wechatPayTransactionTimeColumnName) - } - - if t.hasOriginalColumn(wechatPayTransactionCategoryColumnName) { - data[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = dataRow.GetData(wechatPayTransactionCategoryColumnName) - } - - if t.hasOriginalColumn(wechatPayTransactionAmountColumnName) { - amount, success := utils.ParseFirstConsecutiveNumber(dataRow.GetData(wechatPayTransactionAmountColumnName)) - - if !success { - log.Errorf(ctx, "[wechat_pay_transaction_csv_data_table.parseTransactionData] cannot parse amount \"%s\" of transaction in row \"%s\"", dataRow.GetData(wechatPayTransactionAmountColumnName), rowId) - return nil, false, errs.ErrAmountInvalid - } - - data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = amount - } - - if t.hasOriginalColumn(wechatPayTransactionDescriptionColumnName) && dataRow.GetData(wechatPayTransactionDescriptionColumnName) != "" && dataRow.GetData(wechatPayTransactionDescriptionColumnName) != "/" { - data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = dataRow.GetData(wechatPayTransactionDescriptionColumnName) - } else if t.hasOriginalColumn(wechatPayTransactionProductNameColumnName) && dataRow.GetData(wechatPayTransactionProductNameColumnName) != "" && dataRow.GetData(wechatPayTransactionProductNameColumnName) != "/" { - data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = dataRow.GetData(wechatPayTransactionProductNameColumnName) - } else { - data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = "" - } - - relatedAccountName := "" - - if t.hasOriginalColumn(wechatPayTransactionRelatedAccountColumnName) { - relatedAccountName = dataRow.GetData(wechatPayTransactionRelatedAccountColumnName) - } - - statusName := "" - - if t.hasOriginalColumn(wechatPayTransactionStatusColumnName) { - statusName = dataRow.GetData(wechatPayTransactionStatusColumnName) - } - - locale := user.Language - - if locale == "" { - locale = ctx.GetClientLocale() - } - - localeTextItems := locales.GetLocaleTextItems(locale) - - if t.hasOriginalColumn(wechatPayTransactionTypeColumnName) { - data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = dataRow.GetData(wechatPayTransactionTypeColumnName) - - if dataRow.GetData(wechatPayTransactionTypeColumnName) == wechatPayTransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME] { - if relatedAccountName == "" || relatedAccountName == "/" { - data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = localeTextItems.DataConverterTextItems.WeChatWallet - data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = "" - } else { - data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = relatedAccountName - } - } else if dataRow.GetData(wechatPayTransactionTypeColumnName) == wechatPayTransactionTypeNameMapping[models.TRANSACTION_TYPE_TRANSFER] { - if data[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] == wechatPayTransactionDataCategoryTransferToWeChatWallet { - data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = relatedAccountName - data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = localeTextItems.DataConverterTextItems.WeChatWallet - } else if data[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] == wechatPayTransactionDataCategoryTransferFromWeChatWallet { - data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = localeTextItems.DataConverterTextItems.WeChatWallet - data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = relatedAccountName - } else { - log.Warnf(ctx, "[wechat_pay_transaction_csv_data_table.parseTransactionData] skip parsing transaction in row \"%s\", because unkown transfer transaction category \"%s\"", rowId, data[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY]) - return nil, false, nil - } - } else { - data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = relatedAccountName - data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = "" - } - } - - if data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] == wechatPayTransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME] && statusName != "" { - if strings.Index(statusName, wechatPayTransactionDataStatusRefundName) >= 0 { - amount, err := utils.ParseAmount(data[datatable.TRANSACTION_DATA_TABLE_AMOUNT]) - - if err == nil { - data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = wechatPayTransactionTypeNameMapping[models.TRANSACTION_TYPE_EXPENSE] - data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(-amount) - } - } - } - - return data, true, nil -} - -func createNewWeChatPayTransactionDataTable(ctx core.Context, reader io.Reader) (*wechatPayTransactionDataTable, error) { - dataTable, err := createNewWeChatPayImportedDataTable(ctx, reader) - - if err != nil { - return nil, err - } - - commonDataTable := datatable.CreateNewImportedCommonDataTable(dataTable) - - if !commonDataTable.HasColumn(wechatPayTransactionTimeColumnName) || - !commonDataTable.HasColumn(wechatPayTransactionCategoryColumnName) || - !commonDataTable.HasColumn(wechatPayTransactionTypeColumnName) || - !commonDataTable.HasColumn(wechatPayTransactionAmountColumnName) || - !commonDataTable.HasColumn(wechatPayTransactionStatusColumnName) { - log.Errorf(ctx, "[wechat_pay_transaction_csv_data_table.createNewWeChatPayTransactionDataTable] cannot parse wechat pay csv data, because missing essential columns in header row") - return nil, errs.ErrMissingRequiredFieldInHeaderRow - } - - return &wechatPayTransactionDataTable{ - innerDataTable: commonDataTable, - }, nil -} - -func createNewWeChatPayImportedDataTable(ctx core.Context, reader io.Reader) (datatable.ImportedDataTable, error) { - csvReader := csv.NewReader(reader) - csvReader.FieldsPerRecord = -1 - - allOriginalLines := make([][]string, 0) - hasFileHeader := false - foundContentBeforeDataHeaderLine := false - - for { - items, err := csvReader.Read() - - if err == io.EOF { - break - } - - if err != nil { - log.Errorf(ctx, "[wechat_pay_transaction_csv_data_table.createNewWeChatPayImportedDataTable] cannot parse wechat pay csv data, because %s", err.Error()) - return nil, errs.ErrInvalidCSVFile - } - - if !hasFileHeader { - if len(items) <= 0 { - continue - } else if strings.Index(items[0], wechatPayTransactionDataCsvFileHeader) == 0 || strings.Index(items[0], wechatPayTransactionDataCsvFileHeaderWithUtf8Bom) == 0 { - hasFileHeader = true - continue - } else { - log.Warnf(ctx, "[wechat_pay_transaction_csv_data_table.createNewWeChatPayImportedDataTable] read unexpected line before read file header, line content is %s", strings.Join(items, ",")) - continue - } - } - - if !foundContentBeforeDataHeaderLine { - if len(items) <= 0 { - continue - } else if strings.Index(items[0], wechatPayTransactionDataHeaderStartContentBeginning) == 0 { - foundContentBeforeDataHeaderLine = true - continue - } else { - continue - } - } - - if foundContentBeforeDataHeaderLine { - if len(items) <= 0 { - continue - } - - for i := 0; i < len(items); i++ { - items[i] = strings.Trim(items[i], " ") - } - - if len(allOriginalLines) > 0 && len(items) < len(allOriginalLines[0]) { - log.Errorf(ctx, "[wechat_pay_transaction_csv_data_table.createNewWeChatPayImportedDataTable] cannot parse row \"index:%d\", because may missing some columns (column count %d in data row is less than header column count %d)", len(allOriginalLines), len(items), len(allOriginalLines[0])) - return nil, errs.ErrFewerFieldsInDataRowThanInHeaderRow - } - - allOriginalLines = append(allOriginalLines, items) - } - } - - if !hasFileHeader || !foundContentBeforeDataHeaderLine { - return nil, errs.ErrInvalidFileHeader - } - - if len(allOriginalLines) < 2 { - log.Errorf(ctx, "[wechat_pay_transaction_csv_data_table.createNewWeChatPayImportedDataTable] cannot parse import data, because data table row count is less 1") - return nil, errs.ErrNotFoundTransactionDataInFile - } - - dataTable := csvdatatable.CreateNewCustomCsvImportedDataTable(allOriginalLines) - - return dataTable, nil -} diff --git a/pkg/converters/wechat/wechat_pay_transaction_data_csv_file_importer.go b/pkg/converters/wechat/wechat_pay_transaction_data_csv_file_importer.go index 993194b5..1aa24b91 100644 --- a/pkg/converters/wechat/wechat_pay_transaction_data_csv_file_importer.go +++ b/pkg/converters/wechat/wechat_pay_transaction_data_csv_file_importer.go @@ -2,9 +2,15 @@ package wechat import ( "bytes" + "encoding/csv" + "io" + "strings" + csvdatatable "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" ) @@ -14,6 +20,16 @@ var wechatPayTransactionTypeNameMapping = map[models.TransactionType]string{ models.TRANSACTION_TYPE_TRANSFER: "/", } +var wechatPayTransactionSupportedColumns = 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, +} + // wechatPayTransactionDataCsvFileImporter defines the structure of wechatPay csv importer for transaction data type wechatPayTransactionDataCsvFileImporter struct { fileHeaderLineBeginning string @@ -28,13 +44,102 @@ var ( // ParseImportedData returns the imported data by parsing the wechat pay transaction csv data func (c *wechatPayTransactionDataCsvFileImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, accountMap map[string]*models.Account, expenseCategoryMap map[string]*models.TransactionCategory, incomeCategoryMap map[string]*models.TransactionCategory, transferCategoryMap map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) { reader := bytes.NewReader(data) - transactionDataTable, err := createNewWeChatPayTransactionDataTable(ctx, reader) + + dataTable, err := c.createNewWeChatPayImportedDataTable(ctx, reader) if err != nil { return nil, nil, nil, nil, nil, nil, err } + commonDataTable := datatable.CreateNewImportedCommonDataTable(dataTable) + + if !commonDataTable.HasColumn(wechatPayTransactionTimeColumnName) || + !commonDataTable.HasColumn(wechatPayTransactionCategoryColumnName) || + !commonDataTable.HasColumn(wechatPayTransactionTypeColumnName) || + !commonDataTable.HasColumn(wechatPayTransactionAmountColumnName) || + !commonDataTable.HasColumn(wechatPayTransactionStatusColumnName) { + log.Errorf(ctx, "[wechat_pay_transaction_data_csv_file_importer.ParseImportedData] cannot parse wechat pay csv data, because missing essential columns in header row") + return nil, nil, nil, nil, nil, nil, errs.ErrMissingRequiredFieldInHeaderRow + } + + transactionRowParser := createWeChatPayTransactionDataRowParser() + transactionDataTable := datatable.CreateNewCommonTransactionDataTable(commonDataTable, wechatPayTransactionSupportedColumns, transactionRowParser) dataTableImporter := datatable.CreateNewSimpleImporter(wechatPayTransactionTypeNameMapping) return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap) } + +func (c *wechatPayTransactionDataCsvFileImporter) createNewWeChatPayImportedDataTable(ctx core.Context, reader io.Reader) (datatable.ImportedDataTable, error) { + csvReader := csv.NewReader(reader) + csvReader.FieldsPerRecord = -1 + + allOriginalLines := make([][]string, 0) + hasFileHeader := false + foundContentBeforeDataHeaderLine := false + + for { + items, err := csvReader.Read() + + if err == io.EOF { + break + } + + if err != nil { + log.Errorf(ctx, "[wechat_pay_transaction_data_csv_file_importer.createNewWeChatPayImportedDataTable] cannot parse wechat pay csv data, because %s", err.Error()) + return nil, errs.ErrInvalidCSVFile + } + + if !hasFileHeader { + if len(items) <= 0 { + continue + } else if strings.Index(items[0], wechatPayTransactionDataCsvFileHeader) == 0 || strings.Index(items[0], wechatPayTransactionDataCsvFileHeaderWithUtf8Bom) == 0 { + hasFileHeader = true + continue + } else { + log.Warnf(ctx, "[wechat_pay_transaction_data_csv_file_importer.createNewWeChatPayImportedDataTable] read unexpected line before read file header, line content is %s", strings.Join(items, ",")) + continue + } + } + + if !foundContentBeforeDataHeaderLine { + if len(items) <= 0 { + continue + } else if strings.Index(items[0], wechatPayTransactionDataHeaderStartContentBeginning) == 0 { + foundContentBeforeDataHeaderLine = true + continue + } else { + continue + } + } + + if foundContentBeforeDataHeaderLine { + if len(items) <= 0 { + continue + } + + for i := 0; i < len(items); i++ { + items[i] = strings.Trim(items[i], " ") + } + + if len(allOriginalLines) > 0 && len(items) < len(allOriginalLines[0]) { + log.Errorf(ctx, "[wechat_pay_transaction_data_csv_file_importer.createNewWeChatPayImportedDataTable] cannot parse row \"index:%d\", because may missing some columns (column count %d in data row is less than header column count %d)", len(allOriginalLines), len(items), len(allOriginalLines[0])) + return nil, errs.ErrFewerFieldsInDataRowThanInHeaderRow + } + + allOriginalLines = append(allOriginalLines, items) + } + } + + if !hasFileHeader || !foundContentBeforeDataHeaderLine { + return nil, errs.ErrInvalidFileHeader + } + + if len(allOriginalLines) < 2 { + log.Errorf(ctx, "[wechat_pay_transaction_data_csv_file_importer.createNewWeChatPayImportedDataTable] cannot parse import data, because data table row count is less 1") + return nil, errs.ErrNotFoundTransactionDataInFile + } + + dataTable := csvdatatable.CreateNewCustomCsvImportedDataTable(allOriginalLines) + + return dataTable, nil +} diff --git a/pkg/converters/wechat/wechat_pay_transaction_data_row_parser.go b/pkg/converters/wechat/wechat_pay_transaction_data_row_parser.go new file mode 100644 index 00000000..000f7398 --- /dev/null +++ b/pkg/converters/wechat/wechat_pay_transaction_data_row_parser.go @@ -0,0 +1,139 @@ +package wechat + +import ( + "strings" + + "github.com/mayswind/ezbookkeeping/pkg/converters/datatable" + "github.com/mayswind/ezbookkeeping/pkg/core" + "github.com/mayswind/ezbookkeeping/pkg/errs" + "github.com/mayswind/ezbookkeeping/pkg/locales" + "github.com/mayswind/ezbookkeeping/pkg/log" + "github.com/mayswind/ezbookkeeping/pkg/models" + "github.com/mayswind/ezbookkeeping/pkg/utils" +) + +const wechatPayTransactionDataCsvFileHeader = "微信支付账单明细" +const wechatPayTransactionDataCsvFileHeaderWithUtf8Bom = "\xEF\xBB\xBF" + wechatPayTransactionDataCsvFileHeader +const wechatPayTransactionDataHeaderStartContentBeginning = "----------------------微信支付账单明细列表--------------------" + +const wechatPayTransactionTimeColumnName = "交易时间" +const wechatPayTransactionCategoryColumnName = "交易类型" +const wechatPayTransactionProductNameColumnName = "商品" +const wechatPayTransactionTypeColumnName = "收/支" +const wechatPayTransactionAmountColumnName = "金额(元)" +const wechatPayTransactionRelatedAccountColumnName = "支付方式" +const wechatPayTransactionStatusColumnName = "当前状态" +const wechatPayTransactionDescriptionColumnName = "备注" + +const wechatPayTransactionDataCategoryTransferToWeChatWallet = "零钱充值" +const wechatPayTransactionDataCategoryTransferFromWeChatWallet = "零钱提现" + +const wechatPayTransactionDataStatusRefundName = "退款" + +// weChatPayTransactionDataRowParser defines the structure of wechat pay transaction data row parser +type weChatPayTransactionDataRowParser struct { +} + +// Parse returns the converted transaction data row +func (t *weChatPayTransactionDataRowParser) Parse(ctx core.Context, user *models.User, dataTable *datatable.CommonTransactionDataTable, dataRow datatable.CommonDataRow, rowId string) (rowData map[datatable.TransactionDataTableColumn]string, rowDataValid bool, err error) { + if dataRow.GetData(wechatPayTransactionTypeColumnName) != wechatPayTransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME] && + dataRow.GetData(wechatPayTransactionTypeColumnName) != wechatPayTransactionTypeNameMapping[models.TRANSACTION_TYPE_EXPENSE] && + dataRow.GetData(wechatPayTransactionTypeColumnName) != wechatPayTransactionTypeNameMapping[models.TRANSACTION_TYPE_TRANSFER] { + log.Warnf(ctx, "[wechat_pay_transaction_data_row_parser.Parse] skip parsing transaction in row \"%s\", because type is \"%s\"", rowId, dataRow.GetData(wechatPayTransactionTypeColumnName)) + return nil, false, nil + } + + data := make(map[datatable.TransactionDataTableColumn]string, len(wechatPayTransactionSupportedColumns)) + + if dataTable.HasOriginalColumn(wechatPayTransactionTimeColumnName) { + data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] = dataRow.GetData(wechatPayTransactionTimeColumnName) + } + + if dataTable.HasOriginalColumn(wechatPayTransactionCategoryColumnName) { + data[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = dataRow.GetData(wechatPayTransactionCategoryColumnName) + } + + if dataTable.HasOriginalColumn(wechatPayTransactionAmountColumnName) { + amount, success := utils.ParseFirstConsecutiveNumber(dataRow.GetData(wechatPayTransactionAmountColumnName)) + + if !success { + log.Errorf(ctx, "[wechat_pay_transaction_data_row_parser.Parse] cannot parse amount \"%s\" of transaction in row \"%s\"", dataRow.GetData(wechatPayTransactionAmountColumnName), rowId) + return nil, false, errs.ErrAmountInvalid + } + + data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = amount + } + + if dataTable.HasOriginalColumn(wechatPayTransactionDescriptionColumnName) && dataRow.GetData(wechatPayTransactionDescriptionColumnName) != "" && dataRow.GetData(wechatPayTransactionDescriptionColumnName) != "/" { + data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = dataRow.GetData(wechatPayTransactionDescriptionColumnName) + } else if dataTable.HasOriginalColumn(wechatPayTransactionProductNameColumnName) && dataRow.GetData(wechatPayTransactionProductNameColumnName) != "" && dataRow.GetData(wechatPayTransactionProductNameColumnName) != "/" { + data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = dataRow.GetData(wechatPayTransactionProductNameColumnName) + } else { + data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = "" + } + + relatedAccountName := "" + + if dataTable.HasOriginalColumn(wechatPayTransactionRelatedAccountColumnName) { + relatedAccountName = dataRow.GetData(wechatPayTransactionRelatedAccountColumnName) + } + + statusName := "" + + if dataTable.HasOriginalColumn(wechatPayTransactionStatusColumnName) { + statusName = dataRow.GetData(wechatPayTransactionStatusColumnName) + } + + locale := user.Language + + if locale == "" { + locale = ctx.GetClientLocale() + } + + localeTextItems := locales.GetLocaleTextItems(locale) + + if dataTable.HasOriginalColumn(wechatPayTransactionTypeColumnName) { + data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = dataRow.GetData(wechatPayTransactionTypeColumnName) + + if dataRow.GetData(wechatPayTransactionTypeColumnName) == wechatPayTransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME] { + if relatedAccountName == "" || relatedAccountName == "/" { + data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = localeTextItems.DataConverterTextItems.WeChatWallet + data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = "" + } else { + data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = relatedAccountName + } + } else if dataRow.GetData(wechatPayTransactionTypeColumnName) == wechatPayTransactionTypeNameMapping[models.TRANSACTION_TYPE_TRANSFER] { + if data[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] == wechatPayTransactionDataCategoryTransferToWeChatWallet { + data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = relatedAccountName + data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = localeTextItems.DataConverterTextItems.WeChatWallet + } else if data[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] == wechatPayTransactionDataCategoryTransferFromWeChatWallet { + data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = localeTextItems.DataConverterTextItems.WeChatWallet + data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = relatedAccountName + } else { + log.Warnf(ctx, "[wechat_pay_transaction_data_row_parser.Parse] skip parsing transaction in row \"%s\", because unkown transfer transaction category \"%s\"", rowId, data[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY]) + return nil, false, nil + } + } else { + data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = relatedAccountName + data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = "" + } + } + + if data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] == wechatPayTransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME] && statusName != "" { + if strings.Index(statusName, wechatPayTransactionDataStatusRefundName) >= 0 { + amount, err := utils.ParseAmount(data[datatable.TRANSACTION_DATA_TABLE_AMOUNT]) + + if err == nil { + data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = wechatPayTransactionTypeNameMapping[models.TRANSACTION_TYPE_EXPENSE] + data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(-amount) + } + } + } + + return data, true, nil +} + +// createWeChatPayTransactionDataRowParser returns wechat pay transaction data row parser +func createWeChatPayTransactionDataRowParser() datatable.CommonTransactionDataRowParser { + return &weChatPayTransactionDataRowParser{} +}