mirror of
https://github.com/mayswind/ezbookkeeping.git
synced 2026-05-15 23:47:33 +08:00
support importing transaction in frontend
This commit is contained in:
+242
-12
@@ -1,11 +1,13 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
orderedmap "github.com/wk8/go-ordered-map/v2"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/converters"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/duplicatechecker"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||
@@ -23,12 +25,14 @@ const maximumPicturesCountOfTransaction = 10
|
||||
type TransactionsApi struct {
|
||||
ApiUsingConfig
|
||||
ApiUsingDuplicateChecker
|
||||
transactions *services.TransactionService
|
||||
transactionCategories *services.TransactionCategoryService
|
||||
transactionTags *services.TransactionTagService
|
||||
transactionPictures *services.TransactionPictureService
|
||||
accounts *services.AccountService
|
||||
users *services.UserService
|
||||
ezBookKeepingCsvConverter converters.TransactionDataConverter
|
||||
ezBookKeepingTsvConverter converters.TransactionDataConverter
|
||||
transactions *services.TransactionService
|
||||
transactionCategories *services.TransactionCategoryService
|
||||
transactionTags *services.TransactionTagService
|
||||
transactionPictures *services.TransactionPictureService
|
||||
accounts *services.AccountService
|
||||
users *services.UserService
|
||||
}
|
||||
|
||||
// Initialize a transaction api singleton instance
|
||||
@@ -40,12 +44,14 @@ var (
|
||||
ApiUsingDuplicateChecker: ApiUsingDuplicateChecker{
|
||||
container: duplicatechecker.Container,
|
||||
},
|
||||
transactions: services.Transactions,
|
||||
transactionCategories: services.TransactionCategories,
|
||||
transactionTags: services.TransactionTags,
|
||||
transactionPictures: services.TransactionPictures,
|
||||
accounts: services.Accounts,
|
||||
users: services.Users,
|
||||
ezBookKeepingCsvConverter: converters.EzBookKeepingTransactionDataCSVFileConverter,
|
||||
ezBookKeepingTsvConverter: converters.EzBookKeepingTransactionDataTSVFileConverter,
|
||||
transactions: services.Transactions,
|
||||
transactionCategories: services.TransactionCategories,
|
||||
transactionTags: services.TransactionTags,
|
||||
transactionPictures: services.TransactionPictures,
|
||||
accounts: services.Accounts,
|
||||
users: services.Users,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1004,6 +1010,230 @@ func (a *TransactionsApi) TransactionDeleteHandler(c *core.WebContext) (any, *er
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// TransactionParseImportFileHandler returns the parsed transaction data by request parameters for current user
|
||||
func (a *TransactionsApi) TransactionParseImportFileHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
uid := c.GetCurrentUid()
|
||||
form, err := c.MultipartForm()
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transactions.TransactionParseImportFileHandler] failed to get multi-part form data for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.ErrParameterInvalid
|
||||
}
|
||||
|
||||
utcOffset, err := c.GetClientTimezoneOffset()
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[transactions.TransactionParseImportFileHandler] cannot get client timezone offset, because %s", err.Error())
|
||||
return nil, errs.ErrClientTimezoneOffsetInvalid
|
||||
}
|
||||
|
||||
fileTypes := form.Value["fileType"]
|
||||
|
||||
if len(fileTypes) < 1 || fileTypes[0] == "" {
|
||||
return nil, errs.ErrImportFileTypeIsEmpty
|
||||
}
|
||||
|
||||
fileType := fileTypes[0]
|
||||
var dataImporter converters.TransactionDataImporter
|
||||
|
||||
if fileType == "ezbookkeeping_csv" {
|
||||
dataImporter = a.ezBookKeepingCsvConverter
|
||||
} else if fileType == "ezbookkeeping_tsv" {
|
||||
dataImporter = a.ezBookKeepingTsvConverter
|
||||
} else {
|
||||
return nil, errs.ErrImportFileTypeNotSupported
|
||||
}
|
||||
|
||||
importFiles := form.File["file"]
|
||||
|
||||
if len(importFiles) < 1 {
|
||||
log.Warnf(c, "[transactions.TransactionParseImportFileHandler] there is no import file in request for user \"uid:%d\"", uid)
|
||||
return nil, errs.ErrNoFilesUpload
|
||||
}
|
||||
|
||||
if importFiles[0].Size < 1 {
|
||||
log.Warnf(c, "[transactions.TransactionParseImportFileHandler] the size of import file in request is zero for user \"uid:%d\"", uid)
|
||||
return nil, errs.ErrUploadedFileEmpty
|
||||
}
|
||||
|
||||
if importFiles[0].Size > int64(a.CurrentConfig().MaxImportFileSize) {
|
||||
log.Warnf(c, "[transactions.TransactionParseImportFileHandler] the upload file size \"%d\" exceeds the maximum size \"%d\" of import file for user \"uid:%d\"", importFiles[0].Size, a.CurrentConfig().MaxImportFileSize, uid)
|
||||
return nil, errs.ErrExceedMaxUploadFileSize
|
||||
}
|
||||
|
||||
importFile, err := importFiles[0].Open()
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transactions.TransactionParseImportFileHandler] failed to get import file from request for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.ErrOperationFailed
|
||||
}
|
||||
|
||||
fileData, err := io.ReadAll(importFile)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transactions.TransactionParseImportFileHandler] failed to read import file data for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
user, err := a.users.GetUserById(c, uid)
|
||||
|
||||
if err != nil {
|
||||
if !errs.IsCustomError(err) {
|
||||
log.Errorf(c, "[transactions.TransactionParseImportFileHandler] failed to get user, because %s", err.Error())
|
||||
}
|
||||
|
||||
return nil, errs.ErrUserNotFound
|
||||
}
|
||||
|
||||
accounts, err := a.accounts.GetAllAccountsByUid(c, user.Uid)
|
||||
|
||||
if err != nil {
|
||||
log.BootErrorf(c, "[transactions.TransactionParseImportFileHandler] failed to get accounts for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
accountMap := a.accounts.GetAccountNameMapByList(accounts)
|
||||
|
||||
categories, err := a.transactionCategories.GetAllCategoriesByUid(c, user.Uid, 0, -1)
|
||||
|
||||
if err != nil {
|
||||
log.BootErrorf(c, "[transactions.TransactionParseImportFileHandler] failed to get categories for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
categoryMap := a.transactionCategories.GetCategoryNameMapByList(categories)
|
||||
|
||||
tags, err := a.transactionTags.GetAllTagsByUid(c, user.Uid)
|
||||
|
||||
if err != nil {
|
||||
log.BootErrorf(c, "[transactions.TransactionParseImportFileHandler] failed to get tags for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
tagMap := a.transactionTags.GetTagNameMapByList(tags)
|
||||
|
||||
parsedTransactions, _, _, _, err := dataImporter.ParseImportedData(c, user, fileData, utcOffset, accountMap, categoryMap, tagMap)
|
||||
|
||||
if err != nil {
|
||||
log.BootErrorf(c, "[transactions.TransactionParseImportFileHandler] failed to parse imported data for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
parsedTransactionRespsList := parsedTransactions.ToImportTransactionResponseList()
|
||||
|
||||
if len(parsedTransactionRespsList) < 1 {
|
||||
return nil, errs.ErrNoDataToImport
|
||||
}
|
||||
|
||||
parsedTransactionResps := &models.ImportTransactionResponsePageWrapper{
|
||||
Items: parsedTransactionRespsList,
|
||||
TotalCount: int64(len(parsedTransactionRespsList)),
|
||||
}
|
||||
|
||||
return parsedTransactionResps, nil
|
||||
}
|
||||
|
||||
// TransactionImportHandler imports transactions by request parameters for current user
|
||||
func (a *TransactionsApi) TransactionImportHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
var transactionImportReq models.TransactionImportRequest
|
||||
err := c.ShouldBindJSON(&transactionImportReq)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[transactions.TransactionImportHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
uid := c.GetCurrentUid()
|
||||
|
||||
if a.CurrentConfig().EnableDuplicateSubmissionsCheck && transactionImportReq.ClientSessionId != "" {
|
||||
found, remark := a.GetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_IMPORT_TRANSACTIONS, uid, transactionImportReq.ClientSessionId)
|
||||
|
||||
if found {
|
||||
log.Infof(c, "[transactions.TransactionImportHandler] another \"%s\" transactions has been imported for user \"uid:%d\"", remark, uid)
|
||||
count, err := utils.StringToInt(remark)
|
||||
|
||||
if err == nil {
|
||||
return count, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < len(transactionImportReq.Transactions); i++ {
|
||||
transactionCreateReq := transactionImportReq.Transactions[i]
|
||||
tagIds, err := utils.StringArrayToInt64Array(transactionCreateReq.TagIds)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[transactions.TransactionImportHandler] parse tag ids failed of transaction \"index:%d\", because %s", i, err.Error())
|
||||
return nil, errs.ErrTransactionTagIdInvalid
|
||||
}
|
||||
|
||||
if len(tagIds) > maximumTagsCountOfTransaction {
|
||||
return nil, errs.ErrTransactionHasTooManyTags
|
||||
}
|
||||
|
||||
if transactionCreateReq.Type < models.TRANSACTION_TYPE_MODIFY_BALANCE || transactionCreateReq.Type > models.TRANSACTION_TYPE_TRANSFER {
|
||||
log.Warnf(c, "[transactions.TransactionImportHandler] transaction type of transaction \"index:%d\" is invalid", i)
|
||||
return nil, errs.ErrTransactionTypeInvalid
|
||||
}
|
||||
|
||||
if transactionCreateReq.Type == models.TRANSACTION_TYPE_MODIFY_BALANCE && transactionCreateReq.CategoryId > 0 {
|
||||
log.Warnf(c, "[transactions.TransactionImportHandler] balance modification transaction \"index:%d\" cannot set category id", i)
|
||||
return nil, errs.ErrBalanceModificationTransactionCannotSetCategory
|
||||
}
|
||||
|
||||
if transactionCreateReq.Type != models.TRANSACTION_TYPE_TRANSFER && transactionCreateReq.DestinationAccountId != 0 {
|
||||
log.Warnf(c, "[transactions.TransactionImportHandler] non-transfer transaction \"index:%d\" destination account cannot be set", i)
|
||||
return nil, errs.ErrTransactionDestinationAccountCannotBeSet
|
||||
} else if transactionCreateReq.Type == models.TRANSACTION_TYPE_TRANSFER && transactionCreateReq.SourceAccountId == transactionCreateReq.DestinationAccountId {
|
||||
log.Warnf(c, "[transactions.TransactionImportHandler] transfer transaction \"index:%d\" source account must not be destination account", i)
|
||||
return nil, errs.ErrTransactionSourceAndDestinationIdCannotBeEqual
|
||||
}
|
||||
|
||||
if transactionCreateReq.Type != models.TRANSACTION_TYPE_TRANSFER && transactionCreateReq.DestinationAmount != 0 {
|
||||
log.Warnf(c, "[transactions.TransactionImportHandler] non-transfer transaction \"index:%d\" destination amount cannot be set", i)
|
||||
return nil, errs.ErrTransactionDestinationAmountCannotBeSet
|
||||
}
|
||||
}
|
||||
|
||||
user, err := a.users.GetUserById(c, uid)
|
||||
|
||||
if err != nil {
|
||||
if !errs.IsCustomError(err) {
|
||||
log.Errorf(c, "[transactions.TransactionImportHandler] failed to get user, because %s", err.Error())
|
||||
}
|
||||
|
||||
return nil, errs.ErrUserNotFound
|
||||
}
|
||||
|
||||
newTransactions := make([]*models.Transaction, len(transactionImportReq.Transactions))
|
||||
|
||||
for i := 0; i < len(transactionImportReq.Transactions); i++ {
|
||||
transactionCreateReq := transactionImportReq.Transactions[i]
|
||||
transaction := a.createNewTransactionModel(uid, transactionCreateReq, c.ClientIP())
|
||||
transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, transactionCreateReq.UtcOffset)
|
||||
|
||||
if !transactionEditable {
|
||||
return nil, errs.ErrCannotCreateTransactionWithThisTransactionTime
|
||||
}
|
||||
|
||||
newTransactions[i] = transaction
|
||||
}
|
||||
|
||||
err = a.transactions.BatchCreateTransactions(c, user.Uid, newTransactions)
|
||||
count := len(newTransactions)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transactions.TransactionImportHandler] failed to import %d transactions for user \"uid:%d\", because %s", count, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
log.Infof(c, "[transactions.TransactionImportHandler] user \"uid:%d\" has imported %d transactions successfully", uid, count)
|
||||
|
||||
a.SetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_IMPORT_TRANSACTIONS, uid, transactionImportReq.ClientSessionId, utils.IntToString(count))
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (a *TransactionsApi) filterTransactions(c *core.WebContext, uid int64, transactions []*models.Transaction, accountMap map[int64]*models.Account) []*models.Transaction {
|
||||
finalTransactions := make([]*models.Transaction, 0, len(transactions))
|
||||
|
||||
|
||||
@@ -720,7 +720,7 @@ func (l *UserDataCli) ImportTransaction(c *core.CliContext, username string, fil
|
||||
return errs.ErrOperationFailed
|
||||
}
|
||||
|
||||
err = l.transactions.BatchCreateTransactions(c, user.Uid, newTransactions)
|
||||
err = l.transactions.BatchCreateTransactions(c, user.Uid, newTransactions.ToTransactionsList())
|
||||
|
||||
if err != nil {
|
||||
log.BootErrorf(c, "[user_data.ImportTransaction] failed to create transaction, because %s", err.Error())
|
||||
|
||||
@@ -82,7 +82,7 @@ func (c *DataTableTransactionDataConverter) buildExportedContent(ctx core.Contex
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *DataTableTransactionDataConverter) parseImportedData(ctx core.Context, user *models.User, dataTable ImportedDataTable, defaultTimezoneOffset int16, accountMap map[string]*models.Account, categoryMap map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) ([]*models.Transaction, []*models.Account, []*models.TransactionCategory, []*models.TransactionTag, error) {
|
||||
func (c *DataTableTransactionDataConverter) parseImportedData(ctx core.Context, user *models.User, dataTable ImportedDataTable, defaultTimezoneOffset int16, accountMap map[string]*models.Account, categoryMap map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionTag, error) {
|
||||
if dataTable.DataRowCount() < 1 {
|
||||
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse import data for user \"uid:%d\", because data table row count is less 1", user.Uid)
|
||||
return nil, nil, nil, nil, errs.ErrOperationFailed
|
||||
@@ -127,7 +127,7 @@ func (c *DataTableTransactionDataConverter) parseImportedData(ctx core.Context,
|
||||
tagMap = make(map[string]*models.TransactionTag)
|
||||
}
|
||||
|
||||
allNewTransactions := make(ImportedTransactionSlice, 0, dataTable.DataRowCount())
|
||||
allNewTransactions := make(models.ImportedTransactionSlice, 0, dataTable.DataRowCount())
|
||||
allNewAccounts := make([]*models.Account, 0)
|
||||
allNewSubCategories := make([]*models.TransactionCategory, 0)
|
||||
allNewTags := make([]*models.TransactionTag, 0)
|
||||
@@ -177,6 +177,7 @@ func (c *DataTableTransactionDataConverter) parseImportedData(ctx core.Context,
|
||||
}
|
||||
|
||||
categoryId := int64(0)
|
||||
subCategoryName := ""
|
||||
|
||||
if transactionDbType != models.TRANSACTION_DB_TYPE_MODIFY_BALANCE {
|
||||
transactionCategoryType, err := c.getTransactionCategoryType(transactionDbType)
|
||||
@@ -186,7 +187,7 @@ func (c *DataTableTransactionDataConverter) parseImportedData(ctx core.Context,
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
subCategoryName := dataRow.GetData(subCategoryColumnIdx)
|
||||
subCategoryName = dataRow.GetData(subCategoryColumnIdx)
|
||||
|
||||
if subCategoryName == "" {
|
||||
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] sub category type is empty in data row \"index:%d\" for user \"uid:%d\"", dataRowIndex, user.Uid)
|
||||
@@ -211,30 +212,32 @@ func (c *DataTableTransactionDataConverter) parseImportedData(ctx core.Context,
|
||||
return nil, nil, nil, nil, errs.ErrFormatInvalid
|
||||
}
|
||||
|
||||
accountCurrency := user.DefaultCurrency
|
||||
|
||||
if accountCurrencyColumnExists {
|
||||
accountCurrency = dataRow.GetData(accountCurrencyColumnIdx)
|
||||
|
||||
if _, ok := validators.AllCurrencyNames[accountCurrency]; !ok {
|
||||
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] account currency \"%s\" is not supported in data row \"index:%d\" for user \"uid:%d\"", accountCurrency, dataRowIndex, user.Uid)
|
||||
return nil, nil, nil, nil, errs.ErrAccountCurrencyInvalid
|
||||
}
|
||||
}
|
||||
|
||||
account, exists := accountMap[accountName]
|
||||
|
||||
if !exists {
|
||||
currency := user.DefaultCurrency
|
||||
|
||||
if accountCurrencyColumnExists {
|
||||
currency = dataRow.GetData(accountCurrencyColumnIdx)
|
||||
|
||||
if _, ok := validators.AllCurrencyNames[currency]; !ok {
|
||||
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] account currency \"%s\" is not supported in data row \"index:%d\" for user \"uid:%d\"", currency, dataRowIndex, user.Uid)
|
||||
return nil, nil, nil, nil, errs.ErrAccountCurrencyInvalid
|
||||
}
|
||||
}
|
||||
|
||||
account = c.createNewAccountModel(user.Uid, accountName, currency)
|
||||
account = c.createNewAccountModel(user.Uid, accountName, accountCurrency)
|
||||
allNewAccounts = append(allNewAccounts, account)
|
||||
accountMap[accountName] = account
|
||||
}
|
||||
|
||||
if accountCurrencyColumnExists {
|
||||
if account.Currency != dataRow.GetData(accountCurrencyColumnIdx) {
|
||||
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] currency \"%s\" in data row \"index:%d\" not equals currency \"%s\" of the account for user \"uid:%d\"", dataRow.GetData(accountCurrencyColumnIdx), dataRowIndex, account.Currency, user.Uid)
|
||||
if account.Currency != accountCurrency {
|
||||
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] currency \"%s\" in data row \"index:%d\" not equals currency \"%s\" of the account for user \"uid:%d\"", accountCurrency, dataRowIndex, account.Currency, user.Uid)
|
||||
return nil, nil, nil, nil, errs.ErrAccountCurrencyInvalid
|
||||
}
|
||||
} else if exists {
|
||||
accountCurrency = account.Currency
|
||||
}
|
||||
|
||||
amount, err := utils.ParseAmount(dataRow.GetData(amountColumnIdx))
|
||||
@@ -246,39 +249,43 @@ func (c *DataTableTransactionDataConverter) parseImportedData(ctx core.Context,
|
||||
|
||||
relatedAccountId := int64(0)
|
||||
relatedAccountAmount := int64(0)
|
||||
account2Name := ""
|
||||
account2Currency := ""
|
||||
|
||||
if transactionDbType == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
|
||||
account2Name := dataRow.GetData(account2ColumnIdx)
|
||||
account2Name = dataRow.GetData(account2ColumnIdx)
|
||||
|
||||
if account2Name == "" {
|
||||
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] account2 name is empty in data row \"index:%d\" for user \"uid:%d\"", dataRowIndex, user.Uid)
|
||||
return nil, nil, nil, nil, errs.ErrFormatInvalid
|
||||
}
|
||||
|
||||
account2Currency = user.DefaultCurrency
|
||||
|
||||
if account2CurrencyColumnExists {
|
||||
account2Currency = dataRow.GetData(account2CurrencyColumnIdx)
|
||||
|
||||
if _, ok := validators.AllCurrencyNames[account2Currency]; !ok {
|
||||
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] account2 currency \"%s\" is not supported in data row \"index:%d\" for user \"uid:%d\"", account2Currency, dataRowIndex, user.Uid)
|
||||
return nil, nil, nil, nil, errs.ErrAccountCurrencyInvalid
|
||||
}
|
||||
}
|
||||
|
||||
account2, exists := accountMap[account2Name]
|
||||
|
||||
if !exists {
|
||||
currency := user.DefaultCurrency
|
||||
|
||||
if accountCurrencyColumnExists {
|
||||
currency = dataRow.GetData(account2CurrencyColumnIdx)
|
||||
|
||||
if _, ok := validators.AllCurrencyNames[currency]; !ok {
|
||||
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] account2 currency \"%s\" is not supported in data row \"index:%d\" for user \"uid:%d\"", currency, dataRowIndex, user.Uid)
|
||||
return nil, nil, nil, nil, errs.ErrAccountCurrencyInvalid
|
||||
}
|
||||
}
|
||||
|
||||
account2 = c.createNewAccountModel(user.Uid, account2Name, currency)
|
||||
account2 = c.createNewAccountModel(user.Uid, account2Name, account2Currency)
|
||||
allNewAccounts = append(allNewAccounts, account2)
|
||||
accountMap[account2Name] = account2
|
||||
}
|
||||
|
||||
if account2CurrencyColumnExists {
|
||||
if account2.Currency != dataRow.GetData(account2CurrencyColumnIdx) {
|
||||
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] currency \"%s\" in data row \"index:%d\" not equals currency \"%s\" of the account2 for user \"uid:%d\"", dataRow.GetData(account2CurrencyColumnIdx), dataRowIndex, account2.Currency, user.Uid)
|
||||
if account2.Currency != account2Currency {
|
||||
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] currency \"%s\" in data row \"index:%d\" not equals currency \"%s\" of the account2 for user \"uid:%d\"", account2Currency, dataRowIndex, account2.Currency, user.Uid)
|
||||
return nil, nil, nil, nil, errs.ErrAccountCurrencyInvalid
|
||||
}
|
||||
} else if exists {
|
||||
account2Currency = account2.Currency
|
||||
}
|
||||
|
||||
relatedAccountId = account2.AccountId
|
||||
@@ -313,11 +320,14 @@ func (c *DataTableTransactionDataConverter) parseImportedData(ctx core.Context,
|
||||
}
|
||||
}
|
||||
|
||||
if tagsColumnExists {
|
||||
tagNames := strings.Split(dataRow.GetData(tagsColumnIdx), c.transactionTagSeparator)
|
||||
var tagIds []string
|
||||
var tagNames []string
|
||||
|
||||
for i := 0; i < len(tagNames); i++ {
|
||||
tagName := tagNames[i]
|
||||
if tagsColumnExists {
|
||||
tagNameItems := strings.Split(dataRow.GetData(tagsColumnIdx), c.transactionTagSeparator)
|
||||
|
||||
for i := 0; i < len(tagNameItems); i++ {
|
||||
tagName := tagNameItems[i]
|
||||
|
||||
if tagName == "" {
|
||||
continue
|
||||
@@ -330,6 +340,12 @@ func (c *DataTableTransactionDataConverter) parseImportedData(ctx core.Context,
|
||||
allNewTags = append(allNewTags, tag)
|
||||
tagMap[tagName] = tag
|
||||
}
|
||||
|
||||
if tag != nil {
|
||||
tagIds = append(tagIds, utils.Int64ToString(tag.TagId))
|
||||
}
|
||||
|
||||
tagNames = append(tagNames, tagName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,21 +355,30 @@ func (c *DataTableTransactionDataConverter) parseImportedData(ctx core.Context,
|
||||
description = dataRow.GetData(descriptionColumnIdx)
|
||||
}
|
||||
|
||||
transaction := &models.Transaction{
|
||||
Uid: user.Uid,
|
||||
Type: transactionDbType,
|
||||
CategoryId: categoryId,
|
||||
TransactionTime: utils.GetMinTransactionTimeFromUnixTime(transactionTime.Unix()),
|
||||
TimezoneUtcOffset: timezoneOffset,
|
||||
AccountId: account.AccountId,
|
||||
Amount: amount,
|
||||
HideAmount: false,
|
||||
RelatedAccountId: relatedAccountId,
|
||||
RelatedAccountAmount: relatedAccountAmount,
|
||||
Comment: description,
|
||||
GeoLongitude: geoLongitude,
|
||||
GeoLatitude: geoLatitude,
|
||||
CreatedIp: "127.0.0.1",
|
||||
transaction := &models.ImportTransaction{
|
||||
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",
|
||||
},
|
||||
TagIds: tagIds,
|
||||
OriginalCategoryName: subCategoryName,
|
||||
OriginalSourceAccountName: accountName,
|
||||
OriginalSourceAccountCurrency: accountCurrency,
|
||||
OriginalDestinationAccountName: account2Name,
|
||||
OriginalDestinationAccountCurrency: account2Currency,
|
||||
OriginalTagNames: tagNames,
|
||||
}
|
||||
|
||||
allNewTransactions = append(allNewTransactions, transaction)
|
||||
|
||||
@@ -73,7 +73,7 @@ func (c *ezBookKeepingTransactionDataPlainTextConverter) ToExportedContent(ctx c
|
||||
}
|
||||
|
||||
// ParseImportedData returns the imported data by parsing the transaction plain text data
|
||||
func (c *ezBookKeepingTransactionDataPlainTextConverter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, accountMap map[string]*models.Account, categoryMap map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) ([]*models.Transaction, []*models.Account, []*models.TransactionCategory, []*models.TransactionTag, error) {
|
||||
func (c *ezBookKeepingTransactionDataPlainTextConverter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, accountMap map[string]*models.Account, categoryMap map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionTag, error) {
|
||||
dataTable, err := createNewezbookkeepingTransactionPlainTextDataTable(string(data), c.columnSeparator, c.lineSeparator)
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -147,7 +147,9 @@ func createNewezbookkeepingTransactionPlainTextDataTable(content string, columnS
|
||||
return nil, errs.ErrOperationFailed
|
||||
}
|
||||
|
||||
headerLineItems := strings.Split(allLines[0], columnSeparator)
|
||||
headerLine := allLines[0]
|
||||
headerLine = strings.ReplaceAll(headerLine, "\r", "")
|
||||
headerLineItems := strings.Split(headerLine, columnSeparator)
|
||||
|
||||
return &ezBookKeepingTransactionPlainTextDataTable{
|
||||
columnSeparator: columnSeparator,
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
package converters
|
||||
|
||||
import "github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
|
||||
// ImportedTransactionSlice represents the slice data structure of import transaction data
|
||||
type ImportedTransactionSlice []*models.Transaction
|
||||
|
||||
// Len returns the count of items
|
||||
func (s ImportedTransactionSlice) Len() int {
|
||||
return len(s)
|
||||
}
|
||||
|
||||
// Swap swaps two items
|
||||
func (s ImportedTransactionSlice) Swap(i, j int) {
|
||||
s[i], s[j] = s[j], s[i]
|
||||
}
|
||||
|
||||
// Less reports whether the first item is less than the second one
|
||||
func (s ImportedTransactionSlice) Less(i, j int) bool {
|
||||
if s[i].Type != s[j].Type && (s[i].Type == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE || s[j].Type == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE) {
|
||||
if s[i].Type == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE {
|
||||
return true
|
||||
} else if s[j].Type == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return s[i].TransactionTime < s[j].TransactionTime
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package converters
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
)
|
||||
|
||||
func TestImportTransactionSliceLess(t *testing.T) {
|
||||
var transactionSlice ImportedTransactionSlice
|
||||
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)
|
||||
}
|
||||
@@ -14,7 +14,7 @@ type TransactionDataExporter interface {
|
||||
// TransactionDataImporter defines the structure of transaction data importer
|
||||
type TransactionDataImporter interface {
|
||||
// ParseImportedData returns the imported data
|
||||
ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, accountMap map[string]*models.Account, categoryMap map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) ([]*models.Transaction, []*models.Account, []*models.TransactionCategory, []*models.TransactionTag, error)
|
||||
ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, accountMap map[string]*models.Account, categoryMap map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionTag, error)
|
||||
}
|
||||
|
||||
// TransactionDataConverter defines the structure of transaction data converter
|
||||
|
||||
@@ -11,4 +11,5 @@ const (
|
||||
DUPLICATE_CHECKER_TYPE_NEW_TRANSACTION DuplicateCheckerType = 3
|
||||
DUPLICATE_CHECKER_TYPE_NEW_TEMPLATE DuplicateCheckerType = 4
|
||||
DUPLICATE_CHECKER_TYPE_NEW_PICTURE DuplicateCheckerType = 5
|
||||
DUPLICATE_CHECKER_TYPE_IMPORT_TRANSACTIONS DuplicateCheckerType = 6
|
||||
)
|
||||
|
||||
@@ -22,6 +22,9 @@ var (
|
||||
ErrParameterInvalid = NewNormalError(NormalSubcategoryGlobal, 12, http.StatusBadRequest, "parameter invalid")
|
||||
ErrFormatInvalid = NewNormalError(NormalSubcategoryGlobal, 13, http.StatusBadRequest, "format invalid")
|
||||
ErrNumberInvalid = NewNormalError(NormalSubcategoryGlobal, 14, http.StatusBadRequest, "number invalid")
|
||||
ErrNoFilesUpload = NewNormalError(NormalSubcategoryGlobal, 15, http.StatusBadRequest, "no files uploaded")
|
||||
ErrUploadedFileEmpty = NewNormalError(NormalSubcategoryGlobal, 16, http.StatusBadRequest, "uploaded file is empty")
|
||||
ErrExceedMaxUploadFileSize = NewNormalError(NormalSubcategoryGlobal, 17, http.StatusBadRequest, "uploaded file size exceeds the maximum allowed size")
|
||||
)
|
||||
|
||||
// GetParameterInvalidMessage returns specific error message for invalid parameter error
|
||||
|
||||
+7
-8
@@ -4,12 +4,11 @@ import "net/http"
|
||||
|
||||
// Error codes related to transaction categories
|
||||
var (
|
||||
ErrSystemError = NewSystemError(SystemSubcategoryDefault, 0, http.StatusInternalServerError, "system error")
|
||||
ErrApiNotFound = NewSystemError(SystemSubcategoryDefault, 1, http.StatusNotFound, "api not found")
|
||||
ErrMethodNotAllowed = NewSystemError(SystemSubcategoryDefault, 2, http.StatusMethodNotAllowed, "method not allowed")
|
||||
ErrNotImplemented = NewSystemError(SystemSubcategoryDefault, 3, http.StatusNotImplemented, "not implemented")
|
||||
ErrSystemIsBusy = NewSystemError(SystemSubcategoryDefault, 4, http.StatusServiceUnavailable, "system is busy")
|
||||
ErrNotSupported = NewSystemError(SystemSubcategoryDefault, 5, http.StatusBadRequest, "not supported")
|
||||
ErrImageTypeNotSupported = NewSystemError(SystemSubcategoryDefault, 6, http.StatusBadRequest, "image type not supported")
|
||||
ErrImportFileTypeNotSupported = NewSystemError(SystemSubcategoryDefault, 7, http.StatusBadRequest, "import file type not supported")
|
||||
ErrSystemError = NewSystemError(SystemSubcategoryDefault, 0, http.StatusInternalServerError, "system error")
|
||||
ErrApiNotFound = NewSystemError(SystemSubcategoryDefault, 1, http.StatusNotFound, "api not found")
|
||||
ErrMethodNotAllowed = NewSystemError(SystemSubcategoryDefault, 2, http.StatusMethodNotAllowed, "method not allowed")
|
||||
ErrNotImplemented = NewSystemError(SystemSubcategoryDefault, 3, http.StatusNotImplemented, "not implemented")
|
||||
ErrSystemIsBusy = NewSystemError(SystemSubcategoryDefault, 4, http.StatusServiceUnavailable, "system is busy")
|
||||
ErrNotSupported = NewSystemError(SystemSubcategoryDefault, 5, http.StatusBadRequest, "not supported")
|
||||
ErrImageTypeNotSupported = NewSystemError(SystemSubcategoryDefault, 6, http.StatusBadRequest, "image type not supported")
|
||||
)
|
||||
|
||||
@@ -29,4 +29,7 @@ var (
|
||||
ErrCannotUseHiddenTransactionTag = NewNormalError(NormalSubcategoryTransaction, 22, http.StatusBadRequest, "cannot use hidden transaction tag")
|
||||
ErrTransactionHasTooManyTags = NewNormalError(NormalSubcategoryTransaction, 23, http.StatusBadRequest, "transaction has too many tags")
|
||||
ErrTransactionHasTooManyPictures = NewNormalError(NormalSubcategoryTransaction, 24, http.StatusBadRequest, "transaction has too many pictures")
|
||||
ErrImportFileTypeIsEmpty = NewSystemError(NormalSubcategoryTransaction, 25, http.StatusBadRequest, "import file type is empty")
|
||||
ErrImportFileTypeNotSupported = NewSystemError(NormalSubcategoryTransaction, 26, http.StatusBadRequest, "import file type not supported")
|
||||
ErrNoDataToImport = NewSystemError(NormalSubcategoryTransaction, 27, http.StatusBadRequest, "no data to import")
|
||||
)
|
||||
|
||||
@@ -22,6 +22,7 @@ func ServerSettingsCookie(config *settings.Config) core.MiddlewareHandlerFunc {
|
||||
buildBooleanSetting("p", config.EnableTransactionPictures),
|
||||
buildBooleanSetting("s", config.EnableScheduledTransaction),
|
||||
buildBooleanSetting("e", config.EnableDataExport),
|
||||
buildBooleanSetting("i", config.EnableDataImport),
|
||||
buildStringSetting("m", strings.Replace(config.MapProvider, "_", "-", -1)),
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
package models
|
||||
|
||||
import "github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||
|
||||
// ImportTransaction represents the imported transaction data
|
||||
type ImportTransaction struct {
|
||||
*Transaction
|
||||
TagIds []string
|
||||
OriginalCategoryName string
|
||||
OriginalSourceAccountName string
|
||||
OriginalSourceAccountCurrency string
|
||||
OriginalDestinationAccountName string
|
||||
OriginalDestinationAccountCurrency string
|
||||
OriginalTagNames []string
|
||||
}
|
||||
|
||||
// ImportTransactionResponse represents a view-object of the imported transaction data
|
||||
type ImportTransactionResponse struct {
|
||||
Type TransactionType `json:"type"`
|
||||
CategoryId int64 `json:"categoryId,string"`
|
||||
OriginalCategoryName string `json:"originalCategoryName"`
|
||||
Time int64 `json:"time"`
|
||||
UtcOffset int16 `json:"utcOffset"`
|
||||
SourceAccountId int64 `json:"sourceAccountId,string"`
|
||||
OriginalSourceAccountName string `json:"originalSourceAccountName"`
|
||||
OriginalSourceAccountCurrency string `json:"originalSourceAccountCurrency"`
|
||||
DestinationAccountId int64 `json:"destinationAccountId,string,omitempty"`
|
||||
OriginalDestinationAccountName string `json:"originalDestinationAccountName,omitempty"`
|
||||
OriginalDestinationAccountCurrency string `json:"originalDestinationAccountCurrency,omitempty"`
|
||||
SourceAmount int64 `json:"sourceAmount"`
|
||||
DestinationAmount int64 `json:"destinationAmount,omitempty"`
|
||||
TagIds []string `json:"tagIds"`
|
||||
OriginalTagNames []string `json:"originalTagNames"`
|
||||
Comment string `json:"comment"`
|
||||
GeoLocation *TransactionGeoLocationResponse `json:"geoLocation,omitempty"`
|
||||
}
|
||||
|
||||
// ImportTransactionResponsePageWrapper represents a response of imported transaction which contains items and count
|
||||
type ImportTransactionResponsePageWrapper struct {
|
||||
Items []*ImportTransactionResponse `json:"items"`
|
||||
TotalCount int64 `json:"totalCount"`
|
||||
}
|
||||
|
||||
// ToImportTransactionResponse returns the a view-objects according to imported transaction data
|
||||
func (t ImportTransaction) ToImportTransactionResponse() *ImportTransactionResponse {
|
||||
var transactionType TransactionType
|
||||
|
||||
if t.Type == TRANSACTION_DB_TYPE_MODIFY_BALANCE {
|
||||
transactionType = TRANSACTION_TYPE_MODIFY_BALANCE
|
||||
} else if t.Type == TRANSACTION_DB_TYPE_EXPENSE {
|
||||
transactionType = TRANSACTION_TYPE_EXPENSE
|
||||
} else if t.Type == TRANSACTION_DB_TYPE_INCOME {
|
||||
transactionType = TRANSACTION_TYPE_INCOME
|
||||
} else if t.Type == TRANSACTION_DB_TYPE_TRANSFER_OUT {
|
||||
transactionType = TRANSACTION_TYPE_TRANSFER
|
||||
} else if t.Type == TRANSACTION_DB_TYPE_TRANSFER_IN {
|
||||
transactionType = TRANSACTION_TYPE_TRANSFER
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
||||
geoLocation := &TransactionGeoLocationResponse{}
|
||||
|
||||
if t.GeoLongitude != 0 || t.GeoLatitude != 0 {
|
||||
geoLocation.Longitude = t.GeoLongitude
|
||||
geoLocation.Latitude = t.GeoLatitude
|
||||
} else {
|
||||
geoLocation = nil
|
||||
}
|
||||
|
||||
return &ImportTransactionResponse{
|
||||
Type: transactionType,
|
||||
CategoryId: t.CategoryId,
|
||||
OriginalCategoryName: t.OriginalCategoryName,
|
||||
Time: utils.GetUnixTimeFromTransactionTime(t.TransactionTime),
|
||||
UtcOffset: t.TimezoneUtcOffset,
|
||||
SourceAccountId: t.AccountId,
|
||||
OriginalSourceAccountName: t.OriginalSourceAccountName,
|
||||
OriginalSourceAccountCurrency: t.OriginalSourceAccountCurrency,
|
||||
DestinationAccountId: t.RelatedAccountId,
|
||||
OriginalDestinationAccountName: t.OriginalDestinationAccountName,
|
||||
OriginalDestinationAccountCurrency: t.OriginalDestinationAccountCurrency,
|
||||
SourceAmount: t.Amount,
|
||||
DestinationAmount: t.RelatedAccountAmount,
|
||||
TagIds: t.TagIds,
|
||||
OriginalTagNames: t.OriginalTagNames,
|
||||
Comment: t.Comment,
|
||||
GeoLocation: geoLocation,
|
||||
}
|
||||
}
|
||||
|
||||
// ImportedTransactionSlice represents the slice data structure of import transaction data
|
||||
type ImportedTransactionSlice []*ImportTransaction
|
||||
|
||||
// Len returns the count of items
|
||||
func (s ImportedTransactionSlice) Len() int {
|
||||
return len(s)
|
||||
}
|
||||
|
||||
// Swap swaps two items
|
||||
func (s ImportedTransactionSlice) Swap(i, j int) {
|
||||
s[i], s[j] = s[j], s[i]
|
||||
}
|
||||
|
||||
// Less reports whether the first item is less than the second one
|
||||
func (s ImportedTransactionSlice) Less(i, j int) bool {
|
||||
if s[i].Type != s[j].Type && (s[i].Type == TRANSACTION_DB_TYPE_MODIFY_BALANCE || s[j].Type == TRANSACTION_DB_TYPE_MODIFY_BALANCE) {
|
||||
if s[i].Type == TRANSACTION_DB_TYPE_MODIFY_BALANCE {
|
||||
return true
|
||||
} else if s[j].Type == TRANSACTION_DB_TYPE_MODIFY_BALANCE {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return s[i].TransactionTime < s[j].TransactionTime
|
||||
}
|
||||
|
||||
// ToTransactionsList returns the a list of transactions
|
||||
func (s ImportedTransactionSlice) ToTransactionsList() []*Transaction {
|
||||
transactions := make([]*Transaction, s.Len())
|
||||
|
||||
for i := 0; i < s.Len(); i++ {
|
||||
transactions[i] = s[i].Transaction
|
||||
}
|
||||
|
||||
return transactions
|
||||
}
|
||||
|
||||
// ToImportTransactionResponseList returns the a list of view-objects according to imported transaction data
|
||||
func (s ImportedTransactionSlice) ToImportTransactionResponseList() []*ImportTransactionResponse {
|
||||
transactionResps := make([]*ImportTransactionResponse, 0, s.Len())
|
||||
|
||||
for i := 0; i < s.Len(); i++ {
|
||||
importedTransaction := s[i]
|
||||
importedTransactionResp := importedTransaction.ToImportTransactionResponse()
|
||||
|
||||
if importedTransactionResp == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
transactionResps = append(transactionResps, importedTransactionResp)
|
||||
}
|
||||
|
||||
return transactionResps
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestImportTransactionSliceLess(t *testing.T) {
|
||||
var transactionSlice ImportedTransactionSlice
|
||||
transactionSlice = append(transactionSlice, &ImportTransaction{
|
||||
Transaction: &Transaction{
|
||||
TransactionId: 1,
|
||||
Type: TRANSACTION_DB_TYPE_EXPENSE,
|
||||
TransactionTime: 1,
|
||||
},
|
||||
})
|
||||
transactionSlice = append(transactionSlice, &ImportTransaction{
|
||||
Transaction: &Transaction{
|
||||
TransactionId: 2,
|
||||
Type: TRANSACTION_DB_TYPE_INCOME,
|
||||
TransactionTime: 2,
|
||||
},
|
||||
})
|
||||
transactionSlice = append(transactionSlice, &ImportTransaction{
|
||||
Transaction: &Transaction{
|
||||
TransactionId: 3,
|
||||
Type: TRANSACTION_DB_TYPE_MODIFY_BALANCE,
|
||||
TransactionTime: 10,
|
||||
},
|
||||
})
|
||||
transactionSlice = append(transactionSlice, &ImportTransaction{
|
||||
Transaction: &Transaction{
|
||||
TransactionId: 4,
|
||||
Type: TRANSACTION_DB_TYPE_TRANSFER_IN,
|
||||
TransactionTime: 3,
|
||||
},
|
||||
})
|
||||
transactionSlice = append(transactionSlice, &ImportTransaction{
|
||||
Transaction: &Transaction{
|
||||
TransactionId: 5,
|
||||
Type: TRANSACTION_DB_TYPE_MODIFY_BALANCE,
|
||||
TransactionTime: 11,
|
||||
},
|
||||
})
|
||||
transactionSlice = append(transactionSlice, &ImportTransaction{
|
||||
Transaction: &Transaction{
|
||||
TransactionId: 6,
|
||||
Type: 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)
|
||||
}
|
||||
@@ -115,6 +115,12 @@ type TransactionModifyRequest struct {
|
||||
GeoLocation *TransactionGeoLocationRequest `json:"geoLocation" binding:"omitempty"`
|
||||
}
|
||||
|
||||
// TransactionImportRequest represents all parameters of transaction import request
|
||||
type TransactionImportRequest struct {
|
||||
Transactions []*TransactionCreateRequest `json:"transactions"`
|
||||
ClientSessionId string `json:"clientSessionId"`
|
||||
}
|
||||
|
||||
// TransactionCountRequest represents transaction count request
|
||||
type TransactionCountRequest struct {
|
||||
Type TransactionDbType `form:"type" binding:"min=0,max=4"`
|
||||
|
||||
@@ -138,6 +138,8 @@ const (
|
||||
defaultTransactionPictureFileMaxSize uint32 = 10485760 // 10MB
|
||||
defaultUserAvatarFileMaxSize uint32 = 1048576 // 1MB
|
||||
|
||||
defaultImportFileMaxSize uint32 = 10485760 // 10MB
|
||||
|
||||
defaultExchangeRatesDataRequestTimeout uint32 = 10000 // 10 seconds
|
||||
)
|
||||
|
||||
@@ -282,7 +284,9 @@ type Config struct {
|
||||
MaxAvatarFileSize uint32
|
||||
|
||||
// Data
|
||||
EnableDataExport bool
|
||||
EnableDataExport bool
|
||||
EnableDataImport bool
|
||||
MaxImportFileSize uint32
|
||||
|
||||
// Notification
|
||||
AfterRegisterNotification NotificationConfig
|
||||
@@ -768,6 +772,8 @@ func loadUserConfiguration(config *Config, configFile *ini.File, sectionName str
|
||||
|
||||
func loadDataConfiguration(config *Config, configFile *ini.File, sectionName string) error {
|
||||
config.EnableDataExport = getConfigItemBoolValue(configFile, sectionName, "enable_export", false)
|
||||
config.EnableDataImport = getConfigItemBoolValue(configFile, sectionName, "enable_import", false)
|
||||
config.MaxImportFileSize = getConfigItemUint32Value(configFile, sectionName, "max_import_file_size", defaultImportFileMaxSize)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user