code refactor

This commit is contained in:
MaysWind
2024-09-19 00:03:07 +08:00
parent fceb92eb6f
commit 7ecec2bb64
20 changed files with 283 additions and 277 deletions
@@ -0,0 +1,296 @@
package feidee
import (
"encoding/csv"
"io"
"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/log"
"github.com/mayswind/ezbookkeeping/pkg/models"
)
const feideeMymoneyTransactionDataCsvFileHeader = "随手记导出文件(headers:v5;"
const feideeMymoneyTransactionDataCsvFileHeaderWithUtf8Bom = "\xEF\xBB\xBF" + feideeMymoneyTransactionDataCsvFileHeader
// feideeMymoneyTransactionDataCsvImporter defines the structure of feidee mymoney csv importer for transaction data
type feideeMymoneyTransactionDataCsvImporter struct{}
// Initialize a feidee mymoney transaction data csv file importer singleton instance
var (
FeideeMymoneyTransactionDataCsvImporter = &feideeMymoneyTransactionDataCsvImporter{}
)
// ParseImportedData returns the imported data by parsing the feidee mymoney transaction csv data
func (c *feideeMymoneyTransactionDataCsvImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, accountMap map[string]*models.Account, categoryMap map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionTag, error) {
content := string(data)
if strings.Index(content, feideeMymoneyTransactionDataCsvFileHeader) != 0 && strings.Index(content, feideeMymoneyTransactionDataCsvFileHeaderWithUtf8Bom) != 0 {
return nil, nil, nil, nil, errs.ErrInvalidFileHeader
}
allLines, err := c.parseAllLinesFromCsvData(ctx, content)
if err != nil {
return nil, nil, nil, nil, err
}
if len(allLines) <= 1 {
log.Errorf(ctx, "[feidee_mymoney_transaction_data_csv_file_importer.ParseImportedData] cannot parse import data for user \"uid:%d\", because data table row count is less 1", user.Uid)
return nil, nil, nil, nil, errs.ErrNotFoundTransactionDataInFile
}
headerLineItems := allLines[0]
headerItemMap := make(map[string]int)
for i := 0; i < len(headerLineItems); i++ {
headerItemMap[headerLineItems[i]] = i
}
timeColumnIdx, timeColumnExists := headerItemMap["日期"]
typeColumnIdx, typeColumnExists := headerItemMap["交易类型"]
categoryColumnIdx, categoryColumnExists := headerItemMap["类别"]
subCategoryColumnIdx, subCategoryColumnExists := headerItemMap["子类别"]
accountColumnIdx, accountColumnExists := headerItemMap["账户"]
accountCurrencyColumnIdx, accountCurrencyColumnExists := headerItemMap["账户币种"]
amountColumnIdx, amountColumnExists := headerItemMap["金额"]
descriptionColumnIdx, descriptionColumnExists := headerItemMap["备注"]
relatedIdColumnIdx, relatedIdColumnExists := headerItemMap["关联Id"]
if !timeColumnExists || !typeColumnExists || !subCategoryColumnExists ||
!accountColumnExists || !amountColumnExists || !relatedIdColumnExists {
log.Errorf(ctx, "[feidee_mymoney_transaction_data_csv_file_importer.ParseImportedData] cannot parse import data for user \"uid:%d\", because missing essential columns in header row", user.Uid)
return nil, nil, nil, nil, errs.ErrMissingRequiredFieldInHeaderRow
}
newColumns := make([]datatable.DataTableColumn, 0, 11)
newColumns = append(newColumns, datatable.DATA_TABLE_TRANSACTION_TYPE)
newColumns = append(newColumns, datatable.DATA_TABLE_TRANSACTION_TIME)
if categoryColumnExists {
newColumns = append(newColumns, datatable.DATA_TABLE_CATEGORY)
}
newColumns = append(newColumns, datatable.DATA_TABLE_SUB_CATEGORY)
newColumns = append(newColumns, datatable.DATA_TABLE_ACCOUNT_NAME)
if accountCurrencyColumnExists {
newColumns = append(newColumns, datatable.DATA_TABLE_ACCOUNT_CURRENCY)
}
newColumns = append(newColumns, datatable.DATA_TABLE_AMOUNT)
newColumns = append(newColumns, datatable.DATA_TABLE_RELATED_ACCOUNT_NAME)
if accountCurrencyColumnExists {
newColumns = append(newColumns, datatable.DATA_TABLE_RELATED_ACCOUNT_CURRENCY)
}
newColumns = append(newColumns, datatable.DATA_TABLE_RELATED_AMOUNT)
if descriptionColumnExists {
newColumns = append(newColumns, datatable.DATA_TABLE_DESCRIPTION)
}
dataTable := datatable.CreateNewWritableDataTable(newColumns)
transferTransactionsMap := make(map[string]map[datatable.DataTableColumn]string, 0)
for i := 1; i < len(allLines); i++ {
items := allLines[i]
data, relatedId := c.parseTransactionData(items,
timeColumnIdx,
timeColumnExists,
typeColumnIdx,
typeColumnExists,
categoryColumnIdx,
categoryColumnExists,
subCategoryColumnIdx,
subCategoryColumnExists,
accountColumnIdx,
accountColumnExists,
accountCurrencyColumnIdx,
accountCurrencyColumnExists,
amountColumnIdx,
amountColumnExists,
descriptionColumnIdx,
descriptionColumnExists,
relatedIdColumnIdx,
relatedIdColumnExists,
)
if len(items) < len(headerLineItems) {
log.Errorf(ctx, "[feidee_mymoney_transaction_data_csv_file_importer.ParseImportedData] cannot parse row \"index:%d\" for user \"uid:%d\", because may missing some columns (column count %d in data row is less than header column count %d)", i, user.Uid, len(items), len(headerLineItems))
return nil, nil, nil, nil, errs.ErrFewerFieldsInDataRowThanInHeaderRow
}
transactionType := data[datatable.DATA_TABLE_TRANSACTION_TYPE]
if transactionType == "余额变更" || transactionType == "收入" || transactionType == "支出" {
if transactionType == "余额变更" {
data[datatable.DATA_TABLE_TRANSACTION_TYPE] = feideeMymoneyTransactionTypeNameMapping[models.TRANSACTION_TYPE_MODIFY_BALANCE]
} else if transactionType == "收入" {
data[datatable.DATA_TABLE_TRANSACTION_TYPE] = feideeMymoneyTransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME]
} else if transactionType == "支出" {
data[datatable.DATA_TABLE_TRANSACTION_TYPE] = feideeMymoneyTransactionTypeNameMapping[models.TRANSACTION_TYPE_EXPENSE]
}
data[datatable.DATA_TABLE_RELATED_ACCOUNT_NAME] = ""
data[datatable.DATA_TABLE_RELATED_ACCOUNT_CURRENCY] = ""
data[datatable.DATA_TABLE_RELATED_AMOUNT] = ""
dataTable.Add(data)
} else if transactionType == "转入" || transactionType == "转出" {
if relatedId == "" {
log.Errorf(ctx, "[feidee_mymoney_transaction_data_csv_file_importer.ParseImportedData] transfer transaction has blank related id in row \"index:%d\" for user \"uid:%d\"", i, user.Uid)
return nil, nil, nil, nil, errs.ErrRelatedIdCannotBeBlank
}
relatedData, exists := transferTransactionsMap[relatedId]
if !exists {
transferTransactionsMap[relatedId] = data
continue
}
if transactionType == "转入" && relatedData[datatable.DATA_TABLE_TRANSACTION_TYPE] == "转出" {
relatedData[datatable.DATA_TABLE_TRANSACTION_TYPE] = feideeMymoneyTransactionTypeNameMapping[models.TRANSACTION_TYPE_TRANSFER]
relatedData[datatable.DATA_TABLE_RELATED_ACCOUNT_NAME] = data[datatable.DATA_TABLE_ACCOUNT_NAME]
relatedData[datatable.DATA_TABLE_RELATED_ACCOUNT_CURRENCY] = data[datatable.DATA_TABLE_ACCOUNT_CURRENCY]
relatedData[datatable.DATA_TABLE_RELATED_AMOUNT] = data[datatable.DATA_TABLE_AMOUNT]
dataTable.Add(relatedData)
delete(transferTransactionsMap, relatedId)
} else if transactionType == "转出" && relatedData[datatable.DATA_TABLE_TRANSACTION_TYPE] == "转入" {
data[datatable.DATA_TABLE_TRANSACTION_TYPE] = feideeMymoneyTransactionTypeNameMapping[models.TRANSACTION_TYPE_TRANSFER]
data[datatable.DATA_TABLE_RELATED_ACCOUNT_NAME] = relatedData[datatable.DATA_TABLE_ACCOUNT_NAME]
data[datatable.DATA_TABLE_RELATED_ACCOUNT_CURRENCY] = relatedData[datatable.DATA_TABLE_ACCOUNT_CURRENCY]
data[datatable.DATA_TABLE_RELATED_AMOUNT] = relatedData[datatable.DATA_TABLE_AMOUNT]
dataTable.Add(data)
delete(transferTransactionsMap, relatedId)
} else {
log.Errorf(ctx, "[feidee_mymoney_transaction_data_csv_file_importer.ParseImportedData] transfer transaction type \"%s\" is not expected in row \"index:%d\" for user \"uid:%d\"", transactionType, i, user.Uid)
return nil, nil, nil, nil, errs.ErrTransactionTypeInvalid
}
} else {
log.Errorf(ctx, "[feidee_mymoney_transaction_data_csv_file_importer.ParseImportedData] cannot parse transaction type \"%s\" in row \"index:%d\" for user \"uid:%d\"", transactionType, i, user.Uid)
return nil, nil, nil, nil, errs.ErrTransactionTypeInvalid
}
}
if len(transferTransactionsMap) > 0 {
log.Errorf(ctx, "[feidee_mymoney_transaction_data_csv_file_importer.ParseImportedData] there are %d transactions (related id is %s) which don't have related records", len(transferTransactionsMap), c.getRelatedIds(transferTransactionsMap))
return nil, nil, nil, nil, errs.ErrFoundRecordNotHasRelatedRecord
}
dataTableImporter := datatable.CreateNewSimpleDataTableTransactionDataImporterWithPostProcessFunc(
dataTable.GetDataColumnMapping(),
feideeMymoneyTransactionTypeNameMapping,
feideeMymoneyTransactionDataImporterPostProcess,
)
return dataTableImporter.ParseImportedData(ctx, user, dataTable, defaultTimezoneOffset, accountMap, categoryMap, tagMap)
}
func (c *feideeMymoneyTransactionDataCsvImporter) parseAllLinesFromCsvData(ctx core.Context, content string) ([][]string, error) {
csvReader := csv.NewReader(strings.NewReader(content))
csvReader.FieldsPerRecord = -1
allLines := make([][]string, 0)
for {
items, err := csvReader.Read()
if err == io.EOF {
break
}
if err != nil {
log.Errorf(ctx, "[feidee_mymoney_transaction_data_csv_file_importer.parseCsvData] cannot parse feidee mymoney csv data, because %s", err.Error())
return nil, errs.ErrInvalidCSVFile
}
if len(items) <= 0 || strings.Index(items[0], feideeMymoneyTransactionDataCsvFileHeader) == 0 || strings.Index(items[0], feideeMymoneyTransactionDataCsvFileHeaderWithUtf8Bom) == 0 {
continue
}
allLines = append(allLines, items)
}
return allLines, nil
}
func (c *feideeMymoneyTransactionDataCsvImporter) parseTransactionData(
items []string,
timeColumnIdx int,
timeColumnExists bool,
typeColumnIdx int,
typeColumnExists bool,
categoryColumnIdx int,
categoryColumnExists bool,
subCategoryColumnIdx int,
subCategoryColumnExists bool,
accountColumnIdx int,
accountColumnExists bool,
accountCurrencyColumnIdx int,
accountCurrencyColumnExists bool,
amountColumnIdx int,
amountColumnExists bool,
descriptionColumnIdx int,
descriptionColumnExists bool,
relatedIdColumnIdx int,
relatedIdColumnExists bool,
) (map[datatable.DataTableColumn]string, string) {
data := make(map[datatable.DataTableColumn]string, 11)
relatedId := ""
if timeColumnExists && timeColumnIdx < len(items) {
data[datatable.DATA_TABLE_TRANSACTION_TIME] = items[timeColumnIdx]
}
if typeColumnExists && typeColumnIdx < len(items) {
data[datatable.DATA_TABLE_TRANSACTION_TYPE] = items[typeColumnIdx]
}
if categoryColumnExists && categoryColumnIdx < len(items) {
data[datatable.DATA_TABLE_CATEGORY] = items[categoryColumnIdx]
}
if subCategoryColumnExists && subCategoryColumnIdx < len(items) {
data[datatable.DATA_TABLE_SUB_CATEGORY] = items[subCategoryColumnIdx]
}
if accountColumnExists && accountColumnIdx < len(items) {
data[datatable.DATA_TABLE_ACCOUNT_NAME] = items[accountColumnIdx]
}
if accountCurrencyColumnExists && accountCurrencyColumnIdx < len(items) {
data[datatable.DATA_TABLE_ACCOUNT_CURRENCY] = items[accountCurrencyColumnIdx]
}
if amountColumnExists && amountColumnIdx < len(items) {
data[datatable.DATA_TABLE_AMOUNT] = items[amountColumnIdx]
}
if descriptionColumnExists && descriptionColumnIdx < len(items) {
data[datatable.DATA_TABLE_DESCRIPTION] = items[descriptionColumnIdx]
}
if relatedIdColumnExists && relatedIdColumnIdx < len(items) {
relatedId = items[relatedIdColumnIdx]
}
return data, relatedId
}
func (c *feideeMymoneyTransactionDataCsvImporter) getRelatedIds(transferTransactionsMap map[string]map[datatable.DataTableColumn]string) string {
builder := strings.Builder{}
for relatedId := range transferTransactionsMap {
if builder.Len() > 0 {
builder.WriteRune(',')
}
builder.WriteString(relatedId)
}
return builder.String()
}
@@ -0,0 +1,377 @@
package feidee
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
func TestFeideeMymoneyCsvFileImporterParseImportedData_MinimumValidData(t *testing.T) {
converter := FeideeMymoneyTransactionDataCsvImporter
context := core.NewNullContext()
user := &models.User{
Uid: 1234567890,
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, allNewSubCategories, allNewTags, err := converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"余额变更\",\"2024-09-01 00:00:00\",\"\",\"Test Account\",\"123.45\",\"\",\"\"\n"+
"\"余额变更\",\"2024-09-01 01:00:00\",\"\",\"Test Account2\",\"-0.12\",\"\",\"\"\n"+
"\"收入\",\"2024-09-01 01:23:45\",\"Test Category\",\"Test Account\",\"0.12\",\"\",\"\"\n"+
"\"支出\",\"2024-09-01 12:34:56\",\"Test Category2\",\"Test Account\",\"1.00\",\"\",\"\"\n"+
"\"转出\",\"2024-09-01 23:59:59\",\"Test Category3\",\"Test Account\",\"0.05\",\"\",\"00000000-0000-0000-0000-000000000001\"\n"+
"\"转入\",\"2024-09-01 23:59:59\",\"Test Category3\",\"Test Account2\",\"0.05\",\"\",\"00000000-0000-0000-0000-000000000001\"\n"+
"\"转入\",\"2024-09-02 23:59:59\",\"Test Category3\",\"Test Account\",\"0.5\",\"\",\"00000000-0000-0000-0000-000000000002\"\n"+
"\"转出\",\"2024-09-02 23:59:59\",\"Test Category3\",\"Test Account2\",\"0.5\",\"\",\"00000000-0000-0000-0000-000000000002\""), 0, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 6, len(allNewTransactions))
assert.Equal(t, 2, len(allNewAccounts))
assert.Equal(t, 3, len(allNewSubCategories))
assert.Equal(t, 0, len(allNewTags))
assert.Equal(t, int64(1234567890), allNewTransactions[0].Uid)
assert.Equal(t, models.TRANSACTION_DB_TYPE_INCOME, allNewTransactions[0].Type)
assert.Equal(t, "2024-09-01 00:00:00", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime), time.UTC))
assert.Equal(t, int64(12345), allNewTransactions[0].Amount)
assert.Equal(t, "Test Account", allNewTransactions[0].OriginalSourceAccountName)
assert.Equal(t, "", allNewTransactions[0].OriginalCategoryName)
assert.Equal(t, int64(1234567890), allNewTransactions[1].Uid)
assert.Equal(t, models.TRANSACTION_DB_TYPE_EXPENSE, allNewTransactions[1].Type)
assert.Equal(t, "2024-09-01 01:00:00", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[1].TransactionTime), time.UTC))
assert.Equal(t, int64(12), allNewTransactions[1].Amount)
assert.Equal(t, "Test Account2", allNewTransactions[1].OriginalSourceAccountName)
assert.Equal(t, "", allNewTransactions[1].OriginalCategoryName)
assert.Equal(t, int64(1234567890), allNewTransactions[2].Uid)
assert.Equal(t, models.TRANSACTION_DB_TYPE_INCOME, allNewTransactions[2].Type)
assert.Equal(t, "2024-09-01 01:23:45", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[2].TransactionTime), time.UTC))
assert.Equal(t, int64(12), allNewTransactions[2].Amount)
assert.Equal(t, "Test Account", allNewTransactions[2].OriginalSourceAccountName)
assert.Equal(t, "Test Category", allNewTransactions[2].OriginalCategoryName)
assert.Equal(t, int64(1234567890), allNewTransactions[3].Uid)
assert.Equal(t, models.TRANSACTION_DB_TYPE_EXPENSE, allNewTransactions[3].Type)
assert.Equal(t, "2024-09-01 12:34:56", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[3].TransactionTime), time.UTC))
assert.Equal(t, int64(100), allNewTransactions[3].Amount)
assert.Equal(t, "Test Account", allNewTransactions[3].OriginalSourceAccountName)
assert.Equal(t, "Test Category2", allNewTransactions[3].OriginalCategoryName)
assert.Equal(t, int64(1234567890), allNewTransactions[4].Uid)
assert.Equal(t, models.TRANSACTION_DB_TYPE_TRANSFER_OUT, allNewTransactions[4].Type)
assert.Equal(t, "2024-09-01 23:59:59", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[4].TransactionTime), time.UTC))
assert.Equal(t, int64(5), allNewTransactions[4].Amount)
assert.Equal(t, "Test Account", allNewTransactions[4].OriginalSourceAccountName)
assert.Equal(t, "Test Account2", allNewTransactions[4].OriginalDestinationAccountName)
assert.Equal(t, "Test Category3", allNewTransactions[4].OriginalCategoryName)
assert.Equal(t, int64(1234567890), allNewTransactions[5].Uid)
assert.Equal(t, models.TRANSACTION_DB_TYPE_TRANSFER_OUT, allNewTransactions[5].Type)
assert.Equal(t, "2024-09-02 23:59:59", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[5].TransactionTime), time.UTC))
assert.Equal(t, int64(50), allNewTransactions[5].Amount)
assert.Equal(t, "Test Account2", allNewTransactions[5].OriginalSourceAccountName)
assert.Equal(t, "Test Account", allNewTransactions[5].OriginalDestinationAccountName)
assert.Equal(t, "Test Category3", allNewTransactions[5].OriginalCategoryName)
assert.Equal(t, int64(1234567890), allNewAccounts[0].Uid)
assert.Equal(t, "Test Account", allNewAccounts[0].Name)
assert.Equal(t, "CNY", allNewAccounts[0].Currency)
assert.Equal(t, int64(1234567890), allNewAccounts[1].Uid)
assert.Equal(t, "Test Account2", allNewAccounts[1].Name)
assert.Equal(t, "CNY", allNewAccounts[1].Currency)
assert.Equal(t, int64(1234567890), allNewSubCategories[0].Uid)
assert.Equal(t, "Test Category", allNewSubCategories[0].Name)
assert.Equal(t, int64(1234567890), allNewSubCategories[1].Uid)
assert.Equal(t, "Test Category2", allNewSubCategories[1].Name)
assert.Equal(t, int64(1234567890), allNewSubCategories[2].Uid)
assert.Equal(t, "Test Category3", allNewSubCategories[2].Name)
}
func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseInvalidTime(t *testing.T) {
converter := FeideeMymoneyTransactionDataCsvImporter
context := core.NewNullContext()
user := &models.User{
Uid: 1234567890,
DefaultCurrency: "CNY",
}
_, _, _, _, err := converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"收入\",\"2024-09-01T12:34:56\",\"Test Category\",\"Test Account\",\"0.12\",\"\",\"\""), 0, nil, nil, nil)
assert.NotNil(t, err)
_, _, _, _, err = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"收入\",\"09/01/2024 12:34:56\",\"Test Category\",\"Test Account\",\"0.12\",\"\",\"\""), 0, nil, nil, nil)
assert.NotNil(t, err)
}
func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseInvalidType(t *testing.T) {
converter := FeideeMymoneyTransactionDataCsvImporter
context := core.NewNullContext()
user := &models.User{
Uid: 1234567890,
DefaultCurrency: "CNY",
}
_, _, _, _, err := converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"Type\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account\",\"0.12\",\"\",\"\""), 0, nil, nil, nil)
assert.NotNil(t, err)
}
func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseInvalidAccountName(t *testing.T) {
converter := FeideeMymoneyTransactionDataCsvImporter
context := core.NewNullContext()
user := &models.User{
Uid: 1234567890,
DefaultCurrency: "CNY",
}
_, _, _, _, err := converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"支出\",\"2024-09-01 12:34:56\",\"Test Category\",\"\",\"123.45\",\"\",\"\""), 0, nil, nil, nil)
assert.NotNil(t, err)
_, _, _, _, err = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"转出\",\"2024-09-01 12:34:56\",\"Test Category\",\"\",\"123.45\",\"\",\"00000000-0000-0000-0000-000000000001\"\n"+
"\"转入\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account2\",\"123.45\",\"\",\"00000000-0000-0000-0000-000000000001\""), 0, nil, nil, nil)
assert.NotNil(t, err)
_, _, _, _, err = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"转出\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account\",\"123.45\",\"\",\"00000000-0000-0000-0000-000000000001\"\n"+
"\"转入\",\"2024-09-01 12:34:56\",\"Test Category\",\"\",\"123.45\",\"\",\"00000000-0000-0000-0000-000000000001\""), 0, nil, nil, nil)
assert.NotNil(t, err)
}
func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseValidAccountCurrency(t *testing.T) {
converter := FeideeMymoneyTransactionDataCsvImporter
context := core.NewNullContext()
user := &models.User{
Uid: 1234567890,
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, _, _, err := converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"账户币种\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"余额变更\",\"2024-09-01 01:23:45\",\"\",\"Test Account\",\"USD\",\"123.45\",\"\",\"\"\n"+
"\"转出\",\"2024-09-01 12:34:56\",\"Test Category3\",\"Test Account\",\"USD\",\"1.23\",\"\",\"00000000-0000-0000-0000-000000000001\"\n"+
"\"转入\",\"2024-09-01 12:34:56\",\"Test Category3\",\"Test Account2\",\"EUR\",\"1.10\",\"\",\"00000000-0000-0000-0000-000000000001\""), 0, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 2, len(allNewTransactions))
assert.Equal(t, 2, len(allNewAccounts))
assert.Equal(t, int64(1234567890), allNewAccounts[0].Uid)
assert.Equal(t, "Test Account", allNewAccounts[0].Name)
assert.Equal(t, "USD", allNewAccounts[0].Currency)
assert.Equal(t, int64(1234567890), allNewAccounts[1].Uid)
assert.Equal(t, "Test Account2", allNewAccounts[1].Name)
assert.Equal(t, "EUR", allNewAccounts[1].Currency)
}
func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseInvalidAccountCurrency(t *testing.T) {
converter := FeideeMymoneyTransactionDataCsvImporter
context := core.NewNullContext()
user := &models.User{
Uid: 1234567890,
DefaultCurrency: "CNY",
}
_, _, _, _, err := converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"账户币种\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"余额变更\",\"2024-09-01 01:23:45\",\"\",\"Test Account\",\"USD\",\"123.45\",\"\",\"\"\n"+
"\"转出\",\"2024-09-01 12:34:56\",\"Test Category3\",\"Test Account\",\"CNY\",\"1.23\",\"\",\"00000000-0000-0000-0000-000000000001\"\n"+
"\"转入\",\"2024-09-01 12:34:56\",\"Test Category3\",\"Test Account2\",\"EUR\",\"1.10\",\"\",\"00000000-0000-0000-0000-000000000001\""), 0, nil, nil, nil)
assert.NotNil(t, err)
_, _, _, _, err = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"账户币种\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"余额变更\",\"2024-09-01 01:23:45\",\"\",\"Test Account\",\"USD\",\"123.45\",\"\",\"\"\n"+
"\"转出\",\"2024-09-01 12:34:56\",\"Test Category3\",\"Test Account2\",\"CNY\",\"1.23\",\"\",\"00000000-0000-0000-0000-000000000001\"\n"+
"\"转入\",\"2024-09-01 12:34:56\",\"Test Category3\",\"Test Account\",\"EUR\",\"1.10\",\"\",\"00000000-0000-0000-0000-000000000001\""), 0, nil, nil, nil)
assert.NotNil(t, err)
}
func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseNotSupportedCurrency(t *testing.T) {
converter := FeideeMymoneyTransactionDataCsvImporter
context := core.NewNullContext()
user := &models.User{
Uid: 1234567890,
DefaultCurrency: "CNY",
}
_, _, _, _, err := converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"账户币种\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"余额变更\",\"2024-09-01 01:23:45\",\"\",\"Test Account\",\"XXX\",\"123.45\",\"\",\"\""), 0, nil, nil, nil)
assert.NotNil(t, err)
_, _, _, _, err = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"账户币种\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"转出\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account\",\"USD\",\"123.45\",\"\",\"00000000-0000-0000-0000-000000000001\"\n"+
"\"转入\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account2\",\"XXX\",\"123.45\",\"\",\"00000000-0000-0000-0000-000000000001\""), 0, nil, nil, nil)
assert.NotNil(t, err)
_, _, _, _, err = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"账户币种\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"转出\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account\",\"XXX\",\"123.45\",\"\",\"00000000-0000-0000-0000-000000000001\"\n"+
"\"转入\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account2\",\"USD\",\"123.45\",\"\",\"00000000-0000-0000-0000-000000000001\""), 0, nil, nil, nil)
assert.NotNil(t, err)
}
func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseInvalidAmount(t *testing.T) {
converter := FeideeMymoneyTransactionDataCsvImporter
context := core.NewNullContext()
user := &models.User{
Uid: 1234567890,
DefaultCurrency: "CNY",
}
_, _, _, _, err := converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"余额变更\",\"2024-09-01 01:23:45\",\"\",\"Test Account\",\"123 45\",\"\",\"\""), 0, nil, nil, nil)
assert.NotNil(t, err)
_, _, _, _, err = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"转出\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account\",\"123 45\",\"\",\"00000000-0000-0000-0000-000000000001\"\n"+
"\"转入\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account2\",\"123.45\",\"\",\"00000000-0000-0000-0000-000000000001\""), 0, nil, nil, nil)
assert.NotNil(t, err)
_, _, _, _, err = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"转出\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account\",\"123.45\",\"\",\"00000000-0000-0000-0000-000000000001\"\n"+
"\"转入\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account2\",\"123 45\",\"\",\"00000000-0000-0000-0000-000000000001\""), 0, nil, nil, nil)
assert.NotNil(t, err)
}
func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseDescription(t *testing.T) {
converter := FeideeMymoneyTransactionDataCsvImporter
context := core.NewNullContext()
user := &models.User{
Uid: 1234567890,
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, err := converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"支出\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account\",\"123.45\",\"Test\n"+
"A new line break\",\"\""), 0, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, "Test\nA new line break", allNewTransactions[0].Comment)
}
func TestFeideeMymoneyCsvFileImporterParseImportedData_InvalidRelatedId(t *testing.T) {
converter := FeideeMymoneyTransactionDataCsvImporter
context := core.NewNullContext()
user := &models.User{
Uid: 1234567890,
DefaultCurrency: "CNY",
}
_, _, _, _, err := converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"转出\",\"2024-09-01 23:59:59\",\"Test Category3\",\"Test Account\",\"0.05\",\"\",\"\""), 0, nil, nil, nil)
assert.NotNil(t, err)
_, _, _, _, err = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"转入\",\"2024-09-01 23:59:59\",\"Test Category3\",\"Test Account\",\"0.05\",\"\",\"\""), 0, nil, nil, nil)
assert.NotNil(t, err)
_, _, _, _, err = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"转出\",\"2024-09-01 23:59:59\",\"Test Category3\",\"Test Account\",\"0.05\",\"\",\"00000000-0000-0000-0000-000000000001\"\n"+
"\"转入\",\"2024-09-02 23:59:59\",\"Test Category3\",\"Test Account\",\"0.5\",\"\",\"00000000-0000-0000-0000-000000000002\""), 0, nil, nil, nil)
assert.NotNil(t, err)
}
func TestFeideeMymoneyCsvFileImporterParseImportedData_MissingFileHeader(t *testing.T) {
converter := FeideeMymoneyTransactionDataCsvImporter
context := core.NewNullContext()
user := &models.User{
Uid: 1,
DefaultCurrency: "CNY",
}
_, _, _, _, err := converter.ParseImportedData(context, user, []byte("\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"余额变更\",\"2024-09-01 00:00:00\",\"\",\"Test Account\",\"123.45\",\"\",\"\"\n"), 0, nil, nil, nil)
assert.NotNil(t, err)
}
func TestFeideeMymoneyCsvFileImporterParseImportedData_MissingRequiredColumn(t *testing.T) {
converter := FeideeMymoneyTransactionDataCsvImporter
context := core.NewNullContext()
user := &models.User{
Uid: 1,
DefaultCurrency: "CNY",
}
_, _, _, _, err := converter.ParseImportedData(context, user, []byte(""), 0, nil, nil, nil)
assert.NotNil(t, err)
// Missing Time Column
_, _, _, _, err = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"余额变更\",\"\",\"Test Account\",\"123.45\",\"\",\"\"\n"), 0, nil, nil, nil)
assert.NotNil(t, err)
// Missing Type Column
_, _, _, _, err = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"2024-09-01 00:00:00\",\"Test Category\",\"Test Account\",\"123.45\",\"\",\"\"\n"), 0, nil, nil, nil)
assert.NotNil(t, err)
// Missing Sub Category Column
_, _, _, _, err = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"余额变更\",\"2024-09-01 00:00:00\",\"Test Account\",\"123.45\",\"\",\"\"\n"), 0, nil, nil, nil)
assert.NotNil(t, err)
// Missing Account Name Column
_, _, _, _, err = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"余额变更\",\"2024-09-01 00:00:00\",\"\",\"123.45\",\"\",\"\"\n"), 0, nil, nil, nil)
assert.NotNil(t, err)
// Missing Amount Column
_, _, _, _, err = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"备注\",\"关联Id\"\n"+
"\"余额变更\",\"2024-09-01 00:00:00\",\"\",\"Test Account\",\"\",\"\"\n"), 0, nil, nil, nil)
assert.NotNil(t, err)
// Missing Related ID Column
_, _, _, _, err = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\"\n"+
"\"余额变更\",\"2024-09-01 00:00:00\",\"\",\"Test Account\",\"123.45\",\"\"\n"), 0, nil, nil, nil)
assert.NotNil(t, err)
}
@@ -0,0 +1,230 @@
package feidee
import (
"bytes"
"time"
"github.com/shakinm/xlsReader/xls"
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
// feideeMymoneyTransactionExcelFileDataTable defines the structure of feidee mymoney transaction plain text data table
type feideeMymoneyTransactionExcelFileDataTable struct {
workbook *xls.Workbook
headerLineColumnNames []string
}
// feideeMymoneyTransactionExcelFileDataRow defines the structure of feidee mymoney transaction plain text data row
type feideeMymoneyTransactionExcelFileDataRow struct {
sheet *xls.Sheet
rowIndex int
}
// feideeMymoneyTransactionExcelFileDataRowIterator defines the structure of feidee mymoney transaction plain text data row iterator
type feideeMymoneyTransactionExcelFileDataRowIterator struct {
dataTable *feideeMymoneyTransactionExcelFileDataTable
currentTableIndex int
currentRowIndexInTable int
}
// DataRowCount returns the total count of data row
func (t *feideeMymoneyTransactionExcelFileDataTable) DataRowCount() int {
allSheets := t.workbook.GetSheets()
totalDataRowCount := 0
for i := 0; i < len(allSheets); i++ {
sheet := allSheets[i]
if sheet.GetNumberRows() <= 1 {
continue
}
totalDataRowCount += sheet.GetNumberRows() - 1
}
return totalDataRowCount
}
// HeaderLineColumnNames returns the header column name list
func (t *feideeMymoneyTransactionExcelFileDataTable) HeaderLineColumnNames() []string {
return t.headerLineColumnNames
}
// DataRowIterator returns the iterator of data row
func (t *feideeMymoneyTransactionExcelFileDataTable) DataRowIterator() datatable.ImportedDataRowIterator {
return &feideeMymoneyTransactionExcelFileDataRowIterator{
dataTable: t,
currentTableIndex: 0,
currentRowIndexInTable: 0,
}
}
// ColumnCount returns the total count of column in this data row
func (r *feideeMymoneyTransactionExcelFileDataRow) ColumnCount() int {
row, err := r.sheet.GetRow(r.rowIndex)
if err != nil {
return 0
}
return len(row.GetCols())
}
// GetData returns the data in the specified column index
func (r *feideeMymoneyTransactionExcelFileDataRow) GetData(columnIndex int) string {
row, err := r.sheet.GetRow(r.rowIndex)
if err != nil {
return ""
}
cell, err := row.GetCol(columnIndex)
if err != nil {
return ""
}
return cell.GetString()
}
// GetTime returns the time in the specified column index
func (r *feideeMymoneyTransactionExcelFileDataRow) GetTime(columnIndex int, timezoneOffset int16) (time.Time, error) {
str := r.GetData(columnIndex)
if utils.IsValidLongDateTimeFormat(str) {
return utils.ParseFromLongDateTime(str, timezoneOffset)
}
if utils.IsValidLongDateTimeWithoutSecondFormat(str) {
return utils.ParseFromLongDateTimeWithoutSecond(str, timezoneOffset)
}
if utils.IsValidLongDateFormat(str) {
return utils.ParseFromLongDateTimeWithoutSecond(str+" 00:00", timezoneOffset)
}
return time.Unix(0, 0), errs.ErrTransactionTimeInvalid
}
// GetTimezoneOffset returns the time zone offset in the specified column index
func (r *feideeMymoneyTransactionExcelFileDataRow) GetTimezoneOffset(columnIndex int) (*time.Location, error) {
return nil, errs.ErrNotSupported
}
// HasNext returns whether the iterator does not reach the end
func (t *feideeMymoneyTransactionExcelFileDataRowIterator) HasNext() bool {
allSheets := t.dataTable.workbook.GetSheets()
if t.currentTableIndex >= len(allSheets) {
return false
}
currentSheet := allSheets[t.currentTableIndex]
if t.currentRowIndexInTable+1 < currentSheet.GetNumberRows() {
return true
}
for i := t.currentTableIndex + 1; i < len(allSheets); i++ {
sheet := allSheets[i]
if sheet.GetNumberRows() <= 1 {
continue
}
return true
}
return false
}
// Next returns the next imported data row
func (t *feideeMymoneyTransactionExcelFileDataRowIterator) Next() datatable.ImportedDataRow {
allSheets := t.dataTable.workbook.GetSheets()
currentRowIndexInTable := t.currentRowIndexInTable
for i := t.currentTableIndex; i < len(allSheets); i++ {
sheet := allSheets[i]
if currentRowIndexInTable+1 < sheet.GetNumberRows() {
t.currentRowIndexInTable++
currentRowIndexInTable = t.currentRowIndexInTable
break
}
t.currentTableIndex++
t.currentRowIndexInTable = 0
currentRowIndexInTable = 0
}
if t.currentTableIndex >= len(allSheets) {
return nil
}
currentSheet := allSheets[t.currentTableIndex]
if t.currentRowIndexInTable >= currentSheet.GetNumberRows() {
return nil
}
return &feideeMymoneyTransactionExcelFileDataRow{
sheet: &currentSheet,
rowIndex: t.currentRowIndexInTable,
}
}
func createNewFeideeMymoneyTransactionExcelFileDataTable(data []byte) (*feideeMymoneyTransactionExcelFileDataTable, error) {
reader := bytes.NewReader(data)
workbook, err := xls.OpenReader(reader)
if err != nil {
return nil, err
}
allSheets := workbook.GetSheets()
var headerRowItems []string
for i := 0; i < len(allSheets); i++ {
sheet := allSheets[i]
if sheet.GetNumberRows() < 1 {
continue
}
row, err := sheet.GetRow(0)
if err != nil {
return nil, err
}
cells := row.GetCols()
if i == 0 {
for j := 0; j < len(cells); j++ {
headerItem := cells[j].GetString()
if headerItem == "" {
break
}
headerRowItems = append(headerRowItems, headerItem)
}
} else {
for j := 0; j < min(len(cells), len(headerRowItems)); j++ {
headerItem := cells[j].GetString()
if headerItem != headerRowItems[j] {
return nil, errs.ErrFieldsInMultiTableAreDifferent
}
}
}
}
return &feideeMymoneyTransactionExcelFileDataTable{
workbook: &workbook,
headerLineColumnNames: headerRowItems,
}, nil
}
@@ -0,0 +1,38 @@
package feidee
import (
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/models"
)
var feideeMymoneyDataColumnNameMapping = map[datatable.DataTableColumn]string{
datatable.DATA_TABLE_TRANSACTION_TIME: "日期",
datatable.DATA_TABLE_TRANSACTION_TYPE: "交易类型",
datatable.DATA_TABLE_CATEGORY: "分类",
datatable.DATA_TABLE_SUB_CATEGORY: "子分类",
datatable.DATA_TABLE_ACCOUNT_NAME: "账户1",
datatable.DATA_TABLE_AMOUNT: "金额",
datatable.DATA_TABLE_RELATED_ACCOUNT_NAME: "账户2",
datatable.DATA_TABLE_DESCRIPTION: "备注",
}
var feideeMymoneyTransactionTypeNameMapping = map[models.TransactionType]string{
models.TRANSACTION_TYPE_MODIFY_BALANCE: "余额变更",
models.TRANSACTION_TYPE_INCOME: "收入",
models.TRANSACTION_TYPE_EXPENSE: "支出",
models.TRANSACTION_TYPE_TRANSFER: "转账",
}
func feideeMymoneyTransactionDataImporterPostProcess(ctx core.Context, transaction *models.ImportTransaction) error {
if transaction.Type == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE {
if transaction.Amount >= 0 {
transaction.Type = models.TRANSACTION_DB_TYPE_INCOME
} else if transaction.Amount < 0 {
transaction.Amount = -transaction.Amount
transaction.Type = models.TRANSACTION_DB_TYPE_EXPENSE
}
}
return nil
}
@@ -0,0 +1,34 @@
package feidee
import (
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/models"
)
// feideeMymoneyTransactionDataXlsImporter defines the structure of feidee mymoney xls importer for transaction data
type feideeMymoneyTransactionDataXlsImporter struct {
datatable.DataTableTransactionDataImporter
}
// Initialize a feidee mymoney transaction data xls file importer singleton instance
var (
FeideeMymoneyTransactionDataXlsImporter = &feideeMymoneyTransactionDataXlsImporter{}
)
// ParseImportedData returns the imported data by parsing the feidee mymoney transaction xls data
func (c *feideeMymoneyTransactionDataXlsImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, accountMap map[string]*models.Account, categoryMap map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionTag, error) {
dataTable, err := createNewFeideeMymoneyTransactionExcelFileDataTable(data)
if err != nil {
return nil, nil, nil, nil, err
}
dataTableImporter := datatable.CreateNewSimpleDataTableTransactionDataImporterWithPostProcessFunc(
feideeMymoneyDataColumnNameMapping,
feideeMymoneyTransactionTypeNameMapping,
feideeMymoneyTransactionDataImporterPostProcess,
)
return dataTableImporter.ParseImportedData(ctx, user, dataTable, defaultTimezoneOffset, accountMap, categoryMap, tagMap)
}
@@ -0,0 +1,109 @@
package feidee
import (
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
func TestFeideeMymoneyTransactionDataXlsImporterParseImportedData_MinimumValidData(t *testing.T) {
converter := FeideeMymoneyTransactionDataXlsImporter
context := core.NewNullContext()
user := &models.User{
Uid: 1234567890,
DefaultCurrency: "CNY",
}
testdata, err := os.ReadFile("../../../testdata/feidee_mymoney_test_file.xls")
assert.Nil(t, err)
allNewTransactions, allNewAccounts, allNewSubCategories, allNewTags, err := converter.ParseImportedData(context, user, testdata, 0, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 7, len(allNewTransactions))
assert.Equal(t, 2, len(allNewAccounts))
assert.Equal(t, 5, len(allNewSubCategories))
assert.Equal(t, 0, len(allNewTags))
assert.Equal(t, int64(1234567890), allNewTransactions[0].Uid)
assert.Equal(t, models.TRANSACTION_DB_TYPE_INCOME, allNewTransactions[0].Type)
assert.Equal(t, "2024-09-01 00:00:00", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime), time.UTC))
assert.Equal(t, int64(12345), allNewTransactions[0].Amount)
assert.Equal(t, "Test Account", allNewTransactions[0].OriginalSourceAccountName)
assert.Equal(t, "", allNewTransactions[0].OriginalCategoryName)
assert.Equal(t, int64(1234567890), allNewTransactions[1].Uid)
assert.Equal(t, models.TRANSACTION_DB_TYPE_EXPENSE, allNewTransactions[1].Type)
assert.Equal(t, "2024-09-01 01:00:00", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[1].TransactionTime), time.UTC))
assert.Equal(t, int64(12), allNewTransactions[1].Amount)
assert.Equal(t, "Test Account2", allNewTransactions[1].OriginalSourceAccountName)
assert.Equal(t, "", allNewTransactions[1].OriginalCategoryName)
assert.Equal(t, int64(1234567890), allNewTransactions[2].Uid)
assert.Equal(t, models.TRANSACTION_DB_TYPE_INCOME, allNewTransactions[2].Type)
assert.Equal(t, "2024-09-01 01:23:45", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[2].TransactionTime), time.UTC))
assert.Equal(t, int64(12), allNewTransactions[2].Amount)
assert.Equal(t, "Test Account", allNewTransactions[2].OriginalSourceAccountName)
assert.Equal(t, "Test Category", allNewTransactions[2].OriginalCategoryName)
assert.Equal(t, int64(1234567890), allNewTransactions[3].Uid)
assert.Equal(t, models.TRANSACTION_DB_TYPE_EXPENSE, allNewTransactions[3].Type)
assert.Equal(t, "2024-09-01 12:34:56", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[3].TransactionTime), time.UTC))
assert.Equal(t, int64(100), allNewTransactions[3].Amount)
assert.Equal(t, "Test Account2", allNewTransactions[3].OriginalSourceAccountName)
assert.Equal(t, "Test Category2", allNewTransactions[3].OriginalCategoryName)
assert.Equal(t, int64(1234567890), allNewTransactions[4].Uid)
assert.Equal(t, models.TRANSACTION_DB_TYPE_TRANSFER_OUT, allNewTransactions[4].Type)
assert.Equal(t, "2024-09-01 23:59:59", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[4].TransactionTime), time.UTC))
assert.Equal(t, int64(5), allNewTransactions[4].Amount)
assert.Equal(t, "Test Comment5", allNewTransactions[4].Comment)
assert.Equal(t, "Test Account", allNewTransactions[4].OriginalSourceAccountName)
assert.Equal(t, "Test Account2", allNewTransactions[4].OriginalDestinationAccountName)
assert.Equal(t, "Test Category3", allNewTransactions[4].OriginalCategoryName)
assert.Equal(t, int64(1234567890), allNewTransactions[5].Uid)
assert.Equal(t, models.TRANSACTION_DB_TYPE_INCOME, allNewTransactions[5].Type)
assert.Equal(t, "2024-09-10 00:00:00", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[5].TransactionTime), time.UTC))
assert.Equal(t, int64(-54300), allNewTransactions[5].Amount)
assert.Equal(t, "Test Account2", allNewTransactions[5].OriginalSourceAccountName)
assert.Equal(t, "Test Category5", allNewTransactions[5].OriginalCategoryName)
assert.Equal(t, int64(1234567890), allNewTransactions[6].Uid)
assert.Equal(t, models.TRANSACTION_DB_TYPE_EXPENSE, allNewTransactions[6].Type)
assert.Equal(t, "2024-09-11 05:06:00", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[6].TransactionTime), time.UTC))
assert.Equal(t, int64(-12340), allNewTransactions[6].Amount)
assert.Equal(t, "Line1\nLine2", allNewTransactions[6].Comment)
assert.Equal(t, "Test Account", allNewTransactions[6].OriginalSourceAccountName)
assert.Equal(t, "Test Category4", allNewTransactions[6].OriginalCategoryName)
assert.Equal(t, int64(1234567890), allNewAccounts[0].Uid)
assert.Equal(t, "Test Account", allNewAccounts[0].Name)
assert.Equal(t, "CNY", allNewAccounts[0].Currency)
assert.Equal(t, int64(1234567890), allNewAccounts[1].Uid)
assert.Equal(t, "Test Account2", allNewAccounts[1].Name)
assert.Equal(t, "CNY", allNewAccounts[1].Currency)
assert.Equal(t, int64(1234567890), allNewSubCategories[0].Uid)
assert.Equal(t, "Test Category", allNewSubCategories[0].Name)
assert.Equal(t, int64(1234567890), allNewSubCategories[1].Uid)
assert.Equal(t, "Test Category5", allNewSubCategories[1].Name)
assert.Equal(t, int64(1234567890), allNewSubCategories[2].Uid)
assert.Equal(t, "Test Category2", allNewSubCategories[2].Name)
assert.Equal(t, int64(1234567890), allNewSubCategories[3].Uid)
assert.Equal(t, "Test Category4", allNewSubCategories[3].Name)
assert.Equal(t, int64(1234567890), allNewSubCategories[4].Uid)
assert.Equal(t, "Test Category3", allNewSubCategories[4].Name)
}