support importing transaction data from alipay app

This commit is contained in:
MaysWind
2024-10-08 00:23:22 +08:00
parent ae26f00a36
commit 45d348c0ef
7 changed files with 432 additions and 344 deletions
@@ -0,0 +1,25 @@
package alipay
// alipayAppTransactionDataCsvImporter defines the structure of alipay app csv importer for transaction data
type alipayAppTransactionDataCsvImporter struct {
alipayTransactionDataCsvImporter
}
// Initialize a alipay app transaction data csv file importer singleton instance
var (
AlipayAppTransactionDataCsvImporter = &alipayAppTransactionDataCsvImporter{
alipayTransactionDataCsvImporter{
fileHeaderLine: "------------------------------------------------------------------------------------",
dataHeaderStartContent: "支付宝(中国)网络技术有限公司 电子客户回单",
timeColumnName: "交易时间",
categoryColumnName: "交易分类",
targetNameColumnName: "交易对方",
productNameColumnName: "商品说明",
amountColumnName: "金额",
typeColumnName: "收/支",
relatedAccountColumnName: "收/付款方式",
statusColumnName: "交易状态",
descriptionColumnName: "备注",
},
}
)
@@ -0,0 +1,373 @@
package alipay
import (
"bytes"
"encoding/csv"
"fmt"
"io"
"strings"
"golang.org/x/text/encoding/simplifiedchinese"
"golang.org/x/text/transform"
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/locales"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
const 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{
models.TRANSACTION_TYPE_INCOME: "收入",
models.TRANSACTION_TYPE_EXPENSE: "支出",
models.TRANSACTION_TYPE_TRANSFER: "不计收支",
}
// alipayTransactionDataCsvImporter defines the structure of alipay csv importer for transaction data
type alipayTransactionDataCsvImporter struct {
fileHeaderLine string
dataHeaderStartContent string
dataBottomEndLineRune rune
timeColumnName string
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
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
reader := transform.NewReader(bytes.NewReader(data), enc.NewDecoder())
allLines, err := c.parseAllLinesFromCsvData(ctx, reader)
if err != nil {
return nil, nil, nil, nil, nil, nil, err
}
if len(allLines) <= 1 {
log.Errorf(ctx, "[alipayTransactionDataCsvImporter.ParseImportedData] cannot parse import data for user \"uid:%d\", because data table row count is less 1", user.Uid)
return nil, nil, nil, nil, 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,
)
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, ""
}
@@ -1,349 +1,26 @@
package alipay
import (
"bytes"
"encoding/csv"
"fmt"
"io"
"strings"
"golang.org/x/text/encoding/simplifiedchinese"
"golang.org/x/text/transform"
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/locales"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
const alipayWebTransactionDataCsvFileHeader = "支付宝交易记录明细查询"
const alipayWebTransactionDataCsvDataHeaderLineStartContent = "交易记录明细列表"
const alipayWebTransactionDataCsvDataDataLineEndLineRune = '-'
const alipayWebTransactionDataStatusSuccessName = "交易成功"
const alipayWebTransactionDataStatusPaymentSuccessName = "支付成功"
const alipayWebTransactionDataStatusRepaymentSuccessName = "还款成功"
const alipayWebTransactionDataStatusClosedName = "交易关闭"
const alipayWebTransactionDataStatusRefundSuccessName = "退款成功"
const alipayWebTransactionDataStatusTaxRefundSuccessName = "退税成功"
const alipayWebTransactionDataProductNameRechargePrefix = "充值-"
const alipayWebTransactionDataProductNameCashWithdrawalPrefix = "提现-"
const alipayWebTransactionDataProductNameTransferInText = "转入"
const alipayWebTransactionDataProductNameTransferOutText = "转出"
const alipayWebTransactionDataProductNameRepaymentText = "还款"
var alipayTransactionTypeNameMapping = map[models.TransactionType]string{
models.TRANSACTION_TYPE_INCOME: "收入",
models.TRANSACTION_TYPE_EXPENSE: "支出",
models.TRANSACTION_TYPE_TRANSFER: "不计收支",
}
// alipayWebTransactionDataCsvImporter defines the structure of alipay (web) csv importer for transaction data
type alipayWebTransactionDataCsvImporter struct{}
type alipayWebTransactionDataCsvImporter struct {
alipayTransactionDataCsvImporter
}
// Initialize a alipay (web) transaction data csv file importer singleton instance
var (
AlipayWebTransactionDataCsvImporter = &alipayWebTransactionDataCsvImporter{}
AlipayWebTransactionDataCsvImporter = &alipayWebTransactionDataCsvImporter{
alipayTransactionDataCsvImporter{
fileHeaderLine: "支付宝交易记录明细查询",
dataHeaderStartContent: "交易记录明细列表",
dataBottomEndLineRune: '-',
timeColumnName: "交易创建时间",
categoryColumnName: "",
targetNameColumnName: "交易对方",
productNameColumnName: "商品名称",
amountColumnName: "金额(元)",
typeColumnName: "收/支",
relatedAccountColumnName: "",
statusColumnName: "交易状态",
descriptionColumnName: "备注",
},
}
)
// ParseImportedData returns the imported data by parsing the alipay (web) transaction csv data
func (c *alipayWebTransactionDataCsvImporter) 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
reader := transform.NewReader(bytes.NewReader(data), enc.NewDecoder())
allLines, err := c.parseAllLinesFromCsvData(ctx, reader)
if err != nil {
return nil, nil, nil, nil, nil, nil, err
}
if len(allLines) <= 1 {
log.Errorf(ctx, "[alipayWebTransactionDataCsvImporter.ParseImportedData] cannot parse import data for user \"uid:%d\", because data table row count is less 1", user.Uid)
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["交易创建时间"]
targetNameColumnIdx, targetNameColumnExists := headerItemMap["交易对方"]
productNameColumnIdx, productNameColumnExists := headerItemMap["商品名称"]
amountColumnIdx, amountColumnExists := headerItemMap["金额(元)"]
typeColumnIdx, typeColumnExists := headerItemMap["收/支"]
statusColumnIdx, statusColumnExists := headerItemMap["交易状态"]
descriptionColumnIdx, descriptionColumnExists := headerItemMap["备注"]
if !timeColumnExists || !amountColumnExists || !typeColumnExists || !statusColumnExists {
log.Errorf(ctx, "[alipayWebTransactionDataCsvImporter.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, "[alipayWebTransactionDataCsvImporter.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, "[alipayWebTransactionDataCsvImporter.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] != alipayWebTransactionDataStatusSuccessName &&
items[statusColumnIdx] != alipayWebTransactionDataStatusPaymentSuccessName &&
items[statusColumnIdx] != alipayWebTransactionDataStatusRepaymentSuccessName &&
items[statusColumnIdx] != alipayWebTransactionDataStatusClosedName &&
items[statusColumnIdx] != alipayWebTransactionDataStatusRefundSuccessName &&
items[statusColumnIdx] != alipayWebTransactionDataStatusTaxRefundSuccessName {
log.Warnf(ctx, "[alipayWebTransactionDataCsvImporter.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,
targetNameColumnIdx,
targetNameColumnExists,
productNameColumnIdx,
productNameColumnExists,
amountColumnIdx,
amountColumnExists,
typeColumnIdx,
typeColumnExists,
statusColumnIdx,
statusColumnExists,
descriptionColumnIdx,
descriptionColumnExists,
)
if data == nil {
log.Warnf(ctx, "[alipayWebTransactionDataCsvImporter.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,
)
return dataTableImporter.ParseImportedData(ctx, user, dataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
}
func (c *alipayWebTransactionDataCsvImporter) 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, "[alipayWebTransactionDataCsvImporter.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], alipayWebTransactionDataCsvFileHeader) == 0 {
hasFileHeader = true
continue
} else {
log.Warnf(ctx, "[alipayWebTransactionDataCsvImporter.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], alipayWebTransactionDataCsvDataHeaderLineStartContent) >= 0 {
foundContentBeforeDataHeaderLine = true
continue
} else {
continue
}
}
if foundContentBeforeDataHeaderLine {
if len(items) <= 0 {
continue
} else if len(items) == 1 && utils.ContainsOnlyOneRune(items[0], alipayWebTransactionDataCsvDataDataLineEndLineRune) {
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 *alipayWebTransactionDataCsvImporter) parseTransactionData(
ctx core.Context,
user *models.User,
items []string,
timeColumnIdx int,
timeColumnExists bool,
targetNameColumnIdx int,
targetNameColumnExists bool,
productNameColumnIdx int,
productNameColumnExists bool,
amountColumnIdx int,
amountColumnExists bool,
typeColumnIdx int,
typeColumnExists 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 amountColumnExists && amountColumnIdx < len(items) {
data[datatable.DATA_TABLE_AMOUNT] = items[amountColumnIdx]
}
data[datatable.DATA_TABLE_SUB_CATEGORY] = ""
if descriptionColumnExists && descriptionColumnIdx < len(items) && items[descriptionColumnIdx] != "" {
data[datatable.DATA_TABLE_DESCRIPTION] = items[descriptionColumnIdx]
} else if productNameColumnExists && productNameColumnIdx < len(items) && items[productNameColumnIdx] != "" {
data[datatable.DATA_TABLE_DESCRIPTION] = items[productNameColumnIdx]
} else {
data[datatable.DATA_TABLE_DESCRIPTION] = ""
}
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 == alipayWebTransactionDataStatusClosedName {
return nil, fmt.Sprintf("income transaction is closed")
}
if statusName == alipayWebTransactionDataStatusSuccessName {
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 == alipayWebTransactionDataStatusClosedName {
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 == alipayWebTransactionDataStatusRefundSuccessName {
data[datatable.DATA_TABLE_TRANSACTION_TYPE] = alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME]
data[datatable.DATA_TABLE_ACCOUNT_NAME] = ""
data[datatable.DATA_TABLE_RELATED_ACCOUNT_NAME] = ""
} else {
if strings.Index(productName, alipayWebTransactionDataProductNameRechargePrefix) == 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, alipayWebTransactionDataProductNameCashWithdrawalPrefix) == 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, alipayWebTransactionDataProductNameTransferInText) >= 0 { // transfer in
data[datatable.DATA_TABLE_ACCOUNT_NAME] = ""
data[datatable.DATA_TABLE_RELATED_ACCOUNT_NAME] = targetName
} else if strings.Index(productName, alipayWebTransactionDataProductNameTransferOutText) >= 0 { // transfer out
data[datatable.DATA_TABLE_ACCOUNT_NAME] = ""
data[datatable.DATA_TABLE_RELATED_ACCOUNT_NAME] = targetName
} else if strings.Index(productName, alipayWebTransactionDataProductNameRepaymentText) >= 0 { // repayment
data[datatable.DATA_TABLE_ACCOUNT_NAME] = ""
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] = ""
data[datatable.DATA_TABLE_RELATED_ACCOUNT_NAME] = ""
}
}
if data[datatable.DATA_TABLE_TRANSACTION_TYPE] == alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME] && statusName != "" {
if statusName == alipayWebTransactionDataStatusRefundSuccessName || statusName == alipayWebTransactionDataStatusTaxRefundSuccessName {
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, ""
}
@@ -29,6 +29,8 @@ func GetTransactionDataImporter(fileType string) (base.TransactionDataImporter,
return feidee.FeideeMymoneyTransactionDataCsvImporter, nil
} else if fileType == "feidee_mymoney_xls" {
return feidee.FeideeMymoneyTransactionDataXlsImporter, nil
} else if fileType == "alipay_app_csv" {
return alipay.AlipayAppTransactionDataCsvImporter, nil
} else if fileType == "alipay_web_csv" {
return alipay.AlipayWebTransactionDataCsvImporter, nil
} else {
+9
View File
@@ -37,6 +37,15 @@ const supportedImportFileTypes = [
anchor: '如何获取金蝶随手记web版数据导出文件'
}
},
{
type: 'alipay_app_csv',
name: 'Alipay (App) Data Export File',
extensions: '.csv',
document: {
supportMultiLanguages: 'zh-Hans',
anchor: '如何获取支付宝app数据导出文件'
}
},
{
type: 'alipay_web_csv',
name: 'Alipay (Web) Data Export File',
+2 -1
View File
@@ -1514,8 +1514,9 @@
"How to export this file?": "How to export this file?",
"ezbookkeeping Data Export File (CSV)": "ezbookkeeping Data Export File (CSV)",
"ezbookkeeping Data Export File (TSV)": "ezbookkeeping Data Export File (TSV)",
"Feidee MyMoney (Web) Data Export File": "Feidee MyMoney (Web) Data Export File",
"Feidee MyMoney (App) Data Export File": "Feidee MyMoney (App) Data Export File",
"Feidee MyMoney (Web) Data Export File": "Feidee MyMoney (Web) Data Export File",
"Alipay (App) Data Export File": "Alipay (App) Data Export File",
"Alipay (Web) Data Export File": "Alipay (Web) Data Export File",
"Data File": "Data File",
"No data to import": "No data to import",
+2 -1
View File
@@ -1514,8 +1514,9 @@
"How to export this file?": "如何导出该文件?",
"ezbookkeeping Data Export File (CSV)": "ezbookkeeping 数据导出文件 (CSV)",
"ezbookkeeping Data Export File (TSV)": "ezbookkeeping 数据导出文件 (TSV)",
"Feidee MyMoney (Web) Data Export File": "金蝶随手记 (Web版) 数据导出文件",
"Feidee MyMoney (App) Data Export File": "金蝶随手记 (App) 数据导出文件",
"Feidee MyMoney (Web) Data Export File": "金蝶随手记 (Web版) 数据导出文件",
"Alipay (App) Data Export File": "支付宝 (App) 数据导出文件",
"Alipay (Web) Data Export File": "支付宝 (网页版) 数据导出文件",
"Data File": "数据文件",
"No data to import": "没有可以导入的数据",