code refactor
This commit is contained in:
+23
-23
@@ -20,16 +20,16 @@ const pageCountForDataExport = 1000
|
||||
// DataManagementsApi represents data management api
|
||||
type DataManagementsApi struct {
|
||||
ApiUsingConfig
|
||||
ezBookKeepingCsvExporter *converters.EzBookKeepingCSVFileConverter
|
||||
ezBookKeepingTsvExporter *converters.EzBookKeepingTSVFileConverter
|
||||
tokens *services.TokenService
|
||||
users *services.UserService
|
||||
accounts *services.AccountService
|
||||
transactions *services.TransactionService
|
||||
categories *services.TransactionCategoryService
|
||||
tags *services.TransactionTagService
|
||||
pictures *services.TransactionPictureService
|
||||
templates *services.TransactionTemplateService
|
||||
ezBookKeepingCsvConverter converters.TransactionDataConverter
|
||||
ezBookKeepingTsvConverter converters.TransactionDataConverter
|
||||
tokens *services.TokenService
|
||||
users *services.UserService
|
||||
accounts *services.AccountService
|
||||
transactions *services.TransactionService
|
||||
categories *services.TransactionCategoryService
|
||||
tags *services.TransactionTagService
|
||||
pictures *services.TransactionPictureService
|
||||
templates *services.TransactionTemplateService
|
||||
}
|
||||
|
||||
// Initialize a data management api singleton instance
|
||||
@@ -38,16 +38,16 @@ var (
|
||||
ApiUsingConfig: ApiUsingConfig{
|
||||
container: settings.Container,
|
||||
},
|
||||
ezBookKeepingCsvExporter: &converters.EzBookKeepingCSVFileConverter{},
|
||||
ezBookKeepingTsvExporter: &converters.EzBookKeepingTSVFileConverter{},
|
||||
tokens: services.Tokens,
|
||||
users: services.Users,
|
||||
accounts: services.Accounts,
|
||||
transactions: services.Transactions,
|
||||
categories: services.TransactionCategories,
|
||||
tags: services.TransactionTags,
|
||||
pictures: services.TransactionPictures,
|
||||
templates: services.TransactionTemplates,
|
||||
ezBookKeepingCsvConverter: converters.EzBookKeepingTransactionDataCSVFileConverter,
|
||||
ezBookKeepingTsvConverter: converters.EzBookKeepingTransactionDataTSVFileConverter,
|
||||
tokens: services.Tokens,
|
||||
users: services.Users,
|
||||
accounts: services.Accounts,
|
||||
transactions: services.Transactions,
|
||||
categories: services.TransactionCategories,
|
||||
tags: services.TransactionTags,
|
||||
pictures: services.TransactionPictures,
|
||||
templates: services.TransactionTemplates,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -247,12 +247,12 @@ func (a *DataManagementsApi) getExportedFileContent(c *core.WebContext, fileType
|
||||
return nil, "", errs.ErrOperationFailed
|
||||
}
|
||||
|
||||
var dataExporter converters.DataConverter
|
||||
var dataExporter converters.TransactionDataExporter
|
||||
|
||||
if fileType == "tsv" {
|
||||
dataExporter = a.ezBookKeepingTsvExporter
|
||||
dataExporter = a.ezBookKeepingTsvConverter
|
||||
} else {
|
||||
dataExporter = a.ezBookKeepingCsvExporter
|
||||
dataExporter = a.ezBookKeepingCsvConverter
|
||||
}
|
||||
|
||||
result, err := dataExporter.ToExportedContent(uid, allTransactions, accountMap, categoryMap, tagMap, tagIndexes)
|
||||
|
||||
+26
-26
@@ -20,16 +20,16 @@ const pageCountForDataExport = 1000
|
||||
// UserDataCli represents user data cli
|
||||
type UserDataCli struct {
|
||||
CliUsingConfig
|
||||
ezBookKeepingCsvExporter *converters.EzBookKeepingCSVFileConverter
|
||||
ezBookKeepingTsvExporter *converters.EzBookKeepingTSVFileConverter
|
||||
accounts *services.AccountService
|
||||
transactions *services.TransactionService
|
||||
categories *services.TransactionCategoryService
|
||||
tags *services.TransactionTagService
|
||||
users *services.UserService
|
||||
twoFactorAuthorizations *services.TwoFactorAuthorizationService
|
||||
tokens *services.TokenService
|
||||
forgetPasswords *services.ForgetPasswordService
|
||||
ezBookKeepingCsvConverter converters.TransactionDataConverter
|
||||
ezBookKeepingTsvConverter converters.TransactionDataConverter
|
||||
accounts *services.AccountService
|
||||
transactions *services.TransactionService
|
||||
categories *services.TransactionCategoryService
|
||||
tags *services.TransactionTagService
|
||||
users *services.UserService
|
||||
twoFactorAuthorizations *services.TwoFactorAuthorizationService
|
||||
tokens *services.TokenService
|
||||
forgetPasswords *services.ForgetPasswordService
|
||||
}
|
||||
|
||||
// Initialize an user data cli singleton instance
|
||||
@@ -38,16 +38,16 @@ var (
|
||||
CliUsingConfig: CliUsingConfig{
|
||||
container: settings.Container,
|
||||
},
|
||||
ezBookKeepingCsvExporter: &converters.EzBookKeepingCSVFileConverter{},
|
||||
ezBookKeepingTsvExporter: &converters.EzBookKeepingTSVFileConverter{},
|
||||
accounts: services.Accounts,
|
||||
transactions: services.Transactions,
|
||||
categories: services.TransactionCategories,
|
||||
tags: services.TransactionTags,
|
||||
users: services.Users,
|
||||
twoFactorAuthorizations: services.TwoFactorAuthorizations,
|
||||
tokens: services.Tokens,
|
||||
forgetPasswords: services.ForgetPasswords,
|
||||
ezBookKeepingCsvConverter: converters.EzBookKeepingTransactionDataCSVFileConverter,
|
||||
ezBookKeepingTsvConverter: converters.EzBookKeepingTransactionDataTSVFileConverter,
|
||||
accounts: services.Accounts,
|
||||
transactions: services.Transactions,
|
||||
categories: services.TransactionCategories,
|
||||
tags: services.TransactionTags,
|
||||
users: services.Users,
|
||||
twoFactorAuthorizations: services.TwoFactorAuthorizations,
|
||||
tokens: services.Tokens,
|
||||
forgetPasswords: services.ForgetPasswords,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -645,12 +645,12 @@ func (l *UserDataCli) ExportTransaction(c *core.CliContext, username string, fil
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var dataExporter converters.DataConverter
|
||||
var dataExporter converters.TransactionDataExporter
|
||||
|
||||
if fileType == "tsv" {
|
||||
dataExporter = l.ezBookKeepingTsvExporter
|
||||
dataExporter = l.ezBookKeepingTsvConverter
|
||||
} else {
|
||||
dataExporter = l.ezBookKeepingCsvExporter
|
||||
dataExporter = l.ezBookKeepingCsvConverter
|
||||
}
|
||||
|
||||
result, err := dataExporter.ToExportedContent(uid, allTransactions, accountMap, categoryMap, tagMap, tagIndexesMap)
|
||||
@@ -669,12 +669,12 @@ func (l *UserDataCli) ImportTransaction(c *core.CliContext, username string, fil
|
||||
return errs.ErrUsernameIsEmpty
|
||||
}
|
||||
|
||||
var dataImporter converters.DataConverter
|
||||
var dataImporter converters.TransactionDataImporter
|
||||
|
||||
if fileType == "ezbookkeeping_csv" {
|
||||
dataImporter = l.ezBookKeepingCsvExporter
|
||||
dataImporter = l.ezBookKeepingCsvConverter
|
||||
} else if fileType == "ezbookkeeping_tsv" {
|
||||
dataImporter = l.ezBookKeepingTsvExporter
|
||||
dataImporter = l.ezBookKeepingTsvConverter
|
||||
} else {
|
||||
return errs.ErrImportFileTypeNotSupported
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package converters
|
||||
|
||||
// ImportedDataTable defines the structure of imported data table
|
||||
type ImportedDataTable interface {
|
||||
// DataRowCount returns the total count of data row
|
||||
DataRowCount() int
|
||||
|
||||
// HeaderLineColumnNames returns the header column name list
|
||||
HeaderLineColumnNames() []string
|
||||
|
||||
// DataRowIterator returns the iterator of data row
|
||||
DataRowIterator() ImportedDataRowIterator
|
||||
}
|
||||
|
||||
// ImportedDataRow defines the structure of imported data row
|
||||
type ImportedDataRow interface {
|
||||
// ColumnCount returns the total count of column in this data row
|
||||
ColumnCount() int
|
||||
|
||||
// GetData returns the data in the specified column index
|
||||
GetData(columnIndex int) string
|
||||
}
|
||||
|
||||
// ImportedDataRowIterator defines the structure of imported data row iterator
|
||||
type ImportedDataRowIterator interface {
|
||||
// HasNext returns whether the iterator does not reach the end
|
||||
HasNext() bool
|
||||
|
||||
// Next returns the next imported data row
|
||||
Next() ImportedDataRow
|
||||
}
|
||||
|
||||
// DataTableBuilder defines the structure of data table builder
|
||||
type DataTableBuilder interface {
|
||||
// AppendTransaction appends the specified transaction to data builder
|
||||
AppendTransaction(data map[DataTableColumn]string)
|
||||
}
|
||||
@@ -0,0 +1,493 @@
|
||||
package converters
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/validators"
|
||||
)
|
||||
|
||||
// DataTableColumn represents the data column type of data table
|
||||
type DataTableColumn byte
|
||||
|
||||
// Data table columns
|
||||
const (
|
||||
DATA_TABLE_TRANSACTION_TIME DataTableColumn = 1
|
||||
DATA_TABLE_TRANSACTION_TIMEZONE DataTableColumn = 2
|
||||
DATA_TABLE_TRANSACTION_TYPE DataTableColumn = 3
|
||||
DATA_TABLE_CATEGORY DataTableColumn = 4
|
||||
DATA_TABLE_SUB_CATEGORY DataTableColumn = 5
|
||||
DATA_TABLE_ACCOUNT_NAME DataTableColumn = 6
|
||||
DATA_TABLE_ACCOUNT_CURRENCY DataTableColumn = 7
|
||||
DATA_TABLE_AMOUNT DataTableColumn = 8
|
||||
DATA_TABLE_RELATED_ACCOUNT_NAME DataTableColumn = 9
|
||||
DATA_TABLE_RELATED_ACCOUNT_CURRENCY DataTableColumn = 10
|
||||
DATA_TABLE_RELATED_AMOUNT DataTableColumn = 11
|
||||
DATA_TABLE_GEOGRAPHIC_LOCATION DataTableColumn = 12
|
||||
DATA_TABLE_TAGS DataTableColumn = 13
|
||||
DATA_TABLE_DESCRIPTION DataTableColumn = 14
|
||||
)
|
||||
|
||||
// DataTableTransactionDataConverter defines the structure of data table importer for transaction data
|
||||
type DataTableTransactionDataConverter struct {
|
||||
dataColumnMapping map[DataTableColumn]string
|
||||
transactionTypeMapping map[models.TransactionDbType]string
|
||||
transactionTypeNameMapping map[string]models.TransactionDbType
|
||||
columnSeparator string
|
||||
lineSeparator string
|
||||
geoLocationSeparator string
|
||||
transactionTagSeparator string
|
||||
}
|
||||
|
||||
func (c *DataTableTransactionDataConverter) buildExportedContent(dataTableBuilder DataTableBuilder, uid int64, transactions []*models.Transaction, accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTagIndexes map[int64][]int64) error {
|
||||
for i := 0; i < len(transactions); i++ {
|
||||
transaction := transactions[i]
|
||||
|
||||
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
|
||||
continue
|
||||
}
|
||||
|
||||
dataRowMap := make(map[DataTableColumn]string, 15)
|
||||
transactionTimeZone := time.FixedZone("Transaction Timezone", int(transaction.TimezoneUtcOffset)*60)
|
||||
|
||||
dataRowMap[DATA_TABLE_TRANSACTION_TIME] = utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime), transactionTimeZone)
|
||||
dataRowMap[DATA_TABLE_TRANSACTION_TIMEZONE] = utils.FormatTimezoneOffset(transactionTimeZone)
|
||||
dataRowMap[DATA_TABLE_TRANSACTION_TYPE] = c.replaceDelimiters(c.getDisplayTransactionTypeName(transaction.Type))
|
||||
dataRowMap[DATA_TABLE_CATEGORY] = c.getExportedTransactionCategoryName(transaction.CategoryId, categoryMap)
|
||||
dataRowMap[DATA_TABLE_SUB_CATEGORY] = c.getExportedTransactionSubCategoryName(transaction.CategoryId, categoryMap)
|
||||
dataRowMap[DATA_TABLE_ACCOUNT_NAME] = c.getExportedAccountName(transaction.AccountId, accountMap)
|
||||
dataRowMap[DATA_TABLE_ACCOUNT_CURRENCY] = c.getAccountCurrency(transaction.AccountId, accountMap)
|
||||
dataRowMap[DATA_TABLE_AMOUNT] = utils.FormatAmount(transaction.Amount)
|
||||
|
||||
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
|
||||
dataRowMap[DATA_TABLE_RELATED_ACCOUNT_NAME] = c.getExportedAccountName(transaction.RelatedAccountId, accountMap)
|
||||
dataRowMap[DATA_TABLE_RELATED_ACCOUNT_CURRENCY] = c.getAccountCurrency(transaction.RelatedAccountId, accountMap)
|
||||
dataRowMap[DATA_TABLE_RELATED_AMOUNT] = utils.FormatAmount(transaction.RelatedAccountAmount)
|
||||
}
|
||||
|
||||
dataRowMap[DATA_TABLE_GEOGRAPHIC_LOCATION] = c.getExportedGeographicLocation(transaction)
|
||||
dataRowMap[DATA_TABLE_TAGS] = c.getExportedTags(transaction.TransactionId, allTagIndexes, tagMap)
|
||||
dataRowMap[DATA_TABLE_DESCRIPTION] = c.replaceDelimiters(transaction.Comment)
|
||||
|
||||
dataTableBuilder.AppendTransaction(dataRowMap)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *DataTableTransactionDataConverter) parseImportedData(user *models.User, dataTable ImportedDataTable, defaultTimezoneOffset int16, accountMap map[string]*models.Account, categoryMap map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) ([]*models.Transaction, []*models.Account, []*models.TransactionCategory, []*models.TransactionTag, error) {
|
||||
if dataTable.DataRowCount() < 1 {
|
||||
return nil, nil, nil, nil, errs.ErrOperationFailed
|
||||
}
|
||||
|
||||
headerLineItems := dataTable.HeaderLineColumnNames()
|
||||
headerItemMap := make(map[string]int)
|
||||
|
||||
for i := 0; i < len(headerLineItems); i++ {
|
||||
headerItemMap[headerLineItems[i]] = i
|
||||
}
|
||||
|
||||
timeColumnIdx, timeColumnExists := headerItemMap[c.dataColumnMapping[DATA_TABLE_TRANSACTION_TIME]]
|
||||
timezoneColumnIdx, timezoneColumnExists := headerItemMap[c.dataColumnMapping[DATA_TABLE_TRANSACTION_TIMEZONE]]
|
||||
typeColumnIdx, typeColumnExists := headerItemMap[c.dataColumnMapping[DATA_TABLE_TRANSACTION_TYPE]]
|
||||
subCategoryColumnIdx, subCategoryColumnExists := headerItemMap[c.dataColumnMapping[DATA_TABLE_SUB_CATEGORY]]
|
||||
accountColumnIdx, accountColumnExists := headerItemMap[c.dataColumnMapping[DATA_TABLE_ACCOUNT_NAME]]
|
||||
accountCurrencyColumnIdx, accountCurrencyColumnExists := headerItemMap[c.dataColumnMapping[DATA_TABLE_ACCOUNT_CURRENCY]]
|
||||
amountColumnIdx, amountColumnExists := headerItemMap[c.dataColumnMapping[DATA_TABLE_AMOUNT]]
|
||||
account2ColumnIdx, account2ColumnExists := headerItemMap[c.dataColumnMapping[DATA_TABLE_RELATED_ACCOUNT_NAME]]
|
||||
account2CurrencyColumnIdx, account2CurrencyColumnExists := headerItemMap[c.dataColumnMapping[DATA_TABLE_RELATED_ACCOUNT_CURRENCY]]
|
||||
amount2ColumnIdx, amount2ColumnExists := headerItemMap[c.dataColumnMapping[DATA_TABLE_RELATED_AMOUNT]]
|
||||
geoLocationIdx, geoLocationExists := headerItemMap[c.dataColumnMapping[DATA_TABLE_GEOGRAPHIC_LOCATION]]
|
||||
tagsColumnIdx, tagsColumnExists := headerItemMap[c.dataColumnMapping[DATA_TABLE_TAGS]]
|
||||
descriptionColumnIdx, descriptionColumnExists := headerItemMap[c.dataColumnMapping[DATA_TABLE_DESCRIPTION]]
|
||||
|
||||
if !timeColumnExists || !typeColumnExists || !subCategoryColumnExists ||
|
||||
!accountColumnExists || !amountColumnExists || !account2ColumnExists || !amount2ColumnExists {
|
||||
return nil, nil, nil, nil, errs.ErrFormatInvalid
|
||||
}
|
||||
|
||||
if accountMap == nil {
|
||||
accountMap = make(map[string]*models.Account)
|
||||
}
|
||||
|
||||
if categoryMap == nil {
|
||||
categoryMap = make(map[string]*models.TransactionCategory)
|
||||
}
|
||||
|
||||
if tagMap == nil {
|
||||
tagMap = make(map[string]*models.TransactionTag)
|
||||
}
|
||||
|
||||
allNewTransactions := make(ImportedTransactionSlice, 0, dataTable.DataRowCount())
|
||||
allNewAccounts := make([]*models.Account, 0)
|
||||
allNewSubCategories := make([]*models.TransactionCategory, 0)
|
||||
allNewTags := make([]*models.TransactionTag, 0)
|
||||
|
||||
dataRowIterator := dataTable.DataRowIterator()
|
||||
|
||||
for dataRowIterator.HasNext() {
|
||||
dataRow := dataRowIterator.Next()
|
||||
columnCount := dataRow.ColumnCount()
|
||||
|
||||
if columnCount < 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
if columnCount < len(headerLineItems) {
|
||||
return nil, nil, nil, nil, errs.ErrFormatInvalid
|
||||
}
|
||||
|
||||
timezoneOffset := defaultTimezoneOffset
|
||||
|
||||
if timezoneColumnExists {
|
||||
transactionTimezone, err := utils.ParseFromTimezoneOffset(dataRow.GetData(timezoneColumnIdx))
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
timezoneOffset = utils.GetTimezoneOffsetMinutes(transactionTimezone)
|
||||
}
|
||||
|
||||
transactionTime, err := utils.ParseFromLongDateTime(dataRow.GetData(timeColumnIdx), timezoneOffset)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
transactionDbType, err := c.getTransactionDbType(dataRow.GetData(typeColumnIdx))
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
categoryId := int64(0)
|
||||
|
||||
if transactionDbType != models.TRANSACTION_DB_TYPE_MODIFY_BALANCE {
|
||||
transactionCategoryType, err := c.getTransactionCategoryType(transactionDbType)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
subCategoryName := dataRow.GetData(subCategoryColumnIdx)
|
||||
|
||||
if subCategoryName == "" {
|
||||
return nil, nil, nil, nil, errs.ErrFormatInvalid
|
||||
}
|
||||
|
||||
subCategory, exists := categoryMap[subCategoryName]
|
||||
|
||||
if !exists {
|
||||
subCategory = c.createNewTransactionCategoryModel(user.Uid, subCategoryName, transactionCategoryType)
|
||||
allNewSubCategories = append(allNewSubCategories, subCategory)
|
||||
categoryMap[subCategoryName] = subCategory
|
||||
}
|
||||
|
||||
categoryId = subCategory.CategoryId
|
||||
}
|
||||
|
||||
accountName := dataRow.GetData(accountColumnIdx)
|
||||
|
||||
if accountName == "" {
|
||||
return nil, nil, nil, nil, errs.ErrFormatInvalid
|
||||
}
|
||||
|
||||
account, exists := accountMap[accountName]
|
||||
|
||||
if !exists {
|
||||
currency := user.DefaultCurrency
|
||||
|
||||
if accountCurrencyColumnExists {
|
||||
currency = dataRow.GetData(accountCurrencyColumnIdx)
|
||||
|
||||
if _, ok := validators.AllCurrencyNames[currency]; !ok {
|
||||
return nil, nil, nil, nil, errs.ErrAccountCurrencyInvalid
|
||||
}
|
||||
}
|
||||
|
||||
account = c.createNewAccountModel(user.Uid, accountName, currency)
|
||||
allNewAccounts = append(allNewAccounts, account)
|
||||
accountMap[accountName] = account
|
||||
}
|
||||
|
||||
if accountCurrencyColumnExists {
|
||||
if account.Currency != dataRow.GetData(accountCurrencyColumnIdx) {
|
||||
return nil, nil, nil, nil, errs.ErrAccountCurrencyInvalid
|
||||
}
|
||||
}
|
||||
|
||||
amount, err := utils.ParseAmount(dataRow.GetData(amountColumnIdx))
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
relatedAccountId := int64(0)
|
||||
relatedAccountAmount := int64(0)
|
||||
|
||||
if transactionDbType == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
|
||||
account2Name := dataRow.GetData(account2ColumnIdx)
|
||||
|
||||
if account2Name == "" {
|
||||
return nil, nil, nil, nil, errs.ErrFormatInvalid
|
||||
}
|
||||
|
||||
account2, exists := accountMap[account2Name]
|
||||
|
||||
if !exists {
|
||||
currency := user.DefaultCurrency
|
||||
|
||||
if accountCurrencyColumnExists {
|
||||
currency = dataRow.GetData(account2CurrencyColumnIdx)
|
||||
|
||||
if _, ok := validators.AllCurrencyNames[currency]; !ok {
|
||||
return nil, nil, nil, nil, errs.ErrAccountCurrencyInvalid
|
||||
}
|
||||
}
|
||||
|
||||
account2 = c.createNewAccountModel(user.Uid, account2Name, currency)
|
||||
allNewAccounts = append(allNewAccounts, account2)
|
||||
accountMap[account2Name] = account2
|
||||
}
|
||||
|
||||
if account2CurrencyColumnExists {
|
||||
if account2.Currency != dataRow.GetData(account2CurrencyColumnIdx) {
|
||||
return nil, nil, nil, nil, errs.ErrAccountCurrencyInvalid
|
||||
}
|
||||
}
|
||||
|
||||
relatedAccountId = account2.AccountId
|
||||
relatedAccountAmount, err = utils.ParseAmount(dataRow.GetData(amount2ColumnIdx))
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
geoLongitude := float64(0)
|
||||
geoLatitude := float64(0)
|
||||
|
||||
if geoLocationExists {
|
||||
geoLocationItems := strings.Split(dataRow.GetData(geoLocationIdx), c.geoLocationSeparator)
|
||||
|
||||
if len(geoLocationItems) == 2 {
|
||||
geoLongitude, err = utils.StringToFloat64(geoLocationItems[0])
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
geoLatitude, err = utils.StringToFloat64(geoLocationItems[1])
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if tagsColumnExists {
|
||||
tagNames := strings.Split(dataRow.GetData(tagsColumnIdx), c.transactionTagSeparator)
|
||||
|
||||
for i := 0; i < len(tagNames); i++ {
|
||||
tagName := tagNames[i]
|
||||
|
||||
if tagName == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
tag, exists := tagMap[tagName]
|
||||
|
||||
if !exists {
|
||||
tag = c.createNewTransactionTagModel(user.Uid, tagName)
|
||||
allNewTags = append(allNewTags, tag)
|
||||
tagMap[tagName] = tag
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
description := ""
|
||||
|
||||
if descriptionColumnExists {
|
||||
description = dataRow.GetData(descriptionColumnIdx)
|
||||
}
|
||||
|
||||
transaction := &models.Transaction{
|
||||
Uid: user.Uid,
|
||||
Type: transactionDbType,
|
||||
CategoryId: categoryId,
|
||||
TransactionTime: utils.GetMinTransactionTimeFromUnixTime(transactionTime.Unix()),
|
||||
TimezoneUtcOffset: timezoneOffset,
|
||||
AccountId: account.AccountId,
|
||||
Amount: amount,
|
||||
HideAmount: false,
|
||||
RelatedAccountId: relatedAccountId,
|
||||
RelatedAccountAmount: relatedAccountAmount,
|
||||
Comment: description,
|
||||
GeoLongitude: geoLongitude,
|
||||
GeoLatitude: geoLatitude,
|
||||
CreatedIp: "127.0.0.1",
|
||||
}
|
||||
|
||||
allNewTransactions = append(allNewTransactions, transaction)
|
||||
}
|
||||
|
||||
sort.Sort(allNewTransactions)
|
||||
|
||||
return allNewTransactions, allNewAccounts, allNewSubCategories, allNewTags, nil
|
||||
}
|
||||
|
||||
func (c *DataTableTransactionDataConverter) getTransactionDbType(transactionTypeName string) (models.TransactionDbType, error) {
|
||||
transactionType, exists := c.transactionTypeNameMapping[transactionTypeName]
|
||||
|
||||
if !exists {
|
||||
return 0, errs.ErrTransactionTypeInvalid
|
||||
}
|
||||
|
||||
return transactionType, nil
|
||||
}
|
||||
|
||||
func (c *DataTableTransactionDataConverter) getTransactionCategoryType(transactionType models.TransactionDbType) (models.TransactionCategoryType, error) {
|
||||
if transactionType == models.TRANSACTION_DB_TYPE_INCOME {
|
||||
return models.CATEGORY_TYPE_INCOME, nil
|
||||
} else if transactionType == models.TRANSACTION_DB_TYPE_EXPENSE {
|
||||
return models.CATEGORY_TYPE_EXPENSE, nil
|
||||
} else if transactionType == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
|
||||
return models.CATEGORY_TYPE_TRANSFER, nil
|
||||
} else {
|
||||
return 0, errs.ErrTransactionTypeInvalid
|
||||
}
|
||||
}
|
||||
|
||||
func (c *DataTableTransactionDataConverter) getDisplayTransactionTypeName(transactionDbType models.TransactionDbType) string {
|
||||
transactionTypeName, exists := c.transactionTypeMapping[transactionDbType]
|
||||
|
||||
if !exists {
|
||||
return ""
|
||||
}
|
||||
|
||||
return transactionTypeName
|
||||
}
|
||||
|
||||
func (c *DataTableTransactionDataConverter) getExportedTransactionCategoryName(categoryId int64, categoryMap map[int64]*models.TransactionCategory) string {
|
||||
category, exists := categoryMap[categoryId]
|
||||
|
||||
if !exists {
|
||||
return ""
|
||||
}
|
||||
|
||||
if category.ParentCategoryId == 0 {
|
||||
return c.replaceDelimiters(category.Name)
|
||||
}
|
||||
|
||||
parentCategory, exists := categoryMap[category.ParentCategoryId]
|
||||
|
||||
if !exists {
|
||||
return ""
|
||||
}
|
||||
|
||||
return c.replaceDelimiters(parentCategory.Name)
|
||||
}
|
||||
|
||||
func (c *DataTableTransactionDataConverter) getExportedTransactionSubCategoryName(categoryId int64, categoryMap map[int64]*models.TransactionCategory) string {
|
||||
category, exists := categoryMap[categoryId]
|
||||
|
||||
if exists {
|
||||
return c.replaceDelimiters(category.Name)
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (c *DataTableTransactionDataConverter) getExportedAccountName(accountId int64, accountMap map[int64]*models.Account) string {
|
||||
account, exists := accountMap[accountId]
|
||||
|
||||
if exists {
|
||||
return c.replaceDelimiters(account.Name)
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (c *DataTableTransactionDataConverter) getAccountCurrency(accountId int64, accountMap map[int64]*models.Account) string {
|
||||
account, exists := accountMap[accountId]
|
||||
|
||||
if exists {
|
||||
return c.replaceDelimiters(account.Currency)
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (c *DataTableTransactionDataConverter) getExportedGeographicLocation(transaction *models.Transaction) string {
|
||||
if transaction.GeoLongitude != 0 || transaction.GeoLatitude != 0 {
|
||||
return fmt.Sprintf("%f%s%f", transaction.GeoLongitude, c.geoLocationSeparator, transaction.GeoLatitude)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *DataTableTransactionDataConverter) getExportedTags(transactionId int64, allTagIndexes map[int64][]int64, tagMap map[int64]*models.TransactionTag) string {
|
||||
tagIndexes, exists := allTagIndexes[transactionId]
|
||||
|
||||
if !exists {
|
||||
return ""
|
||||
}
|
||||
|
||||
var ret strings.Builder
|
||||
|
||||
for i := 0; i < len(tagIndexes); i++ {
|
||||
tagIndex := tagIndexes[i]
|
||||
tag, exists := tagMap[tagIndex]
|
||||
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
if ret.Len() > 0 {
|
||||
ret.WriteString(c.transactionTagSeparator)
|
||||
}
|
||||
|
||||
ret.WriteString(strings.Replace(tag.Name, c.transactionTagSeparator, " ", -1))
|
||||
}
|
||||
|
||||
return c.replaceDelimiters(ret.String())
|
||||
}
|
||||
|
||||
func (c *DataTableTransactionDataConverter) replaceDelimiters(text string) string {
|
||||
text = strings.Replace(text, "\r\n", " ", -1)
|
||||
text = strings.Replace(text, "\r", " ", -1)
|
||||
text = strings.Replace(text, "\n", " ", -1)
|
||||
text = strings.Replace(text, c.columnSeparator, " ", -1)
|
||||
text = strings.Replace(text, c.lineSeparator, " ", -1)
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
func (c *DataTableTransactionDataConverter) createNewAccountModel(uid int64, accountName string, currency string) *models.Account {
|
||||
return &models.Account{
|
||||
Uid: uid,
|
||||
Name: accountName,
|
||||
Currency: currency,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *DataTableTransactionDataConverter) createNewTransactionCategoryModel(uid int64, categoryName string, transactionCategoryType models.TransactionCategoryType) *models.TransactionCategory {
|
||||
return &models.TransactionCategory{
|
||||
Uid: uid,
|
||||
Name: categoryName,
|
||||
Type: transactionCategoryType,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *DataTableTransactionDataConverter) createNewTransactionTagModel(uid int64, tagName string) *models.TransactionTag {
|
||||
return &models.TransactionTag{
|
||||
Uid: uid,
|
||||
Name: tagName,
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package converters
|
||||
|
||||
import (
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
)
|
||||
|
||||
// EzBookKeepingCSVFileConverter defines the structure of CSV file converter
|
||||
type EzBookKeepingCSVFileConverter struct {
|
||||
EzBookKeepingPlainFileConverter
|
||||
}
|
||||
|
||||
const csvSeparator = ","
|
||||
|
||||
// ToExportedContent returns the exported CSV data
|
||||
func (e *EzBookKeepingCSVFileConverter) ToExportedContent(uid int64, transactions []*models.Transaction, accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTagIndexes map[int64][]int64) ([]byte, error) {
|
||||
return e.toExportedContent(uid, csvSeparator, transactions, accountMap, categoryMap, tagMap, allTagIndexes)
|
||||
}
|
||||
|
||||
// ParseImportedData parses transactions of ezbookkeeping CSV data
|
||||
func (e *EzBookKeepingCSVFileConverter) ParseImportedData(user *models.User, data []byte, defaultTimezoneOffset int16, accountMap map[string]*models.Account, categoryMap map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) ([]*models.Transaction, []*models.Account, []*models.TransactionCategory, []*models.TransactionTag, error) {
|
||||
return e.parseImportedData(user, csvSeparator, data, defaultTimezoneOffset, accountMap, categoryMap, tagMap)
|
||||
}
|
||||
@@ -1,491 +0,0 @@
|
||||
package converters
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/validators"
|
||||
)
|
||||
|
||||
// EzBookKeepingPlainFileConverter defines the structure of plain file converter
|
||||
type EzBookKeepingPlainFileConverter struct {
|
||||
}
|
||||
|
||||
const lineSeparator = "\n"
|
||||
const geoLocationSeparator = " "
|
||||
const transactionTagSeparator = ";"
|
||||
const headerLine = "Time,Timezone,Type,Category,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description" + lineSeparator
|
||||
const dataLineFormat = "%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s" + lineSeparator
|
||||
|
||||
// toExportedContent returns the exported plain data
|
||||
func (e *EzBookKeepingPlainFileConverter) toExportedContent(uid int64, separator string, transactions []*models.Transaction, accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTagIndexes map[int64][]int64) ([]byte, error) {
|
||||
var ret strings.Builder
|
||||
|
||||
ret.Grow(len(transactions) * 100)
|
||||
|
||||
actualHeaderLine := headerLine
|
||||
actualDataLineFormat := dataLineFormat
|
||||
|
||||
if separator != "," {
|
||||
actualHeaderLine = strings.Replace(headerLine, ",", separator, -1)
|
||||
actualDataLineFormat = strings.Replace(dataLineFormat, ",", separator, -1)
|
||||
}
|
||||
|
||||
ret.WriteString(actualHeaderLine)
|
||||
|
||||
for i := 0; i < len(transactions); i++ {
|
||||
transaction := transactions[i]
|
||||
|
||||
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
|
||||
continue
|
||||
}
|
||||
|
||||
transactionTimeZone := time.FixedZone("Transaction Timezone", int(transaction.TimezoneUtcOffset)*60)
|
||||
transactionTime := utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime), transactionTimeZone)
|
||||
transactionTimezone := utils.FormatTimezoneOffset(transactionTimeZone)
|
||||
transactionType := e.getTransactionTypeName(transaction.Type)
|
||||
category := e.replaceDelimiters(e.getTransactionCategoryName(transaction.CategoryId, categoryMap), separator)
|
||||
subCategory := e.replaceDelimiters(e.getTransactionSubCategoryName(transaction.CategoryId, categoryMap), separator)
|
||||
account := e.replaceDelimiters(e.getAccountName(transaction.AccountId, accountMap), separator)
|
||||
accountCurrency := e.getAccountCurrency(transaction.AccountId, accountMap)
|
||||
amount := utils.FormatAmount(transaction.Amount)
|
||||
account2 := ""
|
||||
account2Currency := ""
|
||||
account2Amount := ""
|
||||
geoLocation := ""
|
||||
|
||||
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
|
||||
account2 = e.replaceDelimiters(e.getAccountName(transaction.RelatedAccountId, accountMap), separator)
|
||||
account2Currency = e.getAccountCurrency(transaction.RelatedAccountId, accountMap)
|
||||
account2Amount = utils.FormatAmount(transaction.RelatedAccountAmount)
|
||||
}
|
||||
|
||||
if transaction.GeoLongitude != 0 || transaction.GeoLatitude != 0 {
|
||||
geoLocation = fmt.Sprintf("%f%s%f", transaction.GeoLongitude, geoLocationSeparator, transaction.GeoLatitude)
|
||||
}
|
||||
|
||||
tags := e.replaceDelimiters(e.getTags(transaction.TransactionId, allTagIndexes, tagMap), separator)
|
||||
comment := e.replaceDelimiters(transaction.Comment, separator)
|
||||
|
||||
ret.WriteString(fmt.Sprintf(actualDataLineFormat, transactionTime, transactionTimezone, transactionType, category, subCategory, account, accountCurrency, amount, account2, account2Currency, account2Amount, geoLocation, tags, comment))
|
||||
}
|
||||
|
||||
return []byte(ret.String()), nil
|
||||
}
|
||||
|
||||
func (e *EzBookKeepingPlainFileConverter) parseImportedData(user *models.User, separator string, data []byte, defaultTimezoneOffset int16, accountMap map[string]*models.Account, categoryMap map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) ([]*models.Transaction, []*models.Account, []*models.TransactionCategory, []*models.TransactionTag, error) {
|
||||
lines := strings.Split(string(data), lineSeparator)
|
||||
|
||||
if len(lines) < 2 {
|
||||
return nil, nil, nil, nil, errs.ErrOperationFailed
|
||||
}
|
||||
|
||||
headerLineItems := strings.Split(lines[0], separator)
|
||||
headerItemMap := make(map[string]int)
|
||||
|
||||
for i := 0; i < len(headerLineItems); i++ {
|
||||
headerItemMap[headerLineItems[i]] = i
|
||||
}
|
||||
|
||||
timeColumnIdx, timeColumnExists := headerItemMap["Time"]
|
||||
timezoneColumnIdx, timezoneColumnExists := headerItemMap["Timezone"]
|
||||
typeColumnIdx, typeColumnExists := headerItemMap["Type"]
|
||||
subCategoryColumnIdx, subCategoryColumnExists := headerItemMap["Sub Category"]
|
||||
accountColumnIdx, accountColumnExists := headerItemMap["Account"]
|
||||
accountCurrencyColumnIdx, accountCurrencyColumnExists := headerItemMap["Account Currency"]
|
||||
amountColumnIdx, amountColumnExists := headerItemMap["Amount"]
|
||||
account2ColumnIdx, account2ColumnExists := headerItemMap["Account2"]
|
||||
account2CurrencyColumnIdx, account2CurrencyColumnExists := headerItemMap["Account2 Currency"]
|
||||
amount2ColumnIdx, amount2ColumnExists := headerItemMap["Account2 Amount"]
|
||||
geoLocationIdx, geoLocationExists := headerItemMap["Geographic Location"]
|
||||
tagsColumnIdx, tagsColumnExists := headerItemMap["Tags"]
|
||||
descriptionColumnIdx, descriptionColumnExists := headerItemMap["Description"]
|
||||
|
||||
if !timeColumnExists || !typeColumnExists || !subCategoryColumnExists ||
|
||||
!accountColumnExists || !amountColumnExists || !account2ColumnExists || !amount2ColumnExists {
|
||||
return nil, nil, nil, nil, errs.ErrFormatInvalid
|
||||
}
|
||||
|
||||
if accountMap == nil {
|
||||
accountMap = make(map[string]*models.Account)
|
||||
}
|
||||
|
||||
if categoryMap == nil {
|
||||
categoryMap = make(map[string]*models.TransactionCategory)
|
||||
}
|
||||
|
||||
if tagMap == nil {
|
||||
tagMap = make(map[string]*models.TransactionTag)
|
||||
}
|
||||
|
||||
allNewTransactions := make(ImportTransactionSlice, 0, len(lines))
|
||||
allNewAccounts := make([]*models.Account, 0)
|
||||
allNewSubCategories := make([]*models.TransactionCategory, 0)
|
||||
allNewTags := make([]*models.TransactionTag, 0)
|
||||
|
||||
for i := 1; i < len(lines); i++ {
|
||||
line := lines[i]
|
||||
|
||||
if len(line) < 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
lineItems := strings.Split(line, separator)
|
||||
|
||||
if len(lineItems) < len(headerLineItems) {
|
||||
return nil, nil, nil, nil, errs.ErrFormatInvalid
|
||||
}
|
||||
|
||||
timezoneOffset := defaultTimezoneOffset
|
||||
|
||||
if timezoneColumnExists {
|
||||
transactionTimezone, err := utils.ParseFromTimezoneOffset(lineItems[timezoneColumnIdx])
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
timezoneOffset = utils.GetTimezoneOffsetMinutes(transactionTimezone)
|
||||
}
|
||||
|
||||
transactionTime, err := utils.ParseFromLongDateTime(lineItems[timeColumnIdx], timezoneOffset)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
transactionDbType, err := e.getTransactionDbType(lineItems[typeColumnIdx])
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
categoryId := int64(0)
|
||||
|
||||
if transactionDbType != models.TRANSACTION_DB_TYPE_MODIFY_BALANCE {
|
||||
transactionCategoryType, err := e.getTransactionCategoryType(transactionDbType)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
subCategoryName := lineItems[subCategoryColumnIdx]
|
||||
|
||||
if subCategoryName == "" {
|
||||
return nil, nil, nil, nil, errs.ErrFormatInvalid
|
||||
}
|
||||
|
||||
subCategory, exists := categoryMap[subCategoryName]
|
||||
|
||||
if !exists {
|
||||
subCategory = e.createNewTransactionCategoryModel(user.Uid, subCategoryName, transactionCategoryType)
|
||||
allNewSubCategories = append(allNewSubCategories, subCategory)
|
||||
categoryMap[subCategoryName] = subCategory
|
||||
}
|
||||
|
||||
categoryId = subCategory.CategoryId
|
||||
}
|
||||
|
||||
accountName := lineItems[accountColumnIdx]
|
||||
|
||||
if accountName == "" {
|
||||
return nil, nil, nil, nil, errs.ErrFormatInvalid
|
||||
}
|
||||
|
||||
account, exists := accountMap[accountName]
|
||||
|
||||
if !exists {
|
||||
currency := user.DefaultCurrency
|
||||
|
||||
if accountCurrencyColumnExists {
|
||||
currency = lineItems[accountCurrencyColumnIdx]
|
||||
|
||||
if _, ok := validators.AllCurrencyNames[currency]; !ok {
|
||||
return nil, nil, nil, nil, errs.ErrAccountCurrencyInvalid
|
||||
}
|
||||
}
|
||||
|
||||
account = e.createNewAccountModel(user.Uid, accountName, currency)
|
||||
allNewAccounts = append(allNewAccounts, account)
|
||||
accountMap[accountName] = account
|
||||
}
|
||||
|
||||
if accountCurrencyColumnExists {
|
||||
if account.Currency != lineItems[accountCurrencyColumnIdx] {
|
||||
return nil, nil, nil, nil, errs.ErrAccountCurrencyInvalid
|
||||
}
|
||||
}
|
||||
|
||||
amount, err := utils.ParseAmount(lineItems[amountColumnIdx])
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
relatedAccountId := int64(0)
|
||||
relatedAccountAmount := int64(0)
|
||||
|
||||
if transactionDbType == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
|
||||
account2Name := lineItems[account2ColumnIdx]
|
||||
|
||||
if account2Name == "" {
|
||||
return nil, nil, nil, nil, errs.ErrFormatInvalid
|
||||
}
|
||||
|
||||
account2, exists := accountMap[account2Name]
|
||||
|
||||
if !exists {
|
||||
currency := user.DefaultCurrency
|
||||
|
||||
if accountCurrencyColumnExists {
|
||||
currency = lineItems[account2CurrencyColumnIdx]
|
||||
|
||||
if _, ok := validators.AllCurrencyNames[currency]; !ok {
|
||||
return nil, nil, nil, nil, errs.ErrAccountCurrencyInvalid
|
||||
}
|
||||
}
|
||||
|
||||
account2 = e.createNewAccountModel(user.Uid, account2Name, currency)
|
||||
allNewAccounts = append(allNewAccounts, account2)
|
||||
accountMap[account2Name] = account2
|
||||
}
|
||||
|
||||
if account2CurrencyColumnExists {
|
||||
if account2.Currency != lineItems[account2CurrencyColumnIdx] {
|
||||
return nil, nil, nil, nil, errs.ErrAccountCurrencyInvalid
|
||||
}
|
||||
}
|
||||
|
||||
relatedAccountId = account2.AccountId
|
||||
relatedAccountAmount, err = utils.ParseAmount(lineItems[amount2ColumnIdx])
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
geoLongitude := float64(0)
|
||||
geoLatitude := float64(0)
|
||||
|
||||
if geoLocationExists {
|
||||
geoLocationItems := strings.Split(lineItems[geoLocationIdx], geoLocationSeparator)
|
||||
|
||||
if len(geoLocationItems) == 2 {
|
||||
geoLongitude, err = utils.StringToFloat64(geoLocationItems[0])
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
geoLatitude, err = utils.StringToFloat64(geoLocationItems[1])
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if tagsColumnExists {
|
||||
tagNames := strings.Split(lineItems[tagsColumnIdx], transactionTagSeparator)
|
||||
|
||||
for i := 0; i < len(tagNames); i++ {
|
||||
tagName := tagNames[i]
|
||||
|
||||
if tagName == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
tag, exists := tagMap[tagName]
|
||||
|
||||
if !exists {
|
||||
tag = e.createNewTransactionTagModel(user.Uid, tagName)
|
||||
allNewTags = append(allNewTags, tag)
|
||||
tagMap[tagName] = tag
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
description := ""
|
||||
|
||||
if descriptionColumnExists {
|
||||
description = lineItems[descriptionColumnIdx]
|
||||
}
|
||||
|
||||
transaction := &models.Transaction{
|
||||
Uid: user.Uid,
|
||||
Type: transactionDbType,
|
||||
CategoryId: categoryId,
|
||||
TransactionTime: utils.GetMinTransactionTimeFromUnixTime(transactionTime.Unix()),
|
||||
TimezoneUtcOffset: timezoneOffset,
|
||||
AccountId: account.AccountId,
|
||||
Amount: amount,
|
||||
HideAmount: false,
|
||||
RelatedAccountId: relatedAccountId,
|
||||
RelatedAccountAmount: relatedAccountAmount,
|
||||
Comment: description,
|
||||
GeoLongitude: geoLongitude,
|
||||
GeoLatitude: geoLatitude,
|
||||
CreatedIp: "127.0.0.1",
|
||||
}
|
||||
|
||||
allNewTransactions = append(allNewTransactions, transaction)
|
||||
}
|
||||
|
||||
sort.Sort(allNewTransactions)
|
||||
|
||||
return allNewTransactions, allNewAccounts, allNewSubCategories, allNewTags, nil
|
||||
}
|
||||
|
||||
func (e *EzBookKeepingPlainFileConverter) getTransactionTypeName(transactionDbType models.TransactionDbType) string {
|
||||
if transactionDbType == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE {
|
||||
return "Balance Modification"
|
||||
} else if transactionDbType == models.TRANSACTION_DB_TYPE_INCOME {
|
||||
return "Income"
|
||||
} else if transactionDbType == models.TRANSACTION_DB_TYPE_EXPENSE {
|
||||
return "Expense"
|
||||
} else if transactionDbType == models.TRANSACTION_DB_TYPE_TRANSFER_OUT || transactionDbType == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
|
||||
return "Transfer"
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EzBookKeepingPlainFileConverter) getTransactionDbType(transactionTypeName string) (models.TransactionDbType, error) {
|
||||
if transactionTypeName == "Balance Modification" {
|
||||
return models.TRANSACTION_DB_TYPE_MODIFY_BALANCE, nil
|
||||
} else if transactionTypeName == "Income" {
|
||||
return models.TRANSACTION_DB_TYPE_INCOME, nil
|
||||
} else if transactionTypeName == "Expense" {
|
||||
return models.TRANSACTION_DB_TYPE_EXPENSE, nil
|
||||
} else if transactionTypeName == "Transfer" {
|
||||
return models.TRANSACTION_DB_TYPE_TRANSFER_OUT, nil
|
||||
} else {
|
||||
return 0, errs.ErrTransactionTypeInvalid
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EzBookKeepingPlainFileConverter) getTransactionCategoryType(transactionType models.TransactionDbType) (models.TransactionCategoryType, error) {
|
||||
if transactionType == models.TRANSACTION_DB_TYPE_INCOME {
|
||||
return models.CATEGORY_TYPE_INCOME, nil
|
||||
} else if transactionType == models.TRANSACTION_DB_TYPE_EXPENSE {
|
||||
return models.CATEGORY_TYPE_EXPENSE, nil
|
||||
} else if transactionType == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
|
||||
return models.CATEGORY_TYPE_TRANSFER, nil
|
||||
} else {
|
||||
return 0, errs.ErrTransactionTypeInvalid
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EzBookKeepingPlainFileConverter) getTransactionCategoryName(categoryId int64, categoryMap map[int64]*models.TransactionCategory) string {
|
||||
category, exists := categoryMap[categoryId]
|
||||
|
||||
if !exists {
|
||||
return ""
|
||||
}
|
||||
|
||||
if category.ParentCategoryId == 0 {
|
||||
return category.Name
|
||||
}
|
||||
|
||||
parentCategory, exists := categoryMap[category.ParentCategoryId]
|
||||
|
||||
if !exists {
|
||||
return ""
|
||||
}
|
||||
|
||||
return parentCategory.Name
|
||||
}
|
||||
|
||||
func (e *EzBookKeepingPlainFileConverter) getTransactionSubCategoryName(categoryId int64, categoryMap map[int64]*models.TransactionCategory) string {
|
||||
category, exists := categoryMap[categoryId]
|
||||
|
||||
if exists {
|
||||
return category.Name
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EzBookKeepingPlainFileConverter) getAccountName(accountId int64, accountMap map[int64]*models.Account) string {
|
||||
account, exists := accountMap[accountId]
|
||||
|
||||
if exists {
|
||||
return account.Name
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EzBookKeepingPlainFileConverter) getAccountCurrency(accountId int64, accountMap map[int64]*models.Account) string {
|
||||
account, exists := accountMap[accountId]
|
||||
|
||||
if exists {
|
||||
return account.Currency
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EzBookKeepingPlainFileConverter) getTags(transactionId int64, allTagIndexes map[int64][]int64, tagMap map[int64]*models.TransactionTag) string {
|
||||
tagIndexes, exists := allTagIndexes[transactionId]
|
||||
|
||||
if !exists {
|
||||
return ""
|
||||
}
|
||||
|
||||
var ret strings.Builder
|
||||
|
||||
for i := 0; i < len(tagIndexes); i++ {
|
||||
tagIndex := tagIndexes[i]
|
||||
tag, exists := tagMap[tagIndex]
|
||||
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
if ret.Len() > 0 {
|
||||
ret.WriteString(transactionTagSeparator)
|
||||
}
|
||||
|
||||
ret.WriteString(strings.Replace(tag.Name, transactionTagSeparator, " ", -1))
|
||||
}
|
||||
|
||||
return ret.String()
|
||||
}
|
||||
|
||||
func (e *EzBookKeepingPlainFileConverter) replaceDelimiters(text string, separator string) string {
|
||||
text = strings.Replace(text, separator, " ", -1)
|
||||
text = strings.Replace(text, "\r\n", " ", -1)
|
||||
text = strings.Replace(text, "\r", " ", -1)
|
||||
text = strings.Replace(text, "\n", " ", -1)
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
func (e *EzBookKeepingPlainFileConverter) createNewAccountModel(uid int64, accountName string, currency string) *models.Account {
|
||||
return &models.Account{
|
||||
Uid: uid,
|
||||
Name: accountName,
|
||||
Currency: currency,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EzBookKeepingPlainFileConverter) createNewTransactionCategoryModel(uid int64, categoryName string, transactionCategoryType models.TransactionCategoryType) *models.TransactionCategory {
|
||||
return &models.TransactionCategory{
|
||||
Uid: uid,
|
||||
Name: categoryName,
|
||||
Type: transactionCategoryType,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EzBookKeepingPlainFileConverter) createNewTransactionTagModel(uid int64, tagName string) *models.TransactionTag {
|
||||
return &models.TransactionTag{
|
||||
Uid: uid,
|
||||
Name: tagName,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package converters
|
||||
|
||||
// ezBookKeepingTransactionDataCSVFileConverter defines the structure of CSV file converter
|
||||
type ezBookKeepingTransactionDataCSVFileConverter struct {
|
||||
ezBookKeepingTransactionDataPlainTextConverter
|
||||
}
|
||||
|
||||
// Initialize an ezbookkeeping transaction data csv file converter singleton instance
|
||||
var (
|
||||
EzBookKeepingTransactionDataCSVFileConverter = &ezBookKeepingTransactionDataCSVFileConverter{
|
||||
ezBookKeepingTransactionDataPlainTextConverter{
|
||||
DataTableTransactionDataConverter: DataTableTransactionDataConverter{
|
||||
dataColumnMapping: ezbookkeepingDataColumnNameMapping,
|
||||
transactionTypeMapping: ezbookkeepingTransactionTypeNameMapping,
|
||||
transactionTypeNameMapping: ezbookkeepingNameTransactionTypeMapping,
|
||||
columnSeparator: ",",
|
||||
lineSeparator: "\n",
|
||||
geoLocationSeparator: " ",
|
||||
transactionTagSeparator: ";",
|
||||
},
|
||||
columns: ezbookkeepingDataColumns,
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,82 @@
|
||||
package converters
|
||||
|
||||
import (
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
)
|
||||
|
||||
// ezBookKeepingTransactionDataPlainTextConverter defines the structure of plain file converter for transaction data
|
||||
type ezBookKeepingTransactionDataPlainTextConverter struct {
|
||||
DataTableTransactionDataConverter
|
||||
columns []DataTableColumn
|
||||
}
|
||||
|
||||
var ezbookkeepingDataColumnNameMapping = map[DataTableColumn]string{
|
||||
DATA_TABLE_TRANSACTION_TIME: "Time",
|
||||
DATA_TABLE_TRANSACTION_TIMEZONE: "Timezone",
|
||||
DATA_TABLE_TRANSACTION_TYPE: "Type",
|
||||
DATA_TABLE_CATEGORY: "Category",
|
||||
DATA_TABLE_SUB_CATEGORY: "Sub Category",
|
||||
DATA_TABLE_ACCOUNT_NAME: "Account",
|
||||
DATA_TABLE_ACCOUNT_CURRENCY: "Account Currency",
|
||||
DATA_TABLE_AMOUNT: "Amount",
|
||||
DATA_TABLE_RELATED_ACCOUNT_NAME: "Account2",
|
||||
DATA_TABLE_RELATED_ACCOUNT_CURRENCY: "Account2 Currency",
|
||||
DATA_TABLE_RELATED_AMOUNT: "Account2 Amount",
|
||||
DATA_TABLE_GEOGRAPHIC_LOCATION: "Geographic Location",
|
||||
DATA_TABLE_TAGS: "Tags",
|
||||
DATA_TABLE_DESCRIPTION: "Description",
|
||||
}
|
||||
|
||||
var ezbookkeepingTransactionTypeNameMapping = map[models.TransactionDbType]string{
|
||||
models.TRANSACTION_DB_TYPE_MODIFY_BALANCE: "Balance Modification",
|
||||
models.TRANSACTION_DB_TYPE_INCOME: "Income",
|
||||
models.TRANSACTION_DB_TYPE_EXPENSE: "Expense",
|
||||
models.TRANSACTION_DB_TYPE_TRANSFER_OUT: "Transfer",
|
||||
models.TRANSACTION_DB_TYPE_TRANSFER_IN: "Transfer",
|
||||
}
|
||||
|
||||
var ezbookkeepingNameTransactionTypeMapping = map[string]models.TransactionDbType{
|
||||
"Balance Modification": models.TRANSACTION_DB_TYPE_MODIFY_BALANCE,
|
||||
"Income": models.TRANSACTION_DB_TYPE_INCOME,
|
||||
"Expense": models.TRANSACTION_DB_TYPE_EXPENSE,
|
||||
"Transfer": models.TRANSACTION_DB_TYPE_TRANSFER_OUT,
|
||||
}
|
||||
|
||||
var ezbookkeepingDataColumns = []DataTableColumn{
|
||||
DATA_TABLE_TRANSACTION_TIME,
|
||||
DATA_TABLE_TRANSACTION_TIMEZONE,
|
||||
DATA_TABLE_TRANSACTION_TYPE,
|
||||
DATA_TABLE_CATEGORY,
|
||||
DATA_TABLE_SUB_CATEGORY,
|
||||
DATA_TABLE_ACCOUNT_NAME,
|
||||
DATA_TABLE_ACCOUNT_CURRENCY,
|
||||
DATA_TABLE_AMOUNT,
|
||||
DATA_TABLE_RELATED_ACCOUNT_NAME,
|
||||
DATA_TABLE_RELATED_ACCOUNT_CURRENCY,
|
||||
DATA_TABLE_RELATED_AMOUNT,
|
||||
DATA_TABLE_GEOGRAPHIC_LOCATION,
|
||||
DATA_TABLE_TAGS,
|
||||
DATA_TABLE_DESCRIPTION,
|
||||
}
|
||||
|
||||
// ToExportedContent returns the exported plain text transaction data
|
||||
func (c *ezBookKeepingTransactionDataPlainTextConverter) ToExportedContent(uid int64, transactions []*models.Transaction, accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTagIndexes map[int64][]int64) ([]byte, error) {
|
||||
dataTableBuilder := createNewezbookkeepingTransactionPlainTextDataTableBuilder(len(transactions), c.columns, c.dataColumnMapping, c.columnSeparator, c.lineSeparator)
|
||||
err := c.buildExportedContent(dataTableBuilder, uid, transactions, accountMap, categoryMap, tagMap, allTagIndexes)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return []byte(dataTableBuilder.String()), nil
|
||||
}
|
||||
|
||||
func (c *ezBookKeepingTransactionDataPlainTextConverter) ParseImportedData(user *models.User, data []byte, defaultTimezoneOffset int16, accountMap map[string]*models.Account, categoryMap map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) ([]*models.Transaction, []*models.Account, []*models.TransactionCategory, []*models.TransactionTag, error) {
|
||||
dataTable, err := createNewezbookkeepingTransactionPlainTextDataTable(string(data), c.columnSeparator, c.lineSeparator)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
return c.parseImportedData(user, dataTable, defaultTimezoneOffset, accountMap, categoryMap, tagMap)
|
||||
}
|
||||
+49
-49
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
func TestEzBookKeepingPlainFileConverterToExportedContent(t *testing.T) {
|
||||
converter := &EzBookKeepingPlainFileConverter{}
|
||||
converter := EzBookKeepingTransactionDataCSVFileConverter
|
||||
|
||||
transactions := make([]*models.Transaction, 3)
|
||||
transactions[0] = &models.Transaction{
|
||||
@@ -116,21 +116,21 @@ func TestEzBookKeepingPlainFileConverterToExportedContent(t *testing.T) {
|
||||
"2024-09-01 12:34:56,+08:00,Income,Test Category,Test Sub Category,Test Account,CNY,123.45,,,,123.450000 45.670000,Test Tag;Test Tag2,Hello World\n" +
|
||||
"2024-09-01 12:34:56,+00:00,Expense,Test Category2,Test Sub Category2,Test Account,CNY,-0.10,,,,,Test Tag,Foo#Bar\n" +
|
||||
"2024-09-01 12:34:56,-05:00,Transfer,Test Category3,Test Sub Category3,Test Account,CNY,123.45,Test Account2,USD,17.35,,Test Tag2,T\te s t test\n"
|
||||
actualContent, err := converter.toExportedContent(123, ",", transactions, accountMap, categoryMap, tagMap, allTagIndexes)
|
||||
actualContent, err := converter.ToExportedContent(123, transactions, accountMap, categoryMap, tagMap, allTagIndexes)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expectedContent, string(actualContent))
|
||||
}
|
||||
|
||||
func TestEzBookKeepingPlainFileConverterParseImportedData_MinimumValidData(t *testing.T) {
|
||||
converter := &EzBookKeepingPlainFileConverter{}
|
||||
converter := EzBookKeepingTransactionDataCSVFileConverter
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
allNewTransactions, allNewAccounts, allNewSubCategories, allNewTags, err := converter.parseImportedData(user, ",", []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
|
||||
allNewTransactions, allNewAccounts, allNewSubCategories, allNewTags, err := converter.ParseImportedData(user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
|
||||
"2024-09-01 00:00:00,Balance Modification,,Test Account,123.45,,\n"+
|
||||
"2024-09-01 01:23:45,Income,Test Category,Test Account,0.12,,\n"+
|
||||
"2024-09-01 12:34:56,Expense,Test Category2,Test Account,1.00,,\n"+
|
||||
@@ -182,44 +182,44 @@ func TestEzBookKeepingPlainFileConverterParseImportedData_MinimumValidData(t *te
|
||||
}
|
||||
|
||||
func TestEzBookKeepingPlainFileConverterParseImportedData_ParseInvalidTime(t *testing.T) {
|
||||
converter := &EzBookKeepingPlainFileConverter{}
|
||||
converter := EzBookKeepingTransactionDataCSVFileConverter
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
_, _, _, _, err := converter.parseImportedData(user, ",", []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
|
||||
_, _, _, _, err := converter.ParseImportedData(user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
|
||||
"2024-09-01T12:34:56,Expense,Test Category,Test Account,123.45,,"), 0, nil, nil, nil)
|
||||
assert.NotNil(t, err)
|
||||
|
||||
_, _, _, _, err = converter.parseImportedData(user, ",", []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
|
||||
_, _, _, _, err = converter.ParseImportedData(user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
|
||||
"09/01/2024 12:34:56,Expense,Test Category,Test Account,123.45,,"), 0, nil, nil, nil)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestEzBookKeepingPlainFileConverterParseImportedData_ParseInvalidType(t *testing.T) {
|
||||
converter := &EzBookKeepingPlainFileConverter{}
|
||||
converter := EzBookKeepingTransactionDataCSVFileConverter
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
_, _, _, _, err := converter.parseImportedData(user, ",", []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
|
||||
_, _, _, _, err := converter.ParseImportedData(user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
|
||||
"2024-09-01 12:34:56,Type,Test Category,Test Account,123.45,,"), 0, nil, nil, nil)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestEzBookKeepingPlainFileConverterParseImportedData_ParseValidTimezone(t *testing.T) {
|
||||
converter := &EzBookKeepingPlainFileConverter{}
|
||||
converter := EzBookKeepingTransactionDataCSVFileConverter
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
allNewTransactions, _, _, _, err := converter.parseImportedData(user, ",", []byte("Time,Timezone,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
|
||||
allNewTransactions, _, _, _, err := converter.ParseImportedData(user, []byte("Time,Timezone,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
|
||||
"2024-09-01 12:34:56,+08:00,Expense,Test Category,Test Account,123.45,,"), 0, nil, nil, nil)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 1, len(allNewTransactions))
|
||||
@@ -227,57 +227,57 @@ func TestEzBookKeepingPlainFileConverterParseImportedData_ParseValidTimezone(t *
|
||||
}
|
||||
|
||||
func TestEzBookKeepingPlainFileConverterParseImportedData_ParseInvalidTimezone(t *testing.T) {
|
||||
converter := &EzBookKeepingPlainFileConverter{}
|
||||
converter := EzBookKeepingTransactionDataCSVFileConverter
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
_, _, _, _, err := converter.parseImportedData(user, ",", []byte("Time,Timezone,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
|
||||
_, _, _, _, err := converter.ParseImportedData(user, []byte("Time,Timezone,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
|
||||
"2024-09-01 12:34:56,Asia/Shanghai,Expense,Test Category,Test Account,123.45,,"), 0, nil, nil, nil)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestEzBookKeepingPlainFileConverterParseImportedData_ParseInvalidSubCategoryName(t *testing.T) {
|
||||
converter := &EzBookKeepingPlainFileConverter{}
|
||||
converter := EzBookKeepingTransactionDataCSVFileConverter
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
_, _, _, _, err := converter.parseImportedData(user, ",", []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
|
||||
_, _, _, _, err := converter.ParseImportedData(user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
|
||||
"2024-09-01 12:34:56,Expense,,Test Account,123.45,,"), 0, nil, nil, nil)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestEzBookKeepingPlainFileConverterParseImportedData_ParseInvalidAccountName(t *testing.T) {
|
||||
converter := &EzBookKeepingPlainFileConverter{}
|
||||
converter := EzBookKeepingTransactionDataCSVFileConverter
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
_, _, _, _, err := converter.parseImportedData(user, ",", []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
|
||||
_, _, _, _, err := converter.ParseImportedData(user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
|
||||
"2024-09-01 12:34:56,Expense,Test Category,,123.45,,"), 0, nil, nil, nil)
|
||||
assert.NotNil(t, err)
|
||||
|
||||
_, _, _, _, err = converter.parseImportedData(user, ",", []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
|
||||
_, _, _, _, err = converter.ParseImportedData(user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
|
||||
"2024-09-01 12:34:56,Transfer,Test Category,Test Account,123.45,,123.45"), 0, nil, nil, nil)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestEzBookKeepingPlainFileConverterParseImportedData_ParseValidAccountCurrency(t *testing.T) {
|
||||
converter := &EzBookKeepingPlainFileConverter{}
|
||||
converter := EzBookKeepingTransactionDataCSVFileConverter
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
allNewTransactions, allNewAccounts, _, _, err := converter.parseImportedData(user, ",", []byte("Time,Type,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount\n"+
|
||||
allNewTransactions, allNewAccounts, _, _, err := converter.ParseImportedData(user, []byte("Time,Type,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount\n"+
|
||||
"2024-09-01 01:23:45,Balance Modification,Test Category,Test Account,USD,123.45,,,\n"+
|
||||
"2024-09-01 12:34:56,Transfer,Test Category2,Test Account,USD,1.23,Test Account2,EUR,1.10"), 0, nil, nil, nil)
|
||||
|
||||
@@ -296,67 +296,67 @@ func TestEzBookKeepingPlainFileConverterParseImportedData_ParseValidAccountCurre
|
||||
}
|
||||
|
||||
func TestEzBookKeepingPlainFileConverterParseImportedData_ParseInvalidAccountCurrency(t *testing.T) {
|
||||
converter := &EzBookKeepingPlainFileConverter{}
|
||||
converter := EzBookKeepingTransactionDataCSVFileConverter
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
_, _, _, _, err := converter.parseImportedData(user, ",", []byte("Time,Type,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount\n"+
|
||||
_, _, _, _, err := converter.ParseImportedData(user, []byte("Time,Type,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount\n"+
|
||||
"2024-09-01 01:23:45,Balance Modification,,Test Account,USD,123.45,,,\n"+
|
||||
"2024-09-01 12:34:56,Transfer,Test Category3,Test Account,CNY,1.23,Test Account2,EUR,1.10"), 0, nil, nil, nil)
|
||||
assert.NotNil(t, err)
|
||||
|
||||
_, _, _, _, err = converter.parseImportedData(user, ",", []byte("Time,Type,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount\n"+
|
||||
_, _, _, _, err = converter.ParseImportedData(user, []byte("Time,Type,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount\n"+
|
||||
"2024-09-01 01:23:45,Balance Modification,,Test Account,USD,123.45,,,\n"+
|
||||
"2024-09-01 12:34:56,Transfer,Test Category3,Test Account2,CNY,1.23,Test Account,EUR,1.10"), 0, nil, nil, nil)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestEzBookKeepingPlainFileConverterParseImportedData_ParseNotSupportedCurrency(t *testing.T) {
|
||||
converter := &EzBookKeepingPlainFileConverter{}
|
||||
converter := EzBookKeepingTransactionDataCSVFileConverter
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
_, _, _, _, err := converter.parseImportedData(user, ",", []byte("Time,Type,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount\n"+
|
||||
_, _, _, _, err := converter.ParseImportedData(user, []byte("Time,Type,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount\n"+
|
||||
"2024-09-01 01:23:45,Balance Modification,Test Category,Test Account,XXX,123.45,,,"), 0, nil, nil, nil)
|
||||
assert.NotNil(t, err)
|
||||
|
||||
_, _, _, _, err = converter.parseImportedData(user, ",", []byte("Time,Type,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount\n"+
|
||||
_, _, _, _, err = converter.ParseImportedData(user, []byte("Time,Type,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount\n"+
|
||||
"2024-09-01 01:23:45,Transfer,Test Category,Test Account,USD,123.45,Test Account2,XXX,123.45"), 0, nil, nil, nil)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestEzBookKeepingPlainFileConverterParseImportedData_ParseInvalidAmount(t *testing.T) {
|
||||
converter := &EzBookKeepingPlainFileConverter{}
|
||||
converter := EzBookKeepingTransactionDataCSVFileConverter
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
_, _, _, _, err := converter.parseImportedData(user, ",", []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
|
||||
_, _, _, _, err := converter.ParseImportedData(user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
|
||||
"2024-09-01 12:34:56,Expense,Test Category,Test Account,123 45,,"), 0, nil, nil, nil)
|
||||
assert.NotNil(t, err)
|
||||
|
||||
_, _, _, _, err = converter.parseImportedData(user, ",", []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
|
||||
_, _, _, _, err = converter.ParseImportedData(user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
|
||||
"2024-09-01 12:34:56,Transfer,Test Category,Test Account,123.45,Test Account2,123 45"), 0, nil, nil, nil)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestEzBookKeepingPlainFileConverterParseImportedData_ParseValidGeographicLocation(t *testing.T) {
|
||||
converter := &EzBookKeepingPlainFileConverter{}
|
||||
converter := EzBookKeepingTransactionDataCSVFileConverter
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
allNewTransactions, _, _, _, err := converter.parseImportedData(user, ",", []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Geographic Location\n"+
|
||||
allNewTransactions, _, _, _, err := converter.ParseImportedData(user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Geographic Location\n"+
|
||||
"2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,,,123.45 45.56"), 0, nil, nil, nil)
|
||||
|
||||
assert.Nil(t, err)
|
||||
@@ -366,38 +366,38 @@ func TestEzBookKeepingPlainFileConverterParseImportedData_ParseValidGeographicLo
|
||||
}
|
||||
|
||||
func TestEzBookKeepingPlainFileConverterParseImportedData_ParseInvalidGeographicLocation(t *testing.T) {
|
||||
converter := &EzBookKeepingPlainFileConverter{}
|
||||
converter := EzBookKeepingTransactionDataCSVFileConverter
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
allNewTransactions, _, _, _, err := converter.parseImportedData(user, ",", []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Geographic Location\n"+
|
||||
allNewTransactions, _, _, _, err := converter.ParseImportedData(user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Geographic Location\n"+
|
||||
"2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,,,1"), 0, nil, nil, nil)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 1, len(allNewTransactions))
|
||||
assert.Equal(t, float64(0), allNewTransactions[0].GeoLongitude)
|
||||
assert.Equal(t, float64(0), allNewTransactions[0].GeoLatitude)
|
||||
|
||||
_, _, _, _, err = converter.parseImportedData(user, ",", []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Geographic Location\n"+
|
||||
_, _, _, _, err = converter.ParseImportedData(user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Geographic Location\n"+
|
||||
"2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,,,a b"), 0, nil, nil, nil)
|
||||
assert.NotNil(t, err)
|
||||
|
||||
_, _, _, _, err = converter.parseImportedData(user, ",", []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Geographic Location\n"+
|
||||
_, _, _, _, err = converter.ParseImportedData(user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Geographic Location\n"+
|
||||
"2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,,,1 "), 0, nil, nil, nil)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestEzBookKeepingPlainFileConverterParseImportedData_ParseTag(t *testing.T) {
|
||||
converter := &EzBookKeepingPlainFileConverter{}
|
||||
converter := EzBookKeepingTransactionDataCSVFileConverter
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
_, _, _, allNewTags, err := converter.parseImportedData(user, ",", []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Tags\n"+
|
||||
_, _, _, allNewTags, err := converter.ParseImportedData(user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Tags\n"+
|
||||
"2024-09-01 00:00:00,Balance Modification,,Test Account,123.45,,,foo;;bar.;#test;hello\tworld;;"), 0, nil, nil, nil)
|
||||
|
||||
assert.Nil(t, err)
|
||||
@@ -418,14 +418,14 @@ func TestEzBookKeepingPlainFileConverterParseImportedData_ParseTag(t *testing.T)
|
||||
}
|
||||
|
||||
func TestEzBookKeepingPlainFileConverterParseImportedData_ParseDescription(t *testing.T) {
|
||||
converter := &EzBookKeepingPlainFileConverter{}
|
||||
converter := EzBookKeepingTransactionDataCSVFileConverter
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
allNewTransactions, _, _, _, err := converter.parseImportedData(user, ",", []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Description\n"+
|
||||
allNewTransactions, _, _, _, err := converter.ParseImportedData(user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Description\n"+
|
||||
"2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,,,foo bar\t#test"), 0, nil, nil, nil)
|
||||
|
||||
assert.Nil(t, err)
|
||||
@@ -434,45 +434,45 @@ func TestEzBookKeepingPlainFileConverterParseImportedData_ParseDescription(t *te
|
||||
}
|
||||
|
||||
func TestEzBookKeepingPlainFileConverterParseImportedData_MissingRequiredColumn(t *testing.T) {
|
||||
converter := &EzBookKeepingPlainFileConverter{}
|
||||
converter := EzBookKeepingTransactionDataCSVFileConverter
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
_, _, _, _, err := converter.parseImportedData(user, ",", []byte(""), 0, nil, nil, nil)
|
||||
_, _, _, _, err := converter.ParseImportedData(user, []byte(""), 0, nil, nil, nil)
|
||||
assert.NotNil(t, err)
|
||||
|
||||
_, _, _, _, err = converter.parseImportedData(user, ",", []byte("Timezone,Type,Category,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
|
||||
_, _, _, _, err = converter.ParseImportedData(user, []byte("Timezone,Type,Category,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
|
||||
"+08:00,Balance Modification,Test Category,Test Sub Category,Test Account,CNY,123.45,,,,,,"), 0, nil, nil, nil)
|
||||
assert.NotNil(t, err)
|
||||
|
||||
_, _, _, _, err = converter.parseImportedData(user, ",", []byte("Time,Category,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
|
||||
_, _, _, _, err = converter.ParseImportedData(user, []byte("Time,Category,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
|
||||
"2024-09-01 00:00:00,+08:00,Test Category,Test Sub Category,Test Account,CNY,123.45,,,,,,"), 0, nil, nil, nil)
|
||||
assert.NotNil(t, err)
|
||||
|
||||
_, _, _, _, err = converter.parseImportedData(user, ",", []byte("Time,Type,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
|
||||
_, _, _, _, err = converter.ParseImportedData(user, []byte("Time,Type,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
|
||||
"2024-09-01 00:00:00,+08:00,Balance Modification,Test Account,CNY,123.45,,,,,,"), 0, nil, nil, nil)
|
||||
assert.NotNil(t, err)
|
||||
|
||||
_, _, _, _, err = converter.parseImportedData(user, ",", []byte("Time,Timezone,Type,Category,Sub Category,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
|
||||
_, _, _, _, err = converter.ParseImportedData(user, []byte("Time,Timezone,Type,Category,Sub Category,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
|
||||
"2024-09-01 00:00:00,+08:00,Balance Modification,Test Category,Test Sub Category,CNY,123.45,,,,,,"), 0, nil, nil, nil)
|
||||
assert.NotNil(t, err)
|
||||
|
||||
_, _, _, _, err = converter.parseImportedData(user, ",", []byte("Time,Timezone,Type,Category,Sub Category,Account,Account Currency,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
|
||||
_, _, _, _, err = converter.ParseImportedData(user, []byte("Time,Timezone,Type,Category,Sub Category,Account,Account Currency,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
|
||||
"2024-09-01 00:00:00,+08:00,Balance Modification,Test Category,Test Sub Category,Test Account,CNY,,,,,,"), 0, nil, nil, nil)
|
||||
assert.NotNil(t, err)
|
||||
|
||||
_, _, _, _, err = converter.parseImportedData(user, ",", []byte("Time,Timezone,Type,Category,Sub Category,Account,Account Currency,Amount,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
|
||||
_, _, _, _, err = converter.ParseImportedData(user, []byte("Time,Timezone,Type,Category,Sub Category,Account,Account Currency,Amount,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
|
||||
"2024-09-01 00:00:00,+08:00,Balance Modification,Test Category,Test Sub Category,Test Account,CNY,123.45,,,,,"), 0, nil, nil, nil)
|
||||
assert.NotNil(t, err)
|
||||
|
||||
_, _, _, _, err = converter.parseImportedData(user, ",", []byte("Time,Timezone,Type,Category,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Geographic Location,Tags,Description\n"+
|
||||
_, _, _, _, err = converter.ParseImportedData(user, []byte("Time,Timezone,Type,Category,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Geographic Location,Tags,Description\n"+
|
||||
"2024-09-01 00:00:00,+08:00,Balance Modification,Test Category,Test Sub Category,Test Account,CNY,123.45,,,,,"), 0, nil, nil, nil)
|
||||
assert.NotNil(t, err)
|
||||
|
||||
_, _, _, _, err = converter.parseImportedData(user, ",", []byte("Time,Timezone,Type,Category,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
|
||||
_, _, _, _, err = converter.ParseImportedData(user, []byte("Time,Timezone,Type,Category,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
|
||||
"2024-09-01 00:00:00,+08:00,Balance Modification,Test Category,Test Sub Category,Test Account,CNY,123.45,,,,,"), 0, nil, nil, nil)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package converters
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||
)
|
||||
|
||||
// ezBookKeepingTransactionPlainTextDataTable defines the structure of ezbookkeeping transaction plain text data table
|
||||
type ezBookKeepingTransactionPlainTextDataTable struct {
|
||||
columnSeparator string
|
||||
lineSeparator string
|
||||
allLines []string
|
||||
headerLineColumnNames []string
|
||||
}
|
||||
|
||||
// ezBookKeepingTransactionPlainTextDataRow defines the structure of ezbookkeeping transaction plain text data row
|
||||
type ezBookKeepingTransactionPlainTextDataRow struct {
|
||||
allItems []string
|
||||
}
|
||||
|
||||
// ezBookKeepingTransactionPlainTextDataRowIterator defines the structure of ezbookkeeping transaction plain text data row iterator
|
||||
type ezBookKeepingTransactionPlainTextDataRowIterator struct {
|
||||
dataTable *ezBookKeepingTransactionPlainTextDataTable
|
||||
currentIndex int
|
||||
}
|
||||
|
||||
// ezBookKeepingTransactionPlainTextDataTableBuilder defines the structure of ezbookkeeping transaction plain text data table builder
|
||||
type ezBookKeepingTransactionPlainTextDataTableBuilder struct {
|
||||
columnSeparator string
|
||||
lineSeparator string
|
||||
columns []DataTableColumn
|
||||
dataColumnNameMapping map[DataTableColumn]string
|
||||
dataLineFormat string
|
||||
builder *strings.Builder
|
||||
}
|
||||
|
||||
// DataRowCount returns the total count of data row
|
||||
func (t *ezBookKeepingTransactionPlainTextDataTable) DataRowCount() int {
|
||||
if len(t.allLines) < 1 {
|
||||
return 0
|
||||
}
|
||||
|
||||
return len(t.allLines) - 1
|
||||
}
|
||||
|
||||
// HeaderLineColumnNames returns the header column name list
|
||||
func (t *ezBookKeepingTransactionPlainTextDataTable) HeaderLineColumnNames() []string {
|
||||
return t.headerLineColumnNames
|
||||
}
|
||||
|
||||
// DataRowIterator returns the iterator of data row
|
||||
func (t *ezBookKeepingTransactionPlainTextDataTable) DataRowIterator() ImportedDataRowIterator {
|
||||
return &ezBookKeepingTransactionPlainTextDataRowIterator{
|
||||
dataTable: t,
|
||||
currentIndex: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// ColumnCount returns the total count of column in this data row
|
||||
func (r *ezBookKeepingTransactionPlainTextDataRow) ColumnCount() int {
|
||||
return len(r.allItems)
|
||||
}
|
||||
|
||||
// GetData returns the data in the specified column index
|
||||
func (r *ezBookKeepingTransactionPlainTextDataRow) GetData(columnIndex int) string {
|
||||
return r.allItems[columnIndex]
|
||||
}
|
||||
|
||||
// HasNext returns whether the iterator does not reach the end
|
||||
func (t *ezBookKeepingTransactionPlainTextDataRowIterator) HasNext() bool {
|
||||
return t.currentIndex+1 < len(t.dataTable.allLines)
|
||||
}
|
||||
|
||||
// Next returns the next imported data row
|
||||
func (t *ezBookKeepingTransactionPlainTextDataRowIterator) Next() ImportedDataRow {
|
||||
if t.currentIndex+1 >= len(t.dataTable.allLines) {
|
||||
return nil
|
||||
}
|
||||
|
||||
t.currentIndex++
|
||||
|
||||
rowContent := t.dataTable.allLines[t.currentIndex]
|
||||
rowItems := strings.Split(rowContent, t.dataTable.columnSeparator)
|
||||
|
||||
return &ezBookKeepingTransactionPlainTextDataRow{
|
||||
allItems: rowItems,
|
||||
}
|
||||
}
|
||||
|
||||
// AppendTransaction appends the specified transaction to data builder
|
||||
func (b *ezBookKeepingTransactionPlainTextDataTableBuilder) AppendTransaction(data map[DataTableColumn]string) {
|
||||
dataRowParams := make([]any, len(b.columns))
|
||||
|
||||
for i := 0; i < len(b.columns); i++ {
|
||||
dataRowParams[i] = data[b.columns[i]]
|
||||
}
|
||||
|
||||
b.builder.WriteString(fmt.Sprintf(b.dataLineFormat, dataRowParams...))
|
||||
}
|
||||
|
||||
// String returns the textual representation of this data
|
||||
func (b *ezBookKeepingTransactionPlainTextDataTableBuilder) String() string {
|
||||
return b.builder.String()
|
||||
}
|
||||
|
||||
func (b *ezBookKeepingTransactionPlainTextDataTableBuilder) generateHeaderLine() string {
|
||||
var ret strings.Builder
|
||||
|
||||
for i := 0; i < len(b.columns); i++ {
|
||||
if ret.Len() > 0 {
|
||||
ret.WriteString(b.columnSeparator)
|
||||
}
|
||||
|
||||
dataColumn := b.columns[i]
|
||||
columnName := b.dataColumnNameMapping[dataColumn]
|
||||
|
||||
ret.WriteString(columnName)
|
||||
}
|
||||
|
||||
ret.WriteString(b.lineSeparator)
|
||||
|
||||
return ret.String()
|
||||
}
|
||||
|
||||
func (b *ezBookKeepingTransactionPlainTextDataTableBuilder) generateDataLineFormat() string {
|
||||
var ret strings.Builder
|
||||
|
||||
for i := 0; i < len(b.columns); i++ {
|
||||
if ret.Len() > 0 {
|
||||
ret.WriteString(b.columnSeparator)
|
||||
}
|
||||
|
||||
ret.WriteString("%s")
|
||||
}
|
||||
|
||||
ret.WriteString(b.lineSeparator)
|
||||
|
||||
return ret.String()
|
||||
}
|
||||
|
||||
func createNewezbookkeepingTransactionPlainTextDataTable(content string, columnSeparator string, lineSeparator string) (*ezBookKeepingTransactionPlainTextDataTable, error) {
|
||||
allLines := strings.Split(content, lineSeparator)
|
||||
|
||||
if len(allLines) < 2 {
|
||||
return nil, errs.ErrOperationFailed
|
||||
}
|
||||
|
||||
headerLineItems := strings.Split(allLines[0], columnSeparator)
|
||||
|
||||
return &ezBookKeepingTransactionPlainTextDataTable{
|
||||
columnSeparator: columnSeparator,
|
||||
lineSeparator: lineSeparator,
|
||||
allLines: allLines,
|
||||
headerLineColumnNames: headerLineItems,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func createNewezbookkeepingTransactionPlainTextDataTableBuilder(transactionCount int, columns []DataTableColumn, dataColumnNameMapping map[DataTableColumn]string, columnSeparator string, lineSeparator string) *ezBookKeepingTransactionPlainTextDataTableBuilder {
|
||||
var builder strings.Builder
|
||||
builder.Grow(transactionCount * 100)
|
||||
|
||||
dataTableBuilder := &ezBookKeepingTransactionPlainTextDataTableBuilder{
|
||||
columnSeparator: columnSeparator,
|
||||
lineSeparator: lineSeparator,
|
||||
columns: columns,
|
||||
dataColumnNameMapping: dataColumnNameMapping,
|
||||
builder: &builder,
|
||||
}
|
||||
|
||||
headerLine := dataTableBuilder.generateHeaderLine()
|
||||
dataLineFormat := dataTableBuilder.generateDataLineFormat()
|
||||
|
||||
dataTableBuilder.builder.WriteString(headerLine)
|
||||
dataTableBuilder.dataLineFormat = dataLineFormat
|
||||
|
||||
return dataTableBuilder
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package converters
|
||||
|
||||
// ezBookKeepingTransactionDataTSVFileConverter defines the structure of TSV file converter
|
||||
type ezBookKeepingTransactionDataTSVFileConverter struct {
|
||||
ezBookKeepingTransactionDataPlainTextConverter
|
||||
}
|
||||
|
||||
// Initialize an ezbookkeeping transaction data tsv file converter singleton instance
|
||||
var (
|
||||
EzBookKeepingTransactionDataTSVFileConverter = &ezBookKeepingTransactionDataTSVFileConverter{
|
||||
ezBookKeepingTransactionDataPlainTextConverter{
|
||||
DataTableTransactionDataConverter: DataTableTransactionDataConverter{
|
||||
dataColumnMapping: ezbookkeepingDataColumnNameMapping,
|
||||
transactionTypeMapping: ezbookkeepingTransactionTypeNameMapping,
|
||||
transactionTypeNameMapping: ezbookkeepingNameTransactionTypeMapping,
|
||||
columnSeparator: "\t",
|
||||
lineSeparator: "\n",
|
||||
geoLocationSeparator: " ",
|
||||
transactionTagSeparator: ";",
|
||||
},
|
||||
columns: ezbookkeepingDataColumns,
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -1,22 +0,0 @@
|
||||
package converters
|
||||
|
||||
import (
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
)
|
||||
|
||||
// EzBookKeepingTSVFileConverter defines the structure of TSV file converter
|
||||
type EzBookKeepingTSVFileConverter struct {
|
||||
EzBookKeepingPlainFileConverter
|
||||
}
|
||||
|
||||
const tsvSeparator = "\t"
|
||||
|
||||
// ToExportedContent returns the exported TSV data
|
||||
func (e *EzBookKeepingTSVFileConverter) ToExportedContent(uid int64, transactions []*models.Transaction, accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTagIndexes map[int64][]int64) ([]byte, error) {
|
||||
return e.toExportedContent(uid, tsvSeparator, transactions, accountMap, categoryMap, tagMap, allTagIndexes)
|
||||
}
|
||||
|
||||
// ParseImportedData parses transactions of ezbookkeeping TSV data
|
||||
func (e *EzBookKeepingTSVFileConverter) ParseImportedData(user *models.User, data []byte, defaultTimezoneOffset int16, accountMap map[string]*models.Account, categoryMap map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) ([]*models.Transaction, []*models.Account, []*models.TransactionCategory, []*models.TransactionTag, error) {
|
||||
return e.parseImportedData(user, tsvSeparator, data, defaultTimezoneOffset, accountMap, categoryMap, tagMap)
|
||||
}
|
||||
@@ -2,21 +2,21 @@ package converters
|
||||
|
||||
import "github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
|
||||
// ImportTransactionSlice represents the slice data structure of import transaction data
|
||||
type ImportTransactionSlice []*models.Transaction
|
||||
// ImportedTransactionSlice represents the slice data structure of import transaction data
|
||||
type ImportedTransactionSlice []*models.Transaction
|
||||
|
||||
// Len returns the count of items
|
||||
func (s ImportTransactionSlice) Len() int {
|
||||
func (s ImportedTransactionSlice) Len() int {
|
||||
return len(s)
|
||||
}
|
||||
|
||||
// Swap swaps two items
|
||||
func (s ImportTransactionSlice) Swap(i, j int) {
|
||||
func (s ImportedTransactionSlice) Swap(i, j int) {
|
||||
s[i], s[j] = s[j], s[i]
|
||||
}
|
||||
|
||||
// Less reports whether the first item is less than the second one
|
||||
func (s ImportTransactionSlice) Less(i, j int) bool {
|
||||
func (s ImportedTransactionSlice) Less(i, j int) bool {
|
||||
if s[i].Type != s[j].Type && (s[i].Type == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE || s[j].Type == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE) {
|
||||
if s[i].Type == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE {
|
||||
return true
|
||||
+1
-1
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
func TestImportTransactionSliceLess(t *testing.T) {
|
||||
var transactionSlice ImportTransactionSlice
|
||||
var transactionSlice ImportedTransactionSlice
|
||||
transactionSlice = append(transactionSlice, &models.Transaction{
|
||||
TransactionId: 1,
|
||||
Type: models.TRANSACTION_DB_TYPE_EXPENSE,
|
||||
@@ -4,11 +4,20 @@ import (
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
)
|
||||
|
||||
// DataConverter defines the structure of data converter
|
||||
type DataConverter interface {
|
||||
// TransactionDataExporter defines the structure of transaction data exporter
|
||||
type TransactionDataExporter interface {
|
||||
// ToExportedContent returns the exported data
|
||||
ToExportedContent(uid int64, transactions []*models.Transaction, accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTagIndexes map[int64][]int64) ([]byte, error)
|
||||
}
|
||||
|
||||
// TransactionDataImporter defines the structure of transaction data importer
|
||||
type TransactionDataImporter interface {
|
||||
// ParseImportedData returns the imported data
|
||||
ParseImportedData(user *models.User, data []byte, defaultTimezoneOffset int16, accountMap map[string]*models.Account, categoryMap map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) ([]*models.Transaction, []*models.Account, []*models.TransactionCategory, []*models.TransactionTag, error)
|
||||
}
|
||||
|
||||
// TransactionDataConverter defines the structure of transaction data converter
|
||||
type TransactionDataConverter interface {
|
||||
TransactionDataExporter
|
||||
TransactionDataImporter
|
||||
}
|
||||
Reference in New Issue
Block a user