Add Balance and Withdrawal
This commit is contained in:
parent
38003f84d1
commit
4c1a819365
@ -31,6 +31,7 @@ type Config struct {
|
|||||||
Midtrans Midtrans `mapstructure:"midtrans"`
|
Midtrans Midtrans `mapstructure:"midtrans"`
|
||||||
Brevo Brevo `mapstructure:"brevo"`
|
Brevo Brevo `mapstructure:"brevo"`
|
||||||
Email Email `mapstructure:"email"`
|
Email Email `mapstructure:"email"`
|
||||||
|
Withdraw Withdraw `mapstructure:"withdrawal"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -73,5 +74,9 @@ func (c *Config) Auth() *AuthConfig {
|
|||||||
secret: c.Jwt.TokenResetPassword.Secret,
|
secret: c.Jwt.TokenResetPassword.Secret,
|
||||||
expireTTL: c.Jwt.TokenResetPassword.ExpiresTTL,
|
expireTTL: c.Jwt.TokenResetPassword.ExpiresTTL,
|
||||||
},
|
},
|
||||||
|
jwtWithdraw: JWT{
|
||||||
|
secret: c.Jwt.TokenWithdraw.Secret,
|
||||||
|
expireTTL: c.Jwt.TokenWithdraw.ExpiresTTL,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ type AuthConfig struct {
|
|||||||
jwtOrderSecret string
|
jwtOrderSecret string
|
||||||
jwtOrderExpiresTTL int
|
jwtOrderExpiresTTL int
|
||||||
jwtSecretResetPassword JWT
|
jwtSecretResetPassword JWT
|
||||||
|
jwtWithdraw JWT
|
||||||
}
|
}
|
||||||
|
|
||||||
type JWT struct {
|
type JWT struct {
|
||||||
@ -23,6 +24,15 @@ func (c *AuthConfig) AccessTokenOrderSecret() string {
|
|||||||
return c.jwtOrderSecret
|
return c.jwtOrderSecret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *AuthConfig) AccessTokenWithdrawSecret() string {
|
||||||
|
return c.jwtWithdraw.secret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AuthConfig) AccessTokenWithdrawExpire() time.Time {
|
||||||
|
duration := time.Duration(c.jwtWithdraw.expireTTL)
|
||||||
|
return time.Now().UTC().Add(time.Minute * duration)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *AuthConfig) AccessTokenOrderExpiresDate() time.Time {
|
func (c *AuthConfig) AccessTokenOrderExpiresDate() time.Time {
|
||||||
duration := time.Duration(c.jwtOrderExpiresTTL)
|
duration := time.Duration(c.jwtOrderExpiresTTL)
|
||||||
return time.Now().UTC().Add(time.Minute * duration)
|
return time.Now().UTC().Add(time.Minute * duration)
|
||||||
|
|||||||
@ -4,6 +4,7 @@ type Jwt struct {
|
|||||||
Token Token `mapstructure:"token"`
|
Token Token `mapstructure:"token"`
|
||||||
TokenOrder Token `mapstructure:"token-order"`
|
TokenOrder Token `mapstructure:"token-order"`
|
||||||
TokenResetPassword Token `mapstructure:"token-reset-password"`
|
TokenResetPassword Token `mapstructure:"token-reset-password"`
|
||||||
|
TokenWithdraw Token `mapstructure:"token-withdraw"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Token struct {
|
type Token struct {
|
||||||
|
|||||||
9
config/withdraw.go
Normal file
9
config/withdraw.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
type Withdraw struct {
|
||||||
|
PlatformFee int64 `mapstructure:"platform_fee"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Withdraw) GetPlatformFee() int64 {
|
||||||
|
return w.PlatformFee
|
||||||
|
}
|
||||||
@ -10,6 +10,9 @@ jwt:
|
|||||||
token-order:
|
token-order:
|
||||||
expires-ttl: 2
|
expires-ttl: 2
|
||||||
secret: "123Lm25V3Qd7aut8dr4QUxm5PZUrSFs"
|
secret: "123Lm25V3Qd7aut8dr4QUxm5PZUrSFs"
|
||||||
|
token-withdraw:
|
||||||
|
expires-ttl: 2
|
||||||
|
secret: "909Lm25V3Qd7aut8dr4QUxm5PZUrSFs"
|
||||||
|
|
||||||
postgresql:
|
postgresql:
|
||||||
host: 103.96.146.124
|
host: 103.96.146.124
|
||||||
@ -47,4 +50,7 @@ email:
|
|||||||
template_path: "templates/reset_password.html"
|
template_path: "templates/reset_password.html"
|
||||||
subject: "Reset Password"
|
subject: "Reset Password"
|
||||||
opening_word: "Terima kasih sudah menjadi bagian dari Furtuna. Anda telah berhasil melakukan reset password, silakan masukan unik password yang dibuat oleh sistem dibawah ini:"
|
opening_word: "Terima kasih sudah menjadi bagian dari Furtuna. Anda telah berhasil melakukan reset password, silakan masukan unik password yang dibuat oleh sistem dibawah ini:"
|
||||||
closing_word: "Silakan login kembali menggunakan email dan password anda diatas, sistem akan secara otomatis meminta anda untuk membuat password baru setelah berhasil login. Mohon maaf atas kendala yang dialami."
|
closing_word: "Silakan login kembali menggunakan email dan password anda diatas, sistem akan secara otomatis meminta anda untuk membuat password baru setelah berhasil login. Mohon maaf atas kendala yang dialami."
|
||||||
|
|
||||||
|
withdrawal:
|
||||||
|
platform_fee: 5000
|
||||||
@ -5,3 +5,21 @@ type Balance struct {
|
|||||||
Balance float64
|
Balance float64
|
||||||
AuthBalance float64
|
AuthBalance float64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BalanceWithdrawInquiry struct {
|
||||||
|
PartnerID int64
|
||||||
|
Amount int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type BalanceWithdrawInquiryResponse struct {
|
||||||
|
PartnerID int64
|
||||||
|
Amount int64
|
||||||
|
Total int64
|
||||||
|
Fee int64
|
||||||
|
Token string
|
||||||
|
}
|
||||||
|
|
||||||
|
type WalletWithdrawResponse struct {
|
||||||
|
TransactionID string
|
||||||
|
Status string
|
||||||
|
}
|
||||||
|
|||||||
@ -18,3 +18,13 @@ type JWTOrderClaims struct {
|
|||||||
OrderID int64 `json:"order_id"`
|
OrderID int64 `json:"order_id"`
|
||||||
jwt.StandardClaims
|
jwt.StandardClaims
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type JWTWithdrawClaims struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
PartnerID int64 `json:"partner_id"`
|
||||||
|
OrderID int64 `json:"order_id"`
|
||||||
|
Amount int64 `json:"amount"`
|
||||||
|
Fee int64 `json:"fee"`
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
jwt.StandardClaims
|
||||||
|
}
|
||||||
|
|||||||
@ -12,6 +12,7 @@ type Transaction struct {
|
|||||||
Status string `gorm:"size:255"`
|
Status string `gorm:"size:255"`
|
||||||
CreatedBy int64 `gorm:"not null"`
|
CreatedBy int64 `gorm:"not null"`
|
||||||
UpdatedBy int64 `gorm:"not null"`
|
UpdatedBy int64 `gorm:"not null"`
|
||||||
|
Amount float64 `gorm:"not null"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,3 +16,12 @@ type Wallet struct {
|
|||||||
func (Wallet) TableName() string {
|
func (Wallet) TableName() string {
|
||||||
return "wallets"
|
return "wallets"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type WalletWithdrawRequest struct {
|
||||||
|
ID int64
|
||||||
|
Token string
|
||||||
|
PartnerID int64
|
||||||
|
Amount int64
|
||||||
|
Fee int64
|
||||||
|
Total int64
|
||||||
|
}
|
||||||
|
|||||||
129
internal/handlers/http/balance/balance.go
Normal file
129
internal/handlers/http/balance/balance.go
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
package balance
|
||||||
|
|
||||||
|
import (
|
||||||
|
"furtuna-be/internal/common/errors"
|
||||||
|
"furtuna-be/internal/entity"
|
||||||
|
"furtuna-be/internal/handlers/request"
|
||||||
|
"furtuna-be/internal/handlers/response"
|
||||||
|
"furtuna-be/internal/services"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Handler struct {
|
||||||
|
service services.Balance
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) {
|
||||||
|
route := group.Group("/balance")
|
||||||
|
|
||||||
|
route.GET("/partner", jwt, h.GetPartnerBalance)
|
||||||
|
route.POST("/withdraw/inquiry", jwt, h.WithdrawBalanceInquiry)
|
||||||
|
route.POST("/withdraw/execute", jwt, h.WithdrawBalanceExecute)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHandler(service services.Balance) *Handler {
|
||||||
|
return &Handler{
|
||||||
|
service: service,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) GetPartnerBalance(c *gin.Context) {
|
||||||
|
ctx := request.GetMyContext(c)
|
||||||
|
|
||||||
|
if !ctx.IsPartnerAdmin() {
|
||||||
|
response.ErrorWrapper(c, errors.ErrorBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedBranch, err := h.service.GetByID(ctx, *ctx.GetPartnerID())
|
||||||
|
if err != nil {
|
||||||
|
response.ErrorWrapper(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, response.BaseResponse{
|
||||||
|
Success: true,
|
||||||
|
Status: http.StatusOK,
|
||||||
|
Data: h.toBalanceResponse(updatedBranch),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) WithdrawBalanceInquiry(c *gin.Context) {
|
||||||
|
var req request.BalanceReq
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.ErrorWrapper(c, errors.ErrorBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := request.GetMyContext(c)
|
||||||
|
|
||||||
|
if !ctx.IsPartnerAdmin() {
|
||||||
|
response.ErrorWrapper(c, errors.ErrorBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
inquiryResp, err := h.service.WithdrawInquiry(ctx, req.ToEntity(*ctx.GetPartnerID()))
|
||||||
|
if err != nil {
|
||||||
|
response.ErrorWrapper(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, response.BaseResponse{
|
||||||
|
Success: true,
|
||||||
|
Status: http.StatusOK,
|
||||||
|
Data: h.toBalanceInquiryResp(inquiryResp),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) WithdrawBalanceExecute(c *gin.Context) {
|
||||||
|
var req request.BalanceReq
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.ErrorWrapper(c, errors.ErrorBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := request.GetMyContext(c)
|
||||||
|
|
||||||
|
if !ctx.IsPartnerAdmin() {
|
||||||
|
response.ErrorWrapper(c, errors.ErrorBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
inquiryResp, err := h.service.WithdrawExecute(ctx, req.ToEntityReq(*ctx.GetPartnerID()))
|
||||||
|
if err != nil {
|
||||||
|
response.ErrorWrapper(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, response.BaseResponse{
|
||||||
|
Success: true,
|
||||||
|
Status: http.StatusOK,
|
||||||
|
Data: h.toBalanceExecuteResp(inquiryResp),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) toBalanceResponse(resp *entity.Balance) response.Balance {
|
||||||
|
return response.Balance{
|
||||||
|
PartnerID: resp.PartnerID,
|
||||||
|
Balance: resp.Balance,
|
||||||
|
AuthBalance: resp.AuthBalance,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) toBalanceInquiryResp(resp *entity.BalanceWithdrawInquiryResponse) response.BalanceInquiryResponse {
|
||||||
|
return response.BalanceInquiryResponse{
|
||||||
|
PartnerID: resp.PartnerID,
|
||||||
|
Amount: resp.Amount,
|
||||||
|
Token: resp.Token,
|
||||||
|
Total: resp.Total,
|
||||||
|
Fee: resp.Fee,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) toBalanceExecuteResp(resp *entity.WalletWithdrawResponse) response.BalanceExecuteResponse {
|
||||||
|
return response.BalanceExecuteResponse{
|
||||||
|
TransactionID: resp.TransactionID,
|
||||||
|
Status: resp.Status,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,56 +0,0 @@
|
|||||||
package balance
|
|
||||||
|
|
||||||
import (
|
|
||||||
"furtuna-be/internal/common/errors"
|
|
||||||
"furtuna-be/internal/entity"
|
|
||||||
"furtuna-be/internal/handlers/request"
|
|
||||||
"furtuna-be/internal/handlers/response"
|
|
||||||
"furtuna-be/internal/services"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Handler struct {
|
|
||||||
service services.Balance
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) {
|
|
||||||
route := group.Group("/balance")
|
|
||||||
|
|
||||||
route.GET("/partner", jwt, h.GetPartnerBalance)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewHandler(service services.Balance) *Handler {
|
|
||||||
return &Handler{
|
|
||||||
service: service,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) GetPartnerBalance(c *gin.Context) {
|
|
||||||
ctx := request.GetMyContext(c)
|
|
||||||
|
|
||||||
if !ctx.IsPartnerAdmin() {
|
|
||||||
response.ErrorWrapper(c, errors.ErrorBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
updatedBranch, err := h.service.GetByID(ctx, *ctx.GetPartnerID())
|
|
||||||
if err != nil {
|
|
||||||
response.ErrorWrapper(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, response.BaseResponse{
|
|
||||||
Success: true,
|
|
||||||
Status: http.StatusOK,
|
|
||||||
Data: h.toBalanceResponse(updatedBranch),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) toBalanceResponse(resp *entity.Balance) response.Balance {
|
|
||||||
return response.Balance{
|
|
||||||
PartnerID: resp.PartnerID,
|
|
||||||
Balance: resp.Balance,
|
|
||||||
AuthBalance: resp.AuthBalance,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
23
internal/handlers/request/balance.go
Normal file
23
internal/handlers/request/balance.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package request
|
||||||
|
|
||||||
|
import "furtuna-be/internal/entity"
|
||||||
|
|
||||||
|
type BalanceReq struct {
|
||||||
|
Amount int64 `json:"amount"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BalanceReq) ToEntity(partnerID int64) *entity.BalanceWithdrawInquiry {
|
||||||
|
return &entity.BalanceWithdrawInquiry{
|
||||||
|
PartnerID: partnerID,
|
||||||
|
Amount: b.Amount,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BalanceReq) ToEntityReq(partnerID int64) *entity.WalletWithdrawRequest {
|
||||||
|
return &entity.WalletWithdrawRequest{
|
||||||
|
PartnerID: partnerID,
|
||||||
|
Amount: b.Amount,
|
||||||
|
Token: b.Token,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,3 +5,16 @@ type Balance struct {
|
|||||||
Balance float64 `json:"balance"`
|
Balance float64 `json:"balance"`
|
||||||
AuthBalance float64 `json:"auth_balance"`
|
AuthBalance float64 `json:"auth_balance"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BalanceInquiryResponse struct {
|
||||||
|
PartnerID int64 `json:"partner_id"`
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
Amount int64 `json:"amount"`
|
||||||
|
Fee int64 `json:"fee"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BalanceExecuteResponse struct {
|
||||||
|
TransactionID string `json:"transaction_id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|||||||
@ -25,6 +25,8 @@ type CryptoConfig interface {
|
|||||||
AccessTokenExpiresDate() time.Time
|
AccessTokenExpiresDate() time.Time
|
||||||
AccessTokenResetPasswordSecret() string
|
AccessTokenResetPasswordSecret() string
|
||||||
AccessTokenResetPasswordExpire() time.Time
|
AccessTokenResetPasswordExpire() time.Time
|
||||||
|
AccessTokenWithdrawSecret() string
|
||||||
|
AccessTokenWithdrawExpire() time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
type CryptoImpl struct {
|
type CryptoImpl struct {
|
||||||
@ -186,3 +188,54 @@ func (c *CryptoImpl) ValidateResetPassword(tokenString string) (int64, error) {
|
|||||||
|
|
||||||
return claims.UserID, nil
|
return claims.UserID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *CryptoImpl) GenerateJWTWithdraw(req *entity.WalletWithdrawRequest) (string, error) {
|
||||||
|
claims := &entity.JWTWithdrawClaims{
|
||||||
|
StandardClaims: jwt.StandardClaims{
|
||||||
|
Subject: strconv.FormatInt(req.ID, 10),
|
||||||
|
ExpiresAt: c.Config.AccessTokenWithdrawExpire().Unix(),
|
||||||
|
IssuedAt: time.Now().Unix(),
|
||||||
|
NotBefore: time.Now().Unix(),
|
||||||
|
},
|
||||||
|
PartnerID: req.PartnerID,
|
||||||
|
Amount: req.Amount,
|
||||||
|
Fee: req.Fee,
|
||||||
|
Total: req.Total,
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := jwt.
|
||||||
|
NewWithClaims(jwt.SigningMethodHS256, claims).
|
||||||
|
SignedString([]byte(c.Config.AccessTokenWithdrawSecret()))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CryptoImpl) ValidateJWTWithdraw(tokenString string) (*entity.WalletWithdrawRequest, error) {
|
||||||
|
token, err := jwt.ParseWithClaims(tokenString, &entity.JWTWithdrawClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||||
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
|
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||||
|
}
|
||||||
|
return []byte(c.Config.AccessTokenWithdrawSecret()), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, ok := token.Claims.(*entity.JWTWithdrawClaims)
|
||||||
|
if !ok || !token.Valid {
|
||||||
|
return nil, fmt.Errorf("invalid token %v", token.Header["alg"])
|
||||||
|
}
|
||||||
|
|
||||||
|
return &entity.WalletWithdrawRequest{
|
||||||
|
ID: claims.ID,
|
||||||
|
PartnerID: claims.PartnerID,
|
||||||
|
Total: claims.Total,
|
||||||
|
Amount: claims.Amount,
|
||||||
|
Fee: claims.Fee,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|||||||
@ -97,6 +97,8 @@ type Crypto interface {
|
|||||||
ValidateJWTOrder(tokenString string) (int64, int64, error)
|
ValidateJWTOrder(tokenString string) (int64, int64, error)
|
||||||
ValidateResetPassword(tokenString string) (int64, error)
|
ValidateResetPassword(tokenString string) (int64, error)
|
||||||
ParseAndValidateJWT(token string) (*entity.JWTAuthClaims, error)
|
ParseAndValidateJWT(token string) (*entity.JWTAuthClaims, error)
|
||||||
|
GenerateJWTWithdraw(req *entity.WalletWithdrawRequest) (string, error)
|
||||||
|
ValidateJWTWithdraw(tokenString string) (*entity.WalletWithdrawRequest, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type User interface {
|
type User interface {
|
||||||
@ -179,6 +181,7 @@ type WalletRepository interface {
|
|||||||
Create(ctx context.Context, tx *gorm.DB, wallet *entity.Wallet) (*entity.Wallet, error)
|
Create(ctx context.Context, tx *gorm.DB, wallet *entity.Wallet) (*entity.Wallet, error)
|
||||||
Update(ctx context.Context, db *gorm.DB, wallet *entity.Wallet) (*entity.Wallet, error)
|
Update(ctx context.Context, db *gorm.DB, wallet *entity.Wallet) (*entity.Wallet, error)
|
||||||
GetByPartnerID(ctx context.Context, db *gorm.DB, partnerID int64) (*entity.Wallet, error)
|
GetByPartnerID(ctx context.Context, db *gorm.DB, partnerID int64) (*entity.Wallet, error)
|
||||||
|
GetForUpdate(ctx context.Context, tx *gorm.DB, partnerID int64) (*entity.Wallet, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Midtrans interface {
|
type Midtrans interface {
|
||||||
@ -205,6 +208,6 @@ type License interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type TransactionRepository interface {
|
type TransactionRepository interface {
|
||||||
Create(ctx context.Context, transaction *entity.Transaction) (*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)
|
GetTransactionList(ctx mycontext.Context, req entity.TransactionSearch) ([]*entity.TransactionList, int, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,12 +20,20 @@ func NewTransactionRepository(db *gorm.DB) *TransactionRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create creates a new transaction in the database.
|
// Create creates a new transaction in the database.
|
||||||
func (r *TransactionRepository) Create(ctx context.Context, transaction *entity.Transaction) (*entity.Transaction, error) {
|
func (r *TransactionRepository) Create(ctx context.Context, trx *gorm.DB, transaction *entity.Transaction) (*entity.Transaction, error) {
|
||||||
if err := r.db.WithContext(ctx).Create(transaction).Error; err != nil {
|
// Create the transaction record
|
||||||
|
if err := trx.WithContext(ctx).Create(transaction).Error; err != nil {
|
||||||
zap.L().Error("error when creating transaction", zap.Error(err))
|
zap.L().Error("error when creating transaction", zap.Error(err))
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return r.FindByID(ctx, transaction.ID)
|
|
||||||
|
// Retrieve the created transaction using the same transaction context
|
||||||
|
var createdTransaction entity.Transaction
|
||||||
|
if err := trx.WithContext(ctx).First(&createdTransaction, "id = ?", transaction.ID).Error; err != nil {
|
||||||
|
zap.L().Error("error when fetching newly created transaction", zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &createdTransaction, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update updates an existing transaction in the database.
|
// Update updates an existing transaction in the database.
|
||||||
@ -126,7 +134,7 @@ func (r *TransactionRepository) GetTransactionList(ctx mycontext.Context, req en
|
|||||||
var transactions []*entity.TransactionList
|
var transactions []*entity.TransactionList
|
||||||
var total int64
|
var total int64
|
||||||
|
|
||||||
query := r.db.Table("transaction t").
|
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").
|
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 sites s on t.site_id = s.id").
|
||||||
Joins("left join partners p on t.partner_id = p.id").
|
Joins("left join partners p on t.partner_id = p.id").
|
||||||
|
|||||||
@ -4,9 +4,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"furtuna-be/internal/common/logger"
|
"furtuna-be/internal/common/logger"
|
||||||
"furtuna-be/internal/entity"
|
"furtuna-be/internal/entity"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WalletRepository struct {
|
type WalletRepository struct {
|
||||||
@ -58,6 +58,22 @@ func (r *WalletRepository) GetByID(ctx context.Context, id int64) (*entity.Walle
|
|||||||
return wallet, nil
|
return wallet, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *WalletRepository) GetForUpdate(ctx context.Context, tx *gorm.DB, partnerID int64) (*entity.Wallet, error) {
|
||||||
|
if tx == nil {
|
||||||
|
tx = r.db
|
||||||
|
}
|
||||||
|
|
||||||
|
query := tx.WithContext(ctx).Where("partner_id = ?", partnerID).
|
||||||
|
Clauses(clause.Locking{Strength: "UPDATE"})
|
||||||
|
|
||||||
|
wallet := new(entity.Wallet)
|
||||||
|
if err := query.First(wallet).Error; err != nil {
|
||||||
|
logger.ContextLogger(ctx).Error("error when finding balance by partner ID", zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return wallet, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *WalletRepository) GetAll(ctx context.Context) ([]*entity.Wallet, error) {
|
func (r *WalletRepository) GetAll(ctx context.Context) ([]*entity.Wallet, error) {
|
||||||
var wallets []*entity.Wallet
|
var wallets []*entity.Wallet
|
||||||
if err := r.db.Find(&wallets).Error; err != nil {
|
if err := r.db.Find(&wallets).Error; err != nil {
|
||||||
|
|||||||
@ -2,20 +2,36 @@ package balance
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"furtuna-be/internal/common/logger"
|
"furtuna-be/internal/common/logger"
|
||||||
|
"furtuna-be/internal/common/mycontext"
|
||||||
"furtuna-be/internal/entity"
|
"furtuna-be/internal/entity"
|
||||||
"furtuna-be/internal/repository"
|
"furtuna-be/internal/repository"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
type BalanceService struct {
|
type Config interface {
|
||||||
repo repository.WalletRepository
|
GetPlatformFee() int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBalanceService(repo repository.WalletRepository) *BalanceService {
|
type BalanceService struct {
|
||||||
|
repo repository.WalletRepository
|
||||||
|
trx repository.TransactionManager
|
||||||
|
crypt repository.Crypto
|
||||||
|
transaction repository.TransactionRepository
|
||||||
|
cfg Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBalanceService(repo repository.WalletRepository,
|
||||||
|
trx repository.TransactionManager,
|
||||||
|
crypt repository.Crypto, cfg Config,
|
||||||
|
transaction repository.TransactionRepository) *BalanceService {
|
||||||
return &BalanceService{
|
return &BalanceService{
|
||||||
repo: repo,
|
repo: repo,
|
||||||
|
trx: trx,
|
||||||
|
crypt: crypt,
|
||||||
|
cfg: cfg,
|
||||||
|
transaction: transaction,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,3 +48,92 @@ func (s *BalanceService) GetByID(ctx context.Context, id int64) (*entity.Balance
|
|||||||
AuthBalance: balanceDB.AuthBalance,
|
AuthBalance: balanceDB.AuthBalance,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *BalanceService) WithdrawInquiry(ctx context.Context, req *entity.BalanceWithdrawInquiry) (*entity.BalanceWithdrawInquiryResponse, error) {
|
||||||
|
balanceDB, err := s.repo.GetForUpdate(ctx, nil, req.PartnerID)
|
||||||
|
if err != nil {
|
||||||
|
logger.ContextLogger(ctx).Error("error when get branch by id", zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if float64(req.Amount) > balanceDB.Balance {
|
||||||
|
logger.ContextLogger(ctx).Error("requested amount exceeds available balance")
|
||||||
|
return nil, errors.New("insufficient balance")
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := s.crypt.GenerateJWTWithdraw(&entity.WalletWithdrawRequest{
|
||||||
|
ID: balanceDB.ID,
|
||||||
|
PartnerID: req.PartnerID,
|
||||||
|
Amount: req.Amount - s.cfg.GetPlatformFee(),
|
||||||
|
Fee: s.cfg.GetPlatformFee(),
|
||||||
|
Total: req.Amount,
|
||||||
|
})
|
||||||
|
|
||||||
|
return &entity.BalanceWithdrawInquiryResponse{
|
||||||
|
PartnerID: req.PartnerID,
|
||||||
|
Amount: req.Amount - s.cfg.GetPlatformFee(),
|
||||||
|
Token: token,
|
||||||
|
Fee: s.cfg.GetPlatformFee(),
|
||||||
|
Total: req.Amount,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BalanceService) WithdrawExecute(ctx mycontext.Context, req *entity.WalletWithdrawRequest) (*entity.WalletWithdrawResponse, error) {
|
||||||
|
decodedReq, err := s.crypt.ValidateJWTWithdraw(req.Token)
|
||||||
|
if err != nil || decodedReq.PartnerID != req.PartnerID {
|
||||||
|
logger.ContextLogger(ctx).Error("invalid withdrawal token", zap.Error(err))
|
||||||
|
return nil, errors.New("invalid withdrawal token")
|
||||||
|
}
|
||||||
|
|
||||||
|
trx, _ := s.trx.Begin(ctx)
|
||||||
|
wallet, err := s.repo.GetForUpdate(ctx, trx, decodedReq.PartnerID)
|
||||||
|
if err != nil {
|
||||||
|
logger.ContextLogger(ctx).Error("error retrieving wallet by partner ID", zap.Error(err))
|
||||||
|
trx.Rollback()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
totalAmount := float64(decodedReq.Total)
|
||||||
|
if totalAmount > wallet.Balance {
|
||||||
|
logger.ContextLogger(ctx).Error("insufficient balance for withdrawal", zap.Float64("available", wallet.Balance), zap.Float64("requested", totalAmount))
|
||||||
|
trx.Rollback()
|
||||||
|
return nil, errors.New("insufficient balance")
|
||||||
|
}
|
||||||
|
|
||||||
|
wallet.Balance -= totalAmount
|
||||||
|
wallet.AuthBalance += totalAmount
|
||||||
|
|
||||||
|
if _, err := s.repo.Update(ctx, trx, wallet); err != nil {
|
||||||
|
logger.ContextLogger(ctx).Error("error updating wallet balance", zap.Error(err))
|
||||||
|
trx.Rollback()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction := &entity.Transaction{
|
||||||
|
PartnerID: wallet.PartnerID,
|
||||||
|
TransactionType: "WITHDRAW",
|
||||||
|
ReferenceID: "",
|
||||||
|
Status: "WAITING_APPROVAL",
|
||||||
|
CreatedBy: ctx.RequestedBy(),
|
||||||
|
Amount: totalAmount,
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction, err = s.transaction.Create(ctx, trx, transaction)
|
||||||
|
if err != nil {
|
||||||
|
logger.ContextLogger(ctx).Error("error creating transaction record", zap.Error(err))
|
||||||
|
trx.Rollback()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := trx.Commit().Error; err != nil {
|
||||||
|
logger.ContextLogger(ctx).Error("error committing transaction", zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
response := &entity.WalletWithdrawResponse{
|
||||||
|
TransactionID: transaction.ID,
|
||||||
|
Status: "WAITING_APPROVAL",
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|||||||
@ -55,7 +55,7 @@ func NewServiceManagerImpl(cfg *config.Config, repo *repository.RepoManagerImpl)
|
|||||||
SiteSvc: site.NewSiteService(repo.Site),
|
SiteSvc: site.NewSiteService(repo.Site),
|
||||||
LicenseSvc: service.NewLicenseService(repo.License),
|
LicenseSvc: service.NewLicenseService(repo.License),
|
||||||
Transaction: transaction.New(repo.Transaction),
|
Transaction: transaction.New(repo.Transaction),
|
||||||
Balance: balance.NewBalanceService(repo.Wallet),
|
Balance: balance.NewBalanceService(repo.Wallet, repo.Trx, repo.Crypto, &cfg.Withdraw, repo.Transaction),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,4 +150,6 @@ type Transaction interface {
|
|||||||
|
|
||||||
type Balance interface {
|
type Balance interface {
|
||||||
GetByID(ctx context.Context, id int64) (*entity.Balance, error)
|
GetByID(ctx context.Context, id int64) (*entity.Balance, error)
|
||||||
|
WithdrawInquiry(ctx context.Context, req *entity.BalanceWithdrawInquiry) (*entity.BalanceWithdrawInquiryResponse, error)
|
||||||
|
WithdrawExecute(ctx mycontext.Context, req *entity.WalletWithdrawRequest) (*entity.WalletWithdrawResponse, error)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user