mirror of
https://github.com/mayswind/ezbookkeeping.git
synced 2026-05-19 17:24:26 +08:00
support importing transaction by csv/tsv file via command line
This commit is contained in:
@@ -244,6 +244,31 @@ var UserData = &cli.Command{
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "transaction-import",
|
||||||
|
Usage: "Import transactions to specified user",
|
||||||
|
Action: bindAction(importUserTransaction),
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "username",
|
||||||
|
Aliases: []string{"n"},
|
||||||
|
Required: true,
|
||||||
|
Usage: "Specific user name",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "file",
|
||||||
|
Aliases: []string{"f"},
|
||||||
|
Required: true,
|
||||||
|
Usage: "Specific import file path (e.g. transaction.csv)",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "type",
|
||||||
|
Aliases: []string{"t"},
|
||||||
|
Required: true,
|
||||||
|
Usage: "Import file type (supports \"ezbookkeeping_csv\", \"ezbookkeeping_tsv\")",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "transaction-export",
|
Name: "transaction-export",
|
||||||
Usage: "Export user all transactions to file",
|
Usage: "Export user all transactions to file",
|
||||||
@@ -639,6 +664,55 @@ func exportUserTransaction(c *core.CliContext) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func importUserTransaction(c *core.CliContext) error {
|
||||||
|
_, err := initializeSystem(c)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
username := c.String("username")
|
||||||
|
filePath := c.String("file")
|
||||||
|
filetype := c.String("type")
|
||||||
|
|
||||||
|
if filePath == "" {
|
||||||
|
log.BootErrorf(c, "[user_data.importUserTransaction] import file path is not specified")
|
||||||
|
return os.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
fileExists, err := utils.IsExists(filePath)
|
||||||
|
|
||||||
|
if !fileExists {
|
||||||
|
log.BootErrorf(c, "[user_data.importUserTransaction] import file does not exist")
|
||||||
|
return os.ErrExist
|
||||||
|
}
|
||||||
|
|
||||||
|
if filetype != "ezbookkeeping_csv" && filetype != "ezbookkeeping_tsv" {
|
||||||
|
log.BootErrorf(c, "[user_data.importUserTransaction] unknown file type \"%s\"", filetype)
|
||||||
|
return errs.ErrImportFileTypeNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(filePath)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.BootErrorf(c, "[user_data.importUserTransaction] failed to load import file")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.BootInfof(c, "[user_data.importUserTransaction] start importing transactions to user \"%s\"", username)
|
||||||
|
|
||||||
|
err = clis.UserData.ImportTransaction(c, username, filetype, data)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.BootErrorf(c, "[user_data.importUserTransaction] error occurs when importing user data")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.BootInfof(c, "[user_data.importUserTransaction] transactions have been imported to user \"%s\"", username)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func printUserInfo(user *models.User) {
|
func printUserInfo(user *models.User) {
|
||||||
fmt.Printf("[Uid] %d\n", user.Uid)
|
fmt.Printf("[Uid] %d\n", user.Uid)
|
||||||
fmt.Printf("[Username] %s\n", user.Username)
|
fmt.Printf("[Username] %s\n", user.Username)
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ const pageCountForDataExport = 1000
|
|||||||
// DataManagementsApi represents data management api
|
// DataManagementsApi represents data management api
|
||||||
type DataManagementsApi struct {
|
type DataManagementsApi struct {
|
||||||
ApiUsingConfig
|
ApiUsingConfig
|
||||||
ezBookKeepingCsvExporter *converters.EzBookKeepingCSVFileExporter
|
ezBookKeepingCsvExporter *converters.EzBookKeepingCSVFileConverter
|
||||||
ezBookKeepingTsvExporter *converters.EzBookKeepingTSVFileExporter
|
ezBookKeepingTsvExporter *converters.EzBookKeepingTSVFileConverter
|
||||||
tokens *services.TokenService
|
tokens *services.TokenService
|
||||||
users *services.UserService
|
users *services.UserService
|
||||||
accounts *services.AccountService
|
accounts *services.AccountService
|
||||||
@@ -38,8 +38,8 @@ var (
|
|||||||
ApiUsingConfig: ApiUsingConfig{
|
ApiUsingConfig: ApiUsingConfig{
|
||||||
container: settings.Container,
|
container: settings.Container,
|
||||||
},
|
},
|
||||||
ezBookKeepingCsvExporter: &converters.EzBookKeepingCSVFileExporter{},
|
ezBookKeepingCsvExporter: &converters.EzBookKeepingCSVFileConverter{},
|
||||||
ezBookKeepingTsvExporter: &converters.EzBookKeepingTSVFileExporter{},
|
ezBookKeepingTsvExporter: &converters.EzBookKeepingTSVFileConverter{},
|
||||||
tokens: services.Tokens,
|
tokens: services.Tokens,
|
||||||
users: services.Users,
|
users: services.Users,
|
||||||
accounts: services.Accounts,
|
accounts: services.Accounts,
|
||||||
|
|||||||
+108
-4
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/services"
|
"github.com/mayswind/ezbookkeeping/pkg/services"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/validators"
|
"github.com/mayswind/ezbookkeeping/pkg/validators"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,8 +20,8 @@ const pageCountForDataExport = 1000
|
|||||||
// UserDataCli represents user data cli
|
// UserDataCli represents user data cli
|
||||||
type UserDataCli struct {
|
type UserDataCli struct {
|
||||||
CliUsingConfig
|
CliUsingConfig
|
||||||
ezBookKeepingCsvExporter *converters.EzBookKeepingCSVFileExporter
|
ezBookKeepingCsvExporter *converters.EzBookKeepingCSVFileConverter
|
||||||
ezBookKeepingTsvExporter *converters.EzBookKeepingTSVFileExporter
|
ezBookKeepingTsvExporter *converters.EzBookKeepingTSVFileConverter
|
||||||
accounts *services.AccountService
|
accounts *services.AccountService
|
||||||
transactions *services.TransactionService
|
transactions *services.TransactionService
|
||||||
categories *services.TransactionCategoryService
|
categories *services.TransactionCategoryService
|
||||||
@@ -37,8 +38,8 @@ var (
|
|||||||
CliUsingConfig: CliUsingConfig{
|
CliUsingConfig: CliUsingConfig{
|
||||||
container: settings.Container,
|
container: settings.Container,
|
||||||
},
|
},
|
||||||
ezBookKeepingCsvExporter: &converters.EzBookKeepingCSVFileExporter{},
|
ezBookKeepingCsvExporter: &converters.EzBookKeepingCSVFileConverter{},
|
||||||
ezBookKeepingTsvExporter: &converters.EzBookKeepingTSVFileExporter{},
|
ezBookKeepingTsvExporter: &converters.EzBookKeepingTSVFileConverter{},
|
||||||
accounts: services.Accounts,
|
accounts: services.Accounts,
|
||||||
transactions: services.Transactions,
|
transactions: services.Transactions,
|
||||||
categories: services.TransactionCategories,
|
categories: services.TransactionCategories,
|
||||||
@@ -662,6 +663,73 @@ func (l *UserDataCli) ExportTransaction(c *core.CliContext, username string, fil
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *UserDataCli) ImportTransaction(c *core.CliContext, username string, fileType string, data []byte) error {
|
||||||
|
if username == "" {
|
||||||
|
log.BootErrorf(c, "[user_data.ImportTransaction] user name is empty")
|
||||||
|
return errs.ErrUsernameIsEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
var dataImporter converters.DataConverter
|
||||||
|
|
||||||
|
if fileType == "ezbookkeeping_csv" {
|
||||||
|
dataImporter = l.ezBookKeepingCsvExporter
|
||||||
|
} else if fileType == "ezbookkeeping_tsv" {
|
||||||
|
dataImporter = l.ezBookKeepingTsvExporter
|
||||||
|
} else {
|
||||||
|
return errs.ErrImportFileTypeNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := l.GetUserByUsername(c, username)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.BootErrorf(c, "[user_data.ImportTransaction] failed to get user by user name \"%s\", because %s", username, err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
accountMap, categoryMap, tagMap, err := l.getUserEssentialDataForImport(c, user.Uid, username)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.BootErrorf(c, "[user_data.ImportTransaction] failed to get essential data for user \"%s\", because %s", username, err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
newTransactions, newAccounts, newCategories, newTags, err := dataImporter.ParseImportedData(user, data, utils.GetTimezoneOffsetMinutes(time.Local), accountMap, categoryMap, tagMap)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.BootErrorf(c, "[user_data.ImportTransaction] failed to parse imported data for \"%s\", because %s", username, err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(newTransactions) < 1 {
|
||||||
|
log.BootErrorf(c, "[user_data.ImportTransaction] there are no transactions in import file")
|
||||||
|
return errs.ErrOperationFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(newAccounts) > 0 {
|
||||||
|
log.BootErrorf(c, "[user_data.ImportTransaction] there are %d accounts need to be created, please create them manually", len(newAccounts))
|
||||||
|
return errs.ErrOperationFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(newCategories) > 0 {
|
||||||
|
log.BootErrorf(c, "[user_data.ImportTransaction] there are %d transaction categories need to be created, please create them manually", len(newCategories))
|
||||||
|
return errs.ErrOperationFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(newTags) > 0 {
|
||||||
|
log.BootErrorf(c, "[user_data.ImportTransaction] there are %d transaction tags need to be created, please create them manually", len(newTags))
|
||||||
|
return errs.ErrOperationFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
err = l.transactions.BatchCreateTransactions(c, user.Uid, newTransactions)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.BootErrorf(c, "[user_data.ImportTransaction] failed to create transaction, because %s", err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (l *UserDataCli) getUserIdByUsername(c *core.CliContext, username string) (int64, error) {
|
func (l *UserDataCli) getUserIdByUsername(c *core.CliContext, username string) (int64, error) {
|
||||||
user, err := l.GetUserByUsername(c, username)
|
user, err := l.GetUserByUsername(c, username)
|
||||||
|
|
||||||
@@ -718,6 +786,42 @@ func (l *UserDataCli) getUserEssentialData(c *core.CliContext, uid int64, userna
|
|||||||
return accountMap, categoryMap, tagMap, tagIndexes, tagIndexesMap, nil
|
return accountMap, categoryMap, tagMap, tagIndexes, tagIndexesMap, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *UserDataCli) getUserEssentialDataForImport(c *core.CliContext, uid int64, username string) (accountMap map[string]*models.Account, categoryMap map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag, err error) {
|
||||||
|
if uid <= 0 {
|
||||||
|
log.BootErrorf(c, "[user_data.getUserEssentialDataForImport] user uid \"%d\" is invalid", uid)
|
||||||
|
return nil, nil, nil, errs.ErrUserIdInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
accounts, err := l.accounts.GetAllAccountsByUid(c, uid)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.BootErrorf(c, "[user_data.getUserEssentialDataForImport] failed to get accounts for user \"%s\", because %s", username, err.Error())
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
accountMap = l.accounts.GetAccountNameMapByList(accounts)
|
||||||
|
|
||||||
|
categories, err := l.categories.GetAllCategoriesByUid(c, uid, 0, -1)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.BootErrorf(c, "[user_data.getUserEssentialDataForImport] failed to get categories for user \"%s\", because %s", username, err.Error())
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
categoryMap = l.categories.GetCategoryNameMapByList(categories)
|
||||||
|
|
||||||
|
tags, err := l.tags.GetAllTagsByUid(c, uid)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.BootErrorf(c, "[user_data.getUserEssentialDataForImport] failed to get tags for user \"%s\", because %s", username, err.Error())
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tagMap = l.tags.GetTagNameMapByList(tags)
|
||||||
|
|
||||||
|
return accountMap, categoryMap, tagMap, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (l *UserDataCli) checkTransactionAccount(c *core.CliContext, transaction *models.Transaction, accountMap map[int64]*models.Account, accountHasChild map[int64]bool) error {
|
func (l *UserDataCli) checkTransactionAccount(c *core.CliContext, transaction *models.Transaction, accountMap map[int64]*models.Account, accountHasChild map[int64]bool) error {
|
||||||
account, exists := accountMap[transaction.AccountId]
|
account, exists := accountMap[transaction.AccountId]
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,11 @@ import (
|
|||||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DataConverter defines the structure of data exporter
|
// DataConverter defines the structure of data converter
|
||||||
type DataConverter interface {
|
type DataConverter interface {
|
||||||
// ToExportedContent returns the exported data
|
// 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)
|
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)
|
||||||
|
|
||||||
|
// 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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,14 +4,19 @@ import (
|
|||||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
// EzBookKeepingCSVFileExporter defines the structure of CSV file exporter
|
// EzBookKeepingCSVFileConverter defines the structure of CSV file converter
|
||||||
type EzBookKeepingCSVFileExporter struct {
|
type EzBookKeepingCSVFileConverter struct {
|
||||||
EzBookKeepingPlainFileExporter
|
EzBookKeepingPlainFileConverter
|
||||||
}
|
}
|
||||||
|
|
||||||
const csvSeparator = ","
|
const csvSeparator = ","
|
||||||
|
|
||||||
// ToExportedContent returns the exported CSV data
|
// ToExportedContent returns the exported CSV data
|
||||||
func (e *EzBookKeepingCSVFileExporter) 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) {
|
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)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,15 +2,18 @@ package converters
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/validators"
|
||||||
)
|
)
|
||||||
|
|
||||||
// EzBookKeepingPlainFileExporter defines the structure of plain file exporter
|
// EzBookKeepingPlainFileConverter defines the structure of plain file converter
|
||||||
type EzBookKeepingPlainFileExporter struct {
|
type EzBookKeepingPlainFileConverter struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
const lineSeparator = "\n"
|
const lineSeparator = "\n"
|
||||||
@@ -20,7 +23,7 @@ const headerLine = "Time,Timezone,Type,Category,Sub Category,Account,Account Cur
|
|||||||
const dataLineFormat = "%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s" + lineSeparator
|
const dataLineFormat = "%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s" + lineSeparator
|
||||||
|
|
||||||
// toExportedContent returns the exported plain data
|
// toExportedContent returns the exported plain data
|
||||||
func (e *EzBookKeepingPlainFileExporter) 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) {
|
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
|
var ret strings.Builder
|
||||||
|
|
||||||
ret.Grow(len(transactions) * 100)
|
ret.Grow(len(transactions) * 100)
|
||||||
@@ -75,7 +78,270 @@ func (e *EzBookKeepingPlainFileExporter) toExportedContent(uid int64, separator
|
|||||||
return []byte(ret.String()), nil
|
return []byte(ret.String()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *EzBookKeepingPlainFileExporter) getTransactionTypeName(transactionDbType models.TransactionDbType) string {
|
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 {
|
if transactionDbType == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE {
|
||||||
return "Balance Modification"
|
return "Balance Modification"
|
||||||
} else if transactionDbType == models.TRANSACTION_DB_TYPE_INCOME {
|
} else if transactionDbType == models.TRANSACTION_DB_TYPE_INCOME {
|
||||||
@@ -89,7 +355,33 @@ func (e *EzBookKeepingPlainFileExporter) getTransactionTypeName(transactionDbTyp
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *EzBookKeepingPlainFileExporter) getTransactionCategoryName(categoryId int64, categoryMap map[int64]*models.TransactionCategory) string {
|
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]
|
category, exists := categoryMap[categoryId]
|
||||||
|
|
||||||
if !exists {
|
if !exists {
|
||||||
@@ -109,7 +401,7 @@ func (e *EzBookKeepingPlainFileExporter) getTransactionCategoryName(categoryId i
|
|||||||
return parentCategory.Name
|
return parentCategory.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *EzBookKeepingPlainFileExporter) getTransactionSubCategoryName(categoryId int64, categoryMap map[int64]*models.TransactionCategory) string {
|
func (e *EzBookKeepingPlainFileConverter) getTransactionSubCategoryName(categoryId int64, categoryMap map[int64]*models.TransactionCategory) string {
|
||||||
category, exists := categoryMap[categoryId]
|
category, exists := categoryMap[categoryId]
|
||||||
|
|
||||||
if exists {
|
if exists {
|
||||||
@@ -119,7 +411,7 @@ func (e *EzBookKeepingPlainFileExporter) getTransactionSubCategoryName(categoryI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *EzBookKeepingPlainFileExporter) getAccountName(accountId int64, accountMap map[int64]*models.Account) string {
|
func (e *EzBookKeepingPlainFileConverter) getAccountName(accountId int64, accountMap map[int64]*models.Account) string {
|
||||||
account, exists := accountMap[accountId]
|
account, exists := accountMap[accountId]
|
||||||
|
|
||||||
if exists {
|
if exists {
|
||||||
@@ -129,7 +421,7 @@ func (e *EzBookKeepingPlainFileExporter) getAccountName(accountId int64, account
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *EzBookKeepingPlainFileExporter) getAccountCurrency(accountId int64, accountMap map[int64]*models.Account) string {
|
func (e *EzBookKeepingPlainFileConverter) getAccountCurrency(accountId int64, accountMap map[int64]*models.Account) string {
|
||||||
account, exists := accountMap[accountId]
|
account, exists := accountMap[accountId]
|
||||||
|
|
||||||
if exists {
|
if exists {
|
||||||
@@ -139,7 +431,7 @@ func (e *EzBookKeepingPlainFileExporter) getAccountCurrency(accountId int64, acc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *EzBookKeepingPlainFileExporter) getTags(transactionId int64, allTagIndexes map[int64][]int64, tagMap map[int64]*models.TransactionTag) string {
|
func (e *EzBookKeepingPlainFileConverter) getTags(transactionId int64, allTagIndexes map[int64][]int64, tagMap map[int64]*models.TransactionTag) string {
|
||||||
tagIndexes, exists := allTagIndexes[transactionId]
|
tagIndexes, exists := allTagIndexes[transactionId]
|
||||||
|
|
||||||
if !exists {
|
if !exists {
|
||||||
@@ -166,10 +458,33 @@ func (e *EzBookKeepingPlainFileExporter) getTags(transactionId int64, allTagInde
|
|||||||
return ret.String()
|
return ret.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *EzBookKeepingPlainFileExporter) replaceDelimiters(text string, separator string) string {
|
func (e *EzBookKeepingPlainFileConverter) replaceDelimiters(text string, separator string) string {
|
||||||
text = strings.Replace(text, separator, " ", -1)
|
text = strings.Replace(text, separator, " ", -1)
|
||||||
text = strings.Replace(text, "\r\n", " ", -1)
|
text = strings.Replace(text, "\r\n", " ", -1)
|
||||||
text = strings.Replace(text, "\n", " ", -1)
|
text = strings.Replace(text, "\n", " ", -1)
|
||||||
|
|
||||||
return text
|
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,365 @@
|
|||||||
|
package converters
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEzBookKeepingPlainFileConverterParseImportedData_MinimumValidData(t *testing.T) {
|
||||||
|
converter := &EzBookKeepingPlainFileConverter{}
|
||||||
|
|
||||||
|
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"+
|
||||||
|
"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"+
|
||||||
|
"2024-09-01 23:59:59,Transfer,Test Category3,Test Account,0.05,Test Account2,0.05"), 0, nil, nil, nil)
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, 4, 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, 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, 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, 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, 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 TestEzBookKeepingPlainFileConverterParseImportedData_ParseInvalidTime(t *testing.T) {
|
||||||
|
converter := &EzBookKeepingPlainFileConverter{}
|
||||||
|
|
||||||
|
user := &models.User{
|
||||||
|
Uid: 1234567890,
|
||||||
|
DefaultCurrency: "CNY",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, _, _, 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"+
|
||||||
|
"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{}
|
||||||
|
|
||||||
|
user := &models.User{
|
||||||
|
Uid: 1234567890,
|
||||||
|
DefaultCurrency: "CNY",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, _, _, 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{}
|
||||||
|
|
||||||
|
user := &models.User{
|
||||||
|
Uid: 1234567890,
|
||||||
|
DefaultCurrency: "CNY",
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
assert.Equal(t, int64(1725165296), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEzBookKeepingPlainFileConverterParseImportedData_ParseInvalidTimezone(t *testing.T) {
|
||||||
|
converter := &EzBookKeepingPlainFileConverter{}
|
||||||
|
|
||||||
|
user := &models.User{
|
||||||
|
Uid: 1234567890,
|
||||||
|
DefaultCurrency: "CNY",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, _, _, 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{}
|
||||||
|
|
||||||
|
user := &models.User{
|
||||||
|
Uid: 1234567890,
|
||||||
|
DefaultCurrency: "CNY",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, _, _, 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{}
|
||||||
|
|
||||||
|
user := &models.User{
|
||||||
|
Uid: 1234567890,
|
||||||
|
DefaultCurrency: "CNY",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, _, _, 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"+
|
||||||
|
"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{}
|
||||||
|
|
||||||
|
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"+
|
||||||
|
"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)
|
||||||
|
|
||||||
|
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 TestEzBookKeepingPlainFileConverterParseImportedData_ParseInvalidAccountCurrency(t *testing.T) {
|
||||||
|
converter := &EzBookKeepingPlainFileConverter{}
|
||||||
|
|
||||||
|
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"+
|
||||||
|
"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"+
|
||||||
|
"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{}
|
||||||
|
|
||||||
|
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"+
|
||||||
|
"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"+
|
||||||
|
"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{}
|
||||||
|
|
||||||
|
user := &models.User{
|
||||||
|
Uid: 1234567890,
|
||||||
|
DefaultCurrency: "CNY",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, _, _, 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"+
|
||||||
|
"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{}
|
||||||
|
|
||||||
|
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"+
|
||||||
|
"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)
|
||||||
|
assert.Equal(t, 1, len(allNewTransactions))
|
||||||
|
assert.Equal(t, 123.45, allNewTransactions[0].GeoLongitude)
|
||||||
|
assert.Equal(t, 45.56, allNewTransactions[0].GeoLatitude)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEzBookKeepingPlainFileConverterParseImportedData_ParseInvalidGeographicLocation(t *testing.T) {
|
||||||
|
converter := &EzBookKeepingPlainFileConverter{}
|
||||||
|
|
||||||
|
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"+
|
||||||
|
"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"+
|
||||||
|
"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"+
|
||||||
|
"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{}
|
||||||
|
|
||||||
|
user := &models.User{
|
||||||
|
Uid: 1234567890,
|
||||||
|
DefaultCurrency: "CNY",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, _, 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)
|
||||||
|
|
||||||
|
assert.Equal(t, 4, len(allNewTags))
|
||||||
|
|
||||||
|
assert.Equal(t, int64(1234567890), allNewTags[0].Uid)
|
||||||
|
assert.Equal(t, "foo", allNewTags[0].Name)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(1234567890), allNewTags[1].Uid)
|
||||||
|
assert.Equal(t, "bar.", allNewTags[1].Name)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(1234567890), allNewTags[2].Uid)
|
||||||
|
assert.Equal(t, "#test", allNewTags[2].Name)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(1234567890), allNewTags[3].Uid)
|
||||||
|
assert.Equal(t, "hello\tworld", allNewTags[3].Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEzBookKeepingPlainFileConverterParseImportedData_ParseDescription(t *testing.T) {
|
||||||
|
converter := &EzBookKeepingPlainFileConverter{}
|
||||||
|
|
||||||
|
user := &models.User{
|
||||||
|
Uid: 1234567890,
|
||||||
|
DefaultCurrency: "CNY",
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
assert.Equal(t, 1, len(allNewTransactions))
|
||||||
|
assert.Equal(t, "foo bar\t#test", allNewTransactions[0].Comment)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEzBookKeepingPlainFileConverterParseImportedData_MissingRequiredColumn(t *testing.T) {
|
||||||
|
converter := &EzBookKeepingPlainFileConverter{}
|
||||||
|
|
||||||
|
user := &models.User{
|
||||||
|
Uid: 1,
|
||||||
|
DefaultCurrency: "CNY",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, _, _, 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"+
|
||||||
|
"+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"+
|
||||||
|
"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"+
|
||||||
|
"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"+
|
||||||
|
"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"+
|
||||||
|
"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"+
|
||||||
|
"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"+
|
||||||
|
"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"+
|
||||||
|
"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)
|
||||||
|
}
|
||||||
@@ -4,14 +4,19 @@ import (
|
|||||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
// EzBookKeepingTSVFileExporter defines the structure of TSV file exporter
|
// EzBookKeepingTSVFileConverter defines the structure of TSV file converter
|
||||||
type EzBookKeepingTSVFileExporter struct {
|
type EzBookKeepingTSVFileConverter struct {
|
||||||
EzBookKeepingPlainFileExporter
|
EzBookKeepingPlainFileConverter
|
||||||
}
|
}
|
||||||
|
|
||||||
const tsvSeparator = "\t"
|
const tsvSeparator = "\t"
|
||||||
|
|
||||||
// ToExportedContent returns the exported TSV data
|
// ToExportedContent returns the exported TSV data
|
||||||
func (e *EzBookKeepingTSVFileExporter) 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) {
|
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)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package converters
|
||||||
|
|
||||||
|
import "github.com/mayswind/ezbookkeeping/pkg/models"
|
||||||
|
|
||||||
|
// ImportTransactionSlice represents the slice data structure of import transaction data
|
||||||
|
type ImportTransactionSlice []*models.Transaction
|
||||||
|
|
||||||
|
// Len returns the count of items
|
||||||
|
func (s ImportTransactionSlice) Len() int {
|
||||||
|
return len(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swap swaps two items
|
||||||
|
func (s ImportTransactionSlice) 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 {
|
||||||
|
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
|
||||||
|
} else if s[j].Type == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s[i].TransactionTime < s[j].TransactionTime
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package converters
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestImportTransactionSliceLess(t *testing.T) {
|
||||||
|
var transactionSlice ImportTransactionSlice
|
||||||
|
transactionSlice = append(transactionSlice, &models.Transaction{
|
||||||
|
TransactionId: 1,
|
||||||
|
Type: models.TRANSACTION_DB_TYPE_EXPENSE,
|
||||||
|
TransactionTime: 1,
|
||||||
|
})
|
||||||
|
transactionSlice = append(transactionSlice, &models.Transaction{
|
||||||
|
TransactionId: 2,
|
||||||
|
Type: models.TRANSACTION_DB_TYPE_INCOME,
|
||||||
|
TransactionTime: 2,
|
||||||
|
})
|
||||||
|
transactionSlice = append(transactionSlice, &models.Transaction{
|
||||||
|
TransactionId: 3,
|
||||||
|
Type: models.TRANSACTION_DB_TYPE_MODIFY_BALANCE,
|
||||||
|
TransactionTime: 10,
|
||||||
|
})
|
||||||
|
transactionSlice = append(transactionSlice, &models.Transaction{
|
||||||
|
TransactionId: 4,
|
||||||
|
Type: models.TRANSACTION_DB_TYPE_TRANSFER_IN,
|
||||||
|
TransactionTime: 3,
|
||||||
|
})
|
||||||
|
transactionSlice = append(transactionSlice, &models.Transaction{
|
||||||
|
TransactionId: 5,
|
||||||
|
Type: models.TRANSACTION_DB_TYPE_MODIFY_BALANCE,
|
||||||
|
TransactionTime: 11,
|
||||||
|
})
|
||||||
|
transactionSlice = append(transactionSlice, &models.Transaction{
|
||||||
|
TransactionId: 6,
|
||||||
|
Type: models.TRANSACTION_DB_TYPE_TRANSFER_OUT,
|
||||||
|
TransactionTime: 4,
|
||||||
|
})
|
||||||
|
|
||||||
|
sort.Sort(transactionSlice)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(3), transactionSlice[0].TransactionId)
|
||||||
|
assert.Equal(t, int64(5), transactionSlice[1].TransactionId)
|
||||||
|
assert.Equal(t, int64(1), transactionSlice[2].TransactionId)
|
||||||
|
assert.Equal(t, int64(2), transactionSlice[3].TransactionId)
|
||||||
|
assert.Equal(t, int64(4), transactionSlice[4].TransactionId)
|
||||||
|
assert.Equal(t, int64(6), transactionSlice[5].TransactionId)
|
||||||
|
}
|
||||||
@@ -6,5 +6,7 @@ import (
|
|||||||
|
|
||||||
// Error codes related to data management
|
// Error codes related to data management
|
||||||
var (
|
var (
|
||||||
ErrDataExportNotAllowed = NewNormalError(NormalSubcategoryDataManagement, 1, http.StatusBadRequest, "data export not allowed")
|
ErrDataExportNotAllowed = NewNormalError(NormalSubcategoryDataManagement, 1, http.StatusBadRequest, "data export not allowed")
|
||||||
|
ErrDataImportNotAllowed = NewNormalError(NormalSubcategoryDataManagement, 2, http.StatusBadRequest, "data import not allowed")
|
||||||
|
ErrImportTooManyTransaction = NewNormalError(NormalSubcategoryDataManagement, 3, http.StatusBadRequest, "import too many transactions")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ var (
|
|||||||
ErrQueryItemsInvalid = NewNormalError(NormalSubcategoryGlobal, 11, http.StatusBadRequest, "query items have invalid item")
|
ErrQueryItemsInvalid = NewNormalError(NormalSubcategoryGlobal, 11, http.StatusBadRequest, "query items have invalid item")
|
||||||
ErrParameterInvalid = NewNormalError(NormalSubcategoryGlobal, 12, http.StatusBadRequest, "parameter invalid")
|
ErrParameterInvalid = NewNormalError(NormalSubcategoryGlobal, 12, http.StatusBadRequest, "parameter invalid")
|
||||||
ErrFormatInvalid = NewNormalError(NormalSubcategoryGlobal, 13, http.StatusBadRequest, "format invalid")
|
ErrFormatInvalid = NewNormalError(NormalSubcategoryGlobal, 13, http.StatusBadRequest, "format invalid")
|
||||||
|
ErrNumberInvalid = NewNormalError(NormalSubcategoryGlobal, 14, http.StatusBadRequest, "number invalid")
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetParameterInvalidMessage returns specific error message for invalid parameter error
|
// GetParameterInvalidMessage returns specific error message for invalid parameter error
|
||||||
|
|||||||
+8
-7
@@ -4,11 +4,12 @@ import "net/http"
|
|||||||
|
|
||||||
// Error codes related to transaction categories
|
// Error codes related to transaction categories
|
||||||
var (
|
var (
|
||||||
ErrSystemError = NewSystemError(SystemSubcategoryDefault, 0, http.StatusInternalServerError, "system error")
|
ErrSystemError = NewSystemError(SystemSubcategoryDefault, 0, http.StatusInternalServerError, "system error")
|
||||||
ErrApiNotFound = NewSystemError(SystemSubcategoryDefault, 1, http.StatusNotFound, "api not found")
|
ErrApiNotFound = NewSystemError(SystemSubcategoryDefault, 1, http.StatusNotFound, "api not found")
|
||||||
ErrMethodNotAllowed = NewSystemError(SystemSubcategoryDefault, 2, http.StatusMethodNotAllowed, "method not allowed")
|
ErrMethodNotAllowed = NewSystemError(SystemSubcategoryDefault, 2, http.StatusMethodNotAllowed, "method not allowed")
|
||||||
ErrNotImplemented = NewSystemError(SystemSubcategoryDefault, 3, http.StatusNotImplemented, "not implemented")
|
ErrNotImplemented = NewSystemError(SystemSubcategoryDefault, 3, http.StatusNotImplemented, "not implemented")
|
||||||
ErrSystemIsBusy = NewSystemError(SystemSubcategoryDefault, 4, http.StatusServiceUnavailable, "system is busy")
|
ErrSystemIsBusy = NewSystemError(SystemSubcategoryDefault, 4, http.StatusServiceUnavailable, "system is busy")
|
||||||
ErrNotSupported = NewSystemError(SystemSubcategoryDefault, 5, http.StatusBadRequest, "not supported")
|
ErrNotSupported = NewSystemError(SystemSubcategoryDefault, 5, http.StatusBadRequest, "not supported")
|
||||||
ErrImageTypeNotSupported = NewSystemError(SystemSubcategoryDefault, 6, http.StatusBadRequest, "image type not supported")
|
ErrImageTypeNotSupported = NewSystemError(SystemSubcategoryDefault, 6, http.StatusBadRequest, "image type not supported")
|
||||||
|
ErrImportFileTypeNotSupported = NewSystemError(SystemSubcategoryDefault, 7, http.StatusBadRequest, "import file type not supported")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
@@ -30,6 +31,24 @@ const (
|
|||||||
TRANSACTION_DB_TYPE_TRANSFER_IN TransactionDbType = 5
|
TRANSACTION_DB_TYPE_TRANSFER_IN TransactionDbType = 5
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// String returns a textual representation of the transaction types for db enum
|
||||||
|
func (s TransactionDbType) String() string {
|
||||||
|
switch s {
|
||||||
|
case TRANSACTION_DB_TYPE_MODIFY_BALANCE:
|
||||||
|
return "Modify Balance"
|
||||||
|
case TRANSACTION_DB_TYPE_INCOME:
|
||||||
|
return "Income"
|
||||||
|
case TRANSACTION_DB_TYPE_EXPENSE:
|
||||||
|
return "Expense"
|
||||||
|
case TRANSACTION_DB_TYPE_TRANSFER_OUT:
|
||||||
|
return "Transfer Out"
|
||||||
|
case TRANSACTION_DB_TYPE_TRANSFER_IN:
|
||||||
|
return "Transfer In"
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("Invalid(%d)", int(s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Transaction represents transaction data stored in database
|
// Transaction represents transaction data stored in database
|
||||||
type Transaction struct {
|
type Transaction struct {
|
||||||
TransactionId int64 `xorm:"PK"`
|
TransactionId int64 `xorm:"PK"`
|
||||||
|
|||||||
@@ -463,3 +463,14 @@ func (s *AccountService) GetAccountMapByList(accounts []*models.Account) map[int
|
|||||||
}
|
}
|
||||||
return accountMap
|
return accountMap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAccountNameMapByList returns an account map by a list
|
||||||
|
func (s *AccountService) GetAccountNameMapByList(accounts []*models.Account) map[string]*models.Account {
|
||||||
|
accountMap := make(map[string]*models.Account)
|
||||||
|
|
||||||
|
for i := 0; i < len(accounts); i++ {
|
||||||
|
account := accounts[i]
|
||||||
|
accountMap[account.Name] = account
|
||||||
|
}
|
||||||
|
return accountMap
|
||||||
|
}
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ func (s *ServiceUsingUuid) GenerateUuid(uuidType uuid.UuidType) int64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GenerateUuids generates new uuids according to given uuid type and count
|
// GenerateUuids generates new uuids according to given uuid type and count
|
||||||
func (s *ServiceUsingUuid) GenerateUuids(uuidType uuid.UuidType, count uint8) []int64 {
|
func (s *ServiceUsingUuid) GenerateUuids(uuidType uuid.UuidType, count uint16) []int64 {
|
||||||
return s.container.GenerateUuids(uuidType, count)
|
return s.container.GenerateUuids(uuidType, count)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -447,3 +447,14 @@ func (s *TransactionCategoryService) GetCategoryMapByList(categories []*models.T
|
|||||||
}
|
}
|
||||||
return categoryMap
|
return categoryMap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetCategoryNameMapByList returns a transaction category map by a list
|
||||||
|
func (s *TransactionCategoryService) GetCategoryNameMapByList(categories []*models.TransactionCategory) map[string]*models.TransactionCategory {
|
||||||
|
categoryMap := make(map[string]*models.TransactionCategory)
|
||||||
|
|
||||||
|
for i := 0; i < len(categories); i++ {
|
||||||
|
category := categories[i]
|
||||||
|
categoryMap[category.Name] = category
|
||||||
|
}
|
||||||
|
return categoryMap
|
||||||
|
}
|
||||||
|
|||||||
@@ -414,6 +414,17 @@ func (s *TransactionTagService) GetTagMapByList(tags []*models.TransactionTag) m
|
|||||||
return tagMap
|
return tagMap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetTagNameMapByList returns a transaction tag map by a list
|
||||||
|
func (s *TransactionTagService) GetTagNameMapByList(tags []*models.TransactionTag) map[string]*models.TransactionTag {
|
||||||
|
tagMap := make(map[string]*models.TransactionTag)
|
||||||
|
|
||||||
|
for i := 0; i < len(tags); i++ {
|
||||||
|
tag := tags[i]
|
||||||
|
tagMap[tag.Name] = tag
|
||||||
|
}
|
||||||
|
return tagMap
|
||||||
|
}
|
||||||
|
|
||||||
func (s *TransactionTagService) GetGroupedTransactionTagIds(tagIndexes []*models.TransactionTagIndex) map[int64][]int64 {
|
func (s *TransactionTagService) GetGroupedTransactionTagIds(tagIndexes []*models.TransactionTagIndex) map[int64][]int64 {
|
||||||
allTransactionTagIds := make(map[int64][]int64)
|
allTransactionTagIds := make(map[int64][]int64)
|
||||||
|
|
||||||
|
|||||||
+233
-161
@@ -223,7 +223,7 @@ func (s *TransactionService) CreateTransaction(c core.Context, transaction *mode
|
|||||||
needUuidCount = 2
|
needUuidCount = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
uuids := s.GenerateUuids(uuid.UUID_TYPE_TRANSACTION, uint8(needUuidCount))
|
uuids := s.GenerateUuids(uuid.UUID_TYPE_TRANSACTION, uint16(needUuidCount))
|
||||||
|
|
||||||
if len(uuids) < needUuidCount {
|
if len(uuids) < needUuidCount {
|
||||||
return errs.ErrSystemIsBusy
|
return errs.ErrSystemIsBusy
|
||||||
@@ -267,187 +267,75 @@ func (s *TransactionService) CreateTransaction(c core.Context, transaction *mode
|
|||||||
}
|
}
|
||||||
|
|
||||||
return s.UserDataDB(transaction.Uid).DoTransaction(c, func(sess *xorm.Session) error {
|
return s.UserDataDB(transaction.Uid).DoTransaction(c, func(sess *xorm.Session) error {
|
||||||
// Get and verify source and destination account
|
return s.doCreateTransaction(sess, transaction, transactionTagIndexes, tagIds, pictureIds, pictureUpdateModel)
|
||||||
sourceAccount, destinationAccount, err := s.getAccountModels(sess, transaction)
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchCreateTransactions saves new transactions to database
|
||||||
|
func (s *TransactionService) BatchCreateTransactions(c core.Context, uid int64, transactions []*models.Transaction) error {
|
||||||
|
now := time.Now().Unix()
|
||||||
|
needUuidCount := uint16(0)
|
||||||
|
|
||||||
|
for i := 0; i < len(transactions); i++ {
|
||||||
|
transaction := transactions[i]
|
||||||
|
|
||||||
|
if transaction.Uid != uid {
|
||||||
|
return errs.ErrUserIdInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check whether account id is valid
|
||||||
|
err := s.isAccountIdValid(transaction)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if sourceAccount.Hidden || (destinationAccount != nil && destinationAccount.Hidden) {
|
|
||||||
return errs.ErrCannotAddTransactionToHiddenAccount
|
|
||||||
}
|
|
||||||
|
|
||||||
if sourceAccount.Type == models.ACCOUNT_TYPE_MULTI_SUB_ACCOUNTS || (destinationAccount != nil && destinationAccount.Type == models.ACCOUNT_TYPE_MULTI_SUB_ACCOUNTS) {
|
|
||||||
return errs.ErrCannotAddTransactionToParentAccount
|
|
||||||
}
|
|
||||||
|
|
||||||
if (transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT || transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN) &&
|
|
||||||
sourceAccount.Currency == destinationAccount.Currency && transaction.Amount != transaction.RelatedAccountAmount {
|
|
||||||
return errs.ErrTransactionSourceAndDestinationAmountNotEqual
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get and verify category
|
|
||||||
err = s.isCategoryValid(sess, transaction)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get and verify tags
|
|
||||||
err = s.isTagsValid(sess, transaction, transactionTagIndexes, tagIds)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get and verify pictures
|
|
||||||
err = s.isPicturesValid(sess, transaction, pictureIds)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify balance modification transaction and calculate real amount
|
|
||||||
if transaction.Type == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE {
|
|
||||||
otherTransactionExists, err := sess.Cols("uid", "deleted", "account_id").Where("uid=? AND deleted=? AND account_id=?", transaction.Uid, false, sourceAccount.AccountId).Limit(1).Exist(&models.Transaction{})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
} else if otherTransactionExists {
|
|
||||||
return errs.ErrBalanceModificationTransactionCannotAddWhenNotEmpty
|
|
||||||
}
|
|
||||||
|
|
||||||
transaction.RelatedAccountId = transaction.AccountId
|
|
||||||
transaction.RelatedAccountAmount = transaction.Amount - sourceAccount.Balance
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert transaction row
|
|
||||||
var relatedTransaction *models.Transaction
|
|
||||||
|
|
||||||
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT || transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
|
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT || transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
|
||||||
relatedTransaction = s.GetRelatedTransferTransaction(transaction)
|
needUuidCount += 2
|
||||||
|
} else {
|
||||||
|
needUuidCount++
|
||||||
}
|
}
|
||||||
|
|
||||||
createdRows, err := sess.Insert(transaction)
|
transaction.TransactionTime = utils.GetMinTransactionTimeFromUnixTime(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime))
|
||||||
|
|
||||||
if err != nil || createdRows < 1 { // maybe another transaction has same time
|
transaction.CreatedUnixTime = now
|
||||||
sameSecondLatestTransaction := &models.Transaction{}
|
transaction.UpdatedUnixTime = now
|
||||||
minTransactionTime := utils.GetMinTransactionTimeFromUnixTime(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime))
|
}
|
||||||
maxTransactionTime := utils.GetMaxTransactionTimeFromUnixTime(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime))
|
|
||||||
|
|
||||||
has, err := sess.Where("uid=? AND transaction_time>=? AND transaction_time<=?", transaction.Uid, minTransactionTime, maxTransactionTime).OrderBy("transaction_time desc").Limit(1).Get(sameSecondLatestTransaction)
|
if needUuidCount > uint16(65535) {
|
||||||
|
return errs.ErrImportTooManyTransaction
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
uuids := s.GenerateUuids(uuid.UUID_TYPE_TRANSACTION, needUuidCount)
|
||||||
return err
|
uuidIndex := 0
|
||||||
} else if !has {
|
|
||||||
return errs.ErrDatabaseOperationFailed
|
|
||||||
} else if sameSecondLatestTransaction.TransactionTime == maxTransactionTime-1 {
|
|
||||||
return errs.ErrTooMuchTransactionInOneSecond
|
|
||||||
}
|
|
||||||
|
|
||||||
transaction.TransactionTime = sameSecondLatestTransaction.TransactionTime + 1
|
if len(uuids) < int(needUuidCount) {
|
||||||
createdRows, err := sess.Insert(transaction)
|
return errs.ErrSystemIsBusy
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
for i := 0; i < len(transactions); i++ {
|
||||||
return err
|
transaction := transactions[i]
|
||||||
} else if createdRows < 1 {
|
|
||||||
return errs.ErrDatabaseOperationFailed
|
transaction.TransactionId = uuids[uuidIndex]
|
||||||
}
|
uuidIndex++
|
||||||
|
|
||||||
|
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT || transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
|
||||||
|
transaction.RelatedId = uuids[uuidIndex]
|
||||||
|
uuidIndex++
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if relatedTransaction != nil {
|
return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error {
|
||||||
relatedTransaction.TransactionTime = transaction.TransactionTime + 1
|
for i := 0; i < len(transactions); i++ {
|
||||||
|
transaction := transactions[i]
|
||||||
if utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime) != utils.GetUnixTimeFromTransactionTime(relatedTransaction.TransactionTime) {
|
err := s.doCreateTransaction(sess, transaction, nil, nil, nil, nil)
|
||||||
return errs.ErrTooMuchTransactionInOneSecond
|
|
||||||
}
|
|
||||||
|
|
||||||
createdRows, err := sess.Insert(relatedTransaction)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
} else if createdRows < 1 {
|
|
||||||
return errs.ErrDatabaseOperationFailed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = nil
|
|
||||||
|
|
||||||
// Insert transaction tag index
|
|
||||||
if len(transactionTagIndexes) > 0 {
|
|
||||||
for i := 0; i < len(transactionTagIndexes); i++ {
|
|
||||||
transactionTagIndex := transactionTagIndexes[i]
|
|
||||||
transactionTagIndex.TransactionTime = transaction.TransactionTime
|
|
||||||
|
|
||||||
_, err := sess.Insert(transactionTagIndex)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update transaction picture
|
|
||||||
if len(pictureIds) > 0 {
|
|
||||||
_, err = sess.Cols("transaction_id", "updated_unix_time").Where("uid=? AND deleted=? AND transaction_id=?", transaction.Uid, false, models.TransactionPictureNewPictureTransactionId).In("picture_id", pictureIds).Update(pictureUpdateModel)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update account table
|
return nil
|
||||||
if transaction.Type == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE {
|
|
||||||
sourceAccount.UpdatedUnixTime = time.Now().Unix()
|
|
||||||
updatedRows, err := sess.ID(sourceAccount.AccountId).SetExpr("balance", fmt.Sprintf("balance+(%d)", transaction.RelatedAccountAmount)).Cols("updated_unix_time").Where("uid=? AND deleted=?", sourceAccount.Uid, false).Update(sourceAccount)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
} else if updatedRows < 1 {
|
|
||||||
return errs.ErrDatabaseOperationFailed
|
|
||||||
}
|
|
||||||
} else if transaction.Type == models.TRANSACTION_DB_TYPE_INCOME {
|
|
||||||
sourceAccount.UpdatedUnixTime = time.Now().Unix()
|
|
||||||
updatedRows, err := sess.ID(sourceAccount.AccountId).SetExpr("balance", fmt.Sprintf("balance+(%d)", transaction.Amount)).Cols("updated_unix_time").Where("uid=? AND deleted=?", sourceAccount.Uid, false).Update(sourceAccount)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
} else if updatedRows < 1 {
|
|
||||||
return errs.ErrDatabaseOperationFailed
|
|
||||||
}
|
|
||||||
} else if transaction.Type == models.TRANSACTION_DB_TYPE_EXPENSE {
|
|
||||||
sourceAccount.UpdatedUnixTime = time.Now().Unix()
|
|
||||||
updatedRows, err := sess.ID(sourceAccount.AccountId).SetExpr("balance", fmt.Sprintf("balance-(%d)", transaction.Amount)).Cols("updated_unix_time").Where("uid=? AND deleted=?", sourceAccount.Uid, false).Update(sourceAccount)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
} else if updatedRows < 1 {
|
|
||||||
return errs.ErrDatabaseOperationFailed
|
|
||||||
}
|
|
||||||
} else if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
|
|
||||||
sourceAccount.UpdatedUnixTime = time.Now().Unix()
|
|
||||||
updatedSourceRows, err := sess.ID(sourceAccount.AccountId).SetExpr("balance", fmt.Sprintf("balance-(%d)", transaction.Amount)).Cols("updated_unix_time").Where("uid=? AND deleted=?", sourceAccount.Uid, false).Update(sourceAccount)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
} else if updatedSourceRows < 1 {
|
|
||||||
return errs.ErrDatabaseOperationFailed
|
|
||||||
}
|
|
||||||
|
|
||||||
destinationAccount.UpdatedUnixTime = time.Now().Unix()
|
|
||||||
updatedDestinationRows, err := sess.ID(destinationAccount.AccountId).SetExpr("balance", fmt.Sprintf("balance+(%d)", transaction.RelatedAccountAmount)).Cols("updated_unix_time").Where("uid=? AND deleted=?", destinationAccount.Uid, false).Update(destinationAccount)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
} else if updatedDestinationRows < 1 {
|
|
||||||
return errs.ErrDatabaseOperationFailed
|
|
||||||
}
|
|
||||||
} else if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
|
|
||||||
return errs.ErrTransactionTypeInvalid
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1587,6 +1475,190 @@ func (s *TransactionService) GetTransactionIds(transactions []*models.Transactio
|
|||||||
return transactionIds
|
return transactionIds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *TransactionService) doCreateTransaction(sess *xorm.Session, transaction *models.Transaction, transactionTagIndexes []*models.TransactionTagIndex, tagIds []int64, pictureIds []int64, pictureUpdateModel *models.TransactionPictureInfo) error {
|
||||||
|
// Get and verify source and destination account
|
||||||
|
sourceAccount, destinationAccount, err := s.getAccountModels(sess, transaction)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if sourceAccount.Hidden || (destinationAccount != nil && destinationAccount.Hidden) {
|
||||||
|
return errs.ErrCannotAddTransactionToHiddenAccount
|
||||||
|
}
|
||||||
|
|
||||||
|
if sourceAccount.Type == models.ACCOUNT_TYPE_MULTI_SUB_ACCOUNTS || (destinationAccount != nil && destinationAccount.Type == models.ACCOUNT_TYPE_MULTI_SUB_ACCOUNTS) {
|
||||||
|
return errs.ErrCannotAddTransactionToParentAccount
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT || transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN) &&
|
||||||
|
sourceAccount.Currency == destinationAccount.Currency && transaction.Amount != transaction.RelatedAccountAmount {
|
||||||
|
return errs.ErrTransactionSourceAndDestinationAmountNotEqual
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get and verify category
|
||||||
|
err = s.isCategoryValid(sess, transaction)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get and verify tags
|
||||||
|
err = s.isTagsValid(sess, transaction, transactionTagIndexes, tagIds)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get and verify pictures
|
||||||
|
err = s.isPicturesValid(sess, transaction, pictureIds)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify balance modification transaction and calculate real amount
|
||||||
|
if transaction.Type == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE {
|
||||||
|
otherTransactionExists, err := sess.Cols("uid", "deleted", "account_id").Where("uid=? AND deleted=? AND account_id=?", transaction.Uid, false, sourceAccount.AccountId).Limit(1).Exist(&models.Transaction{})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if otherTransactionExists {
|
||||||
|
return errs.ErrBalanceModificationTransactionCannotAddWhenNotEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction.RelatedAccountId = transaction.AccountId
|
||||||
|
transaction.RelatedAccountAmount = transaction.Amount - sourceAccount.Balance
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert transaction row
|
||||||
|
var relatedTransaction *models.Transaction
|
||||||
|
|
||||||
|
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT || transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
|
||||||
|
relatedTransaction = s.GetRelatedTransferTransaction(transaction)
|
||||||
|
}
|
||||||
|
|
||||||
|
createdRows, err := sess.Insert(transaction)
|
||||||
|
|
||||||
|
if err != nil || createdRows < 1 { // maybe another transaction has same time
|
||||||
|
sameSecondLatestTransaction := &models.Transaction{}
|
||||||
|
minTransactionTime := utils.GetMinTransactionTimeFromUnixTime(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime))
|
||||||
|
maxTransactionTime := utils.GetMaxTransactionTimeFromUnixTime(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime))
|
||||||
|
|
||||||
|
has, err := sess.Where("uid=? AND transaction_time>=? AND transaction_time<=?", transaction.Uid, minTransactionTime, maxTransactionTime).OrderBy("transaction_time desc").Limit(1).Get(sameSecondLatestTransaction)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if !has {
|
||||||
|
return errs.ErrDatabaseOperationFailed
|
||||||
|
} else if sameSecondLatestTransaction.TransactionTime == maxTransactionTime-1 {
|
||||||
|
return errs.ErrTooMuchTransactionInOneSecond
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction.TransactionTime = sameSecondLatestTransaction.TransactionTime + 1
|
||||||
|
createdRows, err := sess.Insert(transaction)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if createdRows < 1 {
|
||||||
|
return errs.ErrDatabaseOperationFailed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if relatedTransaction != nil {
|
||||||
|
relatedTransaction.TransactionTime = transaction.TransactionTime + 1
|
||||||
|
|
||||||
|
if utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime) != utils.GetUnixTimeFromTransactionTime(relatedTransaction.TransactionTime) {
|
||||||
|
return errs.ErrTooMuchTransactionInOneSecond
|
||||||
|
}
|
||||||
|
|
||||||
|
createdRows, err := sess.Insert(relatedTransaction)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if createdRows < 1 {
|
||||||
|
return errs.ErrDatabaseOperationFailed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = nil
|
||||||
|
|
||||||
|
// Insert transaction tag index
|
||||||
|
if len(transactionTagIndexes) > 0 {
|
||||||
|
for i := 0; i < len(transactionTagIndexes); i++ {
|
||||||
|
transactionTagIndex := transactionTagIndexes[i]
|
||||||
|
transactionTagIndex.TransactionTime = transaction.TransactionTime
|
||||||
|
|
||||||
|
_, err := sess.Insert(transactionTagIndex)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update transaction picture
|
||||||
|
if len(pictureIds) > 0 {
|
||||||
|
_, err = sess.Cols("transaction_id", "updated_unix_time").Where("uid=? AND deleted=? AND transaction_id=?", transaction.Uid, false, models.TransactionPictureNewPictureTransactionId).In("picture_id", pictureIds).Update(pictureUpdateModel)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update account table
|
||||||
|
if transaction.Type == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE {
|
||||||
|
sourceAccount.UpdatedUnixTime = time.Now().Unix()
|
||||||
|
updatedRows, err := sess.ID(sourceAccount.AccountId).SetExpr("balance", fmt.Sprintf("balance+(%d)", transaction.RelatedAccountAmount)).Cols("updated_unix_time").Where("uid=? AND deleted=?", sourceAccount.Uid, false).Update(sourceAccount)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if updatedRows < 1 {
|
||||||
|
return errs.ErrDatabaseOperationFailed
|
||||||
|
}
|
||||||
|
} else if transaction.Type == models.TRANSACTION_DB_TYPE_INCOME {
|
||||||
|
sourceAccount.UpdatedUnixTime = time.Now().Unix()
|
||||||
|
updatedRows, err := sess.ID(sourceAccount.AccountId).SetExpr("balance", fmt.Sprintf("balance+(%d)", transaction.Amount)).Cols("updated_unix_time").Where("uid=? AND deleted=?", sourceAccount.Uid, false).Update(sourceAccount)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if updatedRows < 1 {
|
||||||
|
return errs.ErrDatabaseOperationFailed
|
||||||
|
}
|
||||||
|
} else if transaction.Type == models.TRANSACTION_DB_TYPE_EXPENSE {
|
||||||
|
sourceAccount.UpdatedUnixTime = time.Now().Unix()
|
||||||
|
updatedRows, err := sess.ID(sourceAccount.AccountId).SetExpr("balance", fmt.Sprintf("balance-(%d)", transaction.Amount)).Cols("updated_unix_time").Where("uid=? AND deleted=?", sourceAccount.Uid, false).Update(sourceAccount)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if updatedRows < 1 {
|
||||||
|
return errs.ErrDatabaseOperationFailed
|
||||||
|
}
|
||||||
|
} else if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
|
||||||
|
sourceAccount.UpdatedUnixTime = time.Now().Unix()
|
||||||
|
updatedSourceRows, err := sess.ID(sourceAccount.AccountId).SetExpr("balance", fmt.Sprintf("balance-(%d)", transaction.Amount)).Cols("updated_unix_time").Where("uid=? AND deleted=?", sourceAccount.Uid, false).Update(sourceAccount)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if updatedSourceRows < 1 {
|
||||||
|
return errs.ErrDatabaseOperationFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
destinationAccount.UpdatedUnixTime = time.Now().Unix()
|
||||||
|
updatedDestinationRows, err := sess.ID(destinationAccount.AccountId).SetExpr("balance", fmt.Sprintf("balance+(%d)", transaction.RelatedAccountAmount)).Cols("updated_unix_time").Where("uid=? AND deleted=?", destinationAccount.Uid, false).Update(destinationAccount)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if updatedDestinationRows < 1 {
|
||||||
|
return errs.ErrDatabaseOperationFailed
|
||||||
|
}
|
||||||
|
} else if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
|
||||||
|
return errs.ErrTransactionTypeInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func (s *TransactionService) getTransactionQueryCondition(uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionDbType, categoryIds []int64, accountIds []int64, tagIds []int64, amountFilter string, keyword string, noDuplicated bool) (string, []any) {
|
func (s *TransactionService) getTransactionQueryCondition(uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionDbType, categoryIds []int64, accountIds []int64, tagIds []int64, amountFilter string, keyword string, noDuplicated bool) (string, []any) {
|
||||||
condition := "uid=? AND deleted=?"
|
condition := "uid=? AND deleted=?"
|
||||||
conditionParams := make([]any, 0, 16)
|
conditionParams := make([]any, 0, 16)
|
||||||
|
|||||||
+68
-1
@@ -1,6 +1,11 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import "strconv"
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
|
)
|
||||||
|
|
||||||
// IntToString returns the textual representation of this number
|
// IntToString returns the textual representation of this number
|
||||||
func IntToString(num int) string {
|
func IntToString(num int) string {
|
||||||
@@ -123,3 +128,65 @@ func FormatAmount(amount int64) string {
|
|||||||
|
|
||||||
return integer + "." + decimals
|
return integer + "." + decimals
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParseAmount parses a textual representation of amount
|
||||||
|
func ParseAmount(amount string) (int64, error) {
|
||||||
|
if len(amount) < 1 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sign := int64(1)
|
||||||
|
|
||||||
|
if amount[0] == '-' {
|
||||||
|
amount = amount[1:]
|
||||||
|
sign = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(amount) < 1 {
|
||||||
|
return 0, errs.ErrNumberInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
items := strings.Split(amount, ".")
|
||||||
|
|
||||||
|
if len(items) > 2 {
|
||||||
|
return 0, errs.ErrNumberInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
integer := int64(0)
|
||||||
|
decimals := int64(0)
|
||||||
|
|
||||||
|
if len(items[0]) > 0 {
|
||||||
|
integer, err = StringToInt64(items[0])
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if integer < 0 {
|
||||||
|
return 0, errs.ErrNumberInvalid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(items) == 2 {
|
||||||
|
if len(items[1]) > 2 {
|
||||||
|
return 0, errs.ErrNumberInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
decimals, err = StringToInt64(items[1])
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if decimals < 0 {
|
||||||
|
return 0, errs.ErrNumberInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(items[1]) == 1 {
|
||||||
|
decimals = decimals * 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sign*integer*100 + sign*decimals, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -172,3 +172,133 @@ func TestFormatAmount(t *testing.T) {
|
|||||||
actualValue = FormatAmount(-1234)
|
actualValue = FormatAmount(-1234)
|
||||||
assert.Equal(t, expectedValue, actualValue)
|
assert.Equal(t, expectedValue, actualValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseAmount(t *testing.T) {
|
||||||
|
expectedValue := int64(0)
|
||||||
|
actualValue, err := ParseAmount("")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, expectedValue, actualValue)
|
||||||
|
|
||||||
|
expectedValue = int64(0)
|
||||||
|
actualValue, err = ParseAmount("0")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, expectedValue, actualValue)
|
||||||
|
|
||||||
|
expectedValue = int64(0)
|
||||||
|
actualValue, err = ParseAmount("-0")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, expectedValue, actualValue)
|
||||||
|
|
||||||
|
expectedValue = int64(0)
|
||||||
|
actualValue, err = ParseAmount("0.00")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, expectedValue, actualValue)
|
||||||
|
|
||||||
|
expectedValue = int64(0)
|
||||||
|
actualValue, err = ParseAmount("-0.00")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, expectedValue, actualValue)
|
||||||
|
|
||||||
|
expectedValue = int64(1)
|
||||||
|
actualValue, err = ParseAmount("0.01")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, expectedValue, actualValue)
|
||||||
|
|
||||||
|
expectedValue = int64(-1)
|
||||||
|
actualValue, err = ParseAmount("-0.01")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, expectedValue, actualValue)
|
||||||
|
|
||||||
|
expectedValue = int64(10)
|
||||||
|
actualValue, err = ParseAmount("0.10")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, expectedValue, actualValue)
|
||||||
|
|
||||||
|
expectedValue = int64(-10)
|
||||||
|
actualValue, err = ParseAmount("-0.10")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, expectedValue, actualValue)
|
||||||
|
|
||||||
|
expectedValue = int64(10)
|
||||||
|
actualValue, err = ParseAmount("0.1")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, expectedValue, actualValue)
|
||||||
|
|
||||||
|
expectedValue = int64(-10)
|
||||||
|
actualValue, err = ParseAmount("-0.1")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, expectedValue, actualValue)
|
||||||
|
|
||||||
|
expectedValue = int64(12)
|
||||||
|
actualValue, err = ParseAmount("0.12")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, expectedValue, actualValue)
|
||||||
|
|
||||||
|
expectedValue = int64(-12)
|
||||||
|
actualValue, err = ParseAmount("-0.12")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, expectedValue, actualValue)
|
||||||
|
|
||||||
|
expectedValue = int64(100)
|
||||||
|
actualValue, err = ParseAmount("1")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, expectedValue, actualValue)
|
||||||
|
|
||||||
|
expectedValue = int64(-100)
|
||||||
|
actualValue, err = ParseAmount("-1")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, expectedValue, actualValue)
|
||||||
|
|
||||||
|
expectedValue = int64(120)
|
||||||
|
actualValue, err = ParseAmount("1.2")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, expectedValue, actualValue)
|
||||||
|
|
||||||
|
expectedValue = int64(-120)
|
||||||
|
actualValue, err = ParseAmount("-1.2")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, expectedValue, actualValue)
|
||||||
|
|
||||||
|
expectedValue = int64(123)
|
||||||
|
actualValue, err = ParseAmount("1.23")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, expectedValue, actualValue)
|
||||||
|
|
||||||
|
expectedValue = int64(-123)
|
||||||
|
actualValue, err = ParseAmount("-1.23")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, expectedValue, actualValue)
|
||||||
|
|
||||||
|
expectedValue = int64(1234)
|
||||||
|
actualValue, err = ParseAmount("12.34")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, expectedValue, actualValue)
|
||||||
|
|
||||||
|
expectedValue = int64(-1234)
|
||||||
|
actualValue, err = ParseAmount("-12.34")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, expectedValue, actualValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseAmount_InvalidAmount(t *testing.T) {
|
||||||
|
_, err := ParseAmount("-")
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
|
||||||
|
_, err = ParseAmount("--1")
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
|
||||||
|
_, err = ParseAmount("-.-1")
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
|
||||||
|
_, err = ParseAmount("0.-")
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
|
||||||
|
_, err = ParseAmount("0.-1")
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
|
||||||
|
_, err = ParseAmount("1.2.3")
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
|
||||||
|
_, err = ParseAmount("1.234")
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ func (u *InternalUuidGenerator) GenerateUuid(idType UuidType) int64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GenerateUuids generates new uuids
|
// GenerateUuids generates new uuids
|
||||||
func (u *InternalUuidGenerator) GenerateUuids(idType UuidType, count uint8) []int64 {
|
func (u *InternalUuidGenerator) GenerateUuids(idType UuidType, count uint16) []int64 {
|
||||||
// 63bits = unixTime(32bits) + uuidType(4bits) + uuidServerId(8bits) + sequentialNumber(19bits)
|
// 63bits = unixTime(32bits) + uuidType(4bits) + uuidServerId(8bits) + sequentialNumber(19bits)
|
||||||
|
|
||||||
uuids := make([]int64, count)
|
uuids := make([]int64, count)
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ func TestGenerateUuids_Count0(t *testing.T) {
|
|||||||
func TestGenerateUuids_Count255(t *testing.T) {
|
func TestGenerateUuids_Count255(t *testing.T) {
|
||||||
expectedUuidServerId := uint8(90)
|
expectedUuidServerId := uint8(90)
|
||||||
expectedUuidType := UUID_TYPE_TRANSACTION
|
expectedUuidType := UUID_TYPE_TRANSACTION
|
||||||
expectedUuidCount := uint8(255)
|
expectedUuidCount := uint16(255)
|
||||||
|
|
||||||
generator, _ := NewInternalUuidGenerator(&settings.Config{UuidServerId: expectedUuidServerId})
|
generator, _ := NewInternalUuidGenerator(&settings.Config{UuidServerId: expectedUuidServerId})
|
||||||
|
|
||||||
@@ -250,7 +250,7 @@ func TestGenerateUuids_Count255(t *testing.T) {
|
|||||||
func TestGenerateUuids_30TimesIn3Seconds(t *testing.T) {
|
func TestGenerateUuids_30TimesIn3Seconds(t *testing.T) {
|
||||||
expectedUuidServerId := uint8(90)
|
expectedUuidServerId := uint8(90)
|
||||||
expectedUuidType := UUID_TYPE_TRANSACTION
|
expectedUuidType := UUID_TYPE_TRANSACTION
|
||||||
expectedUuidCount := uint8(255)
|
expectedUuidCount := uint16(255)
|
||||||
|
|
||||||
generator, _ := NewInternalUuidGenerator(&settings.Config{UuidServerId: expectedUuidServerId})
|
generator, _ := NewInternalUuidGenerator(&settings.Config{UuidServerId: expectedUuidServerId})
|
||||||
|
|
||||||
@@ -289,7 +289,7 @@ func TestGenerateUuids_30TimesIn3Seconds(t *testing.T) {
|
|||||||
|
|
||||||
func TestGenerateUuids_20000TimesConcurrent(t *testing.T) {
|
func TestGenerateUuids_20000TimesConcurrent(t *testing.T) {
|
||||||
concurrentCount := 10
|
concurrentCount := 10
|
||||||
expectedUuidCount := uint8(20)
|
expectedUuidCount := uint16(20)
|
||||||
generator, _ := NewInternalUuidGenerator(&settings.Config{UuidServerId: 3})
|
generator, _ := NewInternalUuidGenerator(&settings.Config{UuidServerId: 3})
|
||||||
var mutex sync.Mutex
|
var mutex sync.Mutex
|
||||||
var generatedIds sync.Map
|
var generatedIds sync.Map
|
||||||
@@ -338,7 +338,7 @@ func TestGenerateUuids_20000TimesConcurrent(t *testing.T) {
|
|||||||
|
|
||||||
func TestGenerateUuids_1000000TimesConcurrent(t *testing.T) {
|
func TestGenerateUuids_1000000TimesConcurrent(t *testing.T) {
|
||||||
concurrentCount := 10
|
concurrentCount := 10
|
||||||
expectedUuidCount := uint8(250)
|
expectedUuidCount := uint16(250)
|
||||||
generator, _ := NewInternalUuidGenerator(&settings.Config{UuidServerId: 3})
|
generator, _ := NewInternalUuidGenerator(&settings.Config{UuidServerId: 3})
|
||||||
var mutex sync.Mutex
|
var mutex sync.Mutex
|
||||||
var generatedIds sync.Map
|
var generatedIds sync.Map
|
||||||
@@ -391,7 +391,7 @@ func TestGenerateUuids_1000000TimesConcurrent(t *testing.T) {
|
|||||||
|
|
||||||
func TestGenerateUuid_Over524287Times(t *testing.T) {
|
func TestGenerateUuid_Over524287Times(t *testing.T) {
|
||||||
generator, _ := NewInternalUuidGenerator(&settings.Config{UuidServerId: 1})
|
generator, _ := NewInternalUuidGenerator(&settings.Config{UuidServerId: 1})
|
||||||
onceGenerateCount := uint8(255)
|
onceGenerateCount := uint16(255)
|
||||||
generationStartUnixTime := time.Now().Unix()
|
generationStartUnixTime := time.Now().Unix()
|
||||||
|
|
||||||
for i := 0; i < 2057; i++ { // 2056*255=524280, 2057*255=524,535 (only can generates 524,287 uuids per second)
|
for i := 0; i < 2057; i++ { // 2056*255=524280, 2057*255=524,535 (only can generates 524,287 uuids per second)
|
||||||
|
|||||||
@@ -33,6 +33,6 @@ func (u *UuidContainer) GenerateUuid(uuidType UuidType) int64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GenerateUuids returns new uuids by the current uuid generator
|
// GenerateUuids returns new uuids by the current uuid generator
|
||||||
func (u *UuidContainer) GenerateUuids(uuidType UuidType, count uint8) []int64 {
|
func (u *UuidContainer) GenerateUuids(uuidType UuidType, count uint16) []int64 {
|
||||||
return u.Current.GenerateUuids(uuidType, count)
|
return u.Current.GenerateUuids(uuidType, count)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,5 +3,5 @@ package uuid
|
|||||||
// UuidGenerator is common uuid generator interface
|
// UuidGenerator is common uuid generator interface
|
||||||
type UuidGenerator interface {
|
type UuidGenerator interface {
|
||||||
GenerateUuid(uuidType UuidType) int64
|
GenerateUuid(uuidType UuidType) int64
|
||||||
GenerateUuids(uuidType UuidType, count uint8) []int64
|
GenerateUuids(uuidType UuidType, count uint16) []int64
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -970,6 +970,7 @@
|
|||||||
"system is busy": "System is busy",
|
"system is busy": "System is busy",
|
||||||
"not supported": "Not supported",
|
"not supported": "Not supported",
|
||||||
"image type not supported": "Image type is not supported",
|
"image type not supported": "Image type is not supported",
|
||||||
|
"import file type not supported": "Import file type is not supported",
|
||||||
"database operation failed": "Database operation failed",
|
"database operation failed": "Database operation failed",
|
||||||
"SMTP server is not enabled": "SMTP server is not enabled",
|
"SMTP server is not enabled": "SMTP server is not enabled",
|
||||||
"incomplete or incorrect submission": "Incomplete or incorrect submission",
|
"incomplete or incorrect submission": "Incomplete or incorrect submission",
|
||||||
@@ -1084,6 +1085,8 @@
|
|||||||
"transaction tag is in use and cannot be deleted": "Transaction tag is in use and it cannot be deleted",
|
"transaction tag is in use and cannot be deleted": "Transaction tag is in use and it cannot be deleted",
|
||||||
"transaction tag index not found": "Transaction tag index is not found",
|
"transaction tag index not found": "Transaction tag index is not found",
|
||||||
"data export not allowed": "User data export is not allowed",
|
"data export not allowed": "User data export is not allowed",
|
||||||
|
"data import not allowed": "User data import is not allowed",
|
||||||
|
"import too many transactions": "There are too many transactions to import",
|
||||||
"transaction template id is invalid": "Transaction template ID is invalid",
|
"transaction template id is invalid": "Transaction template ID is invalid",
|
||||||
"transaction template not found": "Transaction template is not found",
|
"transaction template not found": "Transaction template is not found",
|
||||||
"transaction template type is invalid": "Transaction template type is invalid",
|
"transaction template type is invalid": "Transaction template type is invalid",
|
||||||
@@ -1099,7 +1102,9 @@
|
|||||||
"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",
|
||||||
"parameter invalid": "Parameter is invalid"
|
"parameter invalid": "Parameter is invalid",
|
||||||
|
"format invalid": "Format is invalid",
|
||||||
|
"number invalid": "Number is invalid"
|
||||||
},
|
},
|
||||||
"parameter": {
|
"parameter": {
|
||||||
"id": "ID",
|
"id": "ID",
|
||||||
|
|||||||
@@ -970,6 +970,7 @@
|
|||||||
"system is busy": "系统繁忙",
|
"system is busy": "系统繁忙",
|
||||||
"not supported": "不支持",
|
"not supported": "不支持",
|
||||||
"image type not supported": "图片类型不支持",
|
"image type not supported": "图片类型不支持",
|
||||||
|
"import file type not supported": "导入文件类型不支持",
|
||||||
"database operation failed": "数据库操作失败",
|
"database operation failed": "数据库操作失败",
|
||||||
"SMTP server is not enabled": "SMTP 服务器没有启用",
|
"SMTP server is not enabled": "SMTP 服务器没有启用",
|
||||||
"incomplete or incorrect submission": "提交不完整或不正确",
|
"incomplete or incorrect submission": "提交不完整或不正确",
|
||||||
@@ -1084,6 +1085,8 @@
|
|||||||
"transaction tag is in use and cannot be deleted": "交易标签正在被使用,无法删除",
|
"transaction tag is in use and cannot be deleted": "交易标签正在被使用,无法删除",
|
||||||
"transaction tag index not found": "交易标签索引不存在",
|
"transaction tag index not found": "交易标签索引不存在",
|
||||||
"data export not allowed": "不允许用户数据导出",
|
"data export not allowed": "不允许用户数据导出",
|
||||||
|
"data import not allowed": "不允许用户数据导入",
|
||||||
|
"import too many transactions": "导入的交易过多",
|
||||||
"transaction template id is invalid": "交易模板ID无效",
|
"transaction template id is invalid": "交易模板ID无效",
|
||||||
"transaction template not found": "交易模板不存在",
|
"transaction template not found": "交易模板不存在",
|
||||||
"transaction template type is invalid": "交易模板类型无效",
|
"transaction template type is invalid": "交易模板类型无效",
|
||||||
@@ -1099,7 +1102,9 @@
|
|||||||
"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": "请求项目中有非法项目",
|
||||||
"parameter invalid": "参数错误"
|
"parameter invalid": "参数错误",
|
||||||
|
"format invalid": "格式错误",
|
||||||
|
"number invalid": "数字错误"
|
||||||
},
|
},
|
||||||
"parameter": {
|
"parameter": {
|
||||||
"id": "ID",
|
"id": "ID",
|
||||||
|
|||||||
Reference in New Issue
Block a user