Files
ezbookkeeping/pkg/api/transactions.go

1972 lines
77 KiB
Go

package api
import (
"encoding/json"
"fmt"
"io"
"sort"
"strings"
orderedmap "github.com/wk8/go-ordered-map/v2"
"github.com/mayswind/ezbookkeeping/pkg/converters"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/duplicatechecker"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/services"
"github.com/mayswind/ezbookkeeping/pkg/settings"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
const pageCountForAccountStatement = 1000
// TransactionsApi represents transaction api
type TransactionsApi struct {
ApiUsingConfig
ApiUsingDuplicateChecker
transactions *services.TransactionService
transactionCategories *services.TransactionCategoryService
transactionTags *services.TransactionTagService
transactionPictures *services.TransactionPictureService
accounts *services.AccountService
users *services.UserService
}
// Initialize a transaction api singleton instance
var (
Transactions = &TransactionsApi{
ApiUsingConfig: ApiUsingConfig{
container: settings.Container,
},
ApiUsingDuplicateChecker: ApiUsingDuplicateChecker{
ApiUsingConfig: ApiUsingConfig{
container: settings.Container,
},
container: duplicatechecker.Container,
},
transactions: services.Transactions,
transactionCategories: services.TransactionCategories,
transactionTags: services.TransactionTags,
transactionPictures: services.TransactionPictures,
accounts: services.Accounts,
users: services.Users,
}
)
// TransactionCountHandler returns transaction total count of current user
func (a *TransactionsApi) TransactionCountHandler(c *core.WebContext) (any, *errs.Error) {
var transactionCountReq models.TransactionCountRequest
err := c.ShouldBindQuery(&transactionCountReq)
if err != nil {
log.Warnf(c, "[transactions.TransactionCountHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
uid := c.GetCurrentUid()
allAccountIds, err := a.accounts.GetAccountOrSubAccountIds(c, transactionCountReq.AccountIds, uid)
if err != nil {
log.Warnf(c, "[transactions.TransactionCountHandler] get account error, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
allCategoryIds, err := a.transactionCategories.GetCategoryOrSubCategoryIds(c, transactionCountReq.CategoryIds, uid)
if err != nil {
log.Warnf(c, "[transactions.TransactionCountHandler] get transaction category error, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
noTags := transactionCountReq.TagFilter == models.TransactionNoTagFilterValue
var tagFilters []*models.TransactionTagFilter
if !noTags {
tagFilters, err = models.ParseTransactionTagFilter(transactionCountReq.TagFilter)
if err != nil {
log.Warnf(c, "[transactions.TransactionCountHandler] parse transaction filters error, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
}
totalCount, err := a.transactions.GetTransactionCount(c, uid, transactionCountReq.MaxTime, transactionCountReq.MinTime, transactionCountReq.Type, allCategoryIds, allAccountIds, tagFilters, noTags, transactionCountReq.AmountFilter, transactionCountReq.Keyword)
if err != nil {
log.Errorf(c, "[transactions.TransactionCountHandler] failed to get transaction count for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
countResp := &models.TransactionCountResponse{
TotalCount: totalCount,
}
return countResp, nil
}
// TransactionListHandler returns transaction list of current user
func (a *TransactionsApi) TransactionListHandler(c *core.WebContext) (any, *errs.Error) {
var transactionListReq models.TransactionListByMaxTimeRequest
err := c.ShouldBindQuery(&transactionListReq)
if err != nil {
log.Warnf(c, "[transactions.TransactionListHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
utcOffset, err := c.GetClientTimezoneOffset()
if err != nil {
log.Warnf(c, "[transactions.TransactionListHandler] cannot get client timezone offset, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
log.Errorf(c, "[transactions.TransactionListHandler] failed to get user, because %s", err.Error())
}
return nil, errs.ErrUserNotFound
}
allAccountIds, err := a.accounts.GetAccountOrSubAccountIds(c, transactionListReq.AccountIds, uid)
if err != nil {
log.Warnf(c, "[transactions.TransactionListHandler] get account error, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
allCategoryIds, err := a.transactionCategories.GetCategoryOrSubCategoryIds(c, transactionListReq.CategoryIds, uid)
if err != nil {
log.Warnf(c, "[transactions.TransactionListHandler] get transaction category error, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
noTags := transactionListReq.TagFilter == models.TransactionNoTagFilterValue
var tagFilters []*models.TransactionTagFilter
if !noTags {
tagFilters, err = models.ParseTransactionTagFilter(transactionListReq.TagFilter)
if err != nil {
log.Warnf(c, "[transactions.TransactionListHandler] parse transaction tag filters error, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
}
var totalCount int64
if transactionListReq.WithCount {
totalCount, err = a.transactions.GetTransactionCount(c, uid, transactionListReq.MaxTime, transactionListReq.MinTime, transactionListReq.Type, allCategoryIds, allAccountIds, tagFilters, noTags, transactionListReq.AmountFilter, transactionListReq.Keyword)
if err != nil {
log.Errorf(c, "[transactions.TransactionListHandler] failed to get transaction count for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
}
transactions, err := a.transactions.GetTransactionsByMaxTime(c, uid, transactionListReq.MaxTime, transactionListReq.MinTime, transactionListReq.Type, allCategoryIds, allAccountIds, tagFilters, noTags, transactionListReq.AmountFilter, transactionListReq.Keyword, transactionListReq.Page, transactionListReq.Count, true, true)
if err != nil {
log.Errorf(c, "[transactions.TransactionListHandler] failed to get transactions earlier than \"%d\" for user \"uid:%d\", because %s", transactionListReq.MaxTime, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
hasMore := false
var nextTimeSequenceId *int64
if len(transactions) > int(transactionListReq.Count) {
hasMore = true
nextTimeSequenceId = &transactions[transactionListReq.Count].TransactionTime
transactions = transactions[:transactionListReq.Count]
}
transactionResult, err := a.getTransactionResponseListResult(c, user, transactions, utcOffset, transactionListReq.WithPictures, transactionListReq.TrimAccount, transactionListReq.TrimCategory, transactionListReq.TrimTag)
if err != nil {
log.Errorf(c, "[transactions.TransactionListHandler] failed to assemble transaction result for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
transactionResps := &models.TransactionInfoPageWrapperResponse{
Items: transactionResult,
}
if hasMore {
transactionResps.NextTimeSequenceId = nextTimeSequenceId
}
if transactionListReq.WithCount {
transactionResps.TotalCount = &totalCount
}
return transactionResps, nil
}
// TransactionMonthListHandler returns all transaction list of current user by month
func (a *TransactionsApi) TransactionMonthListHandler(c *core.WebContext) (any, *errs.Error) {
var transactionListReq models.TransactionListInMonthByPageRequest
err := c.ShouldBindQuery(&transactionListReq)
if err != nil {
log.Warnf(c, "[transactions.TransactionMonthListHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
utcOffset, err := c.GetClientTimezoneOffset()
if err != nil {
log.Warnf(c, "[transactions.TransactionMonthListHandler] cannot get client timezone offset, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
log.Errorf(c, "[transactions.TransactionMonthListHandler] failed to get user, because %s", err.Error())
}
return nil, errs.ErrUserNotFound
}
allAccountIds, err := a.accounts.GetAccountOrSubAccountIds(c, transactionListReq.AccountIds, uid)
if err != nil {
log.Warnf(c, "[transactions.TransactionMonthListHandler] get account error, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
allCategoryIds, err := a.transactionCategories.GetCategoryOrSubCategoryIds(c, transactionListReq.CategoryIds, uid)
if err != nil {
log.Warnf(c, "[transactions.TransactionMonthListHandler] get transaction category error, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
noTags := transactionListReq.TagFilter == models.TransactionNoTagFilterValue
var tagFilters []*models.TransactionTagFilter
if !noTags {
tagFilters, err = models.ParseTransactionTagFilter(transactionListReq.TagFilter)
if err != nil {
log.Warnf(c, "[transactions.TransactionMonthListHandler] parse transaction tag filters error, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
}
transactions, err := a.transactions.GetTransactionsInMonthByPage(c, uid, transactionListReq.Year, transactionListReq.Month, transactionListReq.Type, allCategoryIds, allAccountIds, tagFilters, noTags, transactionListReq.AmountFilter, transactionListReq.Keyword)
if err != nil {
log.Errorf(c, "[transactions.TransactionMonthListHandler] failed to get transactions in month \"%d-%d\" for user \"uid:%d\", because %s", transactionListReq.Year, transactionListReq.Month, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
transactionResult, err := a.getTransactionResponseListResult(c, user, transactions, utcOffset, transactionListReq.WithPictures, transactionListReq.TrimAccount, transactionListReq.TrimCategory, transactionListReq.TrimTag)
if err != nil {
log.Errorf(c, "[transactions.TransactionMonthListHandler] failed to assemble transaction result for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
transactionResps := &models.TransactionInfoPageWrapperResponse2{
Items: transactionResult,
TotalCount: int64(transactionResult.Len()),
}
return transactionResps, nil
}
// TransactionReconciliationStatementHandler returns transaction reconciliation statement list of current user
func (a *TransactionsApi) TransactionReconciliationStatementHandler(c *core.WebContext) (any, *errs.Error) {
var reconciliationStatementRequest models.TransactionReconciliationStatementRequest
err := c.ShouldBindQuery(&reconciliationStatementRequest)
if err != nil {
log.Warnf(c, "[transactions.TransactionReconciliationStatementHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
utcOffset, err := c.GetClientTimezoneOffset()
if err != nil {
log.Warnf(c, "[transactions.TransactionReconciliationStatementHandler] cannot get client timezone offset, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
log.Errorf(c, "[transactions.TransactionReconciliationStatementHandler] failed to get user, because %s", err.Error())
}
return nil, errs.ErrUserNotFound
}
account, err := a.accounts.GetAccountByAccountId(c, uid, reconciliationStatementRequest.AccountId)
if err != nil {
log.Errorf(c, "[transactions.TransactionReconciliationStatementHandler] failed to get account \"id:%d\" for user \"uid:%d\", because %s", reconciliationStatementRequest.AccountId, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
if account.Type != models.ACCOUNT_TYPE_SINGLE_ACCOUNT {
log.Errorf(c, "[transactions.TransactionReconciliationStatementHandler] account \"id:%d\" for user \"uid:%d\" is not a single account", reconciliationStatementRequest.AccountId, uid)
return nil, errs.ErrAccountTypeInvalid
}
maxTransactionTime := int64(0)
if reconciliationStatementRequest.EndTime > 0 {
maxTransactionTime = utils.GetMaxTransactionTimeFromUnixTime(reconciliationStatementRequest.EndTime)
}
minTransactionTime := int64(0)
if reconciliationStatementRequest.StartTime > 0 {
minTransactionTime = utils.GetMinTransactionTimeFromUnixTime(reconciliationStatementRequest.StartTime)
}
transactionsWithAccountBalance, totalInflows, totalOutflows, openingBalance, closingBalance, err := a.transactions.GetAllTransactionsInOneAccountWithAccountBalanceByMaxTime(c, uid, pageCountForAccountStatement, maxTransactionTime, minTransactionTime, reconciliationStatementRequest.AccountId, account.Category)
if err != nil {
log.Errorf(c, "[transactions.TransactionReconciliationStatementHandler] failed to get transactions from \"%d\" to \"%d\" for user \"uid:%d\", because %s", reconciliationStatementRequest.StartTime, reconciliationStatementRequest.EndTime, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
transactions := make([]*models.Transaction, len(transactionsWithAccountBalance))
transactionAccountBalanceMap := make(map[int64]*models.TransactionWithAccountBalance, len(transactionsWithAccountBalance))
for i := 0; i < len(transactionsWithAccountBalance); i++ {
transactionWithBalance := transactionsWithAccountBalance[i]
transactions[i] = transactionWithBalance.Transaction
transactionAccountBalanceMap[transactionWithBalance.TransactionId] = transactionWithBalance
transactionAccountBalanceMap[transactionWithBalance.RelatedId] = transactionWithBalance
}
transactionResult, err := a.getTransactionResponseListResult(c, user, transactions, utcOffset, false, true, true, true)
if err != nil {
log.Errorf(c, "[transactions.TransactionReconciliationStatementHandler] failed to assemble transaction result for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
responseItems := make([]*models.TransactionReconciliationStatementResponseItem, len(transactionResult))
for i := 0; i < len(transactionResult); i++ {
transactionResult := transactionResult[i]
accountOpeningBalance := int64(0)
accountClosingBalance := int64(0)
if transactionWithBalance, exists := transactionAccountBalanceMap[transactionResult.Id]; exists {
accountOpeningBalance = transactionWithBalance.AccountOpeningBalance
accountClosingBalance = transactionWithBalance.AccountClosingBalance
} else {
log.Warnf(c, "[transactions.TransactionReconciliationStatementHandler] missing account balance for transaction \"id:%d\" of user \"uid:%d\"", transactionResult.Id, uid)
}
responseItems[i] = &models.TransactionReconciliationStatementResponseItem{
TransactionInfoResponse: transactionResult,
AccountOpeningBalance: accountOpeningBalance,
AccountClosingBalance: accountClosingBalance,
}
}
reconciliationStatementResp := &models.TransactionReconciliationStatementResponse{
Transactions: responseItems,
TotalInflows: totalInflows,
TotalOutflows: totalOutflows,
OpeningBalance: openingBalance,
ClosingBalance: closingBalance,
}
return reconciliationStatementResp, nil
}
// TransactionStatisticsHandler returns transaction statistics of current user
func (a *TransactionsApi) TransactionStatisticsHandler(c *core.WebContext) (any, *errs.Error) {
var statisticReq models.TransactionStatisticRequest
err := c.ShouldBindQuery(&statisticReq)
if err != nil {
log.Warnf(c, "[transactions.TransactionStatisticsHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
utcOffset, err := c.GetClientTimezoneOffset()
if err != nil {
log.Warnf(c, "[transactions.TransactionStatisticsHandler] cannot get client timezone offset, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
noTags := statisticReq.TagFilter == models.TransactionNoTagFilterValue
var tagFilters []*models.TransactionTagFilter
if !noTags {
tagFilters, err = models.ParseTransactionTagFilter(statisticReq.TagFilter)
if err != nil {
log.Warnf(c, "[transactions.TransactionStatisticsHandler] parse transaction tag filters error, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
}
uid := c.GetCurrentUid()
totalAmounts, err := a.transactions.GetAccountsAndCategoriesTotalInflowAndOutflow(c, uid, statisticReq.StartTime, statisticReq.EndTime, tagFilters, noTags, statisticReq.Keyword, utcOffset, statisticReq.UseTransactionTimezone)
if err != nil {
log.Errorf(c, "[transactions.TransactionStatisticsHandler] failed to get accounts and categories total income and expense for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
statisticResp := &models.TransactionStatisticResponse{
StartTime: statisticReq.StartTime,
EndTime: statisticReq.EndTime,
}
statisticResp.Items = make([]*models.TransactionStatisticResponseItem, len(totalAmounts))
for i := 0; i < len(totalAmounts); i++ {
totalAmountItem := totalAmounts[i]
statisticResp.Items[i] = &models.TransactionStatisticResponseItem{
CategoryId: totalAmountItem.CategoryId,
AccountId: totalAmountItem.AccountId,
TotalAmount: totalAmountItem.Amount,
}
if totalAmountItem.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT || totalAmountItem.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
statisticResp.Items[i].RelatedAccountId = totalAmountItem.RelatedAccountId
statisticResp.Items[i].RelatedAccountType, _ = totalAmountItem.Type.ToTransactionRelatedAccountType()
}
}
return statisticResp, nil
}
// TransactionStatisticsTrendsHandler returns transaction statistics trends of current user
func (a *TransactionsApi) TransactionStatisticsTrendsHandler(c *core.WebContext) (any, *errs.Error) {
var statisticTrendsReq models.TransactionStatisticTrendsRequest
err := c.ShouldBindQuery(&statisticTrendsReq)
if err != nil {
log.Warnf(c, "[transactions.TransactionStatisticsTrendsHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
utcOffset, err := c.GetClientTimezoneOffset()
if err != nil {
log.Warnf(c, "[transactions.TransactionStatisticsTrendsHandler] cannot get client timezone offset, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
startYear, startMonth, endYear, endMonth, err := statisticTrendsReq.GetNumericYearMonthRange()
if err != nil {
log.Warnf(c, "[transactions.TransactionStatisticsTrendsHandler] cannot parse year month, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
noTags := statisticTrendsReq.TagFilter == models.TransactionNoTagFilterValue
var tagFilters []*models.TransactionTagFilter
if !noTags {
tagFilters, err = models.ParseTransactionTagFilter(statisticTrendsReq.TagFilter)
if err != nil {
log.Warnf(c, "[transactions.TransactionStatisticsTrendsHandler] parse transaction tag filters error, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
}
uid := c.GetCurrentUid()
allMonthlyTotalAmounts, err := a.transactions.GetAccountsAndCategoriesMonthlyInflowAndOutflow(c, uid, startYear, startMonth, endYear, endMonth, tagFilters, noTags, statisticTrendsReq.Keyword, utcOffset, statisticTrendsReq.UseTransactionTimezone)
if err != nil {
log.Errorf(c, "[transactions.TransactionStatisticsTrendsHandler] failed to get accounts and categories total income and expense for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
statisticTrendsResp := make(models.TransactionStatisticTrendsResponseItemSlice, 0, len(allMonthlyTotalAmounts))
for yearMonth, monthlyTotalAmounts := range allMonthlyTotalAmounts {
monthlyStatisticResp := &models.TransactionStatisticTrendsResponseItem{
Year: yearMonth / 100,
Month: yearMonth % 100,
Items: make([]*models.TransactionStatisticResponseItem, len(monthlyTotalAmounts)),
}
for i := 0; i < len(monthlyTotalAmounts); i++ {
totalAmountItem := monthlyTotalAmounts[i]
monthlyStatisticResp.Items[i] = &models.TransactionStatisticResponseItem{
CategoryId: totalAmountItem.CategoryId,
AccountId: totalAmountItem.AccountId,
TotalAmount: totalAmountItem.Amount,
}
if totalAmountItem.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT || totalAmountItem.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
monthlyStatisticResp.Items[i].RelatedAccountId = totalAmountItem.RelatedAccountId
monthlyStatisticResp.Items[i].RelatedAccountType, _ = totalAmountItem.Type.ToTransactionRelatedAccountType()
}
}
statisticTrendsResp = append(statisticTrendsResp, monthlyStatisticResp)
}
sort.Sort(statisticTrendsResp)
return statisticTrendsResp, nil
}
// TransactionStatisticsAssetTrendsHandler returns transaction statistics asset trends of current user
func (a *TransactionsApi) TransactionStatisticsAssetTrendsHandler(c *core.WebContext) (any, *errs.Error) {
var statisticAssetTrendsReq models.TransactionStatisticAssetTrendsRequest
err := c.ShouldBindQuery(&statisticAssetTrendsReq)
if err != nil {
log.Warnf(c, "[transactions.TransactionStatisticsAssetTrendsHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
utcOffset, err := c.GetClientTimezoneOffset()
if err != nil {
log.Warnf(c, "[transactions.TransactionStatisticsAssetTrendsHandler] cannot get client timezone offset, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
uid := c.GetCurrentUid()
maxTransactionTime := int64(0)
if statisticAssetTrendsReq.EndTime > 0 {
maxTransactionTime = utils.GetMaxTransactionTimeFromUnixTime(statisticAssetTrendsReq.EndTime)
}
minTransactionTime := int64(0)
if statisticAssetTrendsReq.StartTime > 0 {
minTransactionTime = utils.GetMinTransactionTimeFromUnixTime(statisticAssetTrendsReq.StartTime)
}
accountDailyBalances, err := a.transactions.GetAllAccountsDailyOpeningAndClosingBalance(c, uid, maxTransactionTime, minTransactionTime, utcOffset)
if err != nil {
log.Errorf(c, "[transactions.TransactionStatisticsAssetTrendsHandler] failed to get transactions from \"%d\" to \"%d\" for user \"uid:%d\", because %s", statisticAssetTrendsReq.StartTime, statisticAssetTrendsReq.EndTime, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
statisticAssetTrendsResp := make(models.TransactionStatisticAssetTrendsResponseItemSlice, 0)
for yearMonthDay, dailyAccountBalances := range accountDailyBalances {
dailyStatisticResp := &models.TransactionStatisticAssetTrendsResponseItem{
Year: yearMonthDay / 10000,
Month: (yearMonthDay % 10000) / 100,
Day: yearMonthDay % 100,
Items: make([]*models.TransactionStatisticAssetTrendsResponseDataItem, len(dailyAccountBalances)),
}
for i := 0; i < len(dailyAccountBalances); i++ {
accountBalance := dailyAccountBalances[i]
dailyStatisticResp.Items[i] = &models.TransactionStatisticAssetTrendsResponseDataItem{
AccountId: accountBalance.AccountId,
AccountOpeningBalance: accountBalance.AccountOpeningBalance,
AccountClosingBalance: accountBalance.AccountClosingBalance,
}
}
statisticAssetTrendsResp = append(statisticAssetTrendsResp, dailyStatisticResp)
}
sort.Sort(statisticAssetTrendsResp)
return statisticAssetTrendsResp, nil
}
// TransactionAmountsHandler returns transaction amounts of current user
func (a *TransactionsApi) TransactionAmountsHandler(c *core.WebContext) (any, *errs.Error) {
var transactionAmountsReq models.TransactionAmountsRequest
err := c.ShouldBindQuery(&transactionAmountsReq)
if err != nil {
log.Warnf(c, "[transactions.TransactionAmountsHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
requestItems, err := transactionAmountsReq.GetTransactionAmountsRequestItems()
if err != nil {
log.Warnf(c, "[transactions.TransactionAmountsHandler] get request item failed, because %s", err.Error())
return nil, errs.ErrQueryItemsInvalid
}
if len(requestItems) < 1 {
log.Warnf(c, "[transactions.TransactionAmountsHandler] parse request failed, because there are no valid items")
return nil, errs.ErrQueryItemsEmpty
}
if len(requestItems) > 20 {
log.Warnf(c, "[transactions.TransactionAmountsHandler] parse request failed, because there are too many items")
return nil, errs.ErrQueryItemsTooMuch
}
excludeAccountIds := make([]int64, 0)
excludeCategoryIds := make([]int64, 0)
if transactionAmountsReq.ExcludeAccountIds != "" {
excludeAccountIds, err = utils.StringArrayToInt64Array(strings.Split(transactionAmountsReq.ExcludeAccountIds, ","))
if err != nil {
return nil, errs.ErrAccountIdInvalid
}
}
if transactionAmountsReq.ExcludeCategoryIds != "" {
excludeCategoryIds, err = utils.StringArrayToInt64Array(strings.Split(transactionAmountsReq.ExcludeCategoryIds, ","))
if err != nil {
return nil, errs.ErrTransactionCategoryIdInvalid
}
}
utcOffset, err := c.GetClientTimezoneOffset()
if err != nil {
log.Warnf(c, "[transactions.TransactionAmountsHandler] cannot get client timezone offset, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
uid := c.GetCurrentUid()
accounts, err := a.accounts.GetAllAccountsByUid(c, uid)
accountMap := a.accounts.GetAccountMapByList(accounts)
if err != nil {
log.Errorf(c, "[transactions.TransactionAmountsHandler] failed to get all accounts for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
amountsResp := orderedmap.New[string, *models.TransactionAmountsResponseItem]()
for i := 0; i < len(requestItems); i++ {
requestItem := requestItems[i]
incomeAmounts, expenseAmounts, err := a.transactions.GetAccountsTotalIncomeAndExpense(c, uid, requestItem.StartTime, requestItem.EndTime, excludeAccountIds, excludeCategoryIds, utcOffset, transactionAmountsReq.UseTransactionTimezone)
if err != nil {
log.Errorf(c, "[transactions.TransactionAmountsHandler] failed to get transaction amounts item for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
amountsMap := make(map[string]*models.TransactionAmountsResponseItemAmountInfo)
for accountId, incomeAmount := range incomeAmounts {
account, exists := accountMap[accountId]
if !exists {
log.Warnf(c, "[transactions.TransactionAmountsHandler] cannot find account for account \"id:%d\" of user \"uid:%d\"", accountId, uid)
continue
}
totalAmounts, exists := amountsMap[account.Currency]
if !exists {
totalAmounts = &models.TransactionAmountsResponseItemAmountInfo{
Currency: account.Currency,
IncomeAmount: 0,
ExpenseAmount: 0,
}
}
totalAmounts.IncomeAmount += incomeAmount
amountsMap[account.Currency] = totalAmounts
}
for accountId, expenseAmount := range expenseAmounts {
account, exists := accountMap[accountId]
if !exists {
log.Warnf(c, "[transactions.TransactionAmountsHandler] cannot find account for account \"id:%d\" of user \"uid:%d\"", accountId, uid)
continue
}
totalAmounts, exists := amountsMap[account.Currency]
if !exists {
totalAmounts = &models.TransactionAmountsResponseItemAmountInfo{
Currency: account.Currency,
IncomeAmount: 0,
ExpenseAmount: 0,
}
}
totalAmounts.ExpenseAmount += expenseAmount
amountsMap[account.Currency] = totalAmounts
}
allTotalAmounts := make(models.TransactionAmountsResponseItemAmountInfoSlice, 0)
for _, totalAmounts := range amountsMap {
allTotalAmounts = append(allTotalAmounts, totalAmounts)
}
sort.Sort(allTotalAmounts)
amountsResp.Set(requestItem.Name, &models.TransactionAmountsResponseItem{
StartTime: requestItem.StartTime,
EndTime: requestItem.EndTime,
Amounts: allTotalAmounts,
})
}
return amountsResp, nil
}
// TransactionGetHandler returns one specific transaction of current user
func (a *TransactionsApi) TransactionGetHandler(c *core.WebContext) (any, *errs.Error) {
var transactionGetReq models.TransactionGetRequest
err := c.ShouldBindQuery(&transactionGetReq)
if err != nil {
log.Warnf(c, "[transactions.TransactionGetHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
utcOffset, err := c.GetClientTimezoneOffset()
if err != nil {
log.Warnf(c, "[transactions.TransactionGetHandler] cannot get client timezone offset, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
log.Errorf(c, "[transactions.TransactionGetHandler] failed to get user, because %s", err.Error())
}
return nil, errs.ErrUserNotFound
}
transaction, err := a.transactions.GetTransactionByTransactionId(c, uid, transactionGetReq.Id)
if err != nil {
log.Errorf(c, "[transactions.TransactionGetHandler] failed to get transaction \"id:%d\" for user \"uid:%d\", because %s", transactionGetReq.Id, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
transaction = a.transactions.GetRelatedTransferTransaction(transaction)
}
accountIds := make([]int64, 0, 2)
accountIds = append(accountIds, transaction.AccountId)
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
accountIds = append(accountIds, transaction.RelatedAccountId)
accountIds = utils.ToUniqueInt64Slice(accountIds)
}
accountMap, err := a.accounts.GetAccountsByAccountIds(c, uid, accountIds)
if _, exists := accountMap[transaction.AccountId]; !exists {
log.Warnf(c, "[transactions.TransactionGetHandler] account of transaction \"id:%d\" does not exist for user \"uid:%d\"", transaction.TransactionId, uid)
return nil, errs.ErrTransactionNotFound
}
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
if _, exists := accountMap[transaction.RelatedAccountId]; !exists {
log.Warnf(c, "[transactions.TransactionGetHandler] related account of transaction \"id:%d\" does not exist for user \"uid:%d\"", transaction.TransactionId, uid)
return nil, errs.ErrTransactionNotFound
}
}
allTransactionTagIds, err := a.transactionTags.GetAllTagIdsOfTransactions(c, uid, []int64{transaction.TransactionId})
if err != nil {
log.Errorf(c, "[transactions.TransactionGetHandler] failed to get transactions tag ids for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
var category *models.TransactionCategory
var tagMap map[int64]*models.TransactionTag
var pictureInfos []*models.TransactionPictureInfo
if !transactionGetReq.TrimCategory {
category, err = a.transactionCategories.GetCategoryByCategoryId(c, uid, transaction.CategoryId)
if err != nil {
log.Errorf(c, "[transactions.TransactionGetHandler] failed to get transactions category for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
}
if !transactionGetReq.TrimTag {
tagMap, err = a.transactionTags.GetTagsByTagIds(c, uid, utils.ToUniqueInt64Slice(a.transactionTags.GetTransactionTagIds(allTransactionTagIds)))
if err != nil {
log.Errorf(c, "[transactions.TransactionGetHandler] failed to get transactions tags for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
}
if transactionGetReq.WithPictures && a.CurrentConfig().EnableTransactionPictures {
pictureInfos, err = a.transactionPictures.GetPictureInfosByTransactionId(c, uid, transaction.TransactionId)
if err != nil {
log.Errorf(c, "[transactions.TransactionGetHandler] failed to get transactions pictures for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
}
transactionEditable := transaction.IsEditable(user, utcOffset, accountMap[transaction.AccountId], accountMap[transaction.RelatedAccountId])
transactionTagIds := allTransactionTagIds[transaction.TransactionId]
transactionResp := transaction.ToTransactionInfoResponse(transactionTagIds, transactionEditable)
if !transactionGetReq.TrimAccount {
if sourceAccount := accountMap[transaction.AccountId]; sourceAccount != nil {
transactionResp.SourceAccount = sourceAccount.ToAccountInfoResponse()
}
if destinationAccount := accountMap[transaction.RelatedAccountId]; destinationAccount != nil {
transactionResp.DestinationAccount = destinationAccount.ToAccountInfoResponse()
}
}
if !transactionGetReq.TrimCategory {
if category != nil {
transactionResp.Category = category.ToTransactionCategoryInfoResponse()
}
}
if !transactionGetReq.TrimTag {
transactionResp.Tags = a.getTransactionTagInfoResponses(transactionTagIds, tagMap)
}
if transactionGetReq.WithPictures && a.CurrentConfig().EnableTransactionPictures {
transactionResp.Pictures = a.GetTransactionPictureInfoResponseList(pictureInfos)
}
return transactionResp, nil
}
// TransactionCreateHandler saves a new transaction by request parameters for current user
func (a *TransactionsApi) TransactionCreateHandler(c *core.WebContext) (any, *errs.Error) {
var transactionCreateReq models.TransactionCreateRequest
err := c.ShouldBindJSON(&transactionCreateReq)
if err != nil {
log.Warnf(c, "[transactions.TransactionCreateHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
tagIds, err := utils.StringArrayToInt64Array(transactionCreateReq.TagIds)
if err != nil {
log.Warnf(c, "[transactions.TransactionCreateHandler] parse tag ids failed, because %s", err.Error())
return nil, errs.ErrTransactionTagIdInvalid
}
if len(tagIds) > models.MaximumTagsCountOfTransaction {
return nil, errs.ErrTransactionHasTooManyTags
}
pictureIds, err := utils.StringArrayToInt64Array(transactionCreateReq.PictureIds)
if err != nil {
log.Warnf(c, "[transactions.TransactionCreateHandler] parse picture ids failed, because %s", err.Error())
return nil, errs.ErrTransactionPictureIdInvalid
}
if len(pictureIds) > models.MaximumPicturesCountOfTransaction {
return nil, errs.ErrTransactionHasTooManyPictures
}
if transactionCreateReq.Type < models.TRANSACTION_TYPE_MODIFY_BALANCE || transactionCreateReq.Type > models.TRANSACTION_TYPE_TRANSFER {
log.Warnf(c, "[transactions.TransactionCreateHandler] transaction type is invalid")
return nil, errs.ErrTransactionTypeInvalid
}
if transactionCreateReq.Type == models.TRANSACTION_TYPE_MODIFY_BALANCE && transactionCreateReq.CategoryId != 0 {
log.Warnf(c, "[transactions.TransactionCreateHandler] balance modification transaction cannot set category id")
return nil, errs.ErrBalanceModificationTransactionCannotSetCategory
}
if transactionCreateReq.Type != models.TRANSACTION_TYPE_TRANSFER && transactionCreateReq.DestinationAccountId != 0 {
log.Warnf(c, "[transactions.TransactionCreateHandler] non-transfer transaction destination account cannot be set")
return nil, errs.ErrTransactionDestinationAccountCannotBeSet
} else if transactionCreateReq.Type == models.TRANSACTION_TYPE_TRANSFER && transactionCreateReq.SourceAccountId == transactionCreateReq.DestinationAccountId {
log.Warnf(c, "[transactions.TransactionCreateHandler] transfer transaction source account must not be destination account")
return nil, errs.ErrTransactionSourceAndDestinationIdCannotBeEqual
}
if transactionCreateReq.Type != models.TRANSACTION_TYPE_TRANSFER && transactionCreateReq.DestinationAmount != 0 {
log.Warnf(c, "[transactions.TransactionCreateHandler] non-transfer transaction destination amount cannot be set")
return nil, errs.ErrTransactionDestinationAmountCannotBeSet
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
log.Errorf(c, "[transactions.TransactionCreateHandler] failed to get user, because %s", err.Error())
}
return nil, errs.ErrUserNotFound
}
transaction := a.createNewTransactionModel(uid, &transactionCreateReq, c.ClientIP())
transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, transactionCreateReq.UtcOffset)
if !transactionEditable {
return nil, errs.ErrCannotCreateTransactionWithThisTransactionTime
}
var pictureInfos []*models.TransactionPictureInfo
if len(pictureIds) > 0 {
pictureInfos, err = a.transactionPictures.GetNewPictureInfosByPictureIds(c, uid, pictureIds)
if err != nil {
log.Errorf(c, "[transactions.TransactionCreateHandler] failed to get transactions pictures for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
notExistsPictureIds := utils.Int64SliceMinus(pictureIds, a.transactionPictures.GetTransactionPictureIds(pictureInfos))
if len(notExistsPictureIds) > 0 {
log.Errorf(c, "[transactions.TransactionCreateHandler] some pictures \"ids:%s\" does not exists for user \"uid:%d\"", strings.Join(utils.Int64ArrayToStringArray(notExistsPictureIds), ","), uid)
return nil, errs.ErrTransactionPictureNotFound
}
}
if a.CurrentConfig().EnableDuplicateSubmissionsCheck && transactionCreateReq.ClientSessionId != "" {
found, remark := a.GetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_TRANSACTION, uid, transactionCreateReq.ClientSessionId)
if found {
log.Infof(c, "[transactions.TransactionCreateHandler] another transaction \"id:%s\" has been created for user \"uid:%d\"", remark, uid)
transactionId, err := utils.StringToInt64(remark)
if err == nil {
transaction, err = a.transactions.GetTransactionByTransactionId(c, uid, transactionId)
if err != nil {
log.Errorf(c, "[transactions.TransactionCreateHandler] failed to get existed transaction \"id:%d\" for user \"uid:%d\", because %s", transactionId, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
transactionResp := transaction.ToTransactionInfoResponse(tagIds, transactionEditable)
transactionResp.Pictures = a.GetTransactionPictureInfoResponseList(pictureInfos)
return transactionResp, nil
}
}
}
err = a.transactions.CreateTransaction(c, transaction, tagIds, pictureIds)
if err != nil {
log.Errorf(c, "[transactions.TransactionCreateHandler] failed to create transaction \"id:%d\" for user \"uid:%d\", because %s", transaction.TransactionId, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.Infof(c, "[transactions.TransactionCreateHandler] user \"uid:%d\" has created a new transaction \"id:%d\" successfully", uid, transaction.TransactionId)
a.SetSubmissionRemarkIfEnable(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_TRANSACTION, uid, transactionCreateReq.ClientSessionId, utils.Int64ToString(transaction.TransactionId))
transactionResp := transaction.ToTransactionInfoResponse(tagIds, transactionEditable)
transactionResp.Pictures = a.GetTransactionPictureInfoResponseList(pictureInfos)
return transactionResp, nil
}
// TransactionModifyHandler saves an existed transaction by request parameters for current user
func (a *TransactionsApi) TransactionModifyHandler(c *core.WebContext) (any, *errs.Error) {
var transactionModifyReq models.TransactionModifyRequest
err := c.ShouldBindJSON(&transactionModifyReq)
if err != nil {
log.Warnf(c, "[transactions.TransactionModifyHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
tagIds, err := utils.StringArrayToInt64Array(transactionModifyReq.TagIds)
if err != nil {
log.Warnf(c, "[transactions.TransactionModifyHandler] parse tag ids failed, because %s", err.Error())
return nil, errs.ErrTransactionTagIdInvalid
}
if len(tagIds) > models.MaximumTagsCountOfTransaction {
return nil, errs.ErrTransactionHasTooManyTags
}
pictureIds, err := utils.StringArrayToInt64Array(transactionModifyReq.PictureIds)
if err != nil {
log.Warnf(c, "[transactions.TransactionModifyHandler] parse picture ids failed, because %s", err.Error())
return nil, errs.ErrTransactionPictureIdInvalid
}
if len(pictureIds) > models.MaximumPicturesCountOfTransaction {
return nil, errs.ErrTransactionHasTooManyPictures
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
log.Errorf(c, "[transactions.TransactionModifyHandler] failed to get user, because %s", err.Error())
}
return nil, errs.ErrUserNotFound
}
transaction, err := a.transactions.GetTransactionByTransactionId(c, uid, transactionModifyReq.Id)
if err != nil {
log.Errorf(c, "[transactions.TransactionModifyHandler] failed to get transaction \"id:%d\" for user \"uid:%d\", because %s", transactionModifyReq.Id, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
log.Warnf(c, "[transactions.TransactionModifyHandler] cannot modify transaction \"id:%d\" for user \"uid:%d\", because transaction type is transfer in", transactionModifyReq.Id, uid)
return nil, errs.ErrTransactionTypeInvalid
}
if transaction.Type == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE && transactionModifyReq.CategoryId != 0 {
log.Warnf(c, "[transactions.TransactionModifyHandler] balance modification transaction cannot set category id")
return nil, errs.ErrBalanceModificationTransactionCannotSetCategory
} else if transaction.Type != models.TRANSACTION_DB_TYPE_MODIFY_BALANCE && transactionModifyReq.CategoryId == 0 {
log.Warnf(c, "[transactions.TransactionModifyHandler] non-balance modification transaction must set category id")
return nil, errs.ErrIncompleteOrIncorrectSubmission
}
allTransactionTagIds, err := a.transactionTags.GetAllTagIdsOfTransactions(c, uid, []int64{transaction.TransactionId})
if err != nil {
log.Errorf(c, "[transactions.TransactionModifyHandler] failed to get transactions tag ids for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
transactionTagIds := allTransactionTagIds[transaction.TransactionId]
if transactionTagIds == nil {
transactionTagIds = make([]int64, 0, 0)
}
transactionPictureInfos, err := a.transactionPictures.GetPictureInfosByTransactionId(c, uid, transaction.TransactionId)
if err != nil {
log.Errorf(c, "[transactions.TransactionModifyHandler] failed to get transaction picture infos for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
transactionPictureIds := a.transactionPictures.GetTransactionPictureIds(transactionPictureInfos)
newTransaction := &models.Transaction{
TransactionId: transaction.TransactionId,
Uid: uid,
CategoryId: transactionModifyReq.CategoryId,
TransactionTime: utils.GetMinTransactionTimeFromUnixTime(transactionModifyReq.Time),
TimezoneUtcOffset: transactionModifyReq.UtcOffset,
AccountId: transactionModifyReq.SourceAccountId,
Amount: transactionModifyReq.SourceAmount,
HideAmount: transactionModifyReq.HideAmount,
Comment: transactionModifyReq.Comment,
}
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
newTransaction.RelatedAccountId = transactionModifyReq.DestinationAccountId
newTransaction.RelatedAccountAmount = transactionModifyReq.DestinationAmount
}
if transactionModifyReq.GeoLocation != nil {
newTransaction.GeoLongitude = transactionModifyReq.GeoLocation.Longitude
newTransaction.GeoLatitude = transactionModifyReq.GeoLocation.Latitude
}
if newTransaction.CategoryId == transaction.CategoryId &&
utils.GetUnixTimeFromTransactionTime(newTransaction.TransactionTime) == utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime) &&
newTransaction.TimezoneUtcOffset == transaction.TimezoneUtcOffset &&
newTransaction.AccountId == transaction.AccountId &&
newTransaction.Amount == transaction.Amount &&
(transaction.Type != models.TRANSACTION_DB_TYPE_TRANSFER_OUT || newTransaction.RelatedAccountId == transaction.RelatedAccountId) &&
(transaction.Type != models.TRANSACTION_DB_TYPE_TRANSFER_OUT || newTransaction.RelatedAccountAmount == transaction.RelatedAccountAmount) &&
newTransaction.HideAmount == transaction.HideAmount &&
newTransaction.Comment == transaction.Comment &&
newTransaction.GeoLongitude == transaction.GeoLongitude &&
newTransaction.GeoLatitude == transaction.GeoLatitude &&
utils.Int64SliceEquals(tagIds, transactionTagIds) &&
utils.Int64SliceEquals(pictureIds, transactionPictureIds) {
return nil, errs.ErrNothingWillBeUpdated
}
transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, transaction.TimezoneUtcOffset)
newTransactionEditable := user.CanEditTransactionByTransactionTime(newTransaction.TransactionTime, transactionModifyReq.UtcOffset)
if !transactionEditable || !newTransactionEditable {
return nil, errs.ErrCannotModifyTransactionWithThisTransactionTime
}
var addTransactionTagIds []int64
var removeTransactionTagIds []int64
if !utils.Int64SliceEquals(tagIds, transactionTagIds) {
removeTransactionTagIds = transactionTagIds
addTransactionTagIds = tagIds
}
addTransactionPictureIds := utils.Int64SliceMinus(pictureIds, transactionPictureIds)
removeTransactionPictureIds := utils.Int64SliceMinus(transactionPictureIds, pictureIds)
var newPictureInfos []*models.TransactionPictureInfo
if !utils.Int64SliceEquals(pictureIds, transactionPictureIds) {
oldAndNewPictureIds := transactionPictureIds
oldAndNewPictureInfoMap := a.transactionPictures.GetPictureInfoMapByList(transactionPictureInfos)
if len(addTransactionPictureIds) > 0 {
addPictureInfos, err := a.transactionPictures.GetNewPictureInfosByPictureIds(c, uid, addTransactionPictureIds)
if err != nil {
log.Errorf(c, "[transactions.TransactionModifyHandler] failed to get transactions pictures for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
oldAndNewPictureIds = append(oldAndNewPictureIds, a.transactionPictures.GetTransactionPictureIds(addPictureInfos)...)
notExistsPictureIds := utils.Int64SliceMinus(pictureIds, oldAndNewPictureIds)
if len(notExistsPictureIds) > 0 {
log.Errorf(c, "[transactions.TransactionModifyHandler] some pictures \"ids:%s\" does not exists for user \"uid:%d\"", strings.Join(utils.Int64ArrayToStringArray(notExistsPictureIds), ","), uid)
return nil, errs.ErrTransactionPictureNotFound
}
for i := 0; i < len(addPictureInfos); i++ {
oldAndNewPictureInfoMap[addPictureInfos[i].PictureId] = addPictureInfos[i]
}
}
for i := 0; i < len(pictureIds); i++ {
pictureId := pictureIds[i]
pictureInfo, exists := oldAndNewPictureInfoMap[pictureId]
if exists {
newPictureInfos = append(newPictureInfos, pictureInfo)
}
}
}
err = a.transactions.ModifyTransaction(c, newTransaction, len(transactionTagIds), addTransactionTagIds, removeTransactionTagIds, addTransactionPictureIds, removeTransactionPictureIds)
if err != nil {
log.Errorf(c, "[transactions.TransactionModifyHandler] failed to update transaction \"id:%d\" for user \"uid:%d\", because %s", transactionModifyReq.Id, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.Infof(c, "[transactions.TransactionModifyHandler] user \"uid:%d\" has updated transaction \"id:%d\" successfully", uid, transactionModifyReq.Id)
newTransaction.Type = transaction.Type
newTransactionResp := newTransaction.ToTransactionInfoResponse(tagIds, transactionEditable)
newTransactionResp.Pictures = a.GetTransactionPictureInfoResponseList(newPictureInfos)
return newTransactionResp, nil
}
// TransactionMoveAllBetweenAccountsHandler moves all transactions from one account to another account for current user
func (a *TransactionsApi) TransactionMoveAllBetweenAccountsHandler(c *core.WebContext) (any, *errs.Error) {
var transactionMoveReq models.TransactionMoveBetweenAccountsRequest
err := c.ShouldBindJSON(&transactionMoveReq)
if err != nil {
log.Warnf(c, "[transactions.TransactionMoveAllBetweenAccountsHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
if transactionMoveReq.FromAccountId == transactionMoveReq.ToAccountId {
return nil, errs.ErrCannotMoveTransactionToSameAccount
}
uid := c.GetCurrentUid()
accountMap, err := a.accounts.GetAccountsByAccountIds(c, uid, []int64{transactionMoveReq.FromAccountId, transactionMoveReq.ToAccountId})
if err != nil {
log.Errorf(c, "[transactions.TransactionMoveAllBetweenAccountsHandler] failed to get accounts for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
fromAccount, exists := accountMap[transactionMoveReq.FromAccountId]
if !exists {
return nil, errs.ErrSourceAccountNotFound
}
toAccount, exists := accountMap[transactionMoveReq.ToAccountId]
if !exists {
return nil, errs.ErrDestinationAccountNotFound
}
if fromAccount.Hidden || toAccount.Hidden {
return nil, errs.ErrCannotMoveTransactionFromOrToHiddenAccount
}
if fromAccount.Type == models.ACCOUNT_TYPE_MULTI_SUB_ACCOUNTS || toAccount.Type == models.ACCOUNT_TYPE_MULTI_SUB_ACCOUNTS {
return nil, errs.ErrCannotMoveTransactionFromOrToParentAccount
}
if fromAccount.Currency != toAccount.Currency {
return nil, errs.ErrCannotMoveTransactionBetweenAccountsWithDifferentCurrencies
}
err = a.transactions.MoveAllTransactionsBetweenAccounts(c, uid, transactionMoveReq.FromAccountId, transactionMoveReq.ToAccountId)
if err != nil {
log.Errorf(c, "[transactions.TransactionMoveAllBetweenAccountsHandler] failed to move all transactions from account \"id:%d\" to account \"id:%d\" for user \"uid:%d\", because %s", transactionMoveReq.FromAccountId, transactionMoveReq.ToAccountId, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.Infof(c, "[transactions.TransactionMoveAllBetweenAccountsHandler] user \"uid:%d\" has moved all transactions from account \"id:%d\" to account \"id:%d\" successfully", uid, transactionMoveReq.FromAccountId, transactionMoveReq.ToAccountId)
return true, nil
}
// TransactionDeleteHandler deletes an existed transaction by request parameters for current user
func (a *TransactionsApi) TransactionDeleteHandler(c *core.WebContext) (any, *errs.Error) {
var transactionDeleteReq models.TransactionDeleteRequest
err := c.ShouldBindJSON(&transactionDeleteReq)
if err != nil {
log.Warnf(c, "[transactions.TransactionDeleteHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
utcOffset, err := c.GetClientTimezoneOffset()
if err != nil {
log.Warnf(c, "[transactions.TransactionDeleteHandler] cannot get client timezone offset, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
log.Errorf(c, "[transactions.TransactionDeleteHandler] failed to get user, because %s", err.Error())
}
return nil, errs.ErrUserNotFound
}
transaction, err := a.transactions.GetTransactionByTransactionId(c, uid, transactionDeleteReq.Id)
if err != nil {
log.Errorf(c, "[transactions.TransactionDeleteHandler] failed to get transaction \"id:%d\" for user \"uid:%d\", because %s", transactionDeleteReq.Id, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
log.Warnf(c, "[transactions.TransactionDeleteHandler] cannot delete transaction \"id:%d\" for user \"uid:%d\", because transaction type is transfer in", transactionDeleteReq.Id, uid)
return nil, errs.ErrTransactionTypeInvalid
}
transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, utcOffset)
if !transactionEditable {
return nil, errs.ErrCannotDeleteTransactionWithThisTransactionTime
}
err = a.transactions.DeleteTransaction(c, uid, transactionDeleteReq.Id)
if err != nil {
log.Errorf(c, "[transactions.TransactionDeleteHandler] failed to delete transaction \"id:%d\" for user \"uid:%d\", because %s", transactionDeleteReq.Id, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.Infof(c, "[transactions.TransactionDeleteHandler] user \"uid:%d\" has deleted transaction \"id:%d\"", uid, transactionDeleteReq.Id)
return true, nil
}
// TransactionParseImportDsvFileDataHandler returns the parsed file data by request parameters for current user
func (a *TransactionsApi) TransactionParseImportDsvFileDataHandler(c *core.WebContext) (any, *errs.Error) {
uid := c.GetCurrentUid()
form, err := c.MultipartForm()
if err != nil {
log.Errorf(c, "[transactions.TransactionParseImportDsvFileDataHandler] failed to get multi-part form data for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrParameterInvalid
}
fileTypes := form.Value["fileType"]
if len(fileTypes) < 1 || fileTypes[0] == "" {
return nil, errs.ErrImportFileTypeIsEmpty
}
fileType := fileTypes[0]
if !converters.IsCustomDelimiterSeparatedValuesFileType(fileType) {
return nil, errs.Or(err, errs.ErrImportFileTypeNotSupported)
}
fileEncodings := form.Value["fileEncoding"]
if len(fileEncodings) < 1 || fileEncodings[0] == "" {
return nil, errs.ErrImportFileEncodingIsEmpty
}
fileEncoding := fileEncodings[0]
dataParser, err := converters.CreateNewDelimiterSeparatedValuesDataParser(fileType, fileEncoding)
if err != nil {
return nil, errs.Or(err, errs.ErrImportFileTypeNotSupported)
}
importFiles := form.File["file"]
if len(importFiles) < 1 {
log.Warnf(c, "[transactions.TransactionParseImportDsvFileDataHandler] 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.TransactionParseImportDsvFileDataHandler] 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.TransactionParseImportDsvFileDataHandler] 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.TransactionParseImportDsvFileDataHandler] failed to get import file from request for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
}
defer importFile.Close()
fileData, err := io.ReadAll(importFile)
if err != nil {
log.Errorf(c, "[transactions.TransactionParseImportDsvFileDataHandler] failed to read import file data for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
allLines, err := dataParser.ParseDsvFileLines(c, fileData)
if err != nil {
log.Errorf(c, "[transactions.TransactionParseImportDsvFileDataHandler] failed to parse import file data for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
return allLines, 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 converter.TransactionDataImporter
if converters.IsCustomDelimiterSeparatedValuesFileType(fileType) {
fileEncodings := form.Value["fileEncoding"]
if len(fileEncodings) < 1 || fileEncodings[0] == "" {
return nil, errs.ErrImportFileEncodingIsEmpty
}
fileEncoding := fileEncodings[0]
columnMappings := form.Value["columnMapping"]
if len(columnMappings) < 1 || columnMappings[0] == "" {
return nil, errs.ErrImportFileColumnMappingInvalid
}
var columnIndexMapping = map[datatable.TransactionDataTableColumn]int{}
err = json.Unmarshal([]byte(columnMappings[0]), &columnIndexMapping)
if err != nil {
log.Errorf(c, "[transactions.TransactionParseImportFileHandler] failed to parse column mapping for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrImportFileColumnMappingInvalid
}
transactionTypeMappings := form.Value["transactionTypeMapping"]
if len(transactionTypeMappings) < 1 || transactionTypeMappings[0] == "" {
return nil, errs.ErrImportFileTransactionTypeMappingInvalid
}
var transactionTypeNameMapping = map[string]models.TransactionType{}
err = json.Unmarshal([]byte(transactionTypeMappings[0]), &transactionTypeNameMapping)
if err != nil {
log.Errorf(c, "[transactions.TransactionParseImportFileHandler] failed to parse transaction type mapping for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrImportFileTransactionTypeMappingInvalid
}
hasHeaderLines := form.Value["hasHeaderLine"]
hasHeaderLine := false
if len(hasHeaderLines) > 0 {
hasHeaderLine = hasHeaderLines[0] == "true"
}
timeFormats := form.Value["timeFormat"]
if len(timeFormats) < 1 || timeFormats[0] == "" {
return nil, errs.ErrImportFileTransactionTimeFormatInvalid
}
timezoneFormats := form.Value["timezoneFormat"]
timezoneFormat := ""
if len(timezoneFormats) > 0 {
timezoneFormat = timezoneFormats[0]
}
amountDecimalSeparators := form.Value["amountDecimalSeparator"]
amountDecimalSeparator := ""
if len(amountDecimalSeparators) > 0 {
amountDecimalSeparator = amountDecimalSeparators[0]
}
amountDigitGroupingSymbols := form.Value["amountDigitGroupingSymbol"]
amountDigitGroupingSymbol := ""
if len(amountDigitGroupingSymbols) > 0 {
amountDigitGroupingSymbol = amountDigitGroupingSymbols[0]
}
geoLocationSeparators := form.Value["geoSeparator"]
geoLocationSeparator := ""
if len(geoLocationSeparators) > 0 {
geoLocationSeparator = geoLocationSeparators[0]
}
geoLocationOrders := form.Value["geoOrder"]
geoLocationOrder := ""
if len(geoLocationOrders) > 0 {
geoLocationOrder = geoLocationOrders[0]
}
transactionTagSeparators := form.Value["tagSeparator"]
transactionTagSeparator := ""
if len(transactionTagSeparators) > 0 {
transactionTagSeparator = transactionTagSeparators[0]
}
dataImporter, err = converters.CreateNewDelimiterSeparatedValuesDataImporter(fileType, fileEncoding, columnIndexMapping, transactionTypeNameMapping, hasHeaderLine, timeFormats[0], timezoneFormat, amountDecimalSeparator, amountDigitGroupingSymbol, geoLocationSeparator, geoLocationOrder, transactionTagSeparator)
} else {
dataImporter, err = converters.GetTransactionDataImporter(fileType)
}
if err != nil {
return nil, errs.Or(err, 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
}
defer importFile.Close()
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
}
if user.FeatureRestriction.Contains(core.USER_FEATURE_RESTRICTION_TYPE_IMPORT_TRANSACTION) {
return nil, errs.ErrNotPermittedToPerformThisAction
}
accounts, err := a.accounts.GetAllAccountsByUid(c, user.Uid)
if err != nil {
log.Errorf(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.GetVisibleAccountNameMapByList(accounts)
categories, err := a.transactionCategories.GetAllCategoriesByUid(c, user.Uid, 0, -1)
if err != nil {
log.Errorf(c, "[transactions.TransactionParseImportFileHandler] failed to get categories for user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
expenseCategoryMap, incomeCategoryMap, transferCategoryMap := a.transactionCategories.GetVisibleSubCategoryNameMapByList(categories)
tags, err := a.transactionTags.GetAllTagsByUid(c, user.Uid)
if err != nil {
log.Errorf(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.GetVisibleTagNameMapByList(tags)
parsedTransactions, _, _, _, _, _, err := dataImporter.ParseImportedData(c, user, fileData, utcOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
if err != nil {
log.Errorf(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 {
items := strings.Split(remark, ":")
if len(items) >= 2 {
if items[0] == "finished" {
log.Infof(c, "[transactions.TransactionImportHandler] another \"%s\" transactions has been imported for user \"uid:%d\"", items[1], uid)
count, err := utils.StringToInt(items[1])
if err == nil {
return count, nil
}
} else if items[0] == "processing" {
return nil, errs.ErrRepeatedRequest
}
} else {
log.Warnf(c, "[transactions.TransactionImportHandler] another transaction import task may be executing, but remark \"%s\" is invalid", remark)
}
}
}
newTransactionTagIdsMap := make(map[int][]int64, len(transactionImportReq.Transactions))
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) > models.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
}
newTransactionTagIdsMap[i] = tagIds
}
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
}
if user.FeatureRestriction.Contains(core.USER_FEATURE_RESTRICTION_TYPE_IMPORT_TRANSACTION) {
return nil, errs.ErrNotPermittedToPerformThisAction
}
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, newTransactionTagIdsMap, func(currentProcess float64) {
a.SetSubmissionRemarkIfEnable(duplicatechecker.DUPLICATE_CHECKER_TYPE_IMPORT_TRANSACTIONS, uid, transactionImportReq.ClientSessionId, fmt.Sprintf("processing:%.2f", currentProcess))
})
count := len(newTransactions)
if err != nil {
a.RemoveSubmissionRemarkIfEnable(duplicatechecker.DUPLICATE_CHECKER_TYPE_IMPORT_TRANSACTIONS, uid, transactionImportReq.ClientSessionId)
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.SetSubmissionRemarkIfEnable(duplicatechecker.DUPLICATE_CHECKER_TYPE_IMPORT_TRANSACTIONS, uid, transactionImportReq.ClientSessionId, fmt.Sprintf("finished:%d", count))
return count, nil
}
// TransactionImportProcessHandler returns the process of specified transaction import task by request parameters for current user
func (a *TransactionsApi) TransactionImportProcessHandler(c *core.WebContext) (any, *errs.Error) {
var transactionImportProcessReq models.TransactionImportProcessRequest
err := c.ShouldBindQuery(&transactionImportProcessReq)
if err != nil {
log.Warnf(c, "[transactions.TransactionImportProcessHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
uid := c.GetCurrentUid()
if !a.CurrentConfig().EnableDuplicateSubmissionsCheck {
return nil, nil
}
found, remark := a.GetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_IMPORT_TRANSACTIONS, uid, transactionImportProcessReq.ClientSessionId)
if !found {
return nil, nil
}
items := strings.Split(remark, ":")
if len(items) < 2 {
return nil, nil
}
if items[0] == "finished" {
return 100, nil
} else if items[0] != "processing" {
return nil, nil
}
process, err := utils.StringToFloat64(items[1])
if err != nil {
log.Warnf(c, "[transactions.TransactionImportProcessHandler] parse process failed, because %s", err.Error())
return nil, nil
}
if process < 0 {
return nil, nil
} else if process >= 100 {
process = 100
}
return process, 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))
for i := 0; i < len(transactions); i++ {
transaction := transactions[i]
if _, exists := accountMap[transaction.AccountId]; !exists {
log.Warnf(c, "[transactions.filterTransactions] account of transaction \"id:%d\" does not exist for user \"uid:%d\"", transaction.TransactionId, uid)
continue
}
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN || transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
if _, exists := accountMap[transaction.RelatedAccountId]; !exists {
log.Warnf(c, "[transactions.filterTransactions] related account of transaction \"id:%d\" does not exist for user \"uid:%d\"", transaction.TransactionId, uid)
continue
}
}
finalTransactions = append(finalTransactions, transaction)
}
return finalTransactions
}
func (a *TransactionsApi) getTransactionTagInfoResponses(tagIds []int64, allTransactionTags map[int64]*models.TransactionTag) []*models.TransactionTagInfoResponse {
allTags := make([]*models.TransactionTagInfoResponse, 0, len(tagIds))
for i := 0; i < len(tagIds); i++ {
tag := allTransactionTags[tagIds[i]]
if tag == nil {
continue
}
allTags = append(allTags, tag.ToTransactionTagInfoResponse())
}
return allTags
}
func (a *TransactionsApi) getTransactionResponseListResult(c *core.WebContext, user *models.User, transactions []*models.Transaction, utcOffset int16, withPictures bool, trimAccount bool, trimCategory bool, trimTag bool) (models.TransactionInfoResponseSlice, error) {
uid := user.Uid
transactionIds := make([]int64, len(transactions))
accountIds := make([]int64, 0, len(transactions)*2)
categoryIds := make([]int64, 0, len(transactions))
for i := 0; i < len(transactions); i++ {
transactionId := transactions[i].TransactionId
if transactions[i].Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
transactionId = transactions[i].RelatedId
}
transactionIds[i] = transactionId
accountIds = append(accountIds, transactions[i].AccountId)
if transactions[i].Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN || transactions[i].Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
accountIds = append(accountIds, transactions[i].RelatedAccountId)
}
categoryIds = append(categoryIds, transactions[i].CategoryId)
}
allAccounts, err := a.accounts.GetAccountsByAccountIds(c, uid, utils.ToUniqueInt64Slice(accountIds))
if err != nil {
log.Errorf(c, "[transactions.getTransactionResponseListResult] failed to get accounts for user \"uid:%d\", because %s", uid, err.Error())
return nil, err
}
transactions = a.filterTransactions(c, uid, transactions, allAccounts)
allTransactionTagIds, err := a.transactionTags.GetAllTagIdsOfTransactions(c, uid, transactionIds)
if err != nil {
log.Errorf(c, "[transactions.getTransactionResponseListResult] failed to get transactions tag ids for user \"uid:%d\", because %s", uid, err.Error())
return nil, err
}
var categoryMap map[int64]*models.TransactionCategory
var tagMap map[int64]*models.TransactionTag
var pictureInfoMap map[int64][]*models.TransactionPictureInfo
if !trimCategory {
categoryMap, err = a.transactionCategories.GetCategoriesByCategoryIds(c, uid, utils.ToUniqueInt64Slice(categoryIds))
if err != nil {
log.Errorf(c, "[transactions.getTransactionResponseListResult] failed to get transactions categories for user \"uid:%d\", because %s", uid, err.Error())
return nil, err
}
}
if !trimTag {
tagMap, err = a.transactionTags.GetTagsByTagIds(c, uid, utils.ToUniqueInt64Slice(a.transactionTags.GetTransactionTagIds(allTransactionTagIds)))
if err != nil {
log.Errorf(c, "[transactions.getTransactionResponseListResult] failed to get transactions tags for user \"uid:%d\", because %s", uid, err.Error())
return nil, err
}
}
if withPictures && a.CurrentConfig().EnableTransactionPictures {
pictureInfoMap, err = a.transactionPictures.GetPictureInfosByTransactionIds(c, uid, utils.ToUniqueInt64Slice(a.transactions.GetTransactionIds(transactions)))
if err != nil {
log.Errorf(c, "[transactions.getTransactionResponseListResult] failed to get transactions pictures for user \"uid:%d\", because %s", uid, err.Error())
return nil, err
}
}
result := make(models.TransactionInfoResponseSlice, len(transactions))
for i := 0; i < len(transactions); i++ {
transaction := transactions[i]
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
transaction = a.transactions.GetRelatedTransferTransaction(transaction)
}
transactionEditable := transaction.IsEditable(user, utcOffset, allAccounts[transaction.AccountId], allAccounts[transaction.RelatedAccountId])
transactionTagIds := allTransactionTagIds[transaction.TransactionId]
result[i] = transaction.ToTransactionInfoResponse(transactionTagIds, transactionEditable)
if !trimAccount {
if sourceAccount := allAccounts[transaction.AccountId]; sourceAccount != nil {
result[i].SourceAccount = sourceAccount.ToAccountInfoResponse()
}
if destinationAccount := allAccounts[transaction.RelatedAccountId]; destinationAccount != nil {
result[i].DestinationAccount = destinationAccount.ToAccountInfoResponse()
}
}
if !trimCategory {
if category := categoryMap[transaction.CategoryId]; category != nil {
result[i].Category = category.ToTransactionCategoryInfoResponse()
}
}
if !trimTag {
result[i].Tags = a.getTransactionTagInfoResponses(transactionTagIds, tagMap)
}
if withPictures && a.CurrentConfig().EnableTransactionPictures {
pictureInfos, exists := pictureInfoMap[transaction.TransactionId]
if exists {
result[i].Pictures = a.GetTransactionPictureInfoResponseList(pictureInfos)
}
}
}
sort.Sort(result)
return result, nil
}
func (a *TransactionsApi) createNewTransactionModel(uid int64, transactionCreateReq *models.TransactionCreateRequest, clientIp string) *models.Transaction {
var transactionDbType models.TransactionDbType
if transactionCreateReq.Type == models.TRANSACTION_TYPE_MODIFY_BALANCE {
transactionDbType = models.TRANSACTION_DB_TYPE_MODIFY_BALANCE
} else if transactionCreateReq.Type == models.TRANSACTION_TYPE_EXPENSE {
transactionDbType = models.TRANSACTION_DB_TYPE_EXPENSE
} else if transactionCreateReq.Type == models.TRANSACTION_TYPE_INCOME {
transactionDbType = models.TRANSACTION_DB_TYPE_INCOME
} else if transactionCreateReq.Type == models.TRANSACTION_TYPE_TRANSFER {
transactionDbType = models.TRANSACTION_DB_TYPE_TRANSFER_OUT
}
transaction := &models.Transaction{
Uid: uid,
Type: transactionDbType,
CategoryId: transactionCreateReq.CategoryId,
TransactionTime: utils.GetMinTransactionTimeFromUnixTime(transactionCreateReq.Time),
TimezoneUtcOffset: transactionCreateReq.UtcOffset,
AccountId: transactionCreateReq.SourceAccountId,
Amount: transactionCreateReq.SourceAmount,
HideAmount: transactionCreateReq.HideAmount,
Comment: transactionCreateReq.Comment,
CreatedIp: clientIp,
}
if transactionCreateReq.Type == models.TRANSACTION_TYPE_TRANSFER {
transaction.RelatedAccountId = transactionCreateReq.DestinationAccountId
transaction.RelatedAccountAmount = transactionCreateReq.DestinationAmount
}
if transactionCreateReq.GeoLocation != nil {
transaction.GeoLongitude = transactionCreateReq.GeoLocation.Longitude
transaction.GeoLatitude = transactionCreateReq.GeoLocation.Latitude
}
return transaction
}