From 128d064df4333600d080d06798771347f0dfb32b Mon Sep 17 00:00:00 2001 From: MaysWind Date: Mon, 7 Dec 2020 01:06:57 +0800 Subject: [PATCH] add transaction basic api --- cmd/database.go | 8 + cmd/webserver.go | 10 +- pkg/api/transactions.go | 235 +++++++++++++ pkg/errs/account.go | 2 + pkg/errs/global.go | 2 + pkg/errs/system.go | 1 + pkg/errs/transaction.go | 15 + pkg/errs/transaction_category.go | 1 + pkg/models/transaction.go | 118 +++++++ pkg/services/transactions.go | 580 +++++++++++++++++++++++++++++++ pkg/utils/datetimes.go | 8 +- src/locales/en.js | 24 ++ src/locales/zh_Hans.js | 24 ++ 13 files changed, 1026 insertions(+), 2 deletions(-) create mode 100644 pkg/api/transactions.go create mode 100644 pkg/errs/transaction.go create mode 100644 pkg/models/transaction.go create mode 100644 pkg/services/transactions.go diff --git a/cmd/database.go b/cmd/database.go index 4b314d1d..505d27cc 100644 --- a/cmd/database.go +++ b/cmd/database.go @@ -83,6 +83,14 @@ func updateAllDatabaseTablesStructure() error { log.BootInfof("[database.updateAllDatabaseTablesStructure] account table maintained successfully") } + err = datastore.Container.UserDataStore.SyncStructs(new(models.Transaction)) + + if err != nil { + return err + } else { + log.BootInfof("[database.updateAllDatabaseTablesStructure] transaction table maintained successfully") + } + err = datastore.Container.UserDataStore.SyncStructs(new(models.TransactionCategory)) if err != nil { diff --git a/cmd/webserver.go b/cmd/webserver.go index 93aa8473..cb01d348 100644 --- a/cmd/webserver.go +++ b/cmd/webserver.go @@ -185,6 +185,14 @@ func startWebServer(c *cli.Context) error { apiV1Route.POST("/accounts/move.json", bindApi(api.Accounts.AccountMoveHandler)) apiV1Route.POST("/accounts/delete.json", bindApi(api.Accounts.AccountDeleteHandler)) + // Transactions + apiV1Route.GET("/transactions/list.json", bindApi(api.Transactions.TransactionListHandler)) + apiV1Route.GET("/transactions/list/by_month.json", bindApi(api.Transactions.TransactionListHandler)) + 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)) + apiV1Route.POST("/transactions/delete.json", bindApi(api.Transactions.TransactionDeleteHandler)) + // Transaction Categories apiV1Route.GET("/transaction/categories/list.json", bindApi(api.TransactionCategories.CategoryListHandler)) apiV1Route.GET("/transaction/categories/get.json", bindApi(api.TransactionCategories.CategoryGetHandler)) @@ -220,7 +228,7 @@ func startWebServer(c *cli.Context) error { } else if config.Protocol == settings.SCHEME_HTTPS { log.BootInfof("[server.startWebServer] will run at https://%s", listenAddr) err = router.RunTLS(listenAddr, config.CertFile, config.CertKeyFile) - } else { + } else { err = errs.ErrInvalidProtocol } diff --git a/pkg/api/transactions.go b/pkg/api/transactions.go new file mode 100644 index 00000000..732eaba5 --- /dev/null +++ b/pkg/api/transactions.go @@ -0,0 +1,235 @@ +package api + +import ( + "sort" + + "github.com/mayswind/lab/pkg/core" + "github.com/mayswind/lab/pkg/errs" + "github.com/mayswind/lab/pkg/log" + "github.com/mayswind/lab/pkg/models" + "github.com/mayswind/lab/pkg/services" +) + +type TransactionsApi struct { + transactions *services.TransactionService +} + +var ( + Transactions = &TransactionsApi{ + transactions: services.Transactions, + } +) + +func (a *TransactionsApi) TransactionListHandler(c *core.Context) (interface{}, *errs.Error) { + var transactionListReq models.TransactionListByMaxTimeRequest + err := c.ShouldBindQuery(&transactionListReq) + + if err != nil { + log.WarnfWithRequestId(c, "[transactions.TransactionListHandler] parse request failed, because %s", err.Error()) + return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) + } + + uid := c.GetCurrentUid() + transactions, err := a.transactions.GetTransactionsByMaxTime(uid, transactionListReq.MaxTime, transactionListReq.Count + 1) + + if err != nil { + log.ErrorfWithRequestId(c, "[transactions.TransactionListHandler] failed to get transactions earlier than \"%d\" for user \"uid:%d\", because %s", transactionListReq.MaxTime, uid, err.Error()) + return nil, errs.ErrOperationFailed + } + + finalCount := transactionListReq.Count + + if len(transactions) < finalCount { + finalCount = len(transactions) + } + + transactionResps := &models.TransactionInfoPageWrapperResponse{} + transactionResps.Items = make(models.TransactionInfoResponseSlice, finalCount) + + for i := 0; i < finalCount; i++ { + transactionResps.Items[i] = transactions[i].ToTransactionInfoResponse(nil) + } + + sort.Sort(transactionResps.Items) + + if finalCount < len(transactions) { + transactionResps.NextTime = &transactions[finalCount].TransactionTime + } + + return transactionResps, nil +} + +func (a *TransactionsApi) TransactionMonthListHandler(c *core.Context) (interface{}, *errs.Error) { + var transactionListReq models.TransactionListInMonthByPageRequest + err := c.ShouldBindQuery(&transactionListReq) + + if err != nil { + log.WarnfWithRequestId(c, "[transactions.TransactionMonthListHandler] parse request failed, because %s", err.Error()) + return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) + } + + uid := c.GetCurrentUid() + transactions, err := a.transactions.GetTransactionsInMonthByPage(uid, transactionListReq.Year, transactionListReq.Month, transactionListReq.Page, transactionListReq.Count) + + if err != nil { + log.ErrorfWithRequestId(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.ErrOperationFailed + } + + transactionResps := make([]*models.TransactionInfoResponse, len(transactions)) + + for i := 0; i < len(transactions); i++ { + transactionResps[i] = transactions[i].ToTransactionInfoResponse(nil) + } + + return transactionResps, nil +} + +func (a *TransactionsApi) TransactionGetHandler(c *core.Context) (interface{}, *errs.Error) { + var transactionGetReq models.TransactionGetRequest + err := c.ShouldBindQuery(&transactionGetReq) + + if err != nil { + log.WarnfWithRequestId(c, "[transactions.TransactionGetHandler] parse request failed, because %s", err.Error()) + return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) + } + + uid := c.GetCurrentUid() + transaction, err := a.transactions.GetTransactionByTransactionId(uid, transactionGetReq.Id) + + if err != nil { + log.ErrorfWithRequestId(c, "[transactions.TransactionGetHandler] failed to get transaction \"id:%d\" for user \"uid:%d\", because %s", transactionGetReq.Id, uid, err.Error()) + return nil, errs.ErrOperationFailed + } + + transactionResp := transaction.ToTransactionInfoResponse(nil) + + return transactionResp, nil +} + +func (a *TransactionsApi) TransactionCreateHandler(c *core.Context) (interface{}, *errs.Error) { + var transactionCreateReq models.TransactionCreateRequest + err := c.ShouldBindJSON(&transactionCreateReq) + + if err != nil { + log.WarnfWithRequestId(c, "[transactions.TransactionCreateHandler] parse request failed, because %s", err.Error()) + return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) + } + + if transactionCreateReq.Type < models.TRANSACTION_TYPE_MODIFY_BALANCE || transactionCreateReq.Type > models.TRANSACTION_TYPE_TRANSFER { + log.WarnfWithRequestId(c, "[transactions.TransactionCreateHandler] transaction type is invalid") + return nil, errs.ErrTransactionTypeInvalid + } + + if transactionCreateReq.Type == models.TRANSACTION_TYPE_MODIFY_BALANCE && transactionCreateReq.CategoryId > 0 { + return nil, errs.ErrBalanceModificationTransactionCannotSetCategory + } + + if transactionCreateReq.Type != models.TRANSACTION_TYPE_TRANSFER && transactionCreateReq.SourceAccountId != transactionCreateReq.DestinationAccountId { + return nil, errs.ErrTransactionSourceAndDestinationIdNotEqual + } else if transactionCreateReq.Type != models.TRANSACTION_TYPE_TRANSFER && transactionCreateReq.SourceAmount != transactionCreateReq.DestinationAmount { + return nil, errs.ErrTransactionSourceAndDestinationAmountNotEqual + } + + uid := c.GetCurrentUid() + transaction := a.createNewTransactionModel(uid, &transactionCreateReq) + + err = a.transactions.CreateTransaction(transaction) + + if err != nil { + log.ErrorfWithRequestId(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.InfofWithRequestId(c, "[transactions.TransactionCreateHandler] user \"uid:%d\" has created a new transaction \"id:%d\" successfully", uid, transaction.TransactionId) + + transactionResp := transaction.ToTransactionInfoResponse(nil) + + return transactionResp, nil +} + +func (a *TransactionsApi) TransactionModifyHandler(c *core.Context) (interface{}, *errs.Error) { + var transactionModifyReq models.TransactionModifyRequest + err := c.ShouldBindJSON(&transactionModifyReq) + + if err != nil { + log.WarnfWithRequestId(c, "[transactions.TransactionModifyHandler] parse request failed, because %s", err.Error()) + return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) + } + + uid := c.GetCurrentUid() + transaction, err := a.transactions.GetTransactionByTransactionId(uid, transactionModifyReq.Id) + + if err != nil { + log.ErrorfWithRequestId(c, "[transactions.TransactionModifyHandler] failed to get transaction \"id:%d\" for user \"uid:%d\", because %s", transactionModifyReq.Id, uid, err.Error()) + return nil, errs.ErrOperationFailed + } + + newTransaction := &models.Transaction{ + TransactionId: transaction.TransactionId, + Uid: uid, + CategoryId: transactionModifyReq.CategoryId, + TransactionTime: transactionModifyReq.Time, + SourceAccountId: transactionModifyReq.SourceAccountId, + DestinationAccountId: transactionModifyReq.DestinationAccountId, + SourceAmount: transactionModifyReq.SourceAmount, + DestinationAmount: transactionModifyReq.DestinationAmount, + Comment: transactionModifyReq.Comment, + } + + if newTransaction.CategoryId == transaction.CategoryId && + newTransaction.TransactionTime / 1000 == transaction.TransactionTime / 1000 && + newTransaction.SourceAccountId == transaction.SourceAccountId && + newTransaction.DestinationAccountId == transaction.DestinationAccountId && + newTransaction.SourceAmount == transaction.SourceAmount && + newTransaction.DestinationAmount == transaction.DestinationAmount && + newTransaction.Comment == transaction.Comment { + return nil, errs.ErrNothingWillBeUpdated + } + + err = a.transactions.ModifyTransaction(newTransaction) + + if err != nil { + log.ErrorfWithRequestId(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.InfofWithRequestId(c, "[transactions.TransactionModifyHandler] user \"uid:%d\" has updated transaction \"id:%d\" successfully", uid, transactionModifyReq.Id) + + return true, nil +} + +func (a *TransactionsApi) TransactionDeleteHandler(c *core.Context) (interface{}, *errs.Error) { + var transactionDeleteReq models.TransactionDeleteRequest + err := c.ShouldBindJSON(&transactionDeleteReq) + + if err != nil { + log.WarnfWithRequestId(c, "[transactions.TransactionDeleteHandler] parse request failed, because %s", err.Error()) + return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) + } + + uid := c.GetCurrentUid() + err = a.transactions.DeleteTransaction(uid, transactionDeleteReq.Id) + + if err != nil { + log.ErrorfWithRequestId(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.InfofWithRequestId(c, "[transactions.TransactionDeleteHandler] user \"uid:%d\" has deleted transaction \"id:%d\"", uid, transactionDeleteReq.Id) + return true, nil +} + +func (a *TransactionsApi) createNewTransactionModel(uid int64, transactionCreateReq *models.TransactionCreateRequest) *models.Transaction { + return &models.Transaction{ + Uid: uid, + Type: transactionCreateReq.Type, + CategoryId: transactionCreateReq.CategoryId, + TransactionTime: transactionCreateReq.Time, + SourceAccountId: transactionCreateReq.SourceAccountId, + DestinationAccountId: transactionCreateReq.DestinationAccountId, + SourceAmount: transactionCreateReq.SourceAmount, + DestinationAmount: transactionCreateReq.DestinationAmount, + Comment: transactionCreateReq.Comment, + } +} diff --git a/pkg/errs/account.go b/pkg/errs/account.go index 8fa8a4fd..006bc561 100644 --- a/pkg/errs/account.go +++ b/pkg/errs/account.go @@ -12,4 +12,6 @@ var ( ErrSubAccountCategoryNotEqualsToParent = NewNormalError(NORMAL_SUBCATEGORY_ACCOUNT, 6, http.StatusBadRequest, "sub account category not equals to parent") ErrSubAccountTypeInvalid = NewNormalError(NORMAL_SUBCATEGORY_ACCOUNT, 7, http.StatusBadRequest, "sub account type invalid") ErrCannotAddOrDeleteSubAccountsWhenModify = NewNormalError(NORMAL_SUBCATEGORY_ACCOUNT, 8, http.StatusBadRequest, "cannot add or delete sub accounts when modify account") + ErrSourceAccountNotFound = NewNormalError(NORMAL_SUBCATEGORY_ACCOUNT, 9, http.StatusBadRequest, "source account not found") + ErrDestinationAccountNotFound = NewNormalError(NORMAL_SUBCATEGORY_ACCOUNT, 10, http.StatusBadRequest, "destination account not found") ) diff --git a/pkg/errs/global.go b/pkg/errs/global.go index 45af222a..f9350aad 100644 --- a/pkg/errs/global.go +++ b/pkg/errs/global.go @@ -12,6 +12,8 @@ var ( ErrCiphertextInvalid = NewNormalError(NORMAL_SUBCATEGORY_GLOBAL, 3, http.StatusInternalServerError, "ciphertext is invalid") ErrNothingWillBeUpdated = NewNormalError(NORMAL_SUBCATEGORY_GLOBAL, 4, http.StatusBadRequest, "nothing will be updated") ErrFailedToRequestRemoteApi = NewNormalError(NORMAL_SUBCATEGORY_GLOBAL, 5, http.StatusBadRequest, "failed to request third party api") + ErrPageIndexInvalid = NewNormalError(NORMAL_SUBCATEGORY_GLOBAL, 6, http.StatusBadRequest, "page index is invalid") + ErrPageCountInvalid = NewNormalError(NORMAL_SUBCATEGORY_GLOBAL, 7, http.StatusBadRequest, "page count is invalid") ) func GetParameterInvalidMessage(field string) string { diff --git a/pkg/errs/system.go b/pkg/errs/system.go index 6dc33be4..2b4fdccd 100644 --- a/pkg/errs/system.go +++ b/pkg/errs/system.go @@ -6,4 +6,5 @@ var ( ErrSystemError = NewSystemError(SYSTEM_SUBCATEGORY_DEFAULT, 0, http.StatusInternalServerError, "system error") ErrApiNotFound = NewSystemError(SYSTEM_SUBCATEGORY_DEFAULT, 1, http.StatusNotFound, "api not found") ErrMethodNotAllowed = NewSystemError(SYSTEM_SUBCATEGORY_DEFAULT, 2, http.StatusMethodNotAllowed, "method not allowed") + ErrNotImplemented = NewSystemError(SYSTEM_SUBCATEGORY_DEFAULT, 3, http.StatusNotImplemented, "not implemented") ) diff --git a/pkg/errs/transaction.go b/pkg/errs/transaction.go new file mode 100644 index 00000000..b3ff4a62 --- /dev/null +++ b/pkg/errs/transaction.go @@ -0,0 +1,15 @@ +package errs + +import "net/http" + +var ( + ErrTransactionIdInvalid = NewNormalError(NORMAL_SUBCATEGORY_TRANSACTION, 0, http.StatusBadRequest, "transaction id is invalid") + ErrTransactionNotFound = NewNormalError(NORMAL_SUBCATEGORY_TRANSACTION, 1, http.StatusBadRequest, "transaction not found") + ErrTransactionTypeInvalid = NewNormalError(NORMAL_SUBCATEGORY_TRANSACTION, 2, http.StatusBadRequest, "transaction type is invalid") + ErrTransactionSourceAndDestinationIdNotEqual = NewNormalError(NORMAL_SUBCATEGORY_TRANSACTION, 3, http.StatusBadRequest, "transaction source and destination account id not equal") + ErrTransactionSourceAndDestinationIdCannotBeEqual = NewNormalError(NORMAL_SUBCATEGORY_TRANSACTION, 4, http.StatusBadRequest, "transaction source and destination account id cannot be equal") + ErrTransactionSourceAndDestinationAmountNotEqual = NewNormalError(NORMAL_SUBCATEGORY_TRANSACTION, 5, http.StatusBadRequest, "transaction source and destination amount not equal") + ErrTooMuchTransactionInOneSecond = NewNormalError(NORMAL_SUBCATEGORY_TRANSACTION, 6, http.StatusBadRequest, "too much transaction in one second") + ErrBalanceModificationTransactionCannotSetCategory = NewNormalError(NORMAL_SUBCATEGORY_TRANSACTION, 7, http.StatusBadRequest, "balance modification transaction cannot set category") + ErrBalanceModificationTransactionCannotChangeAccountId = NewNormalError(NORMAL_SUBCATEGORY_TRANSACTION, 8, http.StatusBadRequest, "balance modification transaction cannot change account id") +) diff --git a/pkg/errs/transaction_category.go b/pkg/errs/transaction_category.go index aa1b7c1d..90012b66 100644 --- a/pkg/errs/transaction_category.go +++ b/pkg/errs/transaction_category.go @@ -8,4 +8,5 @@ var ( ErrTransactionCategoryTypeInvalid = NewNormalError(NORMAL_SUBCATEGORY_CATEGORY, 2, http.StatusBadRequest, "transaction category type is invalid") ErrParentTransactionCategoryNotFound = NewNormalError(NORMAL_SUBCATEGORY_CATEGORY, 3, http.StatusBadRequest, "parent transaction category not found") ErrCannotAddToSecondaryTransactionCategory = NewNormalError(NORMAL_SUBCATEGORY_CATEGORY, 4, http.StatusBadRequest, "cannot add to secondary transaction category") + ErrCannotUsePrimaryCategoryForTransaction = NewNormalError(NORMAL_SUBCATEGORY_CATEGORY, 5, http.StatusBadRequest, "cannot use primary category for transaction category") ) diff --git a/pkg/models/transaction.go b/pkg/models/transaction.go new file mode 100644 index 00000000..6072d49e --- /dev/null +++ b/pkg/models/transaction.go @@ -0,0 +1,118 @@ +package models + +type TransactionType byte + +const ( + TRANSACTION_TYPE_MODIFY_BALANCE TransactionType = 1 + TRANSACTION_TYPE_INCOME TransactionType = 2 + TRANSACTION_TYPE_EXPENSE TransactionType = 3 + TRANSACTION_TYPE_TRANSFER TransactionType = 4 +) + +type Transaction struct { + TransactionId int64 `xorm:"PK"` + Uid int64 `xorm:"UNIQUE(IDX_transaction_uid_transaction_time) INDEX(IDX_transaction_uid_deleted_transaction_time) INDEX(IDX_transaction_uid_deleted_type_time) INDEX(IDX_transaction_uid_deleted_category_id_time) NOT NULL"` + Deleted bool `xorm:"INDEX(IDX_transaction_uid_deleted_transaction_time) INDEX(IDX_transaction_uid_deleted_type_time) INDEX(IDX_transaction_uid_deleted_category_id_time) NOT NULL"` + Type TransactionType `xorm:"INDEX(IDX_transaction_uid_deleted_type_time) NOT NULL"` + CategoryId int64 `xorm:"INDEX(IDX_transaction_uid_deleted_category_id_time) NOT NULL"` + TransactionTime int64 `xorm:"UNIQUE(IDX_transaction_uid_transaction_time) INDEX(IDX_transaction_uid_deleted_transaction_time) INDEX(IDX_transaction_uid_deleted_type_time) INDEX(IDX_transaction_uid_deleted_category_id_time) NOT NULL"` + SourceAccountId int64 `xorm:"NOT NULL"` + DestinationAccountId int64 `xorm:"NOT NULL"` + SourceAmount int64 `xorm:"NOT NULL"` + DestinationAmount int64 `xorm:"NOT NULL"` + Comment string `xorm:"VARCHAR(255) NOT NULL"` + CreatedUnixTime int64 + UpdatedUnixTime int64 + DeletedUnixTime int64 +} + +type TransactionCreateRequest struct { + Type TransactionType `json:"type" binding:"required"` + CategoryId int64 `json:"categoryId,string"` + Time int64 `json:"time" binding:"required,min=1"` + SourceAccountId int64 `json:"sourceAccountId,string" binding:"required,min=1"` + DestinationAccountId int64 `json:"destinationAccountId,string" binding:"required,min=1"` + SourceAmount int64 `json:"sourceAmount"` + DestinationAmount int64 `json:"destinationAmount"` + TagIds []int64 `json:"tagIds,string"` + Comment string `json:"comment" binding:"max=255"` +} + +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"` + SourceAccountId int64 `json:"sourceAccountId,string" binding:"required,min=1"` + DestinationAccountId int64 `json:"destinationAccountId,string" binding:"required,min=1"` + SourceAmount int64 `json:"sourceAmount"` + DestinationAmount int64 `json:"destinationAmount"` + TagIds []int64 `json:"tagIds,string"` + Comment string `json:"comment" binding:"max=255"` +} + +type TransactionListByMaxTimeRequest struct { + MaxTime int64 `form:"max_time" binding:"required,min=1"` + Count int `form:"count" binding:"required,min=1,max=50"` +} + +type TransactionListInMonthByPageRequest struct { + Year int `form:"year" binding:"required,min=1"` + Month int `form:"month" binding:"required,min=1"` + Page int `form:"page" binding:"required,min=1"` + Count int `form:"count" binding:"required,min=1,max=50"` +} + +type TransactionGetRequest struct { + Id int64 `form:"id,string" binding:"required,min=1"` +} + +type TransactionDeleteRequest struct { + Id int64 `json:"id,string" binding:"required,min=1"` +} + +type TransactionInfoResponse struct { + Id int64 `json:"id,string"` + Type TransactionType `json:"type"` + CategoryId int64 `json:"categoryId,string"` + Time int64 `json:"time"` + SourceAccountId int64 `json:"sourceAccountId,string"` + DestinationAccountId int64 `json:"destinationAccountId,string"` + SourceAmount int64 `json:"sourceAmount"` + DestinationAmount int64 `json:"destinationAmount"` + TagIds []int64 `json:"tagIds,string"` + Comment string `json:"comment"` +} + +type TransactionInfoPageWrapperResponse struct { + Items TransactionInfoResponseSlice `json:"items"` + NextTime *int64 `json:"nextTime,string"` +} + +func (c *Transaction) ToTransactionInfoResponse(tagIds []int64) *TransactionInfoResponse { + return &TransactionInfoResponse{ + Id: c.TransactionId, + Type: c.Type, + CategoryId: c.CategoryId, + Time: c.TransactionTime, + SourceAccountId: c.SourceAccountId, + DestinationAccountId: c.DestinationAccountId, + SourceAmount: c.SourceAmount, + DestinationAmount: c.DestinationAmount, + TagIds: tagIds, + Comment: c.Comment, + } +} + +type TransactionInfoResponseSlice []*TransactionInfoResponse + +func (c TransactionInfoResponseSlice) Len() int { + return len(c) +} + +func (c TransactionInfoResponseSlice) Swap(i, j int) { + c[i], c[j] = c[j], c[i] +} + +func (c TransactionInfoResponseSlice) Less(i, j int) bool { + return c[i].Time < c[j].Time +} diff --git a/pkg/services/transactions.go b/pkg/services/transactions.go new file mode 100644 index 00000000..580fd57b --- /dev/null +++ b/pkg/services/transactions.go @@ -0,0 +1,580 @@ +package services + +import ( + "fmt" + "time" + + "xorm.io/xorm" + + "github.com/mayswind/lab/pkg/datastore" + "github.com/mayswind/lab/pkg/errs" + "github.com/mayswind/lab/pkg/models" + "github.com/mayswind/lab/pkg/utils" + "github.com/mayswind/lab/pkg/uuid" +) + +type TransactionService struct { + ServiceUsingDB + ServiceUsingUuid +} + +var ( + Transactions = &TransactionService{ + ServiceUsingDB: ServiceUsingDB{ + container: datastore.Container, + }, + ServiceUsingUuid: ServiceUsingUuid{ + container: uuid.Container, + }, + } +) + +func (s *TransactionService) GetTransactionsByMaxTime(uid int64, maxTime int64, count int) ([]*models.Transaction, error) { + if uid <= 0 { + return nil, errs.ErrUserIdInvalid + } + + if count < 1 { + return nil, errs.ErrPageCountInvalid + } + + var transactions []*models.Transaction + err := s.UserDataDB(uid).Where("uid=? AND deleted=? AND transaction_time<=?", uid, false, maxTime).Limit(count, 0).OrderBy("transaction_time desc").Find(&transactions) + + return transactions, err +} + +func (s *TransactionService) GetTransactionsInMonthByPage(uid int64, year int, month int, page int, count int) ([]*models.Transaction, error) { + if uid <= 0 { + return nil, errs.ErrUserIdInvalid + } + + if page < 1 { + return nil, errs.ErrPageIndexInvalid + } + + if count < 1 { + return nil, errs.ErrPageCountInvalid + } + + startTime, err := utils.ParseFromLongDateTime(fmt.Sprintf("%d-%d-01 00:00:00", year, month)) + + if err != nil { + return nil, errs.ErrSystemError + } + + endTime := startTime.AddDate(0, 1, 0) + + startUnixTime := startTime.Unix() + endUnixTime := endTime.Unix() + + var transactions []*models.Transaction + err = s.UserDataDB(uid).Where("uid=? AND deleted=? AND transaction_time>=? AND transaction_time=? AND transaction_time=? AND transaction_time 0 { + return errs.ErrBalanceModificationTransactionCannotSetCategory + } + } else { + category := &models.TransactionCategory{} + has, err = sess.ID(transaction.CategoryId).Where("uid=? AND deleted=?", transaction.Uid, false).Get(category) + + if err != nil { + return err + } else if !has { + return errs.ErrTransactionCategoryNotFound + } + + if category.ParentCategoryId < 1 { + return errs.ErrCannotUsePrimaryCategoryForTransaction + } + + if (oldTransaction.Type == models.TRANSACTION_TYPE_INCOME && category.Type != models.CATEGORY_TYPE_INCOME) || + (oldTransaction.Type == models.TRANSACTION_TYPE_EXPENSE && category.Type != models.CATEGORY_TYPE_EXPENSE) || + (oldTransaction.Type == models.TRANSACTION_TYPE_TRANSFER && category.Type != models.CATEGORY_TYPE_TRANSFER) { + return errs.ErrTransactionCategoryTypeInvalid + } + } + + updateCols = append(updateCols, "category_id") + } + + if transaction.TransactionTime / 1000 != oldTransaction.TransactionTime / 1000 { + sameSecondLatestTransaction := &models.Transaction{} + currentSecondUnixtime := (transaction.TransactionTime / 1000) * 1000 + nextSecondUnixtime := currentSecondUnixtime + 1000 + + has, err = sess.Where("uid=? AND deleted=? AND transaction_time>=? AND transaction_time