support importing transaction data from feidee mymoney app export data

This commit is contained in:
MaysWind
2024-09-17 17:06:24 +08:00
parent 6d0fdc6860
commit 20b28f2a68
9 changed files with 1049 additions and 0 deletions
@@ -0,0 +1,303 @@
package converters
import (
"encoding/csv"
"io"
"strings"
"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 {
transactionTypeMapping map[models.TransactionType]string
}
// Initialize a feidee mymoney transaction data csv file importer singleton instance
var (
FeideeMymoneyTransactionDataCsvImporter = &feideeMymoneyTransactionDataCsvImporter{
transactionTypeMapping: feideeMymoneyTransactionTypeNameMapping,
}
)
// 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([]DataTableColumn, 0, 11)
newColumns = append(newColumns, DATA_TABLE_TRANSACTION_TYPE)
newColumns = append(newColumns, DATA_TABLE_TRANSACTION_TIME)
if categoryColumnExists {
newColumns = append(newColumns, DATA_TABLE_CATEGORY)
}
newColumns = append(newColumns, DATA_TABLE_SUB_CATEGORY)
newColumns = append(newColumns, DATA_TABLE_ACCOUNT_NAME)
if accountCurrencyColumnExists {
newColumns = append(newColumns, DATA_TABLE_ACCOUNT_CURRENCY)
}
newColumns = append(newColumns, DATA_TABLE_AMOUNT)
newColumns = append(newColumns, DATA_TABLE_RELATED_ACCOUNT_NAME)
if accountCurrencyColumnExists {
newColumns = append(newColumns, DATA_TABLE_RELATED_ACCOUNT_CURRENCY)
}
newColumns = append(newColumns, DATA_TABLE_RELATED_AMOUNT)
if descriptionColumnExists {
newColumns = append(newColumns, DATA_TABLE_DESCRIPTION)
}
dataTable, err := createNewWritableDataTable(newColumns)
if err != nil {
return nil, nil, nil, nil, err
}
transferTransactionsMap := make(map[string]map[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[DATA_TABLE_TRANSACTION_TYPE]
if transactionType == "余额变更" || transactionType == "收入" || transactionType == "支出" {
if transactionType == "余额变更" {
data[DATA_TABLE_TRANSACTION_TYPE] = c.transactionTypeMapping[models.TRANSACTION_TYPE_MODIFY_BALANCE]
} else if transactionType == "收入" {
data[DATA_TABLE_TRANSACTION_TYPE] = c.transactionTypeMapping[models.TRANSACTION_TYPE_INCOME]
} else if transactionType == "支出" {
data[DATA_TABLE_TRANSACTION_TYPE] = c.transactionTypeMapping[models.TRANSACTION_TYPE_EXPENSE]
}
data[DATA_TABLE_RELATED_ACCOUNT_NAME] = ""
data[DATA_TABLE_RELATED_ACCOUNT_CURRENCY] = ""
data[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[DATA_TABLE_TRANSACTION_TYPE] == "转出" {
relatedData[DATA_TABLE_TRANSACTION_TYPE] = c.transactionTypeMapping[models.TRANSACTION_TYPE_TRANSFER]
relatedData[DATA_TABLE_RELATED_ACCOUNT_NAME] = data[DATA_TABLE_ACCOUNT_NAME]
relatedData[DATA_TABLE_RELATED_ACCOUNT_CURRENCY] = data[DATA_TABLE_ACCOUNT_CURRENCY]
relatedData[DATA_TABLE_RELATED_AMOUNT] = data[DATA_TABLE_AMOUNT]
dataTable.Add(relatedData)
delete(transferTransactionsMap, relatedId)
} else if transactionType == "转出" && relatedData[DATA_TABLE_TRANSACTION_TYPE] == "转入" {
data[DATA_TABLE_TRANSACTION_TYPE] = c.transactionTypeMapping[models.TRANSACTION_TYPE_TRANSFER]
data[DATA_TABLE_RELATED_ACCOUNT_NAME] = relatedData[DATA_TABLE_ACCOUNT_NAME]
data[DATA_TABLE_RELATED_ACCOUNT_CURRENCY] = relatedData[DATA_TABLE_ACCOUNT_CURRENCY]
data[DATA_TABLE_RELATED_AMOUNT] = relatedData[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 := DataTableTransactionDataImporter{
dataColumnMapping: dataTable.GetDataColumnMapping(),
transactionTypeMapping: c.transactionTypeMapping,
}
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[DataTableColumn]string, string) {
data := make(map[DataTableColumn]string, 11)
relatedId := ""
if timeColumnExists && timeColumnIdx < len(items) {
data[DATA_TABLE_TRANSACTION_TIME] = items[timeColumnIdx]
}
if typeColumnExists && typeColumnIdx < len(items) {
data[DATA_TABLE_TRANSACTION_TYPE] = items[typeColumnIdx]
}
if categoryColumnExists && categoryColumnIdx < len(items) {
data[DATA_TABLE_CATEGORY] = items[categoryColumnIdx]
}
if subCategoryColumnExists && subCategoryColumnIdx < len(items) {
data[DATA_TABLE_SUB_CATEGORY] = items[subCategoryColumnIdx]
}
if accountColumnExists && accountColumnIdx < len(items) {
data[DATA_TABLE_ACCOUNT_NAME] = items[accountColumnIdx]
}
if accountCurrencyColumnExists && accountCurrencyColumnIdx < len(items) {
data[DATA_TABLE_ACCOUNT_CURRENCY] = items[accountCurrencyColumnIdx]
}
if amountColumnExists && amountColumnIdx < len(items) {
data[DATA_TABLE_AMOUNT] = items[amountColumnIdx]
}
if descriptionColumnExists && descriptionColumnIdx < len(items) {
data[DATA_TABLE_DESCRIPTION] = items[descriptionColumnIdx]
}
if relatedIdColumnExists && relatedIdColumnIdx < len(items) {
relatedId = items[relatedIdColumnIdx]
}
return data, relatedId
}
func (c *feideeMymoneyTransactionDataCsvImporter) getRelatedIds(transferTransactionsMap map[string]map[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,368 @@
package converters
import (
"testing"
"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: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, 5, 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_MODIFY_BALANCE, allNewTransactions[0].Type)
assert.Equal(t, int64(1725148800), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
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_INCOME, allNewTransactions[1].Type)
assert.Equal(t, int64(1725153825), utils.GetUnixTimeFromTransactionTime(allNewTransactions[1].TransactionTime))
assert.Equal(t, int64(12), allNewTransactions[1].Amount)
assert.Equal(t, "Test Account", allNewTransactions[1].OriginalSourceAccountName)
assert.Equal(t, "Test Category", allNewTransactions[1].OriginalCategoryName)
assert.Equal(t, int64(1234567890), allNewTransactions[2].Uid)
assert.Equal(t, models.TRANSACTION_DB_TYPE_EXPENSE, allNewTransactions[2].Type)
assert.Equal(t, int64(1725194096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[2].TransactionTime))
assert.Equal(t, int64(100), allNewTransactions[2].Amount)
assert.Equal(t, "Test Account", allNewTransactions[2].OriginalSourceAccountName)
assert.Equal(t, "Test Category2", allNewTransactions[2].OriginalCategoryName)
assert.Equal(t, int64(1234567890), allNewTransactions[3].Uid)
assert.Equal(t, models.TRANSACTION_DB_TYPE_TRANSFER_OUT, allNewTransactions[3].Type)
assert.Equal(t, int64(1725235199), utils.GetUnixTimeFromTransactionTime(allNewTransactions[3].TransactionTime))
assert.Equal(t, int64(5), allNewTransactions[3].Amount)
assert.Equal(t, "Test Account", allNewTransactions[3].OriginalSourceAccountName)
assert.Equal(t, "Test Account2", allNewTransactions[3].OriginalDestinationAccountName)
assert.Equal(t, "Test Category3", 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, int64(1725321599), utils.GetUnixTimeFromTransactionTime(allNewTransactions[4].TransactionTime))
assert.Equal(t, int64(50), allNewTransactions[4].Amount)
assert.Equal(t, "Test Account2", allNewTransactions[4].OriginalSourceAccountName)
assert.Equal(t, "Test Account", allNewTransactions[4].OriginalDestinationAccountName)
assert.Equal(t, "Test Category3", allNewTransactions[4].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)
}
@@ -19,6 +19,8 @@ func GetTransactionDataImporter(fileType string) (TransactionDataImporter, error
return EzBookKeepingTransactionDataCSVFileConverter, nil return EzBookKeepingTransactionDataCSVFileConverter, nil
} else if fileType == "ezbookkeeping_tsv" { } else if fileType == "ezbookkeeping_tsv" {
return EzBookKeepingTransactionDataTSVFileConverter, nil return EzBookKeepingTransactionDataTSVFileConverter, nil
} else if fileType == "feidee_mymoney_csv" {
return FeideeMymoneyTransactionDataCsvImporter, nil
} else if fileType == "feidee_mymoney_xls" { } else if fileType == "feidee_mymoney_xls" {
return FeideeMymoneyTransactionDataXlsImporter, nil return FeideeMymoneyTransactionDataXlsImporter, nil
} else { } else {
+144
View File
@@ -0,0 +1,144 @@
package converters
import (
"time"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
// WritableDataTable defines the structure of writable data table
type WritableDataTable struct {
allData []map[DataTableColumn]string
columns []DataTableColumn
}
// WritableDataRow defines the structure of data row of writable data table
type WritableDataRow struct {
dataTable *WritableDataTable
rowData map[DataTableColumn]string
}
// WritableDataRowIterator defines the structure of data row iterator of writable data table
type WritableDataRowIterator struct {
dataTable *WritableDataTable
nextIndex int
}
// Add appends a new record to data table
func (t *WritableDataTable) Add(data map[DataTableColumn]string) {
finalData := make(map[DataTableColumn]string, len(data))
for i := 0; i < len(t.columns); i++ {
column := t.columns[i]
if value, exists := data[column]; exists {
finalData[column] = value
}
}
t.allData = append(t.allData, finalData)
}
// Get returns the record in the specified index
func (t *WritableDataTable) Get(index int) ImportedDataRow {
if index >= len(t.allData) {
return nil
}
rowData := t.allData[index]
return &WritableDataRow{
dataTable: t,
rowData: rowData,
}
}
// DataRowCount returns the total count of data row
func (t *WritableDataTable) DataRowCount() int {
return len(t.allData)
}
// GetDataColumnMapping returns data column map for data importer
func (t *WritableDataTable) GetDataColumnMapping() map[DataTableColumn]string {
dataColumnMapping := make(map[DataTableColumn]string, len(t.columns))
for i := 0; i < len(t.columns); i++ {
column := t.columns[i]
dataColumnMapping[column] = utils.IntToString(int(column))
}
return dataColumnMapping
}
// HeaderLineColumnNames returns the header column name list
func (t *WritableDataTable) HeaderLineColumnNames() []string {
columnIndexes := make([]string, len(t.columns))
for i := 0; i < len(t.columns); i++ {
columnIndexes[i] = utils.IntToString(int(t.columns[i]))
}
return columnIndexes
}
// DataRowIterator returns the iterator of data row
func (t *WritableDataTable) DataRowIterator() ImportedDataRowIterator {
return &WritableDataRowIterator{
dataTable: t,
nextIndex: 0,
}
}
// ColumnCount returns the total count of column in this data row
func (r *WritableDataRow) ColumnCount() int {
return len(r.rowData)
}
// GetData returns the data in the specified column index
func (r *WritableDataRow) GetData(columnIndex int) string {
if columnIndex >= len(r.dataTable.columns) {
return ""
}
dataColumn := r.dataTable.columns[columnIndex]
return r.rowData[dataColumn]
}
// GetTime returns the time in the specified column index
func (r *WritableDataRow) 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 *WritableDataRow) GetTimezoneOffset(columnIndex int) (*time.Location, error) {
return utils.ParseFromTimezoneOffset(r.GetData(columnIndex))
}
// HasNext returns whether the iterator does not reach the end
func (t *WritableDataRowIterator) HasNext() bool {
return t.nextIndex < len(t.dataTable.allData)
}
// Next returns the next imported data row
func (t *WritableDataRowIterator) Next() ImportedDataRow {
if t.nextIndex >= len(t.dataTable.allData) {
return nil
}
rowData := t.dataTable.allData[t.nextIndex]
t.nextIndex++
return &WritableDataRow{
dataTable: t.dataTable,
rowData: rowData,
}
}
func createNewWritableDataTable(columns []DataTableColumn) (*WritableDataTable, error) {
return &WritableDataTable{
allData: make([]map[DataTableColumn]string, 0),
columns: columns,
}, nil
}
+213
View File
@@ -0,0 +1,213 @@
package converters
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
func TestWritableDataTableAdd(t *testing.T) {
columns := make([]DataTableColumn, 5)
columns[0] = DATA_TABLE_TRANSACTION_TIME
columns[1] = DATA_TABLE_TRANSACTION_TYPE
columns[2] = DATA_TABLE_SUB_CATEGORY
columns[3] = DATA_TABLE_ACCOUNT_NAME
columns[4] = DATA_TABLE_AMOUNT
writableDataTable, err := createNewWritableDataTable(columns)
assert.Nil(t, err)
assert.Equal(t, 0, writableDataTable.DataRowCount())
expectedTransactionUnixTime := time.Now().Unix()
expectedTextualTransactionTime := utils.FormatUnixTimeToLongDateTime(expectedTransactionUnixTime, time.Local)
expectedTransactionType := "Expense"
expectedSubCategory := "Test Category"
expectedAccountName := "Test Account"
expectedAmount := "123.45"
writableDataTable.Add(map[DataTableColumn]string{
DATA_TABLE_TRANSACTION_TIME: expectedTextualTransactionTime,
DATA_TABLE_TRANSACTION_TYPE: expectedTransactionType,
DATA_TABLE_SUB_CATEGORY: expectedSubCategory,
DATA_TABLE_ACCOUNT_NAME: expectedAccountName,
DATA_TABLE_AMOUNT: expectedAmount,
})
assert.Equal(t, 1, writableDataTable.DataRowCount())
dataRow := writableDataTable.Get(0)
actualTransactionTime, err := dataRow.GetTime(0, utils.GetTimezoneOffsetMinutes(time.Local))
assert.Nil(t, err)
actualTransactionUnixTime := actualTransactionTime.Unix()
assert.Equal(t, expectedTransactionUnixTime, actualTransactionUnixTime)
actualTextualTransactionTime := dataRow.GetData(0)
assert.Equal(t, expectedTextualTransactionTime, actualTextualTransactionTime)
actualTransactionType := dataRow.GetData(1)
assert.Equal(t, expectedTransactionType, actualTransactionType)
actualSubCategory := dataRow.GetData(2)
assert.Equal(t, expectedSubCategory, actualSubCategory)
actualAccountName := dataRow.GetData(3)
assert.Equal(t, expectedAccountName, actualAccountName)
actualAmount := dataRow.GetData(4)
assert.Equal(t, expectedAmount, actualAmount)
}
func TestWritableDataTableAdd_NotExistsColumn(t *testing.T) {
columns := make([]DataTableColumn, 1)
columns[0] = DATA_TABLE_TRANSACTION_TIME
writableDataTable, err := createNewWritableDataTable(columns)
assert.Nil(t, err)
expectedTransactionUnixTime := time.Now().Unix()
expectedTextualTransactionTime := utils.FormatUnixTimeToLongDateTime(expectedTransactionUnixTime, time.Local)
expectedTransactionType := "Expense"
writableDataTable.Add(map[DataTableColumn]string{
DATA_TABLE_TRANSACTION_TIME: expectedTextualTransactionTime,
DATA_TABLE_TRANSACTION_TYPE: expectedTransactionType,
})
assert.Equal(t, 1, writableDataTable.DataRowCount())
dataRow := writableDataTable.Get(0)
assert.Equal(t, 1, dataRow.ColumnCount())
}
func TestWritableDataTableGet_NotExistsRow(t *testing.T) {
columns := make([]DataTableColumn, 1)
columns[0] = DATA_TABLE_TRANSACTION_TIME
writableDataTable, err := createNewWritableDataTable(columns)
assert.Nil(t, err)
assert.Equal(t, 0, writableDataTable.DataRowCount())
dataRow := writableDataTable.Get(0)
assert.Nil(t, dataRow)
}
func TestWritableDataRowGetData_NotExistsColumn(t *testing.T) {
columns := make([]DataTableColumn, 1)
columns[0] = DATA_TABLE_TRANSACTION_TIME
writableDataTable, err := createNewWritableDataTable(columns)
assert.Nil(t, err)
expectedTransactionUnixTime := time.Now().Unix()
expectedTextualTransactionTime := utils.FormatUnixTimeToLongDateTime(expectedTransactionUnixTime, time.Local)
writableDataTable.Add(map[DataTableColumn]string{
DATA_TABLE_TRANSACTION_TIME: expectedTextualTransactionTime,
})
assert.Equal(t, 1, writableDataTable.DataRowCount())
dataRow := writableDataTable.Get(0)
assert.Equal(t, 1, dataRow.ColumnCount())
assert.Equal(t, "", dataRow.GetData(1))
}
func TestWritableDataTableDataRowIterator(t *testing.T) {
columns := make([]DataTableColumn, 5)
columns[0] = DATA_TABLE_TRANSACTION_TIME
columns[1] = DATA_TABLE_TRANSACTION_TYPE
columns[2] = DATA_TABLE_SUB_CATEGORY
columns[3] = DATA_TABLE_ACCOUNT_NAME
columns[4] = DATA_TABLE_AMOUNT
writableDataTable, err := createNewWritableDataTable(columns)
assert.Nil(t, err)
assert.Equal(t, 0, writableDataTable.DataRowCount())
expectedTransactionUnixTimes := make([]int64, 3)
expectedTextualTransactionTimes := make([]string, 3)
expectedTransactionTypes := make([]string, 3)
expectedSubCategories := make([]string, 3)
expectedAccountNames := make([]string, 3)
expectedAmounts := make([]string, 3)
expectedTransactionUnixTimes[0] = time.Now().Add(-5 * time.Hour).Unix()
expectedTextualTransactionTimes[0] = utils.FormatUnixTimeToLongDateTime(expectedTransactionUnixTimes[0], time.Local)
expectedTransactionTypes[0] = "Balance Modification"
expectedSubCategories[0] = ""
expectedAccountNames[0] = "Test Account"
expectedAmounts[0] = "123.45"
writableDataTable.Add(map[DataTableColumn]string{
DATA_TABLE_TRANSACTION_TIME: expectedTextualTransactionTimes[0],
DATA_TABLE_TRANSACTION_TYPE: expectedTransactionTypes[0],
DATA_TABLE_SUB_CATEGORY: expectedSubCategories[0],
DATA_TABLE_ACCOUNT_NAME: expectedAccountNames[0],
DATA_TABLE_AMOUNT: expectedAmounts[0],
})
expectedTransactionUnixTimes[1] = time.Now().Add(-45 * time.Minute).Unix()
expectedTextualTransactionTimes[1] = utils.FormatUnixTimeToLongDateTime(expectedTransactionUnixTimes[1], time.Local)
expectedTransactionTypes[1] = "Expense"
expectedSubCategories[1] = "Test Category2"
expectedAccountNames[1] = "Test Account"
expectedAmounts[1] = "-23.4"
writableDataTable.Add(map[DataTableColumn]string{
DATA_TABLE_TRANSACTION_TIME: expectedTextualTransactionTimes[1],
DATA_TABLE_TRANSACTION_TYPE: expectedTransactionTypes[1],
DATA_TABLE_SUB_CATEGORY: expectedSubCategories[1],
DATA_TABLE_ACCOUNT_NAME: expectedAccountNames[1],
DATA_TABLE_AMOUNT: expectedAmounts[1],
})
expectedTransactionUnixTimes[2] = time.Now().Unix()
expectedTextualTransactionTimes[2] = utils.FormatUnixTimeToLongDateTime(expectedTransactionUnixTimes[2], time.Local)
expectedTransactionTypes[2] = "Income"
expectedSubCategories[2] = "Test Category3"
expectedAccountNames[2] = "Test Account2"
expectedAmounts[2] = "123"
writableDataTable.Add(map[DataTableColumn]string{
DATA_TABLE_TRANSACTION_TIME: expectedTextualTransactionTimes[2],
DATA_TABLE_TRANSACTION_TYPE: expectedTransactionTypes[2],
DATA_TABLE_SUB_CATEGORY: expectedSubCategories[2],
DATA_TABLE_ACCOUNT_NAME: expectedAccountNames[2],
DATA_TABLE_AMOUNT: expectedAmounts[2],
})
assert.Equal(t, 3, writableDataTable.DataRowCount())
index := 0
iterator := writableDataTable.DataRowIterator()
for iterator.HasNext() {
dataRow := iterator.Next()
actualTransactionTime, err := dataRow.GetTime(0, utils.GetTimezoneOffsetMinutes(time.Local))
assert.Nil(t, err)
actualTransactionUnixTime := actualTransactionTime.Unix()
assert.Equal(t, expectedTransactionUnixTimes[index], actualTransactionUnixTime)
actualTextualTransactionTime := dataRow.GetData(0)
assert.Equal(t, expectedTextualTransactionTimes[index], actualTextualTransactionTime)
actualTransactionType := dataRow.GetData(1)
assert.Equal(t, expectedTransactionTypes[index], actualTransactionType)
actualSubCategory := dataRow.GetData(2)
assert.Equal(t, expectedSubCategories[index], actualSubCategory)
actualAccountName := dataRow.GetData(3)
assert.Equal(t, expectedAccountNames[index], actualAccountName)
actualAmount := dataRow.GetData(4)
assert.Equal(t, expectedAmounts[index], actualAmount)
index++
}
assert.Equal(t, 3, index)
}
+4
View File
@@ -16,4 +16,8 @@ var (
ErrAmountInvalid = NewNormalError(NormalSubcategoryConverter, 9, http.StatusBadRequest, "transaction amount is invalid") ErrAmountInvalid = NewNormalError(NormalSubcategoryConverter, 9, http.StatusBadRequest, "transaction amount is invalid")
ErrGeographicLocationInvalid = NewNormalError(NormalSubcategoryConverter, 10, http.StatusBadRequest, "geographic location is invalid") ErrGeographicLocationInvalid = NewNormalError(NormalSubcategoryConverter, 10, http.StatusBadRequest, "geographic location is invalid")
ErrFieldsInMultiTableAreDifferent = NewNormalError(NormalSubcategoryConverter, 11, http.StatusBadRequest, "fields in multiple table headers are different") ErrFieldsInMultiTableAreDifferent = NewNormalError(NormalSubcategoryConverter, 11, http.StatusBadRequest, "fields in multiple table headers are different")
ErrInvalidFileHeader = NewNormalError(NormalSubcategoryConverter, 12, http.StatusBadRequest, "invalid file header")
ErrInvalidCSVFile = NewNormalError(NormalSubcategoryConverter, 13, http.StatusBadRequest, "invalid csv file")
ErrRelatedIdCannotBeBlank = NewNormalError(NormalSubcategoryConverter, 14, http.StatusBadRequest, "related id cannot be blank")
ErrFoundRecordNotHasRelatedRecord = NewNormalError(NormalSubcategoryConverter, 15, http.StatusBadRequest, "found some transactions without related records")
) )
+5
View File
@@ -11,6 +11,11 @@ const supportedImportFileTypes = [
name: 'ezbookkeeping Data Export File (TSV)', name: 'ezbookkeeping Data Export File (TSV)',
extensions: '.tsv' extensions: '.tsv'
}, },
{
type: 'feidee_mymoney_csv',
name: 'Feidee MyMoney (App) Data Export File',
extensions: '.csv'
},
{ {
type: 'feidee_mymoney_xls', type: 'feidee_mymoney_xls',
name: 'Feidee MyMoney (Web) Data Export File', name: 'Feidee MyMoney (Web) Data Export File',
+5
View File
@@ -1118,6 +1118,10 @@
"transaction amount is invalid": "Transaction amount is invalid", "transaction amount is invalid": "Transaction amount is invalid",
"geographic location is invalid": "Geographic location is invalid", "geographic location is invalid": "Geographic location is invalid",
"fields in multiple table headers are different": "Fields in multiple table headers are different", "fields in multiple table headers are different": "Fields in multiple table headers are different",
"invalid file header": "Invalid file header",
"invalid csv file": "Invalid CSV file",
"related id cannot be blank": "Related ID cannot be blank",
"found some transactions without related records": "There are some transactions which don't have related records",
"query items cannot be blank": "There are no query items", "query items cannot be blank": "There are no query items",
"query items too much": "There are too many query items", "query items too much": "There are too many query items",
"query items have invalid item": "There is invalid item in query items", "query items have invalid item": "There is invalid item in query items",
@@ -1497,6 +1501,7 @@
"ezbookkeeping Data Export File (CSV)": "ezbookkeeping Data Export File (CSV)", "ezbookkeeping Data Export File (CSV)": "ezbookkeeping Data Export File (CSV)",
"ezbookkeeping Data Export File (TSV)": "ezbookkeeping Data Export File (TSV)", "ezbookkeeping Data Export File (TSV)": "ezbookkeeping Data Export File (TSV)",
"Feidee MyMoney (Web) Data Export File": "Feidee MyMoney (Web) Data Export File", "Feidee MyMoney (Web) Data Export File": "Feidee MyMoney (Web) Data Export File",
"Feidee MyMoney (App) Data Export File": "Feidee MyMoney (App) Data Export File",
"Data File": "Data File", "Data File": "Data File",
"Click to select import file": "Click to select import file", "Click to select import file": "Click to select import file",
"No data to import": "No data to import", "No data to import": "No data to import",
+5
View File
@@ -1118,6 +1118,10 @@
"transaction amount is invalid": "交易金额无效", "transaction amount is invalid": "交易金额无效",
"geographic location is invalid": "地理位置无效", "geographic location is invalid": "地理位置无效",
"fields in multiple table headers are different": "多个表头中的字段不同", "fields in multiple table headers are different": "多个表头中的字段不同",
"invalid file header": "无效的文件头",
"invalid csv file": "无效的 CSV 文件",
"related id cannot be blank": "关联Id不能为空",
"found some transactions without related records": "有一些交易没有关联记录",
"query items cannot be blank": "请求项目不能为空", "query items cannot be blank": "请求项目不能为空",
"query items too much": "请求项目过多", "query items too much": "请求项目过多",
"query items have invalid item": "请求项目中有非法项目", "query items have invalid item": "请求项目中有非法项目",
@@ -1497,6 +1501,7 @@
"ezbookkeeping Data Export File (CSV)": "ezbookkeeping 数据导出文件 (CSV)", "ezbookkeeping Data Export File (CSV)": "ezbookkeeping 数据导出文件 (CSV)",
"ezbookkeeping Data Export File (TSV)": "ezbookkeeping 数据导出文件 (TSV)", "ezbookkeeping Data Export File (TSV)": "ezbookkeeping 数据导出文件 (TSV)",
"Feidee MyMoney (Web) Data Export File": "金蝶随手记 (Web版) 数据导出文件", "Feidee MyMoney (Web) Data Export File": "金蝶随手记 (Web版) 数据导出文件",
"Feidee MyMoney (App) Data Export File": "金蝶随手记 (App) 数据导出文件",
"Data File": "数据文件", "Data File": "数据文件",
"Click to select import file": "点击选择导入文件", "Click to select import file": "点击选择导入文件",
"No data to import": "没有可以导入的数据", "No data to import": "没有可以导入的数据",