From a4bad0c0884f43641a68ace6cc24ba59161021fc Mon Sep 17 00:00:00 2001 From: "aditya.siregar" Date: Wed, 31 Jul 2024 23:54:17 +0700 Subject: [PATCH] Update Tranaction --- infra/furtuna.development.yaml | 2 +- internal/common/errors/errors.go | 29 ++++--- internal/entity/transaction.go | 5 ++ .../handlers/http/transaction/transaction.go | 31 +++++++ internal/handlers/request/transaction.go | 33 +++++++ internal/repository/repository.go | 2 + .../repository/transaction/transaction.go | 16 +++- internal/services/service.go | 3 +- internal/services/transaction/transaction.go | 86 ++++++++++++++++++- 9 files changed, 186 insertions(+), 21 deletions(-) diff --git a/infra/furtuna.development.yaml b/infra/furtuna.development.yaml index 464c7cd..9a78cb4 100644 --- a/infra/furtuna.development.yaml +++ b/infra/furtuna.development.yaml @@ -25,7 +25,7 @@ postgresql: max-idle-connections-in-second: 600 max-open-connections-in-second: 600 connection-max-life-time-in-second: 600 - debug: false + debug: true oss: access_key_id: e50b31e5eddf63c0ZKB2 diff --git a/internal/common/errors/errors.go b/internal/common/errors/errors.go index 336aaae..726d848 100644 --- a/internal/common/errors/errors.go +++ b/internal/common/errors/errors.go @@ -17,21 +17,23 @@ const ( errUserIsNotFound ErrType = "User is not found" errInvalidLogin ErrType = "User email or password is invalid" errUnauthorized ErrType = "Unauthorized" + errInsufficientBalance ErrType = "Insufficient Balance" ) var ( - ErrorBadRequest = NewServiceException(errBadRequest) - ErrorInvalidRequest = NewServiceException(errInvalidRequest) - ErrorUnauthorized = NewServiceException(errUnauthorized) - ErrorOrderNotFound = NewServiceException(errOrderNotFound) - ErrorClientIDNotDefined = NewServiceException(errCheckoutIDNotDefined) - ErrorRequestTimeout = NewServiceException(errRequestTimeOut) - ErrorExternalCall = NewServiceException(errExternalCall) - ErrorFailedExternalCall = NewServiceException(errFailedExternalCall) - ErrorConnectionTimeOut = NewServiceException(errConnectTimeOut) - ErrorInternalServer = NewServiceException(errInternalServer) - ErrorUserIsNotFound = NewServiceException(errUserIsNotFound) - ErrorUserInvalidLogin = NewServiceException(errInvalidLogin) + ErrorBadRequest = NewServiceException(errBadRequest) + ErrorInvalidRequest = NewServiceException(errInvalidRequest) + ErrorUnauthorized = NewServiceException(errUnauthorized) + ErrorOrderNotFound = NewServiceException(errOrderNotFound) + ErrorClientIDNotDefined = NewServiceException(errCheckoutIDNotDefined) + ErrorRequestTimeout = NewServiceException(errRequestTimeOut) + ErrorExternalCall = NewServiceException(errExternalCall) + ErrorFailedExternalCall = NewServiceException(errFailedExternalCall) + ErrorConnectionTimeOut = NewServiceException(errConnectTimeOut) + ErrorInternalServer = NewServiceException(errInternalServer) + ErrorUserIsNotFound = NewServiceException(errUserIsNotFound) + ErrorUserInvalidLogin = NewServiceException(errInvalidLogin) + ErrorInsufficientBalance = NewServiceException(errInsufficientBalance) ) type Error interface { @@ -105,6 +107,9 @@ func (s *ServiceException) MapErrorsToCode() Code { case errInvalidLogin: return BadRequest + case errInsufficientBalance: + return BadRequest + default: return BadRequest } diff --git a/internal/entity/transaction.go b/internal/entity/transaction.go index cdf21ed..094406b 100644 --- a/internal/entity/transaction.go +++ b/internal/entity/transaction.go @@ -50,3 +50,8 @@ type TransactionList struct { PartnerName string Amount int64 } + +type TransactionApproval struct { + TransactionID string + Status string +} diff --git a/internal/handlers/http/transaction/transaction.go b/internal/handlers/http/transaction/transaction.go index 055a1ff..e41e3d6 100644 --- a/internal/handlers/http/transaction/transaction.go +++ b/internal/handlers/http/transaction/transaction.go @@ -18,6 +18,7 @@ func (h *TransactionHandler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) route := group.Group("/transaction") route.GET("/search", jwt, h.Search) + route.POST("/approval", jwt, h.Approval) } func New(service services.Transaction) *TransactionHandler { @@ -63,6 +64,36 @@ func (h *TransactionHandler) Search(c *gin.Context) { }) } +func (h *TransactionHandler) Approval(c *gin.Context) { + var req request.ApprovalRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.ErrorWrapper(c, errors.ErrorBadRequest) + return + } + ctx := request.GetMyContext(c) + + if !ctx.IsAdmin() { + response.ErrorWrapper(c, errors.ErrorBadRequest) + return + } + + if err := req.Validate(); err != nil { + response.ErrorWrapper(c, errors.NewError(errors.ErrorBadRequest.ErrorType(), err.Error())) + return + } + + err := h.service.Approval(ctx, req.ToEntity()) + if err != nil { + response.ErrorWrapper(c, err) + return + } + + c.JSON(http.StatusOK, response.BaseResponse{ + Success: true, + Status: http.StatusOK, + }) +} + func (h *TransactionHandler) ToTransactionListResponse(transactions []*entity.TransactionList, totalCount, limit, offset int) response.TransactionListResponse { responseItems := make([]response.TransactionListItem, len(transactions)) for i, transaction := range transactions { diff --git a/internal/handlers/request/transaction.go b/internal/handlers/request/transaction.go index 7a1c656..59769ae 100644 --- a/internal/handlers/request/transaction.go +++ b/internal/handlers/request/transaction.go @@ -4,6 +4,7 @@ import ( "furtuna-be/internal/common/mycontext" "furtuna-be/internal/constants/transaction" "furtuna-be/internal/entity" + "github.com/go-playground/validator/v10" ) type Transaction struct { @@ -19,9 +20,41 @@ type TransactionSearch struct { Type string `form:"type,default="` } +type ApprovalRequest struct { + Status string `json:"status" validate:"required,approvalStatus"` + TransactionID string `json:"transaction_id" validate:"required"` +} + +func (a *ApprovalRequest) ToEntity() *entity.TransactionApproval { + return &entity.TransactionApproval{ + Status: a.Status, + TransactionID: a.TransactionID, + } +} + +func approvalStatus(fl validator.FieldLevel) bool { + status := fl.Field().String() + return status == "APPROVE" || status == "REJECT" +} + +func (a *ApprovalRequest) Validate() error { + validate := validator.New() + err := validate.RegisterValidation("approvalStatus", approvalStatus) + if err != nil { + return err + } + + if err := validate.Struct(a); err != nil { + return err + } + + return nil +} + func (t *TransactionSearch) ToEntity(ctx mycontext.Context) entity.TransactionSearch { return entity.TransactionSearch{ PartnerID: ctx.GetPartnerID(), + SiteID: ctx.GetSiteID(), Type: t.Type, Status: t.Status, Limit: t.Limit, diff --git a/internal/repository/repository.go b/internal/repository/repository.go index e608524..c10fa60 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -208,6 +208,8 @@ type License interface { } type TransactionRepository interface { + FindByID(ctx context.Context, id string) (*entity.Transaction, error) Create(ctx context.Context, trx *gorm.DB, transaction *entity.Transaction) (*entity.Transaction, error) GetTransactionList(ctx mycontext.Context, req entity.TransactionSearch) ([]*entity.TransactionList, int, error) + Update(ctx context.Context, trx *gorm.DB, transaction *entity.Transaction) (*entity.Transaction, error) } diff --git a/internal/repository/transaction/transaction.go b/internal/repository/transaction/transaction.go index 5615b4c..3c70f7d 100644 --- a/internal/repository/transaction/transaction.go +++ b/internal/repository/transaction/transaction.go @@ -37,8 +37,8 @@ func (r *TransactionRepository) Create(ctx context.Context, trx *gorm.DB, transa } // Update updates an existing transaction in the database. -func (r *TransactionRepository) Update(ctx context.Context, transaction *entity.Transaction) (*entity.Transaction, error) { - if err := r.db.WithContext(ctx).Save(transaction).Error; err != nil { +func (r *TransactionRepository) Update(ctx context.Context, trx *gorm.DB, transaction *entity.Transaction) (*entity.Transaction, error) { + if err := trx.WithContext(ctx).Save(transaction).Error; err != nil { zap.L().Error("error when updating transaction", zap.Error(err)) return nil, err } @@ -137,22 +137,30 @@ func (r *TransactionRepository) GetTransactionList(ctx mycontext.Context, req en query := r.db.Table("transactions t"). Select("t.id, t.transaction_type, t.status, t.created_at, s.name as site_name, p.name as partner_name, t.amount"). Joins("left join sites s on t.site_id = s.id"). - Joins("left join partners p on t.partner_id = p.id"). - Where("t.partner_id = ?", req.PartnerID) + Joins("left join partners p on t.partner_id = p.id") if req.SiteID != nil { query = query.Where("t.site_id = ?", req.SiteID) } + if req.Type != "" { query = query.Where("t.transaction_type = ?", req.Type) } + if req.Status != "" { query = query.Where("t.status = ?", req.Status) } + if req.Date != "" { query = query.Where("DATE(t.created_at) = ?", req.Date) } + if req.PartnerID != nil { + query = query.Where("t.partner_id = ?", req.PartnerID) + } + + query = query.Order("t.created_at DESC") + query = query.Count(&total) if req.Offset > 0 { diff --git a/internal/services/service.go b/internal/services/service.go index 6b8a002..3b62b4d 100644 --- a/internal/services/service.go +++ b/internal/services/service.go @@ -54,7 +54,7 @@ func NewServiceManagerImpl(cfg *config.Config, repo *repository.RepoManagerImpl) repo.Partner, users.NewUserService(repo.User, repo.Branch), repo.Trx, repo.Wallet), SiteSvc: site.NewSiteService(repo.Site), LicenseSvc: service.NewLicenseService(repo.License), - Transaction: transaction.New(repo.Transaction), + Transaction: transaction.New(repo.Transaction, repo.Wallet, repo.Trx), Balance: balance.NewBalanceService(repo.Wallet, repo.Trx, repo.Crypto, &cfg.Withdraw, repo.Transaction), } } @@ -146,6 +146,7 @@ type License interface { type Transaction interface { GetTransactionList(ctx mycontext.Context, req entity.TransactionSearch) ([]*entity.TransactionList, int, error) + Approval(ctx mycontext.Context, req *entity.TransactionApproval) error } type Balance interface { diff --git a/internal/services/transaction/transaction.go b/internal/services/transaction/transaction.go index 237f430..a8d70db 100644 --- a/internal/services/transaction/transaction.go +++ b/internal/services/transaction/transaction.go @@ -1,6 +1,7 @@ package transaction import ( + errors2 "furtuna-be/internal/common/errors" "furtuna-be/internal/common/logger" "furtuna-be/internal/common/mycontext" "furtuna-be/internal/entity" @@ -10,12 +11,19 @@ import ( ) type TransactionService struct { - repo repository.TransactionRepository + repo repository.TransactionRepository + wallet repository.WalletRepository + trx repository.TransactionManager } -func New(repo repository.TransactionRepository) *TransactionService { +func New(repo repository.TransactionRepository, + wallet repository.WalletRepository, + trx repository.TransactionManager, +) *TransactionService { return &TransactionService{ - repo: repo, + repo: repo, + wallet: wallet, + trx: trx, } } @@ -29,3 +37,75 @@ func (s *TransactionService) GetTransactionList(ctx mycontext.Context, return transactions, total, nil } + +func (s *TransactionService) Approval(ctx mycontext.Context, req *entity.TransactionApproval) error { + // Start a transaction + trx, _ := s.trx.Begin(ctx) + + // Retrieve the transaction by ID + transaction, err := s.repo.FindByID(ctx, req.TransactionID) + if err != nil { + logger.ContextLogger(ctx).Error("error when retrieving transaction by ID", zap.Error(err)) + trx.Rollback() + return errors2.ErrorInternalServer + } + + if transaction.Status != "WAITING_APPROVAL" { + return errors2.NewError(errors2.ErrorBadRequest.ErrorType(), + "invalid state, transaction already approved or rejected") + } + + // Retrieve the wallet associated with the transaction's partner ID + wallet, err := s.wallet.GetForUpdate(ctx, trx, transaction.PartnerID) + if err != nil { + logger.ContextLogger(ctx).Error("error retrieving wallet by partner ID", zap.Error(err)) + trx.Rollback() + return errors2.ErrorInternalServer + } + + // Approve or Reject the transaction + switch req.Status { + case "APPROVE": + if wallet.AuthBalance < transaction.Amount { + trx.Rollback() + return errors2.ErrorInsufficientBalance + } + wallet.AuthBalance -= transaction.Amount + + case "REJECT": + if wallet.AuthBalance < transaction.Amount { + trx.Rollback() + return errors2.ErrorInsufficientBalance + } + wallet.AuthBalance -= transaction.Amount + wallet.Balance += transaction.Amount + + default: + trx.Rollback() + return errors2.ErrorBadRequest + } + + // Update the wallet with the new balances + if _, err := s.wallet.Update(ctx, trx, wallet); err != nil { + logger.ContextLogger(ctx).Error("error updating wallet balance", zap.Error(err)) + trx.Rollback() + return errors2.ErrorInternalServer + } + + // Update the transaction status and persist changes + transaction.Status = req.Status + transaction.UpdatedBy = ctx.RequestedBy() + + if _, err := s.repo.Update(ctx, trx, transaction); err != nil { + logger.ContextLogger(ctx).Error("error updating transaction status", zap.Error(err)) + trx.Rollback() + return errors2.ErrorInternalServer + } + + if err := trx.Commit().Error; err != nil { + logger.ContextLogger(ctx).Error("error committing transaction", zap.Error(err)) + return errors2.ErrorInternalServer + } + + return nil +}