Files
ezbookkeeping/pkg/models/transaction.go
T
2023-06-28 21:41:59 +08:00

472 lines
20 KiB
Go

package models
import (
"fmt"
"strings"
"time"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
// TransactionType represents transaction type
type TransactionType byte
// Transaction types
const (
TRANSACTION_TYPE_MODIFY_BALANCE TransactionType = 1
TRANSACTION_TYPE_INCOME TransactionType = 2
TRANSACTION_TYPE_EXPENSE TransactionType = 3
TRANSACTION_TYPE_TRANSFER TransactionType = 4
)
// TransactionDbType represents transaction type in database
type TransactionDbType byte
// Transaction db types
const (
TRANSACTION_DB_TYPE_MODIFY_BALANCE TransactionDbType = 1
TRANSACTION_DB_TYPE_INCOME TransactionDbType = 2
TRANSACTION_DB_TYPE_EXPENSE TransactionDbType = 3
TRANSACTION_DB_TYPE_TRANSFER_OUT TransactionDbType = 4
TRANSACTION_DB_TYPE_TRANSFER_IN TransactionDbType = 5
)
// Transaction represents transaction data stored in database
type Transaction struct {
TransactionId int64 `xorm:"PK"`
Uid int64 `xorm:"UNIQUE(UQE_transaction_uid_time) INDEX(IDX_transaction_uid_deleted_time) INDEX(IDX_transaction_uid_deleted_type_time) INDEX(IDX_transaction_uid_deleted_category_id_time) INDEX(IDX_transaction_uid_deleted_account_id_time) INDEX(IDX_transaction_uid_deleted_time_longitude_latitude) NOT NULL"`
Deleted bool `xorm:"INDEX(IDX_transaction_uid_deleted_time) INDEX(IDX_transaction_uid_deleted_type_time) INDEX(IDX_transaction_uid_deleted_category_id_time) INDEX(IDX_transaction_uid_deleted_account_id_time) INDEX(IDX_transaction_uid_deleted_time_longitude_latitude) NOT NULL"`
Type TransactionDbType `xorm:"INDEX(IDX_transaction_uid_deleted_type_time) NOT NULL"`
CategoryId int64 `xorm:"INDEX(IDX_transaction_uid_deleted_category_id_time) NOT NULL"`
AccountId int64 `xorm:"INDEX(IDX_transaction_uid_deleted_account_id_time) NOT NULL"`
TransactionTime int64 `xorm:"UNIQUE(UQE_transaction_uid_time) INDEX(IDX_transaction_uid_deleted_time) INDEX(IDX_transaction_uid_deleted_type_time) INDEX(IDX_transaction_uid_deleted_category_id_time) INDEX(IDX_transaction_uid_deleted_account_id_time) NOT NULL"`
TimezoneUtcOffset int16 `xorm:"NOT NULL"`
Amount int64 `xorm:"NOT NULL"`
RelatedId int64 `xorm:"NOT NULL"`
RelatedAccountId int64 `xorm:"NOT NULL"`
RelatedAccountAmount int64 `xorm:"NOT NULL"`
HideAmount bool `xorm:"NOT NULL"`
Comment string `xorm:"VARCHAR(255) NOT NULL"`
GeoLongitude float64 `xorm:"INDEX(IDX_transaction_uid_deleted_time_longitude_latitude)"`
GeoLatitude float64 `xorm:"INDEX(IDX_transaction_uid_deleted_time_longitude_latitude)"`
CreatedIp string `xorm:"VARCHAR(39)"`
CreatedUnixTime int64
UpdatedUnixTime int64
DeletedUnixTime int64
}
// TransactionGeoLocationRequest represents all parameters of transaction geographic location info update request
type TransactionGeoLocationRequest struct {
Latitude float64 `json:"latitude" binding:"required"`
Longitude float64 `json:"longitude" binding:"required"`
}
// TransactionCreateRequest represents all parameters of transaction creation request
type TransactionCreateRequest struct {
Type TransactionType `json:"type" binding:"required"`
CategoryId int64 `json:"categoryId,string"`
Time int64 `json:"time" binding:"required,min=1"`
UtcOffset int16 `json:"utcOffset" binding:"min=-720,max=840"`
SourceAccountId int64 `json:"sourceAccountId,string" binding:"required,min=1"`
DestinationAccountId int64 `json:"destinationAccountId,string" binding:"min=0"`
SourceAmount int64 `json:"sourceAmount" binding:"min=-99999999999,max=99999999999"`
DestinationAmount int64 `json:"destinationAmount" binding:"min=-99999999999,max=99999999999"`
HideAmount bool `json:"hideAmount"`
TagIds []string `json:"tagIds"`
Comment string `json:"comment" binding:"max=255"`
GeoLocation *TransactionGeoLocationRequest `json:"geoLocation" binding:"omitempty"`
}
// TransactionModifyRequest represents all parameters of transaction modification request
type TransactionModifyRequest struct {
Id int64 `json:"id,string" binding:"required,min=1"`
CategoryId int64 `json:"categoryId,string"`
Time int64 `json:"time" binding:"required,min=1"`
UtcOffset int16 `json:"utcOffset" binding:"min=-720,max=840"`
SourceAccountId int64 `json:"sourceAccountId,string" binding:"required,min=1"`
DestinationAccountId int64 `json:"destinationAccountId,string" binding:"min=0"`
SourceAmount int64 `json:"sourceAmount" binding:"min=-99999999999,max=99999999999"`
DestinationAmount int64 `json:"destinationAmount" binding:"min=-99999999999,max=99999999999"`
HideAmount bool `json:"hideAmount"`
TagIds []string `json:"tagIds"`
Comment string `json:"comment" binding:"max=255"`
GeoLocation *TransactionGeoLocationRequest `json:"geoLocation" binding:"omitempty"`
}
// TransactionCountRequest represents transaction count request
type TransactionCountRequest struct {
Type TransactionDbType `form:"type" binding:"min=0,max=4"`
CategoryId int64 `form:"category_id" binding:"min=0"`
AccountId int64 `form:"account_id" binding:"min=0"`
Keyword string `form:"keyword"`
MaxTime int64 `form:"max_time" binding:"min=0"`
MinTime int64 `form:"min_time" binding:"min=0"`
}
// TransactionListByMaxTimeRequest represents all parameters of transaction listing by max time request
type TransactionListByMaxTimeRequest struct {
Type TransactionDbType `form:"type" binding:"min=0,max=4"`
CategoryId int64 `form:"category_id" binding:"min=0"`
AccountId int64 `form:"account_id" binding:"min=0"`
Keyword string `form:"keyword"`
MaxTime int64 `form:"max_time" binding:"min=0"`
MinTime int64 `form:"min_time" binding:"min=0"`
Count int32 `form:"count" binding:"required,min=1,max=50"`
TrimAccount bool `form:"trim_account"`
TrimCategory bool `form:"trim_category"`
TrimTag bool `form:"trim_tag"`
}
// TransactionListInMonthByPageRequest represents all parameters of transaction listing by month request
type TransactionListInMonthByPageRequest struct {
Year int32 `form:"year" binding:"required,min=1"`
Month int32 `form:"month" binding:"required,min=1"`
Type TransactionDbType `form:"type" binding:"min=0,max=4"`
CategoryId int64 `form:"category_id" binding:"min=0"`
AccountId int64 `form:"account_id" binding:"min=0"`
Keyword string `form:"keyword"`
Page int32 `form:"page" binding:"required,min=1"`
Count int32 `form:"count" binding:"required,min=1,max=50"`
TrimAccount bool `form:"trim_account"`
TrimCategory bool `form:"trim_category"`
TrimTag bool `form:"trim_tag"`
}
// TransactionStatisticRequest represents all parameters of transaction statistic request
type TransactionStatisticRequest struct {
StartTime int64 `form:"start_time" binding:"min=0"`
EndTime int64 `form:"end_time" binding:"min=0"`
}
// TransactionAmountsRequest represents all parameters of transaction amounts request
type TransactionAmountsRequest struct {
Query string `form:"query"`
}
// TransactionAmountsRequestItem represents an item of transaction amounts request
type TransactionAmountsRequestItem struct {
Name string
StartTime int64
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"`
TrimAccount bool `form:"trim_account"`
TrimCategory bool `form:"trim_category"`
TrimTag bool `form:"trim_tag"`
}
// TransactionDeleteRequest represents all parameters of transaction deleting request
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
}
// TransactionGeoLocationResponse represents a view-object of transaction geographic location info
type TransactionGeoLocationResponse struct {
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
}
// TransactionInfoResponse represents a view-object of transaction
type TransactionInfoResponse struct {
Id int64 `json:"id,string"`
TimeSequenceId int64 `json:"timeSequenceId,string"`
Type TransactionType `json:"type"`
CategoryId int64 `json:"categoryId,string"`
Category *TransactionCategoryInfoResponse `json:"category,omitempty"`
Time int64 `json:"time"`
UtcOffset int16 `json:"utcOffset"`
SourceAccountId int64 `json:"sourceAccountId,string"`
SourceAccount *AccountInfoResponse `json:"sourceAccount,omitempty"`
DestinationAccountId int64 `json:"destinationAccountId,string,omitempty"`
DestinationAccount *AccountInfoResponse `json:"destinationAccount,omitempty"`
SourceAmount int64 `json:"sourceAmount"`
DestinationAmount int64 `json:"destinationAmount,omitempty"`
HideAmount bool `json:"hideAmount"`
TagIds []string `json:"tagIds"`
Tags []*TransactionTagInfoResponse `json:"tags,omitempty"`
Comment string `json:"comment"`
GeoLocation *TransactionGeoLocationResponse `json:"geoLocation,omitempty"`
Editable bool `json:"editable"`
}
// TransactionCountResponse represents transaction count response
type TransactionCountResponse struct {
TotalCount int64 `json:"totalCount"`
}
// TransactionInfoPageWrapperResponse represents a response of transaction which contains items and next id
type TransactionInfoPageWrapperResponse struct {
Items TransactionInfoResponseSlice `json:"items"`
NextTimeSequenceId *int64 `json:"nextTimeSequenceId,string"`
}
// TransactionInfoPageWrapperResponse2 represents a response of transaction which contains items and count
type TransactionInfoPageWrapperResponse2 struct {
Items TransactionInfoResponseSlice `json:"items"`
TotalCount int64 `json:"totalCount"`
}
// TransactionStatisticResponse represents an item of transaction amounts
type TransactionStatisticResponse struct {
StartTime int64 `json:"startTime"`
EndTime int64 `json:"endTime"`
Items []*TransactionStatisticResponseItem `json:"items"`
}
// TransactionStatisticResponseItem represents total amount item for an response
type TransactionStatisticResponseItem struct {
CategoryId int64 `json:"categoryId,string"`
AccountId int64 `json:"accountId,string"`
TotalAmount int64 `json:"amount"`
}
// TransactionAmountsResponseItem represents an item of transaction amounts
type TransactionAmountsResponseItem struct {
StartTime int64 `json:"startTime"`
EndTime int64 `json:"endTime"`
Amounts []*TransactionAmountsResponseItemAmountInfo `json:"amounts"`
}
// TransactionMonthAmountsResponseItem represents an item of transaction month amounts
type TransactionMonthAmountsResponseItem struct {
Year int32 `json:"year"`
Month int32 `json:"month"`
Amounts []*TransactionAmountsResponseItemAmountInfo `json:"amounts"`
}
// TransactionAmountsResponseItemAmountInfo represents amount info for an response item
type TransactionAmountsResponseItemAmountInfo struct {
Currency string `json:"currency"`
IncomeAmount int64 `json:"incomeAmount"`
ExpenseAmount int64 `json:"expenseAmount"`
}
// IsEditable returns whether this transaction can be edited
func (t *Transaction) IsEditable(currentUser *User, utcOffset int16, account *Account, relatedAccount *Account) bool {
if currentUser == nil || !currentUser.CanEditTransactionByTransactionTime(t.TransactionTime, utcOffset) {
return false
}
if account == nil || account.Hidden {
return false
}
if t.Type == TRANSACTION_DB_TYPE_TRANSFER_OUT {
if relatedAccount == nil || relatedAccount.Hidden {
return false
}
}
return true
}
// ToTransactionInfoResponse returns a view-object according to database model
func (t *Transaction) ToTransactionInfoResponse(tagIds []int64, editable bool) *TransactionInfoResponse {
var transactionType TransactionType
if t.Type == TRANSACTION_DB_TYPE_MODIFY_BALANCE {
transactionType = TRANSACTION_TYPE_MODIFY_BALANCE
} else if t.Type == TRANSACTION_DB_TYPE_EXPENSE {
transactionType = TRANSACTION_TYPE_EXPENSE
} else if t.Type == TRANSACTION_DB_TYPE_INCOME {
transactionType = TRANSACTION_TYPE_INCOME
} else if t.Type == TRANSACTION_DB_TYPE_TRANSFER_OUT {
transactionType = TRANSACTION_TYPE_TRANSFER
} else if t.Type == TRANSACTION_DB_TYPE_TRANSFER_IN {
transactionType = TRANSACTION_TYPE_TRANSFER
} else {
return nil
}
sourceAccountId := t.AccountId
sourceAmount := t.Amount
destinationAccountId := int64(0)
destinationAmount := int64(0)
if t.Type == TRANSACTION_DB_TYPE_TRANSFER_OUT {
destinationAccountId = t.RelatedAccountId
destinationAmount = t.RelatedAccountAmount
} else if t.Type == TRANSACTION_DB_TYPE_TRANSFER_IN {
sourceAccountId = t.RelatedAccountId
sourceAmount = t.RelatedAccountAmount
destinationAccountId = t.AccountId
destinationAmount = t.Amount
}
geoLocation := &TransactionGeoLocationResponse{}
if t.GeoLongitude != 0 || t.GeoLatitude != 0 {
geoLocation.Longitude = t.GeoLongitude
geoLocation.Latitude = t.GeoLatitude
} else {
geoLocation = nil
}
return &TransactionInfoResponse{
Id: t.TransactionId,
TimeSequenceId: t.TransactionTime,
Type: transactionType,
CategoryId: t.CategoryId,
Time: utils.GetUnixTimeFromTransactionTime(t.TransactionTime),
UtcOffset: t.TimezoneUtcOffset,
SourceAccountId: sourceAccountId,
DestinationAccountId: destinationAccountId,
SourceAmount: sourceAmount,
DestinationAmount: destinationAmount,
HideAmount: t.HideAmount,
TagIds: utils.Int64ArrayToStringArray(tagIds),
Comment: t.Comment,
GeoLocation: geoLocation,
Editable: editable,
}
}
// GetTransactionAmountsRequestItems returns request items by query parameters
func (t *TransactionAmountsRequest) GetTransactionAmountsRequestItems() ([]*TransactionAmountsRequestItem, error) {
items := strings.Split(t.Query, "|")
requestItems := make([]*TransactionAmountsRequestItem, 0, len(items))
for i := 0; i < len(items); i++ {
itemValues := strings.Split(items[i], "_")
if len(itemValues) != 3 {
return nil, errs.ErrQueryItemsInvalid
}
startTime, err := utils.StringToInt64(itemValues[1])
if err != nil {
return nil, err
}
endTime, err := utils.StringToInt64(itemValues[2])
if err != nil {
return nil, err
}
requestItem := &TransactionAmountsRequestItem{
Name: itemValues[0],
StartTime: startTime,
EndTime: endTime,
}
requestItems = append(requestItems, requestItem)
}
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
// Len returns the count of items
func (s TransactionInfoResponseSlice) Len() int {
return len(s)
}
// Swap swaps two items
func (s TransactionInfoResponseSlice) 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 TransactionInfoResponseSlice) Less(i, j int) bool {
if s[i].Time != s[j].Time {
return s[i].Time > s[j].Time
}
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
}
// TransactionAmountsResponseItemAmountInfoSlice represents the slice data structure of TransactionAmountsResponseItemAmountInfo
type TransactionAmountsResponseItemAmountInfoSlice []*TransactionAmountsResponseItemAmountInfo
// Len returns the count of items
func (s TransactionAmountsResponseItemAmountInfoSlice) Len() int {
return len(s)
}
// Swap swaps two items
func (s TransactionAmountsResponseItemAmountInfoSlice) 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 TransactionAmountsResponseItemAmountInfoSlice) Less(i, j int) bool {
return strings.Compare(s[i].Currency, s[j].Currency) < 0
}