add reconciliation statement in desktop version

This commit is contained in:
MaysWind
2025-07-21 00:40:02 +08:00
parent 4ba3893b83
commit 515b9af61a
25 changed files with 882 additions and 4 deletions
+103
View File
@@ -22,6 +22,8 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
const pageCountForAccountStatement = 1000
// TransactionsApi represents transaction api
type TransactionsApi struct {
ApiUsingConfig
@@ -286,6 +288,107 @@ func (a *TransactionsApi) TransactionMonthListHandler(c *core.WebContext) (any,
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, err := a.transactions.GetAllTransactionsWithAccountBalanceByMaxTime(c, uid, pageCountForAccountStatement, maxTransactionTime, minTransactionTime, reconciliationStatementRequest.AccountId)
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]
accountBalance := int64(0)
if transactionWithBalance, exists := transactionAccountBalanceMap[transactionResult.Id]; exists {
accountBalance = transactionWithBalance.AccountBalance
} 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,
AccountBalance: accountBalance,
}
}
reconciliationStatementResp := &models.TransactionReconciliationStatementResponse{
Transactions: responseItems,
}
return reconciliationStatementResp, nil
}
// TransactionStatisticsHandler returns transaction statistics of current user
func (a *TransactionsApi) TransactionStatisticsHandler(c *core.WebContext) (any, *errs.Error) {
var statisticReq models.TransactionStatisticRequest
+24
View File
@@ -120,6 +120,12 @@ type Transaction struct {
DeletedUnixTime int64
}
// TransactionWithAccountBalance represents a transaction item with account balance
type TransactionWithAccountBalance struct {
*Transaction
AccountBalance int64
}
// TransactionGeoLocationRequest represents all parameters of transaction geographic location info update request
type TransactionGeoLocationRequest struct {
Latitude float64 `json:"latitude" binding:"required"`
@@ -222,6 +228,13 @@ type TransactionListInMonthByPageRequest struct {
TrimTag bool `form:"trim_tag"`
}
// TransactionReconciliationStatementRequest represents all parameters of transaction reconciliation statement request
type TransactionReconciliationStatementRequest struct {
AccountId int64 `form:"account_id,string" binding:"required,min=1"`
StartTime int64 `form:"start_time"`
EndTime int64 `form:"end_time"`
}
// TransactionStatisticRequest represents all parameters of transaction statistic request
type TransactionStatisticRequest struct {
StartTime int64 `form:"start_time" binding:"min=0"`
@@ -322,6 +335,17 @@ type TransactionInfoPageWrapperResponse2 struct {
TotalCount int64 `json:"totalCount"`
}
// TransactionReconciliationStatementResponseItem represents a transaction reconciliation statement response
type TransactionReconciliationStatementResponseItem struct {
*TransactionInfoResponse
AccountBalance int64 `json:"accountBalance"`
}
// TransactionReconciliationStatementResponse represents the response of all transaction reconciliation statement response
type TransactionReconciliationStatementResponse struct {
Transactions []*TransactionReconciliationStatementResponseItem `json:"transactions"`
}
// TransactionStatisticResponse represents transaction statistic response
type TransactionStatisticResponse struct {
StartTime int64 `json:"startTime"`
+22
View File
@@ -56,6 +56,28 @@ func (s *AccountService) GetAllAccountsByUid(c core.Context, uid int64) ([]*mode
return accounts, err
}
// GetAccountByAccountId returns account model according to account id
func (s *AccountService) GetAccountByAccountId(c core.Context, uid int64, accountId int64) (*models.Account, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
if accountId <= 0 {
return nil, errs.ErrAccountIdInvalid
}
account := &models.Account{}
has, err := s.UserDataDB(uid).NewSession(c).Where("uid=? AND deleted=? AND account_id=?", uid, false, accountId).Get(account)
if err != nil {
return nil, err
} else if !has {
return nil, errs.ErrAccountNotFound
}
return account, err
}
// GetAccountAndSubAccountsByAccountId returns account model and sub-account models according to account id
func (s *AccountService) GetAccountAndSubAccountsByAccountId(c core.Context, uid int64, accountId int64) ([]*models.Account, error) {
if uid <= 0 {
+66
View File
@@ -107,6 +107,72 @@ func (s *TransactionService) GetAllSpecifiedTransactions(c core.Context, uid int
return allTransactions, nil
}
// GetAllTransactionsWithAccountBalanceByMaxTime returns account statement within time range
func (s *TransactionService) GetAllTransactionsWithAccountBalanceByMaxTime(c core.Context, uid int64, pageCount int32, maxTransactionTime int64, minTransactionTime int64, accountId int64) ([]*models.TransactionWithAccountBalance, error) {
if maxTransactionTime <= 0 {
maxTransactionTime = utils.GetMaxTransactionTimeFromUnixTime(time.Now().Unix())
}
var allTransactions []*models.Transaction
for maxTransactionTime > 0 {
transactions, err := s.GetTransactionsByMaxTime(c, uid, maxTransactionTime, 0, 0, nil, []int64{accountId}, nil, false, models.TRANSACTION_TAG_FILTER_HAS_ANY, "", "", 1, pageCount, false, true)
if err != nil {
return nil, err
}
allTransactions = append(allTransactions, transactions...)
if len(transactions) < int(pageCount) {
maxTransactionTime = 0
break
}
maxTransactionTime = transactions[len(transactions)-1].TransactionTime - 1
}
allTransactionsAndAccountBalance := make([]*models.TransactionWithAccountBalance, 0, len(allTransactions))
if len(allTransactions) < 1 {
return allTransactionsAndAccountBalance, nil
}
accumulatedBalance := int64(0)
for i := len(allTransactions) - 1; i >= 0; i-- {
transaction := allTransactions[i]
if transaction.Type == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE {
accumulatedBalance = accumulatedBalance + transaction.RelatedAccountAmount
} else if transaction.Type == models.TRANSACTION_DB_TYPE_INCOME {
accumulatedBalance = accumulatedBalance + transaction.Amount
} else if transaction.Type == models.TRANSACTION_DB_TYPE_EXPENSE {
accumulatedBalance = accumulatedBalance - transaction.Amount
} else if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
accumulatedBalance = accumulatedBalance - transaction.Amount
} else if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
accumulatedBalance = accumulatedBalance + transaction.Amount
} else {
log.Errorf(c, "[transactions.GetAllTransactionsWithAccountBalanceByMaxTime] trasaction type (%d) is invalid (id:%d)", transaction.TransactionId, transaction.Type)
return nil, errs.ErrTransactionTypeInvalid
}
if transaction.TransactionTime < minTransactionTime {
continue
}
transactionsAndAccountBalance := &models.TransactionWithAccountBalance{
Transaction: transaction,
AccountBalance: accumulatedBalance,
}
allTransactionsAndAccountBalance = append(allTransactionsAndAccountBalance, transactionsAndAccountBalance)
}
return allTransactionsAndAccountBalance, nil
}
// GetTransactionsByMaxTime returns transactions before given time
func (s *TransactionService) GetTransactionsByMaxTime(c core.Context, uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionType, categoryIds []int64, accountIds []int64, tagIds []int64, noTags bool, tagFilterType models.TransactionTagFilterType, amountFilter string, keyword string, page int32, count int32, needOneMoreItem bool, noDuplicated bool) ([]*models.Transaction, error) {
if uid <= 0 {