support importing transaction data from feidee mymoney app export data
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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": "没有可以导入的数据",
|
||||||
|
|||||||
Reference in New Issue
Block a user