mirror of
https://github.com/mayswind/ezbookkeeping.git
synced 2026-05-18 08:44:25 +08:00
code refactor
This commit is contained in:
@@ -9,17 +9,19 @@ type alipayAppTransactionDataCsvImporter struct {
|
|||||||
var (
|
var (
|
||||||
AlipayAppTransactionDataCsvImporter = &alipayAppTransactionDataCsvImporter{
|
AlipayAppTransactionDataCsvImporter = &alipayAppTransactionDataCsvImporter{
|
||||||
alipayTransactionDataCsvImporter{
|
alipayTransactionDataCsvImporter{
|
||||||
fileHeaderLine: "------------------------------------------------------------------------------------",
|
fileHeaderLine: "------------------------------------------------------------------------------------",
|
||||||
dataHeaderStartContent: "支付宝(中国)网络技术有限公司 电子客户回单",
|
dataHeaderStartContent: "支付宝(中国)网络技术有限公司 电子客户回单",
|
||||||
timeColumnName: "交易时间",
|
originalColumnNames: alipayTransactionColumnNames{
|
||||||
categoryColumnName: "交易分类",
|
timeColumnName: "交易时间",
|
||||||
targetNameColumnName: "交易对方",
|
categoryColumnName: "交易分类",
|
||||||
productNameColumnName: "商品说明",
|
targetNameColumnName: "交易对方",
|
||||||
amountColumnName: "金额",
|
productNameColumnName: "商品说明",
|
||||||
typeColumnName: "收/支",
|
amountColumnName: "金额",
|
||||||
relatedAccountColumnName: "收/付款方式",
|
typeColumnName: "收/支",
|
||||||
statusColumnName: "交易状态",
|
relatedAccountColumnName: "收/付款方式",
|
||||||
descriptionColumnName: "备注",
|
statusColumnName: "交易状态",
|
||||||
|
descriptionColumnName: "备注",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,36 +2,15 @@ package alipay
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/csv"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"golang.org/x/text/encoding/simplifiedchinese"
|
"golang.org/x/text/encoding/simplifiedchinese"
|
||||||
"golang.org/x/text/transform"
|
"golang.org/x/text/transform"
|
||||||
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
|
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
"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/models"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const alipayTransactionDataStatusSuccessName = "交易成功"
|
|
||||||
const alipayTransactionDataStatusPaymentSuccessName = "支付成功"
|
|
||||||
const alipayTransactionDataStatusRepaymentSuccessName = "还款成功"
|
|
||||||
const alipayTransactionDataStatusClosedName = "交易关闭"
|
|
||||||
const alipayTransactionDataStatusRefundSuccessName = "退款成功"
|
|
||||||
const alipayTransactionDataStatusTaxRefundSuccessName = "退税成功"
|
|
||||||
|
|
||||||
const alipayTransactionDataProductNameRechargePrefix = "充值-"
|
|
||||||
const alipayTransactionDataProductNameCashWithdrawalPrefix = "提现-"
|
|
||||||
const alipayTransactionDataProductNameTransferInText = "转入"
|
|
||||||
const alipayTransactionDataProductNameTransferOutText = "转出"
|
|
||||||
const alipayTransactionDataProductNameRepaymentText = "还款"
|
|
||||||
|
|
||||||
var alipayTransactionTypeNameMapping = map[models.TransactionType]string{
|
var alipayTransactionTypeNameMapping = map[models.TransactionType]string{
|
||||||
models.TRANSACTION_TYPE_INCOME: "收入",
|
models.TRANSACTION_TYPE_INCOME: "收入",
|
||||||
models.TRANSACTION_TYPE_EXPENSE: "支出",
|
models.TRANSACTION_TYPE_EXPENSE: "支出",
|
||||||
@@ -40,334 +19,34 @@ var alipayTransactionTypeNameMapping = map[models.TransactionType]string{
|
|||||||
|
|
||||||
// alipayTransactionDataCsvImporter defines the structure of alipay csv importer for transaction data
|
// alipayTransactionDataCsvImporter defines the structure of alipay csv importer for transaction data
|
||||||
type alipayTransactionDataCsvImporter struct {
|
type alipayTransactionDataCsvImporter struct {
|
||||||
fileHeaderLine string
|
fileHeaderLine string
|
||||||
dataHeaderStartContent string
|
dataHeaderStartContent string
|
||||||
dataBottomEndLineRune rune
|
dataBottomEndLineRune rune
|
||||||
timeColumnName string
|
originalColumnNames alipayTransactionColumnNames
|
||||||
categoryColumnName string
|
|
||||||
targetNameColumnName string
|
|
||||||
productNameColumnName string
|
|
||||||
amountColumnName string
|
|
||||||
typeColumnName string
|
|
||||||
relatedAccountColumnName string
|
|
||||||
statusColumnName string
|
|
||||||
descriptionColumnName string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseImportedData returns the imported data by parsing the alipay transaction csv data
|
// 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, 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) {
|
func (c *alipayTransactionDataCsvImporter) 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) {
|
||||||
enc := simplifiedchinese.GB18030
|
enc := simplifiedchinese.GB18030
|
||||||
reader := transform.NewReader(bytes.NewReader(data), enc.NewDecoder())
|
reader := transform.NewReader(bytes.NewReader(data), enc.NewDecoder())
|
||||||
allLines, err := c.parseAllLinesFromCsvData(ctx, reader)
|
|
||||||
|
dataTable, err := createNewAlipayTransactionPlainTextDataTable(
|
||||||
|
ctx,
|
||||||
|
reader,
|
||||||
|
c.fileHeaderLine,
|
||||||
|
c.dataHeaderStartContent,
|
||||||
|
c.dataBottomEndLineRune,
|
||||||
|
c.originalColumnNames,
|
||||||
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, nil, nil, nil, err
|
return nil, nil, nil, nil, nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(allLines) <= 1 {
|
dataTableImporter := datatable.CreateNewSimpleImporter(
|
||||||
log.Errorf(ctx, "[alipayTransactionDataCsvImporter.ParseImportedData] cannot parse import data for user \"uid:%d\", because data table row count is less 1", user.Uid)
|
dataTable.GetDataColumnMapping(),
|
||||||
return nil, nil, 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[c.timeColumnName]
|
|
||||||
categoryColumnIdx, categoryColumnExists := headerItemMap[c.categoryColumnName]
|
|
||||||
targetNameColumnIdx, targetNameColumnExists := headerItemMap[c.targetNameColumnName]
|
|
||||||
productNameColumnIdx, productNameColumnExists := headerItemMap[c.productNameColumnName]
|
|
||||||
amountColumnIdx, amountColumnExists := headerItemMap[c.amountColumnName]
|
|
||||||
typeColumnIdx, typeColumnExists := headerItemMap[c.typeColumnName]
|
|
||||||
relatedAccountColumnIdx, relatedAccountColumnExists := headerItemMap[c.relatedAccountColumnName]
|
|
||||||
statusColumnIdx, statusColumnExists := headerItemMap[c.statusColumnName]
|
|
||||||
descriptionColumnIdx, descriptionColumnExists := headerItemMap[c.descriptionColumnName]
|
|
||||||
|
|
||||||
if !timeColumnExists || !amountColumnExists || !typeColumnExists || !statusColumnExists {
|
|
||||||
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, 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 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, nil, nil, errs.ErrFewerFieldsInDataRowThanInHeaderRow
|
|
||||||
}
|
|
||||||
|
|
||||||
if items[typeColumnIdx] != alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME] &&
|
|
||||||
items[typeColumnIdx] != alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_EXPENSE] &&
|
|
||||||
items[typeColumnIdx] != alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_TRANSFER] {
|
|
||||||
log.Warnf(ctx, "[alipayTransactionDataCsvImporter.ParseImportedData] skip parsing transaction in row \"index:%d\" for user \"uid:%d\", because type is \"%s\"", i, user.Uid, items[typeColumnIdx])
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if items[statusColumnIdx] != alipayTransactionDataStatusSuccessName &&
|
|
||||||
items[statusColumnIdx] != alipayTransactionDataStatusPaymentSuccessName &&
|
|
||||||
items[statusColumnIdx] != alipayTransactionDataStatusRepaymentSuccessName &&
|
|
||||||
items[statusColumnIdx] != alipayTransactionDataStatusClosedName &&
|
|
||||||
items[statusColumnIdx] != alipayTransactionDataStatusRefundSuccessName &&
|
|
||||||
items[statusColumnIdx] != alipayTransactionDataStatusTaxRefundSuccessName {
|
|
||||||
log.Warnf(ctx, "[alipayTransactionDataCsvImporter.ParseImportedData] skip parsing transaction in row \"index:%d\" for user \"uid:%d\", because status is \"%s\"", i, user.Uid, items[statusColumnIdx])
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
data, errMsg := c.parseTransactionData(ctx,
|
|
||||||
user,
|
|
||||||
items,
|
|
||||||
timeColumnIdx,
|
|
||||||
timeColumnExists,
|
|
||||||
categoryColumnIdx,
|
|
||||||
categoryColumnExists,
|
|
||||||
targetNameColumnIdx,
|
|
||||||
targetNameColumnExists,
|
|
||||||
productNameColumnIdx,
|
|
||||||
productNameColumnExists,
|
|
||||||
amountColumnIdx,
|
|
||||||
amountColumnExists,
|
|
||||||
typeColumnIdx,
|
|
||||||
typeColumnExists,
|
|
||||||
relatedAccountColumnIdx,
|
|
||||||
relatedAccountColumnExists,
|
|
||||||
statusColumnIdx,
|
|
||||||
statusColumnExists,
|
|
||||||
descriptionColumnIdx,
|
|
||||||
descriptionColumnExists,
|
|
||||||
)
|
|
||||||
|
|
||||||
if data == nil {
|
|
||||||
log.Warnf(ctx, "[alipayTransactionDataCsvImporter.ParseImportedData] skip parsing transaction in row \"index:%d\" for user \"uid:%d\", because %s", i, user.Uid, errMsg)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
dataTable.Add(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
dataTableImporter := datatable.CreateNewSimpleImporterFromWritableDataTable(
|
|
||||||
dataTable,
|
|
||||||
alipayTransactionTypeNameMapping,
|
alipayTransactionTypeNameMapping,
|
||||||
)
|
)
|
||||||
|
|
||||||
return dataTableImporter.ParseImportedData(ctx, user, dataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
|
return dataTableImporter.ParseImportedData(ctx, user, dataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, 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], c.fileHeaderLine) == 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], c.dataHeaderStartContent) >= 0 {
|
|
||||||
foundContentBeforeDataHeaderLine = true
|
|
||||||
continue
|
|
||||||
} else {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if foundContentBeforeDataHeaderLine {
|
|
||||||
if len(items) <= 0 {
|
|
||||||
continue
|
|
||||||
} else if len(items) == 1 && c.dataBottomEndLineRune > 0 && utils.ContainsOnlyOneRune(items[0], c.dataBottomEndLineRune) {
|
|
||||||
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,
|
|
||||||
categoryColumnIdx int,
|
|
||||||
categoryColumnExists bool,
|
|
||||||
targetNameColumnIdx int,
|
|
||||||
targetNameColumnExists bool,
|
|
||||||
productNameColumnIdx int,
|
|
||||||
productNameColumnExists bool,
|
|
||||||
amountColumnIdx int,
|
|
||||||
amountColumnExists bool,
|
|
||||||
typeColumnIdx int,
|
|
||||||
typeColumnExists bool,
|
|
||||||
relatedAccountColumnIdx int,
|
|
||||||
relatedAccountColumnExists bool,
|
|
||||||
statusColumnIdx int,
|
|
||||||
statusColumnExists bool,
|
|
||||||
descriptionColumnIdx int,
|
|
||||||
descriptionColumnExists bool,
|
|
||||||
) (map[datatable.DataTableColumn]string, string) {
|
|
||||||
data := make(map[datatable.DataTableColumn]string, 11)
|
|
||||||
|
|
||||||
if timeColumnExists && timeColumnIdx < len(items) {
|
|
||||||
data[datatable.DATA_TABLE_TRANSACTION_TIME] = items[timeColumnIdx]
|
|
||||||
}
|
|
||||||
|
|
||||||
if categoryColumnExists && categoryColumnIdx < len(items) {
|
|
||||||
data[datatable.DATA_TABLE_SUB_CATEGORY] = items[categoryColumnIdx]
|
|
||||||
} else {
|
|
||||||
data[datatable.DATA_TABLE_SUB_CATEGORY] = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if amountColumnExists && amountColumnIdx < len(items) {
|
|
||||||
data[datatable.DATA_TABLE_AMOUNT] = items[amountColumnIdx]
|
|
||||||
}
|
|
||||||
|
|
||||||
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] = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
relatedAccountName := ""
|
|
||||||
|
|
||||||
if relatedAccountColumnExists && relatedAccountColumnIdx < len(items) {
|
|
||||||
relatedAccountName = items[relatedAccountColumnIdx]
|
|
||||||
}
|
|
||||||
|
|
||||||
statusName := ""
|
|
||||||
|
|
||||||
if statusColumnExists && statusColumnIdx < len(items) {
|
|
||||||
statusName = items[statusColumnIdx]
|
|
||||||
}
|
|
||||||
|
|
||||||
locale := user.Language
|
|
||||||
|
|
||||||
if locale == "" {
|
|
||||||
locale = ctx.GetClientLocale()
|
|
||||||
}
|
|
||||||
|
|
||||||
localeTextItems := locales.GetLocaleTextItems(locale)
|
|
||||||
|
|
||||||
if typeColumnExists && typeColumnIdx < len(items) {
|
|
||||||
data[datatable.DATA_TABLE_TRANSACTION_TYPE] = items[typeColumnIdx]
|
|
||||||
|
|
||||||
if items[typeColumnIdx] == alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME] {
|
|
||||||
if statusName == alipayTransactionDataStatusClosedName {
|
|
||||||
return nil, fmt.Sprintf("income transaction is closed")
|
|
||||||
}
|
|
||||||
|
|
||||||
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[typeColumnIdx] == alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_TRANSFER] {
|
|
||||||
if statusName == alipayTransactionDataStatusClosedName {
|
|
||||||
return nil, fmt.Sprintf("non-income/expense transaction is closed")
|
|
||||||
}
|
|
||||||
|
|
||||||
targetName := ""
|
|
||||||
productName := ""
|
|
||||||
|
|
||||||
if targetNameColumnExists && targetNameColumnIdx < len(items) {
|
|
||||||
targetName = items[targetNameColumnIdx]
|
|
||||||
}
|
|
||||||
|
|
||||||
if productNameColumnExists && productNameColumnIdx < len(items) {
|
|
||||||
productName = items[productNameColumnIdx]
|
|
||||||
}
|
|
||||||
|
|
||||||
if statusName == alipayTransactionDataStatusRefundSuccessName {
|
|
||||||
data[datatable.DATA_TABLE_TRANSACTION_TYPE] = alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME]
|
|
||||||
data[datatable.DATA_TABLE_ACCOUNT_NAME] = relatedAccountName
|
|
||||||
data[datatable.DATA_TABLE_RELATED_ACCOUNT_NAME] = ""
|
|
||||||
} else {
|
|
||||||
if strings.Index(productName, alipayTransactionDataProductNameRechargePrefix) == 0 { // transfer to alipay wallet
|
|
||||||
data[datatable.DATA_TABLE_ACCOUNT_NAME] = ""
|
|
||||||
data[datatable.DATA_TABLE_RELATED_ACCOUNT_NAME] = localeTextItems.DataConverterTextItems.Alipay
|
|
||||||
} 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] = relatedAccountName
|
|
||||||
data[datatable.DATA_TABLE_RELATED_ACCOUNT_NAME] = targetName
|
|
||||||
} else if strings.Index(productName, alipayTransactionDataProductNameTransferOutText) >= 0 { // transfer out
|
|
||||||
data[datatable.DATA_TABLE_ACCOUNT_NAME] = relatedAccountName
|
|
||||||
data[datatable.DATA_TABLE_RELATED_ACCOUNT_NAME] = targetName
|
|
||||||
} else if strings.Index(productName, alipayTransactionDataProductNameRepaymentText) >= 0 { // repayment
|
|
||||||
data[datatable.DATA_TABLE_ACCOUNT_NAME] = relatedAccountName
|
|
||||||
data[datatable.DATA_TABLE_RELATED_ACCOUNT_NAME] = targetName
|
|
||||||
} else {
|
|
||||||
return nil, fmt.Sprintf("product name (\"%s\") is unknown", productName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
data[datatable.DATA_TABLE_ACCOUNT_NAME] = relatedAccountName
|
|
||||||
data[datatable.DATA_TABLE_RELATED_ACCOUNT_NAME] = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if data[datatable.DATA_TABLE_TRANSACTION_TYPE] == alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME] && statusName != "" {
|
|
||||||
if statusName == alipayTransactionDataStatusRefundSuccessName || statusName == alipayTransactionDataStatusTaxRefundSuccessName {
|
|
||||||
amount, err := utils.ParseAmount(data[datatable.DATA_TABLE_AMOUNT])
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
data[datatable.DATA_TABLE_TRANSACTION_TYPE] = alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_EXPENSE]
|
|
||||||
data[datatable.DATA_TABLE_AMOUNT] = utils.FormatAmount(-amount)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return data, ""
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,466 @@
|
|||||||
|
package alipay
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/csv"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/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 alipayTransactionDataProductNameRechargePrefix = "充值-"
|
||||||
|
const alipayTransactionDataProductNameCashWithdrawalPrefix = "提现-"
|
||||||
|
const alipayTransactionDataProductNameTransferInText = "转入"
|
||||||
|
const alipayTransactionDataProductNameTransferOutText = "转出"
|
||||||
|
const alipayTransactionDataProductNameRepaymentText = "还款"
|
||||||
|
|
||||||
|
var alipayTransactionSupportedColumns = []datatable.DataTableColumn{
|
||||||
|
datatable.DATA_TABLE_TRANSACTION_TYPE,
|
||||||
|
datatable.DATA_TABLE_TRANSACTION_TIME,
|
||||||
|
datatable.DATA_TABLE_SUB_CATEGORY,
|
||||||
|
datatable.DATA_TABLE_ACCOUNT_NAME,
|
||||||
|
datatable.DATA_TABLE_AMOUNT,
|
||||||
|
datatable.DATA_TABLE_RELATED_ACCOUNT_NAME,
|
||||||
|
datatable.DATA_TABLE_DESCRIPTION,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// alipayTransactionPlainTextDataTable defines the structure of alipay transaction plain text data table
|
||||||
|
type alipayTransactionPlainTextDataTable struct {
|
||||||
|
allOriginalLines [][]string
|
||||||
|
originalHeaderLineColumnNames []string
|
||||||
|
originalTimeColumnIndex int
|
||||||
|
originalCategoryColumnIndex int
|
||||||
|
originalTargetNameColumnIndex int
|
||||||
|
originalProductNameColumnIndex int
|
||||||
|
originalAmountColumnIndex int
|
||||||
|
originalTypeColumnIndex int
|
||||||
|
originalRelatedAccountColumnIndex int
|
||||||
|
originalStatusColumnIndex int
|
||||||
|
originalDescriptionColumnIndex int
|
||||||
|
}
|
||||||
|
|
||||||
|
// alipayTransactionPlainTextDataRow defines the structure of alipay transaction plain text data row
|
||||||
|
type alipayTransactionPlainTextDataRow struct {
|
||||||
|
dataTable *alipayTransactionPlainTextDataTable
|
||||||
|
isValid bool
|
||||||
|
originalItems []string
|
||||||
|
finalItems map[datatable.DataTableColumn]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// alipayTransactionPlainTextDataRowIterator defines the structure of alipay transaction plain text data row iterator
|
||||||
|
type alipayTransactionPlainTextDataRowIterator struct {
|
||||||
|
dataTable *alipayTransactionPlainTextDataTable
|
||||||
|
currentIndex int
|
||||||
|
}
|
||||||
|
|
||||||
|
// DataRowCount returns the total count of data row
|
||||||
|
func (t *alipayTransactionPlainTextDataTable) DataRowCount() int {
|
||||||
|
if len(t.allOriginalLines) < 1 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(t.allOriginalLines) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDataColumnMapping returns data column map for data importer
|
||||||
|
func (t *alipayTransactionPlainTextDataTable) GetDataColumnMapping() map[datatable.DataTableColumn]string {
|
||||||
|
dataColumnMapping := make(map[datatable.DataTableColumn]string, len(alipayTransactionSupportedColumns))
|
||||||
|
|
||||||
|
for i := 0; i < len(alipayTransactionSupportedColumns); i++ {
|
||||||
|
column := alipayTransactionSupportedColumns[i]
|
||||||
|
dataColumnMapping[column] = utils.IntToString(int(column))
|
||||||
|
}
|
||||||
|
|
||||||
|
return dataColumnMapping
|
||||||
|
}
|
||||||
|
|
||||||
|
// HeaderLineColumnNames returns the header column name list
|
||||||
|
func (t *alipayTransactionPlainTextDataTable) HeaderLineColumnNames() []string {
|
||||||
|
columnIndexes := make([]string, len(alipayTransactionSupportedColumns))
|
||||||
|
|
||||||
|
for i := 0; i < len(alipayTransactionSupportedColumns); i++ {
|
||||||
|
columnIndexes[i] = utils.IntToString(int(alipayTransactionSupportedColumns[i]))
|
||||||
|
}
|
||||||
|
|
||||||
|
return columnIndexes
|
||||||
|
}
|
||||||
|
|
||||||
|
// DataRowIterator returns the iterator of data row
|
||||||
|
func (t *alipayTransactionPlainTextDataTable) DataRowIterator() datatable.ImportedDataRowIterator {
|
||||||
|
return &alipayTransactionPlainTextDataRowIterator{
|
||||||
|
dataTable: t,
|
||||||
|
currentIndex: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValid returns whether this row contains valid data for importing
|
||||||
|
func (r *alipayTransactionPlainTextDataRow) IsValid() bool {
|
||||||
|
return r.isValid
|
||||||
|
}
|
||||||
|
|
||||||
|
// ColumnCount returns the total count of column in this data row
|
||||||
|
func (r *alipayTransactionPlainTextDataRow) ColumnCount() int {
|
||||||
|
return len(r.finalItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetData returns the data in the specified column index
|
||||||
|
func (r *alipayTransactionPlainTextDataRow) GetData(columnIndex int) string {
|
||||||
|
if columnIndex >= len(alipayTransactionSupportedColumns) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
dataColumn := alipayTransactionSupportedColumns[columnIndex]
|
||||||
|
|
||||||
|
return r.finalItems[dataColumn]
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTime returns the time in the specified column index
|
||||||
|
func (r *alipayTransactionPlainTextDataRow) GetTime(columnIndex int, timezoneOffset int16) (time.Time, error) {
|
||||||
|
return utils.ParseFromLongDateTime(r.GetData(columnIndex), timezoneOffset)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTimezoneOffset returns the time zone offset in the specified column index
|
||||||
|
func (r *alipayTransactionPlainTextDataRow) GetTimezoneOffset(columnIndex int) (*time.Location, error) {
|
||||||
|
return nil, errs.ErrNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasNext returns whether the iterator does not reach the end
|
||||||
|
func (t *alipayTransactionPlainTextDataRowIterator) HasNext() bool {
|
||||||
|
return t.currentIndex+1 < len(t.dataTable.allOriginalLines)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next returns the next imported data row
|
||||||
|
func (t *alipayTransactionPlainTextDataRowIterator) Next(ctx core.Context, user *models.User) datatable.ImportedDataRow {
|
||||||
|
if t.currentIndex+1 >= len(t.dataTable.allOriginalLines) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
t.currentIndex++
|
||||||
|
|
||||||
|
rowItems := t.dataTable.allOriginalLines[t.currentIndex]
|
||||||
|
isValid := true
|
||||||
|
|
||||||
|
if t.dataTable.originalTypeColumnIndex >= 0 &&
|
||||||
|
rowItems[t.dataTable.originalTypeColumnIndex] != alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME] &&
|
||||||
|
rowItems[t.dataTable.originalTypeColumnIndex] != alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_EXPENSE] &&
|
||||||
|
rowItems[t.dataTable.originalTypeColumnIndex] != alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_TRANSFER] {
|
||||||
|
log.Warnf(ctx, "[alipay_transaction_data_plain_text_data_table.Next] skip parsing transaction in row \"index:%d\", because type is \"%s\"", t.currentIndex, rowItems[t.dataTable.originalTypeColumnIndex])
|
||||||
|
isValid = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.dataTable.originalStatusColumnIndex >= 0 &&
|
||||||
|
rowItems[t.dataTable.originalStatusColumnIndex] != alipayTransactionDataStatusSuccessName &&
|
||||||
|
rowItems[t.dataTable.originalStatusColumnIndex] != alipayTransactionDataStatusPaymentSuccessName &&
|
||||||
|
rowItems[t.dataTable.originalStatusColumnIndex] != alipayTransactionDataStatusRepaymentSuccessName &&
|
||||||
|
rowItems[t.dataTable.originalStatusColumnIndex] != alipayTransactionDataStatusClosedName &&
|
||||||
|
rowItems[t.dataTable.originalStatusColumnIndex] != alipayTransactionDataStatusRefundSuccessName &&
|
||||||
|
rowItems[t.dataTable.originalStatusColumnIndex] != alipayTransactionDataStatusTaxRefundSuccessName {
|
||||||
|
log.Warnf(ctx, "[alipay_transaction_data_plain_text_data_table.Next] skip parsing transaction in row \"index:%d\", because status is \"%s\"", t.currentIndex, rowItems[t.dataTable.originalStatusColumnIndex])
|
||||||
|
isValid = false
|
||||||
|
}
|
||||||
|
|
||||||
|
var finalItems map[datatable.DataTableColumn]string
|
||||||
|
var errMsg string
|
||||||
|
|
||||||
|
if isValid {
|
||||||
|
finalItems, errMsg = t.dataTable.parseTransactionData(ctx, user, rowItems)
|
||||||
|
|
||||||
|
if finalItems == nil {
|
||||||
|
log.Warnf(ctx, "[alipay_transaction_data_plain_text_data_table.Next] skip parsing transaction in row \"index:%d\", because %s", t.currentIndex, errMsg)
|
||||||
|
isValid = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &alipayTransactionPlainTextDataRow{
|
||||||
|
dataTable: t.dataTable,
|
||||||
|
isValid: isValid,
|
||||||
|
originalItems: rowItems,
|
||||||
|
finalItems: finalItems,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *alipayTransactionPlainTextDataTable) parseTransactionData(ctx core.Context, user *models.User, items []string) (map[datatable.DataTableColumn]string, string) {
|
||||||
|
data := make(map[datatable.DataTableColumn]string, 7)
|
||||||
|
|
||||||
|
if t.originalTimeColumnIndex >= 0 && t.originalTimeColumnIndex < len(items) {
|
||||||
|
data[datatable.DATA_TABLE_TRANSACTION_TIME] = items[t.originalTimeColumnIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.originalCategoryColumnIndex >= 0 && t.originalCategoryColumnIndex < len(items) {
|
||||||
|
data[datatable.DATA_TABLE_SUB_CATEGORY] = items[t.originalCategoryColumnIndex]
|
||||||
|
} else {
|
||||||
|
data[datatable.DATA_TABLE_SUB_CATEGORY] = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.originalAmountColumnIndex >= 0 && t.originalAmountColumnIndex < len(items) {
|
||||||
|
data[datatable.DATA_TABLE_AMOUNT] = items[t.originalAmountColumnIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.originalDescriptionColumnIndex >= 0 && t.originalDescriptionColumnIndex < len(items) && items[t.originalDescriptionColumnIndex] != "" {
|
||||||
|
data[datatable.DATA_TABLE_DESCRIPTION] = items[t.originalDescriptionColumnIndex]
|
||||||
|
} else if t.originalProductNameColumnIndex >= 0 && t.originalProductNameColumnIndex < len(items) && items[t.originalProductNameColumnIndex] != "" {
|
||||||
|
data[datatable.DATA_TABLE_DESCRIPTION] = items[t.originalProductNameColumnIndex]
|
||||||
|
} else {
|
||||||
|
data[datatable.DATA_TABLE_DESCRIPTION] = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
relatedAccountName := ""
|
||||||
|
|
||||||
|
if t.originalRelatedAccountColumnIndex >= 0 && t.originalRelatedAccountColumnIndex < len(items) {
|
||||||
|
relatedAccountName = items[t.originalRelatedAccountColumnIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
statusName := ""
|
||||||
|
|
||||||
|
if t.originalStatusColumnIndex >= 0 && t.originalStatusColumnIndex < len(items) {
|
||||||
|
statusName = items[t.originalStatusColumnIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
locale := user.Language
|
||||||
|
|
||||||
|
if locale == "" {
|
||||||
|
locale = ctx.GetClientLocale()
|
||||||
|
}
|
||||||
|
|
||||||
|
localeTextItems := locales.GetLocaleTextItems(locale)
|
||||||
|
|
||||||
|
if t.originalTypeColumnIndex >= 0 && t.originalTypeColumnIndex < len(items) {
|
||||||
|
data[datatable.DATA_TABLE_TRANSACTION_TYPE] = items[t.originalTypeColumnIndex]
|
||||||
|
|
||||||
|
if items[t.originalTypeColumnIndex] == alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME] {
|
||||||
|
if statusName == alipayTransactionDataStatusClosedName {
|
||||||
|
return nil, fmt.Sprintf("income transaction is closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
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[t.originalTypeColumnIndex] == alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_TRANSFER] {
|
||||||
|
if statusName == alipayTransactionDataStatusClosedName {
|
||||||
|
return nil, fmt.Sprintf("non-income/expense transaction is closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
targetName := ""
|
||||||
|
productName := ""
|
||||||
|
|
||||||
|
if t.originalTargetNameColumnIndex >= 0 && t.originalTargetNameColumnIndex < len(items) {
|
||||||
|
targetName = items[t.originalTargetNameColumnIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.originalProductNameColumnIndex >= 0 && t.originalProductNameColumnIndex < len(items) {
|
||||||
|
productName = items[t.originalProductNameColumnIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
if statusName == alipayTransactionDataStatusRefundSuccessName {
|
||||||
|
data[datatable.DATA_TABLE_TRANSACTION_TYPE] = alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME]
|
||||||
|
data[datatable.DATA_TABLE_ACCOUNT_NAME] = relatedAccountName
|
||||||
|
data[datatable.DATA_TABLE_RELATED_ACCOUNT_NAME] = ""
|
||||||
|
} else {
|
||||||
|
if strings.Index(productName, alipayTransactionDataProductNameRechargePrefix) == 0 { // transfer to alipay wallet
|
||||||
|
data[datatable.DATA_TABLE_ACCOUNT_NAME] = ""
|
||||||
|
data[datatable.DATA_TABLE_RELATED_ACCOUNT_NAME] = localeTextItems.DataConverterTextItems.Alipay
|
||||||
|
} 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] = relatedAccountName
|
||||||
|
data[datatable.DATA_TABLE_RELATED_ACCOUNT_NAME] = targetName
|
||||||
|
} else if strings.Index(productName, alipayTransactionDataProductNameTransferOutText) >= 0 { // transfer out
|
||||||
|
data[datatable.DATA_TABLE_ACCOUNT_NAME] = relatedAccountName
|
||||||
|
data[datatable.DATA_TABLE_RELATED_ACCOUNT_NAME] = targetName
|
||||||
|
} else if strings.Index(productName, alipayTransactionDataProductNameRepaymentText) >= 0 { // repayment
|
||||||
|
data[datatable.DATA_TABLE_ACCOUNT_NAME] = relatedAccountName
|
||||||
|
data[datatable.DATA_TABLE_RELATED_ACCOUNT_NAME] = targetName
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Sprintf("product name (\"%s\") is unknown", productName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data[datatable.DATA_TABLE_ACCOUNT_NAME] = relatedAccountName
|
||||||
|
data[datatable.DATA_TABLE_RELATED_ACCOUNT_NAME] = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if data[datatable.DATA_TABLE_TRANSACTION_TYPE] == alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME] && statusName != "" {
|
||||||
|
if statusName == alipayTransactionDataStatusRefundSuccessName || statusName == alipayTransactionDataStatusTaxRefundSuccessName {
|
||||||
|
amount, err := utils.ParseAmount(data[datatable.DATA_TABLE_AMOUNT])
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
data[datatable.DATA_TABLE_TRANSACTION_TYPE] = alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_EXPENSE]
|
||||||
|
data[datatable.DATA_TABLE_AMOUNT] = utils.FormatAmount(-amount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func createNewAlipayTransactionPlainTextDataTable(ctx core.Context, reader io.Reader, fileHeaderLine string, dataHeaderStartContent string, dataBottomEndLineRune rune, originalColumnNames alipayTransactionColumnNames) (*alipayTransactionPlainTextDataTable, error) {
|
||||||
|
allOriginalLines, err := parseAllLinesFromAlipayTransactionPlainText(ctx, reader, fileHeaderLine, dataHeaderStartContent, dataBottomEndLineRune)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(allOriginalLines) < 2 {
|
||||||
|
log.Errorf(ctx, "[alipay_transaction_data_plain_text_data_table.createNewAlipayTransactionPlainTextDataTable] cannot parse import data, because data table row count is less 1")
|
||||||
|
return nil, errs.ErrNotFoundTransactionDataInFile
|
||||||
|
}
|
||||||
|
|
||||||
|
originalHeaderItems := allOriginalLines[0]
|
||||||
|
originalHeaderItemMap := make(map[string]int)
|
||||||
|
|
||||||
|
for i := 0; i < len(originalHeaderItems); i++ {
|
||||||
|
originalHeaderItemMap[originalHeaderItems[i]] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
timeColumnIdx, timeColumnExists := originalHeaderItemMap[originalColumnNames.timeColumnName]
|
||||||
|
categoryColumnIdx, categoryColumnExists := originalHeaderItemMap[originalColumnNames.categoryColumnName]
|
||||||
|
targetNameColumnIdx, targetNameColumnExists := originalHeaderItemMap[originalColumnNames.targetNameColumnName]
|
||||||
|
productNameColumnIdx, productNameColumnExists := originalHeaderItemMap[originalColumnNames.productNameColumnName]
|
||||||
|
amountColumnIdx, amountColumnExists := originalHeaderItemMap[originalColumnNames.amountColumnName]
|
||||||
|
typeColumnIdx, typeColumnExists := originalHeaderItemMap[originalColumnNames.typeColumnName]
|
||||||
|
relatedAccountColumnIdx, relatedAccountColumnExists := originalHeaderItemMap[originalColumnNames.relatedAccountColumnName]
|
||||||
|
statusColumnIdx, statusColumnExists := originalHeaderItemMap[originalColumnNames.statusColumnName]
|
||||||
|
descriptionColumnIdx, descriptionColumnExists := originalHeaderItemMap[originalColumnNames.descriptionColumnName]
|
||||||
|
|
||||||
|
if !timeColumnExists || !amountColumnExists || !typeColumnExists || !statusColumnExists {
|
||||||
|
log.Errorf(ctx, "[alipay_transaction_data_plain_text_data_table.createNewAlipayTransactionPlainTextDataTable] cannot parse alipay csv data, because missing essential columns in header row")
|
||||||
|
return nil, errs.ErrMissingRequiredFieldInHeaderRow
|
||||||
|
}
|
||||||
|
|
||||||
|
if originalColumnNames.categoryColumnName == "" || !categoryColumnExists {
|
||||||
|
categoryColumnIdx = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
if originalColumnNames.targetNameColumnName == "" || !targetNameColumnExists {
|
||||||
|
targetNameColumnIdx = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
if originalColumnNames.productNameColumnName == "" || !productNameColumnExists {
|
||||||
|
productNameColumnIdx = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
if originalColumnNames.relatedAccountColumnName == "" || !relatedAccountColumnExists {
|
||||||
|
relatedAccountColumnIdx = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
if originalColumnNames.descriptionColumnName == "" || !descriptionColumnExists {
|
||||||
|
descriptionColumnIdx = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
return &alipayTransactionPlainTextDataTable{
|
||||||
|
allOriginalLines: allOriginalLines,
|
||||||
|
originalHeaderLineColumnNames: originalHeaderItems,
|
||||||
|
originalTimeColumnIndex: timeColumnIdx,
|
||||||
|
originalCategoryColumnIndex: categoryColumnIdx,
|
||||||
|
originalTargetNameColumnIndex: targetNameColumnIdx,
|
||||||
|
originalProductNameColumnIndex: productNameColumnIdx,
|
||||||
|
originalAmountColumnIndex: amountColumnIdx,
|
||||||
|
originalTypeColumnIndex: typeColumnIdx,
|
||||||
|
originalRelatedAccountColumnIndex: relatedAccountColumnIdx,
|
||||||
|
originalStatusColumnIndex: statusColumnIdx,
|
||||||
|
originalDescriptionColumnIndex: descriptionColumnIdx,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseAllLinesFromAlipayTransactionPlainText(ctx core.Context, reader io.Reader, fileHeaderLine string, dataHeaderStartContent string, dataBottomEndLineRune rune) ([][]string, 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_data_plain_text_data_table.parseAllLinesFromAlipayTransactionPlainText] 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_data_plain_text_data_table.parseAllLinesFromAlipayTransactionPlainText] 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], 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_data_plain_text_data_table.parseAllLinesFromAlipayTransactionPlainText] 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
|
||||||
|
}
|
||||||
|
|
||||||
|
return allOriginalLines, nil
|
||||||
|
}
|
||||||
@@ -9,18 +9,20 @@ type alipayWebTransactionDataCsvImporter struct {
|
|||||||
var (
|
var (
|
||||||
AlipayWebTransactionDataCsvImporter = &alipayWebTransactionDataCsvImporter{
|
AlipayWebTransactionDataCsvImporter = &alipayWebTransactionDataCsvImporter{
|
||||||
alipayTransactionDataCsvImporter{
|
alipayTransactionDataCsvImporter{
|
||||||
fileHeaderLine: "支付宝交易记录明细查询",
|
fileHeaderLine: "支付宝交易记录明细查询",
|
||||||
dataHeaderStartContent: "交易记录明细列表",
|
dataHeaderStartContent: "交易记录明细列表",
|
||||||
dataBottomEndLineRune: '-',
|
dataBottomEndLineRune: '-',
|
||||||
timeColumnName: "交易创建时间",
|
originalColumnNames: alipayTransactionColumnNames{
|
||||||
categoryColumnName: "",
|
timeColumnName: "交易创建时间",
|
||||||
targetNameColumnName: "交易对方",
|
categoryColumnName: "",
|
||||||
productNameColumnName: "商品名称",
|
targetNameColumnName: "交易对方",
|
||||||
amountColumnName: "金额(元)",
|
productNameColumnName: "商品名称",
|
||||||
typeColumnName: "收/支",
|
amountColumnName: "金额(元)",
|
||||||
relatedAccountColumnName: "",
|
typeColumnName: "收/支",
|
||||||
statusColumnName: "交易状态",
|
relatedAccountColumnName: "",
|
||||||
descriptionColumnName: "备注",
|
statusColumnName: "交易状态",
|
||||||
|
descriptionColumnName: "备注",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
package datatable
|
package datatable
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||||
|
)
|
||||||
|
|
||||||
// ImportedDataTable defines the structure of imported data table
|
// ImportedDataTable defines the structure of imported data table
|
||||||
type ImportedDataTable interface {
|
type ImportedDataTable interface {
|
||||||
@@ -16,6 +21,9 @@ type ImportedDataTable interface {
|
|||||||
|
|
||||||
// ImportedDataRow defines the structure of imported data row
|
// ImportedDataRow defines the structure of imported data row
|
||||||
type ImportedDataRow interface {
|
type ImportedDataRow interface {
|
||||||
|
// IsValid returns whether this row contains valid data for importing
|
||||||
|
IsValid() bool
|
||||||
|
|
||||||
// ColumnCount returns the total count of column in this data row
|
// ColumnCount returns the total count of column in this data row
|
||||||
ColumnCount() int
|
ColumnCount() int
|
||||||
|
|
||||||
@@ -35,7 +43,7 @@ type ImportedDataRowIterator interface {
|
|||||||
HasNext() bool
|
HasNext() bool
|
||||||
|
|
||||||
// Next returns the next imported data row
|
// Next returns the next imported data row
|
||||||
Next() ImportedDataRow
|
Next(ctx core.Context, user *models.User) ImportedDataRow
|
||||||
}
|
}
|
||||||
|
|
||||||
// DataTableBuilder defines the structure of data table builder
|
// DataTableBuilder defines the structure of data table builder
|
||||||
|
|||||||
@@ -75,6 +75,14 @@ func CreateNewImporter(dataColumnMapping map[DataTableColumn]string, transaction
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateNewSimpleImporter returns a new data table transaction data importer according to the specified arguments
|
||||||
|
func CreateNewSimpleImporter(dataColumnMapping map[DataTableColumn]string, transactionTypeMapping map[models.TransactionType]string) *DataTableTransactionDataImporter {
|
||||||
|
return &DataTableTransactionDataImporter{
|
||||||
|
dataColumnMapping: dataColumnMapping,
|
||||||
|
transactionTypeMapping: transactionTypeMapping,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// CreateNewSimpleImporterWithPostProcessFunc returns a new data table transaction data importer according to the specified arguments
|
// CreateNewSimpleImporterWithPostProcessFunc returns a new data table transaction data importer according to the specified arguments
|
||||||
func CreateNewSimpleImporterWithPostProcessFunc(dataColumnMapping map[DataTableColumn]string, transactionTypeMapping map[models.TransactionType]string, postProcessFunc DataTableTransactionDataImporterPostProcessFunc) *DataTableTransactionDataImporter {
|
func CreateNewSimpleImporterWithPostProcessFunc(dataColumnMapping map[DataTableColumn]string, transactionTypeMapping map[models.TransactionType]string, postProcessFunc DataTableTransactionDataImporterPostProcessFunc) *DataTableTransactionDataImporter {
|
||||||
return &DataTableTransactionDataImporter{
|
return &DataTableTransactionDataImporter{
|
||||||
@@ -311,7 +319,12 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u
|
|||||||
|
|
||||||
for dataRowIterator.HasNext() {
|
for dataRowIterator.HasNext() {
|
||||||
dataRowIndex++
|
dataRowIndex++
|
||||||
dataRow := dataRowIterator.Next()
|
dataRow := dataRowIterator.Next(ctx, user)
|
||||||
|
|
||||||
|
if !dataRow.IsValid() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
columnCount := dataRow.ColumnCount()
|
columnCount := dataRow.ColumnCount()
|
||||||
|
|
||||||
if columnCount < 1 || (columnCount == 1 && dataRow.GetData(0) == "") {
|
if columnCount < 1 || (columnCount == 1 && dataRow.GetData(0) == "") {
|
||||||
@@ -577,6 +590,11 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u
|
|||||||
allNewTransactions = append(allNewTransactions, transaction)
|
allNewTransactions = append(allNewTransactions, transaction)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(allNewTransactions) < 1 {
|
||||||
|
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] no transaction data parsed for \"uid:%d\"", user.Uid)
|
||||||
|
return nil, nil, nil, nil, nil, nil, errs.ErrNotFoundTransactionDataInFile
|
||||||
|
}
|
||||||
|
|
||||||
sort.Sort(allNewTransactions)
|
sort.Sort(allNewTransactions)
|
||||||
|
|
||||||
return allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, nil
|
return allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, nil
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package datatable
|
|||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -89,6 +91,11 @@ func (t *WritableDataTable) DataRowIterator() ImportedDataRowIterator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsValid returns whether this row contains valid data for importing
|
||||||
|
func (r *WritableDataRow) IsValid() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// ColumnCount returns the total count of column in this data row
|
// ColumnCount returns the total count of column in this data row
|
||||||
func (r *WritableDataRow) ColumnCount() int {
|
func (r *WritableDataRow) ColumnCount() int {
|
||||||
return len(r.rowData)
|
return len(r.rowData)
|
||||||
@@ -121,7 +128,7 @@ func (t *WritableDataRowIterator) HasNext() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Next returns the next imported data row
|
// Next returns the next imported data row
|
||||||
func (t *WritableDataRowIterator) Next() ImportedDataRow {
|
func (t *WritableDataRowIterator) Next(ctx core.Context, user *models.User) ImportedDataRow {
|
||||||
if t.nextIndex >= len(t.dataTable.allData) {
|
if t.nextIndex >= len(t.dataTable.allData) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -176,7 +178,7 @@ func TestWritableDataTableDataRowIterator(t *testing.T) {
|
|||||||
iterator := writableDataTable.DataRowIterator()
|
iterator := writableDataTable.DataRowIterator()
|
||||||
|
|
||||||
for iterator.HasNext() {
|
for iterator.HasNext() {
|
||||||
dataRow := iterator.Next()
|
dataRow := iterator.Next(core.NewNullContext(), &models.User{})
|
||||||
|
|
||||||
actualTransactionTime, err := dataRow.GetTime(0, utils.GetTimezoneOffsetMinutes(time.Local))
|
actualTransactionTime, err := dataRow.GetTime(0, utils.GetTimezoneOffsetMinutes(time.Local))
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
|
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -61,6 +63,11 @@ func (t *ezBookKeepingTransactionPlainTextDataTable) DataRowIterator() datatable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsValid returns whether this row contains valid data for importing
|
||||||
|
func (r *ezBookKeepingTransactionPlainTextDataRow) IsValid() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// ColumnCount returns the total count of column in this data row
|
// ColumnCount returns the total count of column in this data row
|
||||||
func (r *ezBookKeepingTransactionPlainTextDataRow) ColumnCount() int {
|
func (r *ezBookKeepingTransactionPlainTextDataRow) ColumnCount() int {
|
||||||
return len(r.allItems)
|
return len(r.allItems)
|
||||||
@@ -91,7 +98,7 @@ func (t *ezBookKeepingTransactionPlainTextDataRowIterator) HasNext() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Next returns the next imported data row
|
// Next returns the next imported data row
|
||||||
func (t *ezBookKeepingTransactionPlainTextDataRowIterator) Next() datatable.ImportedDataRow {
|
func (t *ezBookKeepingTransactionPlainTextDataRowIterator) Next(ctx core.Context, user *models.User) datatable.ImportedDataRow {
|
||||||
if t.currentIndex+1 >= len(t.dataTable.allLines) {
|
if t.currentIndex+1 >= len(t.dataTable.allLines) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import (
|
|||||||
"github.com/shakinm/xlsReader/xls"
|
"github.com/shakinm/xlsReader/xls"
|
||||||
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
|
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -62,6 +64,11 @@ func (t *feideeMymoneyTransactionExcelFileDataTable) DataRowIterator() datatable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsValid returns whether this row contains valid data for importing
|
||||||
|
func (r *feideeMymoneyTransactionExcelFileDataRow) IsValid() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// ColumnCount returns the total count of column in this data row
|
// ColumnCount returns the total count of column in this data row
|
||||||
func (r *feideeMymoneyTransactionExcelFileDataRow) ColumnCount() int {
|
func (r *feideeMymoneyTransactionExcelFileDataRow) ColumnCount() int {
|
||||||
row, err := r.sheet.GetRow(r.rowIndex)
|
row, err := r.sheet.GetRow(r.rowIndex)
|
||||||
@@ -142,7 +149,7 @@ func (t *feideeMymoneyTransactionExcelFileDataRowIterator) HasNext() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Next returns the next imported data row
|
// Next returns the next imported data row
|
||||||
func (t *feideeMymoneyTransactionExcelFileDataRowIterator) Next() datatable.ImportedDataRow {
|
func (t *feideeMymoneyTransactionExcelFileDataRowIterator) Next(ctx core.Context, user *models.User) datatable.ImportedDataRow {
|
||||||
allSheets := t.dataTable.workbook.GetSheets()
|
allSheets := t.dataTable.workbook.GetSheets()
|
||||||
currentRowIndexInTable := t.currentRowIndexInTable
|
currentRowIndexInTable := t.currentRowIndexInTable
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user