add api for getting total income/expense amounts by month
This commit is contained in:
@@ -206,6 +206,7 @@ func startWebServer(c *cli.Context) error {
|
||||
apiV1Route.GET("/transactions/list/by_month.json", bindApi(api.Transactions.TransactionMonthListHandler))
|
||||
apiV1Route.GET("/transactions/statistics.json", bindApi(api.Transactions.TransactionStatisticsHandler))
|
||||
apiV1Route.GET("/transactions/amounts.json", bindApi(api.Transactions.TransactionAmountsHandler))
|
||||
apiV1Route.GET("/transactions/amounts/by_month.json", bindApi(api.Transactions.TransactionMonthAmountsHandler))
|
||||
apiV1Route.GET("/transactions/get.json", bindApi(api.Transactions.TransactionGetHandler))
|
||||
apiV1Route.POST("/transactions/add.json", bindApi(api.Transactions.TransactionCreateHandler))
|
||||
apiV1Route.POST("/transactions/modify.json", bindApi(api.Transactions.TransactionModifyHandler))
|
||||
|
||||
@@ -2,6 +2,7 @@ package api
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/mayswind/lab/pkg/core"
|
||||
"github.com/mayswind/lab/pkg/errs"
|
||||
@@ -11,6 +12,8 @@ import (
|
||||
"github.com/mayswind/lab/pkg/utils"
|
||||
)
|
||||
|
||||
const pageCountForLoadTransactionAmounts = 1000
|
||||
|
||||
// TransactionsApi represents transaction api
|
||||
type TransactionsApi struct {
|
||||
transactions *services.TransactionService
|
||||
@@ -335,6 +338,112 @@ func (a *TransactionsApi) TransactionAmountsHandler(c *core.Context) (interface{
|
||||
return amountsResp, nil
|
||||
}
|
||||
|
||||
// TransactionMonthAmountsHandler returns every month transaction amounts of current user
|
||||
func (a *TransactionsApi) TransactionMonthAmountsHandler(c *core.Context) (interface{}, *errs.Error) {
|
||||
var transactionAmountsReq models.TransactionMonthAmountsRequest
|
||||
err := c.ShouldBindQuery(&transactionAmountsReq)
|
||||
|
||||
if err != nil {
|
||||
log.WarnfWithRequestId(c, "[transactions.TransactionMonthAmountsHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
utcOffset, err := c.GetClientTimezoneOffset()
|
||||
|
||||
if err != nil {
|
||||
log.WarnfWithRequestId(c, "[transactions.TransactionMonthAmountsHandler] cannot get client timezone offset, because %s", err.Error())
|
||||
return nil, errs.ErrClientTimezoneOffsetInvalid
|
||||
}
|
||||
|
||||
startTime, endTime, err := transactionAmountsReq.GetStartTimeAndEndTime(utcOffset)
|
||||
|
||||
if err != nil {
|
||||
log.WarnfWithRequestId(c, "[transactions.TransactionMonthAmountsHandler] parse request start or end date failed, because %s", err.Error())
|
||||
return nil, errs.ErrParameterInvalid
|
||||
}
|
||||
|
||||
uid := c.GetCurrentUid()
|
||||
|
||||
accounts, err := a.accounts.GetAllAccountsByUid(uid)
|
||||
accountMap := a.accounts.GetAccountMapByList(accounts)
|
||||
|
||||
if err != nil {
|
||||
log.ErrorfWithRequestId(c, "[transactions.TransactionMonthAmountsHandler] failed to get all accounts for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.ErrOperationFailed
|
||||
}
|
||||
|
||||
totalAmounts, err := a.transactions.GetAccountsMonthTotalIncomeAndExpense(uid, startTime, endTime, pageCountForLoadTransactionAmounts)
|
||||
amountsMap := make(map[string]map[string]*models.TransactionAmountsResponseItemAmountInfo)
|
||||
|
||||
for yearMonth, monthAccountsAmounts := range totalAmounts {
|
||||
for accountId, monthAccountAmounts := range monthAccountsAmounts {
|
||||
account, exists := accountMap[accountId]
|
||||
|
||||
if !exists {
|
||||
log.WarnfWithRequestId(c, "[transactions.TransactionMonthAmountsHandler] cannot find account for account \"id:%d\" of user \"uid:%d\", because %s", accountId, uid)
|
||||
continue
|
||||
}
|
||||
|
||||
monthTotalAmounts, exists := amountsMap[yearMonth]
|
||||
|
||||
if !exists {
|
||||
monthTotalAmounts = make(map[string]*models.TransactionAmountsResponseItemAmountInfo)
|
||||
amountsMap[yearMonth] = monthTotalAmounts
|
||||
}
|
||||
|
||||
monthTotalAmount, exists := monthTotalAmounts[account.Currency]
|
||||
|
||||
if !exists {
|
||||
monthTotalAmount = &models.TransactionAmountsResponseItemAmountInfo{
|
||||
Currency: account.Currency,
|
||||
IncomeAmount: 0,
|
||||
ExpenseAmount: 0,
|
||||
}
|
||||
}
|
||||
|
||||
monthTotalAmount.IncomeAmount += monthAccountAmounts.TotalIncomeAmount
|
||||
monthTotalAmount.ExpenseAmount += monthAccountAmounts.TotalExpenseAmount
|
||||
|
||||
monthTotalAmounts[account.Currency] = monthTotalAmount
|
||||
}
|
||||
}
|
||||
|
||||
amountsResp := make(models.TransactionMonthAmountsResponseItemSlice, 0)
|
||||
|
||||
for yearMonth, monthTotalAmounts := range amountsMap {
|
||||
yearMonthItems := strings.Split(yearMonth, "-")
|
||||
year, err := utils.StringToInt32(yearMonthItems[0])
|
||||
|
||||
if err != nil {
|
||||
log.WarnfWithRequestId(c, "[transactions.TransactionMonthAmountsHandler] cannot get year from year-month item \"%s\" for user \"uid:%d\", because %s", yearMonth, uid)
|
||||
continue
|
||||
}
|
||||
|
||||
month, err := utils.StringToInt32(yearMonthItems[1])
|
||||
|
||||
if err != nil {
|
||||
log.WarnfWithRequestId(c, "[transactions.TransactionMonthAmountsHandler] cannot get month from year-month item \"%s\" for user \"uid:%d\", because %s", yearMonth, uid)
|
||||
continue
|
||||
}
|
||||
|
||||
amounts := make([]*models.TransactionAmountsResponseItemAmountInfo, 0, len(monthTotalAmounts))
|
||||
|
||||
for _, monthTotalAmount := range monthTotalAmounts {
|
||||
amounts = append(amounts, monthTotalAmount)
|
||||
}
|
||||
|
||||
amountsResp = append(amountsResp, &models.TransactionMonthAmountsResponseItem{
|
||||
Year: year,
|
||||
Month: month,
|
||||
Amounts: amounts,
|
||||
})
|
||||
}
|
||||
|
||||
sort.Sort(amountsResp)
|
||||
|
||||
return amountsResp, nil
|
||||
}
|
||||
|
||||
// TransactionGetHandler returns one specific transaction of current user
|
||||
func (a *TransactionsApi) TransactionGetHandler(c *core.Context) (interface{}, *errs.Error) {
|
||||
var transactionGetReq models.TransactionGetRequest
|
||||
|
||||
@@ -19,6 +19,7 @@ var (
|
||||
ErrQueryItemsEmpty = NewNormalError(NormalSubcategoryGlobal, 9, http.StatusBadRequest, "query items cannot be empty")
|
||||
ErrQueryItemsTooMuch = NewNormalError(NormalSubcategoryGlobal, 10, http.StatusBadRequest, "query items too much")
|
||||
ErrQueryItemsInvalid = NewNormalError(NormalSubcategoryGlobal, 11, http.StatusBadRequest, "query items have invalid item")
|
||||
ErrParameterInvalid = NewNormalError(NormalSubcategoryGlobal, 12, http.StatusBadRequest, "parameter invalid")
|
||||
)
|
||||
|
||||
// GetParameterInvalidMessage returns specific error message for invalid parameter error
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mayswind/lab/pkg/errs"
|
||||
"github.com/mayswind/lab/pkg/utils"
|
||||
@@ -135,6 +137,12 @@ type TransactionAmountsRequestItem struct {
|
||||
EndTime int64
|
||||
}
|
||||
|
||||
// TransactionMonthAmountsRequest represents all parameters of transaction month amounts request
|
||||
type TransactionMonthAmountsRequest struct {
|
||||
StartYearMonth string `form:"start_year_month"`
|
||||
EndYearMonth string `form:"end_year_month"`
|
||||
}
|
||||
|
||||
// TransactionGetRequest represents all parameters of transaction getting request
|
||||
type TransactionGetRequest struct {
|
||||
Id int64 `form:"id,string" binding:"required,min=1"`
|
||||
@@ -148,6 +156,16 @@ type TransactionDeleteRequest struct {
|
||||
Id int64 `json:"id,string" binding:"required,min=1"`
|
||||
}
|
||||
|
||||
// TransactionAccountsAmount represents transaction accounts amount map
|
||||
type TransactionAccountsAmount map[int64]*TransactionAccountAmount
|
||||
|
||||
// TransactionAccountAmount represents transaction account amount
|
||||
type TransactionAccountAmount struct {
|
||||
AccountId int64
|
||||
TotalIncomeAmount int64
|
||||
TotalExpenseAmount int64
|
||||
}
|
||||
|
||||
// TransactionInfoResponse represents a view-object of transaction
|
||||
type TransactionInfoResponse struct {
|
||||
Id int64 `json:"id,string"`
|
||||
@@ -207,6 +225,13 @@ type TransactionAmountsResponseItem struct {
|
||||
Amounts []*TransactionAmountsResponseItemAmountInfo `json:"amounts"`
|
||||
}
|
||||
|
||||
// TransactionMonthAmountsResponseItem represents an item of transaction month amounts
|
||||
type TransactionMonthAmountsResponseItem struct {
|
||||
Year int `json:"year"`
|
||||
Month int `json:"month"`
|
||||
Amounts []*TransactionAmountsResponseItemAmountInfo `json:"amounts"`
|
||||
}
|
||||
|
||||
// TransactionAmountsResponseItemAmountInfo represents amount info for an response item
|
||||
type TransactionAmountsResponseItemAmountInfo struct {
|
||||
Currency string `json:"currency"`
|
||||
@@ -321,6 +346,35 @@ func (t *TransactionAmountsRequest) GetTransactionAmountsRequestItems() ([]*Tran
|
||||
return requestItems, nil
|
||||
}
|
||||
|
||||
// GetStartTimeAndEndTime returns start unix time and end unix time by request parameter
|
||||
func (t *TransactionMonthAmountsRequest) GetStartTimeAndEndTime(utcOffset int16) (int64, int64, error) {
|
||||
startUnixTime := int64(0)
|
||||
endUnixTime := time.Now().Unix()
|
||||
|
||||
if t.StartYearMonth != "" {
|
||||
startTime, err := utils.ParseFromShortDateTime(fmt.Sprintf("%s-1 0:0:0", t.StartYearMonth), utcOffset)
|
||||
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
startUnixTime = startTime.Unix()
|
||||
}
|
||||
|
||||
if t.EndYearMonth != "" {
|
||||
endTime, err := utils.ParseFromShortDateTime(fmt.Sprintf("%s-1 0:0:0", t.EndYearMonth), utcOffset)
|
||||
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
endTime = endTime.AddDate(0, 1, 0)
|
||||
endUnixTime = endTime.Unix() - 1
|
||||
}
|
||||
|
||||
return startUnixTime, endUnixTime, nil
|
||||
}
|
||||
|
||||
// TransactionInfoResponseSlice represents the slice data structure of TransactionInfoResponse
|
||||
type TransactionInfoResponseSlice []*TransactionInfoResponse
|
||||
|
||||
@@ -342,3 +396,25 @@ func (s TransactionInfoResponseSlice) Less(i, j int) bool {
|
||||
|
||||
return s[i].Id > s[j].Id
|
||||
}
|
||||
|
||||
// TransactionMonthAmountsResponseItemSlice represents the slice data structure of TransactionMonthAmountsResponseItem
|
||||
type TransactionMonthAmountsResponseItemSlice []*TransactionMonthAmountsResponseItem
|
||||
|
||||
// Len returns the count of items
|
||||
func (s TransactionMonthAmountsResponseItemSlice) Len() int {
|
||||
return len(s)
|
||||
}
|
||||
|
||||
// Swap swaps two items
|
||||
func (s TransactionMonthAmountsResponseItemSlice) 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 TransactionMonthAmountsResponseItemSlice) Less(i, j int) bool {
|
||||
if s[i].Year != s[j].Year {
|
||||
return s[i].Year > s[j].Year
|
||||
}
|
||||
|
||||
return s[i].Month > s[j].Month
|
||||
}
|
||||
|
||||
@@ -969,6 +969,73 @@ func (s *TransactionService) GetAccountsTotalIncomeAndExpense(uid int64, startUn
|
||||
return incomeAmounts, expenseAmounts, nil
|
||||
}
|
||||
|
||||
// GetAccountsMonthTotalIncomeAndExpense returns the every accounts total income and expense amount in month by specific date range
|
||||
func (s *TransactionService) GetAccountsMonthTotalIncomeAndExpense(uid int64, startUnixTime int64, endUnixTime int64, pageCount int) (map[string]models.TransactionAccountsAmount, error) {
|
||||
if uid <= 0 {
|
||||
return nil, errs.ErrUserIdInvalid
|
||||
}
|
||||
|
||||
startTransactionTime := utils.GetMinTransactionTimeFromUnixTime(startUnixTime)
|
||||
endTransactionTime := utils.GetMaxTransactionTimeFromUnixTime(endUnixTime)
|
||||
|
||||
minTransactionTime := startTransactionTime
|
||||
maxTransactionTime := endTransactionTime
|
||||
var allTransactions []*models.Transaction
|
||||
|
||||
for maxTransactionTime > 0 {
|
||||
var transactions []*models.Transaction
|
||||
|
||||
err := s.UserDataDB(uid).Select("uid, type, account_id, transaction_time, timezone_utc_offset, amount").Where("uid=? AND deleted=? AND (type=? OR type=?) AND transaction_time>=? AND transaction_time<=?", uid, false, models.TRANSACTION_DB_TYPE_INCOME, models.TRANSACTION_DB_TYPE_EXPENSE, minTransactionTime, maxTransactionTime).Limit(pageCount, 0).OrderBy("transaction_time desc").Find(&transactions)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
allTransactions = append(allTransactions, transactions...)
|
||||
|
||||
if len(transactions) < pageCount {
|
||||
maxTransactionTime = 0
|
||||
break
|
||||
}
|
||||
|
||||
maxTransactionTime = transactions[len(transactions)-1].TransactionTime - 1
|
||||
}
|
||||
|
||||
totalAmounts := make(map[string]models.TransactionAccountsAmount)
|
||||
|
||||
for i := 0; i < len(allTransactions); i++ {
|
||||
transaction := allTransactions[i]
|
||||
transactionTimeZone := time.FixedZone("Transaction Timezone", int(transaction.TimezoneUtcOffset)*60)
|
||||
yearMonth := utils.FormatUnixTimeToYearMonth(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime), transactionTimeZone)
|
||||
|
||||
monthAccountsAmounts, exists := totalAmounts[yearMonth]
|
||||
|
||||
if !exists {
|
||||
monthAccountsAmounts = make(models.TransactionAccountsAmount)
|
||||
totalAmounts[yearMonth] = monthAccountsAmounts
|
||||
}
|
||||
|
||||
monthAccountAmount, exists := monthAccountsAmounts[transaction.AccountId]
|
||||
|
||||
if !exists {
|
||||
monthAccountAmount = &models.TransactionAccountAmount{
|
||||
AccountId: transaction.AccountId,
|
||||
TotalIncomeAmount: 0,
|
||||
TotalExpenseAmount: 0,
|
||||
}
|
||||
monthAccountsAmounts[transaction.AccountId] = monthAccountAmount
|
||||
}
|
||||
|
||||
if transaction.Type == models.TRANSACTION_DB_TYPE_INCOME {
|
||||
monthAccountAmount.TotalIncomeAmount += transaction.Amount
|
||||
} else if transaction.Type == models.TRANSACTION_DB_TYPE_EXPENSE {
|
||||
monthAccountAmount.TotalExpenseAmount += transaction.Amount
|
||||
}
|
||||
}
|
||||
|
||||
return totalAmounts, nil
|
||||
}
|
||||
|
||||
// GetAccountsAndCategoriesTotalIncomeAndExpense returns the every accounts and categories total income and expense amount by specific date range
|
||||
func (s *TransactionService) GetAccountsAndCategoriesTotalIncomeAndExpense(uid int64, startUnixTime int64, endUnixTime int64) ([]*models.Transaction, error) {
|
||||
if uid <= 0 {
|
||||
|
||||
@@ -5,6 +5,8 @@ import "time"
|
||||
const (
|
||||
longDateTimeFormat = "2006-01-02 15:04:05"
|
||||
longDateTimeWithoutSecondFormat = "2006-01-02 15:04"
|
||||
shortDateTimeFormat = "2006-1-2 15:4:5"
|
||||
yearMonthDateTimeFormat = "2006-01"
|
||||
)
|
||||
|
||||
// FormatUnixTimeToLongDateTimeInServerTimezone returns a textual representation of the unix time formatted by long date time format
|
||||
@@ -23,6 +25,17 @@ func FormatUnixTimeToLongDateTimeWithoutSecond(unixTime int64, timezone *time.Lo
|
||||
return t.Format(longDateTimeWithoutSecondFormat)
|
||||
}
|
||||
|
||||
// FormatUnixTimeToYearMonth returns year and month of specified unix time
|
||||
func FormatUnixTimeToYearMonth(unixTime int64, timezone *time.Location) string {
|
||||
t := ParseFromUnixTime(unixTime)
|
||||
|
||||
if timezone != nil {
|
||||
t = t.In(timezone)
|
||||
}
|
||||
|
||||
return t.Format(yearMonthDateTimeFormat)
|
||||
}
|
||||
|
||||
// ParseFromUnixTime parses a unix time and returns a golang time struct
|
||||
func ParseFromUnixTime(unixTime int64) time.Time {
|
||||
return time.Unix(unixTime, 0)
|
||||
@@ -34,6 +47,12 @@ func ParseFromLongDateTime(t string, utcOffset int16) (time.Time, error) {
|
||||
return time.ParseInLocation(longDateTimeFormat, t, timezone)
|
||||
}
|
||||
|
||||
// ParseFromShortDateTime parses a formatted string in short date time format
|
||||
func ParseFromShortDateTime(t string, utcOffset int16) (time.Time, error) {
|
||||
timezone := time.FixedZone("Timezone", int(utcOffset)*60)
|
||||
return time.ParseInLocation(shortDateTimeFormat, t, timezone)
|
||||
}
|
||||
|
||||
// GetMinTransactionTimeFromUnixTime returns the minimum transaction time from unix time
|
||||
func GetMinTransactionTimeFromUnixTime(unixTime int64) int64 {
|
||||
return unixTime * 1000
|
||||
|
||||
@@ -564,6 +564,8 @@ export default {
|
||||
'transaction tag is in use and cannot be deleted': 'Transaction tag is in use and it cannot be deleted',
|
||||
'query items cannot be empty': 'There are no query items',
|
||||
'query items too much': 'There are too many query items',
|
||||
'query items have invalid item': 'There is invalid item in query items',
|
||||
'parameter invalid': 'Parameter is invalid',
|
||||
},
|
||||
'parameter': {
|
||||
'id': 'ID',
|
||||
|
||||
@@ -564,6 +564,8 @@ export default {
|
||||
'transaction tag is in use and cannot be deleted': '交易标签正在被使用,无法删除',
|
||||
'query items cannot be empty': '请求项目不能为空',
|
||||
'query items too much': '请求项目过多',
|
||||
'query items have invalid item': '请求项目中有非法项目',
|
||||
'parameter invalid': '参数错误',
|
||||
},
|
||||
'parameter': {
|
||||
'id': 'ID',
|
||||
|
||||
Reference in New Issue
Block a user