Add callback and update user role

This commit is contained in:
aditya.siregar 2024-06-05 00:24:53 +07:00
parent 4d402e32c9
commit 04a89c5508
27 changed files with 388 additions and 49 deletions

View File

@ -15,6 +15,7 @@ type Context interface {
IsSuperAdmin() bool IsSuperAdmin() bool
IsCasheer() bool IsCasheer() bool
GetPartnerID() *int64 GetPartnerID() *int64
GetSiteID() *int64
} }
type MyContextImpl struct { type MyContextImpl struct {
@ -24,6 +25,7 @@ type MyContextImpl struct {
requestID string requestID string
partnerID int64 partnerID int64
roleID int roleID int
siteID int64
} }
func (m *MyContextImpl) RequestedBy() int64 { func (m *MyContextImpl) RequestedBy() int64 {
@ -45,12 +47,20 @@ func (m *MyContextImpl) GetPartnerID() *int64 {
return nil return nil
} }
func (m *MyContextImpl) GetSiteID() *int64 {
if m.siteID != 0 {
return &m.siteID
}
return nil
}
func NewMyContext(parent context.Context, claims *entity.JWTAuthClaims) (*MyContextImpl, error) { func NewMyContext(parent context.Context, claims *entity.JWTAuthClaims) (*MyContextImpl, error) {
return &MyContextImpl{ return &MyContextImpl{
Context: parent, Context: parent,
requestedBy: claims.UserID, requestedBy: claims.UserID,
partnerID: claims.PartnerID, partnerID: claims.PartnerID,
roleID: claims.Role, roleID: claims.Role,
siteID: claims.SiteID,
}, nil }, nil
} }

View File

@ -25,6 +25,7 @@ type UserDB struct {
RoleID int64 `gorm:"column:role_id" json:"role_id"` RoleID int64 `gorm:"column:role_id" json:"role_id"`
RoleName string `gorm:"column:role_name" json:"role_name"` RoleName string `gorm:"column:role_name" json:"role_name"`
PartnerID *int64 `gorm:"column:partner_id" json:"partner_id"` PartnerID *int64 `gorm:"column:partner_id" json:"partner_id"`
SiteID *int64 `gorm:"column:site_id" json:"site_id"`
PartnerName string `gorm:"column:partner_name" json:"partner_name"` PartnerName string `gorm:"column:partner_name" json:"partner_name"`
PartnerStatus string `gorm:"column:partner_status" json:"partner_status"` PartnerStatus string `gorm:"column:partner_status" json:"partner_status"`
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
@ -50,6 +51,7 @@ func (u *UserDB) ToUser() *User {
RoleName: u.RoleName, RoleName: u.RoleName,
PartnerID: u.PartnerID, PartnerID: u.PartnerID,
PartnerName: u.PartnerName, PartnerName: u.PartnerName,
SiteID: u.SiteID,
} }
return userEntity return userEntity
@ -67,6 +69,7 @@ func (u *UserDB) ToUserRoleDB() *UserRoleDB {
PartnerID: u.PartnerID, PartnerID: u.PartnerID,
CreatedAt: u.CreatedAt, CreatedAt: u.CreatedAt,
UpdatedAt: u.UpdatedAt, UpdatedAt: u.UpdatedAt,
SiteID: u.SiteID,
} }
return userRole return userRole

View File

@ -8,6 +8,7 @@ type JWTAuthClaims struct {
Email string `json:"email"` Email string `json:"email"`
Role int `json:"role"` Role int `json:"role"`
PartnerID int64 `json:"partner_id"` PartnerID int64 `json:"partner_id"`
SiteID int64 `json:"site_id"`
jwt.StandardClaims jwt.StandardClaims
} }

View File

@ -44,6 +44,7 @@ type OrderItem struct {
UpdatedAt time.Time `gorm:"autoUpdateTime;column:updated_at"` UpdatedAt time.Time `gorm:"autoUpdateTime;column:updated_at"`
CreatedBy int64 `gorm:"type:int;column:created_by"` CreatedBy int64 `gorm:"type:int;column:created_by"`
UpdatedBy int64 `gorm:"type:int;column:updated_by"` UpdatedBy int64 `gorm:"type:int;column:updated_by"`
Product *Product `gorm:"foreignKey:ItemID;references:ID"`
} }
func (OrderItem) TableName() string { func (OrderItem) TableName() string {
@ -75,3 +76,8 @@ func (o *Order) SetExecutePaymentStatus() {
} }
o.Status = "PENDING" o.Status = "PENDING"
} }
type CallbackRequest struct {
TransactionStatus string `json:"transaction_status"`
TransactionID string `json:"transaction_id"`
}

View File

@ -8,8 +8,8 @@ import (
type Payment struct { type Payment struct {
ID uuid.UUID `gorm:"type:uuid;default:uuid_generate_v4();primaryKey;column:id"` ID uuid.UUID `gorm:"type:uuid;default:uuid_generate_v4();primaryKey;column:id"`
PartnerID string `gorm:"type:varchar;not null;column:partner_id"` PartnerID int64 `gorm:"type:numeric;not null;column:partner_id"`
OrderID string `gorm:"type:varchar;not null;column:order_id"` OrderID int64 `gorm:"type:numeric;not null;column:order_id"`
ReferenceID string `gorm:"type:varchar;not null;column:reference_id"` ReferenceID string `gorm:"type:varchar;not null;column:reference_id"`
Channel string `gorm:"type:varchar;not null;column:channel"` Channel string `gorm:"type:varchar;not null;column:channel"`
PaymentType string `gorm:"type:varchar;not null;column:payment_type"` PaymentType string `gorm:"type:varchar;not null;column:payment_type"`

View File

@ -37,6 +37,11 @@ type ProductSearch struct {
Offset int Offset int
} }
type ProductPOS struct {
PartnerID int64
SiteID int64
}
type ProductList []*ProductDB type ProductList []*ProductDB
type ProductDB struct { type ProductDB struct {

View File

@ -21,6 +21,7 @@ type User struct {
RoleID role.Role RoleID role.Role
RoleName string RoleName string
PartnerID *int64 PartnerID *int64
SiteID *int64
PartnerName string PartnerName string
} }
@ -39,6 +40,7 @@ type UserRoleDB struct {
UserID int64 `gorm:"column:user_id"` UserID int64 `gorm:"column:user_id"`
RoleID int64 `gorm:"column:role_id"` RoleID int64 `gorm:"column:role_id"`
PartnerID *int64 `gorm:"column:partner_id"` PartnerID *int64 `gorm:"column:partner_id"`
SiteID *int64 `gorm:"column:site_id"`
CreatedAt time.Time `gorm:"column:created_at"` CreatedAt time.Time `gorm:"column:created_at"`
UpdatedAt time.Time `gorm:"column:updated_at"` UpdatedAt time.Time `gorm:"column:updated_at"`
} }
@ -66,6 +68,7 @@ func (u *User) ToUserDB(createdBy int64) (*UserDB, error) {
PartnerID: u.PartnerID, PartnerID: u.PartnerID,
Status: userstatus.Active, Status: userstatus.Active,
CreatedBy: createdBy, CreatedBy: createdBy,
SiteID: u.SiteID,
}, nil }, nil
} }

View File

@ -0,0 +1,71 @@
package mdtrns
import (
"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.Order
}
func (h *Handler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) {
route := group.Group("/midtrans")
route.POST("/callback", h.Callback)
}
func NewHandler(service services.Order) *Handler {
return &Handler{
service: service,
}
}
func (h *Handler) Callback(c *gin.Context) {
var callbackData request.MidtransCallbackRequest
if err := c.ShouldBindJSON(&callbackData); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
validStatuses := []string{"settlement", "expire", "deny", "cancel", "capture", "failure"}
isValidStatus := false
for _, status := range validStatuses {
if callbackData.TransactionStatus == status {
isValidStatus = true
break
}
}
if !isValidStatus {
c.JSON(http.StatusOK, response.BaseResponse{
Success: true,
Status: http.StatusOK,
Message: "",
})
return
}
err := h.service.ProcessCallback(c, callbackData.ToEntity())
if err != nil {
c.JSON(http.StatusUnauthorized, response.BaseResponse{
Success: false,
Status: http.StatusBadRequest,
Message: err.Error(),
Data: nil,
})
return
}
c.JSON(http.StatusOK, response.BaseResponse{
Success: true,
Status: http.StatusOK,
Message: "order",
})
}

View File

@ -108,6 +108,7 @@ func MapOrderToCreateOrderResponse(orderResponse *entity.OrderResponse) response
ItemID: item.ItemID, ItemID: item.ItemID,
Quantity: item.Quantity, Quantity: item.Quantity,
Price: item.Price, Price: item.Price,
Name: item.Product.Name,
} }
} }
@ -133,6 +134,7 @@ func MapOrderToExecuteOrderResponse(orderResponse *entity.ExecuteOrderResponse)
ItemID: item.ItemID, ItemID: item.ItemID,
Quantity: item.Quantity, Quantity: item.Quantity,
Price: item.Price, Price: item.Price,
Name: item.Product.Name,
} }
} }

View File

@ -22,6 +22,7 @@ func (h *Handler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) {
route := group.Group("/product") route := group.Group("/product")
route.POST("/", jwt, h.Create) route.POST("/", jwt, h.Create)
route.GET("/pos", jwt, h.GetPOSProduct)
route.GET("/list", jwt, h.GetAll) route.GET("/list", jwt, h.GetAll)
route.PUT("/:id", jwt, h.Update) route.PUT("/:id", jwt, h.Update)
route.GET("/:id", jwt, h.GetByID) route.GET("/:id", jwt, h.GetByID)
@ -157,6 +158,37 @@ func (h *Handler) GetAll(c *gin.Context) {
}) })
} }
func (h *Handler) GetPOSProduct(c *gin.Context) {
ctx := request.GetMyContext(c)
var req request.ProductParam
if err := c.ShouldBindQuery(&req); err != nil {
response.ErrorWrapper(c, errors.ErrorBadRequest)
return
}
if !ctx.IsCasheer() {
response.ErrorWrapper(c, errors.ErrorBadRequest)
return
}
products, err := h.service.GetProductPOS(c.Request.Context(), entity.ProductPOS{
PartnerID: *ctx.GetPartnerID(),
SiteID: *ctx.GetSiteID(),
})
if err != nil {
response.ErrorWrapper(c, err)
return
}
c.JSON(http.StatusOK, response.BaseResponse{
Success: true,
Status: http.StatusOK,
Data: h.toProductResponseList(products, int64(len(products)), req),
})
}
// Delete handles the deletion of a product by ID. // Delete handles the deletion of a product by ID.
// @Summary Delete a product by ID // @Summary Delete a product by ID
// @Description Delete a product based on the provided ID. // @Description Delete a product based on the provided ID.
@ -248,6 +280,10 @@ func (h *Handler) toProductResponse(resp *entity.Product) response.Product {
Description: resp.Description, Description: resp.Description,
CreatedAt: resp.CreatedAt.Format(time.RFC3339), CreatedAt: resp.CreatedAt.Format(time.RFC3339),
UpdatedAt: resp.CreatedAt.Format(time.RFC3339), UpdatedAt: resp.CreatedAt.Format(time.RFC3339),
PartnerID: resp.PartnerID,
SiteID: resp.SiteID,
IsSeasonTicket: resp.IsSeasonTicket,
IsWeekendTicket: resp.IsWeekendTicket,
} }
} }

View File

@ -1,6 +1,7 @@
package user package user
import ( import (
"furtuna-be/internal/constants/role"
"net/http" "net/http"
"strconv" "strconv"
"time" "time"
@ -64,6 +65,11 @@ func (h *Handler) Create(c *gin.Context) {
} }
} }
if req.RoleID == role.Casheer && req.SiteID == nil {
response.ErrorWrapper(c, errors.NewServiceException("site id is required for cashier"))
return
}
res, err := h.service.Create(ctx, req.ToEntity()) res, err := h.service.Create(ctx, req.ToEntity())
if err != nil { if err != nil {
response.ErrorWrapper(c, err) response.ErrorWrapper(c, err)

View File

@ -0,0 +1,39 @@
package request
import "furtuna-be/internal/entity"
type MidtransCallbackRequest struct {
VANumbers []VANumber `json:"va_numbers"`
TransactionTime string `json:"transaction_time"`
TransactionStatus string `json:"transaction_status"`
TransactionID string `json:"transaction_id"`
StatusMessage string `json:"status_message"`
StatusCode string `json:"status_code"`
SignatureKey string `json:"signature_key"`
SettlementTime string `json:"settlement_time"`
PaymentType string `json:"payment_type"`
OrderID string `json:"order_id"`
MerchantID string `json:"merchant_id"`
GrossAmount string `json:"gross_amount"`
FraudStatus string `json:"fraud_status"`
ExpiryTime string `json:"expiry_time"`
Currency string `json:"currency"`
}
type VANumber struct {
VANumber string `json:"va_number"`
Bank string `json:"bank"`
}
type MidtransCallbackBank struct {
Bank string `json:"bank"`
VaNumber string `json:"va_number"`
BillerCode string `json:"biller_code"`
}
func (m *MidtransCallbackRequest) ToEntity() *entity.CallbackRequest {
return &entity.CallbackRequest{
TransactionID: m.OrderID,
TransactionStatus: m.TransactionStatus,
}
}

View File

@ -12,7 +12,8 @@ type User struct {
Email string `json:"email" validate:"required"` Email string `json:"email" validate:"required"`
Password string `json:"password" validate:"required"` Password string `json:"password" validate:"required"`
PartnerID *int64 `json:"partner_id"` PartnerID *int64 `json:"partner_id"`
RoleID int64 `json:"role_id" validate:"required"` SiteID *int64 `json:"site_id"`
RoleID role.Role `json:"role_id" validate:"required"`
NIK string `json:"nik"` NIK string `json:"nik"`
UserType string `json:"user_type"` UserType string `json:"user_type"`
PhoneNumber string `json:"phone_number"` PhoneNumber string `json:"phone_number"`
@ -34,6 +35,7 @@ func (u *User) ToEntity() *entity.User {
Password: u.Password, Password: u.Password,
RoleID: role.Role(u.RoleID), RoleID: role.Role(u.RoleID),
PartnerID: u.PartnerID, PartnerID: u.PartnerID,
SiteID: u.SiteID,
} }
} }

View File

@ -82,4 +82,5 @@ type CreateOrderItemResponse struct {
ItemID int64 `json:"item_id"` ItemID int64 `json:"item_id"`
Quantity int64 `json:"quantity"` Quantity int64 `json:"quantity"`
Price float64 `json:"price"` Price float64 `json:"price"`
Name string `json:"name"`
} }

View File

@ -26,7 +26,7 @@ func (r *AuthRepository) CheckExistsUserAccount(ctx context.Context, email strin
err := r.db. err := r.db.
Table("users"). Table("users").
Select("users.*, user_roles.role_id, user_roles.partner_id, roles.role_name, partners.name as partner_name, partners.status as partner_status"). Select("users.*, user_roles.role_id, user_roles.partner_id, user_roles.site_id, roles.role_name, partners.name as partner_name, partners.status as partner_status").
Where("users.email = ?", email). Where("users.email = ?", email).
Joins("left join user_roles on users.id = user_roles.user_id"). Joins("left join user_roles on users.id = user_roles.user_id").
Joins("left join roles on user_roles.role_id = roles.role_id"). Joins("left join roles on user_roles.role_id = roles.role_id").

View File

@ -51,6 +51,11 @@ func (c *CryptoImpl) GenerateJWT(user *entity.User) (string, error) {
partnerID = *user.PartnerID partnerID = *user.PartnerID
} }
siteID := int64(0)
if user.SiteID != nil {
siteID = *user.SiteID
}
claims := &entity.JWTAuthClaims{ claims := &entity.JWTAuthClaims{
StandardClaims: jwt.StandardClaims{ StandardClaims: jwt.StandardClaims{
Subject: strconv.FormatInt(user.ID, 10), Subject: strconv.FormatInt(user.ID, 10),
@ -63,6 +68,7 @@ func (c *CryptoImpl) GenerateJWT(user *entity.User) (string, error) {
Email: user.Email, Email: user.Email,
Role: int(user.RoleID), Role: int(user.RoleID),
PartnerID: partnerID, PartnerID: partnerID,
SiteID: siteID,
} }
token, err := jwt. token, err := jwt.

View File

@ -24,7 +24,7 @@ func (r *OrderRepository) Create(ctx context.Context, order *entity.Order) (*ent
logger.ContextLogger(ctx).Error("error when creating order", zap.Error(err)) logger.ContextLogger(ctx).Error("error when creating order", zap.Error(err))
return nil, err return nil, err
} }
return order, nil return r.FindByID(ctx, order.ID)
} }
func (r *OrderRepository) UpdateStatus(ctx context.Context, orderID int64, status string) (*entity.Order, error) { func (r *OrderRepository) UpdateStatus(ctx context.Context, orderID int64, status string) (*entity.Order, error) {
@ -43,13 +43,36 @@ func (r *OrderRepository) UpdateStatus(ctx context.Context, orderID int64, statu
func (r *OrderRepository) FindByID(ctx context.Context, id int64) (*entity.Order, error) { func (r *OrderRepository) FindByID(ctx context.Context, id int64) (*entity.Order, error) {
var order entity.Order var order entity.Order
if err := r.db.WithContext(ctx).Preload("OrderItems").First(&order, id).Error; err != nil {
err := r.db.WithContext(ctx).Preload("OrderItems", func(db *gorm.DB) *gorm.DB {
return db.Preload("Product")
}).First(&order, id).Error
if err != nil {
logger.ContextLogger(ctx).Error("error when finding order by ID", zap.Error(err)) logger.ContextLogger(ctx).Error("error when finding order by ID", zap.Error(err))
return nil, err return nil, err
} }
return &order, nil return &order, nil
} }
func (r *OrderRepository) SetOrderStatus(ctx context.Context, db *gorm.DB, orderID int64, status string) error {
var order entity.Order
if err := db.WithContext(ctx).Preload("OrderItems").First(&order, orderID).Error; err != nil {
logger.ContextLogger(ctx).Error("error when finding order by ID", zap.Error(err))
return err
}
order.Status = status
if err := db.WithContext(ctx).Save(&order).Error; err != nil {
logger.ContextLogger(ctx).Error("error when updating order status", zap.Error(err))
return err
}
return nil
}
func (r *OrderRepository) Update(ctx context.Context, order *entity.Order) (*entity.Order, error) { func (r *OrderRepository) Update(ctx context.Context, order *entity.Order) (*entity.Order, error) {
if err := r.db.WithContext(ctx).Save(order).Error; err != nil { if err := r.db.WithContext(ctx).Save(order).Error; err != nil {
logger.ContextLogger(ctx).Error("error when updating order", zap.Error(err)) logger.ContextLogger(ctx).Error("error when updating order", zap.Error(err))

View File

@ -38,6 +38,14 @@ func (r *PaymentRepository) Update(ctx context.Context, payment *entity.Payment)
return payment, nil return payment, nil
} }
func (r *PaymentRepository) UpdateWithTx(ctx context.Context, tx *gorm.DB, payment *entity.Payment) (*entity.Payment, error) {
if err := tx.WithContext(ctx).Save(payment).Error; err != nil {
logger.ContextLogger(ctx).Error("error when updating payment", zap.Error(err))
return nil, err
}
return payment, nil
}
// FindByID retrieves a payment record by its ID // FindByID retrieves a payment record by its ID
func (r *PaymentRepository) FindByID(ctx context.Context, id uuid.UUID) (*entity.Payment, error) { func (r *PaymentRepository) FindByID(ctx context.Context, id uuid.UUID) (*entity.Payment, error) {
payment := new(entity.Payment) payment := new(entity.Payment)
@ -62,9 +70,9 @@ func (r *PaymentRepository) FindByOrderAndPartnerID(ctx context.Context, orderID
} }
// FindByReferenceID retrieves a payment record by its reference ID // FindByReferenceID retrieves a payment record by its reference ID
func (r *PaymentRepository) FindByReferenceID(ctx context.Context, referenceID string) (*entity.Payment, error) { func (r *PaymentRepository) FindByReferenceID(ctx context.Context, db *gorm.DB, referenceID string) (*entity.Payment, error) {
payment := new(entity.Payment) payment := new(entity.Payment)
if err := r.db.WithContext(ctx).Where("reference_id = ?", referenceID).First(payment).Error; err != nil { if err := db.WithContext(ctx).Where("reference_id = ?", referenceID).First(payment).Error; err != nil {
logger.ContextLogger(ctx).Error("error when finding payment by reference ID", zap.Error(err)) logger.ContextLogger(ctx).Error("error when finding payment by reference ID", zap.Error(err))
return nil, err return nil, err
} }

View File

@ -45,6 +45,16 @@ func (b *ProductRepository) GetProductByID(ctx context.Context, id int64) (*enti
return product, nil return product, nil
} }
func (b *ProductRepository) GetProductByPartnerIDAndSiteID(ctx context.Context, partnerID, siteID int64) (entity.ProductList, error) {
var products []*entity.ProductDB
if err := b.db.WithContext(ctx).Where("partner_id = ? AND site_id = ?", partnerID, siteID).Find(&products).Error; err != nil {
logger.ContextLogger(ctx).Error("error when finding product by partner ID and site id", zap.Error(err))
return nil, err
}
return products, nil
}
func (b *ProductRepository) GetAllProducts(ctx context.Context, req entity.ProductSearch) (entity.ProductList, int, error) { func (b *ProductRepository) GetAllProducts(ctx context.Context, req entity.ProductSearch) (entity.ProductList, int, error) {
var products []*entity.ProductDB var products []*entity.ProductDB
var total int64 var total int64

View File

@ -112,6 +112,7 @@ type Product interface {
CreateProduct(ctx context.Context, product *entity.ProductDB) (*entity.ProductDB, error) CreateProduct(ctx context.Context, product *entity.ProductDB) (*entity.ProductDB, error)
UpdateProduct(ctx context.Context, product *entity.ProductDB) (*entity.ProductDB, error) UpdateProduct(ctx context.Context, product *entity.ProductDB) (*entity.ProductDB, error)
GetProductByID(ctx context.Context, id int64) (*entity.ProductDB, error) GetProductByID(ctx context.Context, id int64) (*entity.ProductDB, error)
GetProductByPartnerIDAndSiteID(ctx context.Context, partnerID, siteID int64) (entity.ProductList, error)
GetAllProducts(ctx context.Context, req entity.ProductSearch) (entity.ProductList, int, error) GetAllProducts(ctx context.Context, req entity.ProductSearch) (entity.ProductList, int, error)
DeleteProduct(ctx context.Context, id int64) error DeleteProduct(ctx context.Context, id int64) error
GetProductsByIDs(ctx context.Context, ids []int64, partnerID int64) ([]*entity.ProductDB, error) GetProductsByIDs(ctx context.Context, ids []int64, partnerID int64) ([]*entity.ProductDB, error)
@ -121,6 +122,7 @@ type Order interface {
Create(ctx context.Context, order *entity.Order) (*entity.Order, error) Create(ctx context.Context, order *entity.Order) (*entity.Order, error)
FindByID(ctx context.Context, id int64) (*entity.Order, error) FindByID(ctx context.Context, id int64) (*entity.Order, error)
Update(ctx context.Context, order *entity.Order) (*entity.Order, error) Update(ctx context.Context, order *entity.Order) (*entity.Order, error)
SetOrderStatus(ctx context.Context, db *gorm.DB, orderID int64, status string) error
} }
type OSSRepository interface { type OSSRepository interface {
@ -154,6 +156,8 @@ type TransactionManager interface {
type WalletRepository interface { 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)
GetByPartnerID(ctx context.Context, db *gorm.DB, partnerID int64) (*entity.Wallet, error)
} }
type Midtrans interface { type Midtrans interface {
@ -163,5 +167,7 @@ type Midtrans interface {
type Payment interface { type Payment interface {
Create(ctx context.Context, payment *entity.Payment) (*entity.Payment, error) Create(ctx context.Context, payment *entity.Payment) (*entity.Payment, error)
Update(ctx context.Context, payment *entity.Payment) (*entity.Payment, error) Update(ctx context.Context, payment *entity.Payment) (*entity.Payment, error)
UpdateWithTx(ctx context.Context, tx *gorm.DB, payment *entity.Payment) (*entity.Payment, error)
FindByOrderAndPartnerID(ctx context.Context, orderID, partnerID int64) (*entity.Payment, error) FindByOrderAndPartnerID(ctx context.Context, orderID, partnerID int64) (*entity.Payment, error)
FindByReferenceID(ctx context.Context, db *gorm.DB, referenceID string) (*entity.Payment, error)
} }

View File

@ -28,14 +28,23 @@ func (r *WalletRepository) Create(ctx context.Context, tx *gorm.DB, wallet *enti
return wallet, nil return wallet, nil
} }
func (r *WalletRepository) Update(ctx context.Context, wallet *entity.Wallet) (*entity.Wallet, error) { func (r *WalletRepository) Update(ctx context.Context, db *gorm.DB, wallet *entity.Wallet) (*entity.Wallet, error) {
if err := r.db.Save(wallet).Error; err != nil { if err := db.Save(wallet).Error; err != nil {
logger.ContextLogger(ctx).Error("error when updating wallet", zap.Error(err)) logger.ContextLogger(ctx).Error("error when updating wallet", zap.Error(err))
return nil, err return nil, err
} }
return wallet, nil return wallet, nil
} }
func (r *WalletRepository) GetByPartnerID(ctx context.Context, db *gorm.DB, partnerID int64) (*entity.Wallet, error) {
wallet := new(entity.Wallet)
if err := db.WithContext(ctx).Where("partner_id = ?", partnerID).First(wallet).Error; err != nil {
logger.ContextLogger(ctx).Error("error when finding wallet by partner ID", zap.Error(err))
return nil, err
}
return wallet, nil
}
func (r *WalletRepository) GetByID(ctx context.Context, id int64) (*entity.Wallet, error) { func (r *WalletRepository) GetByID(ctx context.Context, id int64) (*entity.Wallet, error) {
wallet := new(entity.Wallet) wallet := new(entity.Wallet)
if err := r.db.First(wallet, id).Error; err != nil { if err := r.db.First(wallet, id).Error; err != nil {

View File

@ -2,6 +2,7 @@ package routes
import ( import (
"furtuna-be/internal/handlers/http/branch" "furtuna-be/internal/handlers/http/branch"
mdtrns "furtuna-be/internal/handlers/http/midtrans"
"furtuna-be/internal/handlers/http/order" "furtuna-be/internal/handlers/http/order"
"furtuna-be/internal/handlers/http/oss" "furtuna-be/internal/handlers/http/oss"
"furtuna-be/internal/handlers/http/partner" "furtuna-be/internal/handlers/http/partner"
@ -54,6 +55,7 @@ func RegisterPrivateRoutes(app *app.Server, serviceManager *services.ServiceMana
oss.NewOssHandler(serviceManager.OSSSvc), oss.NewOssHandler(serviceManager.OSSSvc),
partner.NewHandler(serviceManager.PartnerSvc), partner.NewHandler(serviceManager.PartnerSvc),
site.NewHandler(serviceManager.SiteSvc), site.NewHandler(serviceManager.SiteSvc),
mdtrns.NewHandler(serviceManager.OrderSvc),
} }
for _, handler := range serverRoutes { for _, handler := range serverRoutes {

View File

@ -1,8 +1,10 @@
package order package order
import ( import (
"database/sql"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"furtuna-be/internal/common/logger" "furtuna-be/internal/common/logger"
order2 "furtuna-be/internal/constants/order" order2 "furtuna-be/internal/constants/order"
"furtuna-be/internal/entity" "furtuna-be/internal/entity"
@ -21,16 +23,24 @@ type OrderService struct {
product repository.Product product repository.Product
midtrans repository.Midtrans midtrans repository.Midtrans
payment repository.Payment payment repository.Payment
txmanager repository.TransactionManager
wallet repository.WalletRepository
} }
func NewOrderService(repo repository.Order, product repository.Product, crypt repository.Crypto, func NewOrderService(
midtrans repository.Midtrans, payment repository.Payment) *OrderService { repo repository.Order,
product repository.Product, crypt repository.Crypto,
midtrans repository.Midtrans, payment repository.Payment,
txmanager repository.TransactionManager,
wallet repository.WalletRepository) *OrderService {
return &OrderService{ return &OrderService{
repo: repo, repo: repo,
product: product, product: product,
crypt: crypt, crypt: crypt,
midtrans: midtrans, midtrans: midtrans,
payment: payment, payment: payment,
txmanager: txmanager,
wallet: wallet,
} }
} }
@ -78,6 +88,7 @@ func (s *OrderService) CreateOrder(ctx context.Context, req *entity.OrderRequest
Price: productMap[item.ProductID].Price, Price: productMap[item.ProductID].Price,
Quantity: item.Quantity, Quantity: item.Quantity,
CreatedBy: req.CreatedBy, CreatedBy: req.CreatedBy,
Product: productMap[item.ProductID].ToProduct(),
}) })
} }
@ -116,7 +127,6 @@ func (s *OrderService) Execute(ctx context.Context, req *entity.OrderExecuteRequ
return nil, err return nil, err
} }
// Check for existing payment to handle idempotency
payment, err := s.payment.FindByOrderAndPartnerID(ctx, orderID, partnerID) payment, err := s.payment.FindByOrderAndPartnerID(ctx, orderID, partnerID)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
logger.ContextLogger(ctx).Error("error getting payment data from db", zap.Error(err)) logger.ContextLogger(ctx).Error("error getting payment data from db", zap.Error(err))
@ -199,13 +209,13 @@ func (s *OrderService) processNonCashPayment(ctx context.Context, order *entity.
} }
payment := &entity.Payment{ payment := &entity.Payment{
PartnerID: strconv.FormatInt(partnerID, 10), PartnerID: partnerID,
OrderID: strconv.FormatInt(order.ID, 10), OrderID: order.ID,
ReferenceID: paymentRequest.PaymentReferenceID, ReferenceID: paymentRequest.PaymentReferenceID,
Channel: "xendit", Channel: "XENDIT",
PaymentType: order.PaymentType, PaymentType: order.PaymentType,
Amount: order.Amount, Amount: order.Amount,
State: "pending", State: "PENDING",
CreatedAt: time.Now(), CreatedAt: time.Now(),
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
RequestMetadata: requestMetadata, RequestMetadata: requestMetadata,
@ -219,3 +229,70 @@ func (s *OrderService) processNonCashPayment(ctx context.Context, order *entity.
return paymentResponse, nil return paymentResponse, nil
} }
func (s *OrderService) ProcessCallback(ctx context.Context, req *entity.CallbackRequest) error {
tx, err := s.txmanager.Begin(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})
if err != nil {
return fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Rollback()
err = s.processPayment(ctx, tx, req)
if err != nil {
return fmt.Errorf("failed to process payment: %w", err)
}
return tx.Commit().Error
}
func (s *OrderService) processPayment(ctx context.Context, tx *gorm.DB, req *entity.CallbackRequest) error {
existingPayment, err := s.payment.FindByReferenceID(ctx, tx, req.TransactionID)
if err != nil {
return fmt.Errorf("failed to retrieve payment: %w", err)
}
existingPayment.State = updatePaymentState(req.TransactionStatus)
_, err = s.payment.UpdateWithTx(ctx, tx, existingPayment)
if err != nil {
return fmt.Errorf("failed to update payment: %w", err)
}
if err := s.updateOrderStatus(ctx, tx, existingPayment.State, existingPayment.OrderID); err != nil {
return fmt.Errorf("failed to update order status: %w", err)
}
if existingPayment.State == "PAID" {
if err := s.updateWalletBalance(ctx, tx, existingPayment.PartnerID, existingPayment.Amount); err != nil {
return fmt.Errorf("failed to update wallet balance: %w", err)
}
}
return nil
}
func updatePaymentState(status string) string {
switch status {
case "settlement", "capture":
return "PAID"
case "expire", "deny", "cancel", "failure":
return "EXPIRED"
default:
return status
}
}
func (s *OrderService) updateOrderStatus(ctx context.Context, tx *gorm.DB, status string, orderID int64) error {
if status != "PENDING" {
return s.repo.SetOrderStatus(ctx, tx, orderID, status)
}
return nil
}
func (s *OrderService) updateWalletBalance(ctx context.Context, tx *gorm.DB, partnerID int64, amount float64) error {
wallet, err := s.wallet.GetByPartnerID(ctx, tx, partnerID)
if err != nil {
return fmt.Errorf("failed to get wallet: %w", err)
}
wallet.Balance += amount
_, err = s.wallet.Update(ctx, tx, wallet)
return err
}

View File

@ -70,6 +70,16 @@ func (s *ProductService) GetAll(ctx context.Context, search entity.ProductSearch
return products.ToProductList(), total, nil return products.ToProductList(), total, nil
} }
func (s *ProductService) GetProductPOS(ctx context.Context, search entity.ProductPOS) ([]*entity.Product, error) {
products, err := s.repo.GetProductByPartnerIDAndSiteID(ctx, search.PartnerID, search.SiteID)
if err != nil {
logger.ContextLogger(ctx).Error("error when get all products", zap.Error(err))
return nil, err
}
return products.ToProductList(), nil
}
func (s *ProductService) Delete(ctx mycontext.Context, id int64) error { func (s *ProductService) Delete(ctx mycontext.Context, id int64) error {
productDB, err := s.repo.GetProductByID(ctx, id) productDB, err := s.repo.GetProductByID(ctx, id)
if err != nil { if err != nil {

View File

@ -41,7 +41,8 @@ func NewServiceManagerImpl(cfg *config.Config, repo *repository.RepoManagerImpl)
BranchSvc: branch.NewBranchService(repo.Branch), BranchSvc: branch.NewBranchService(repo.Branch),
StudioSvc: studio.NewStudioService(repo.Studio), StudioSvc: studio.NewStudioService(repo.Studio),
ProductSvc: product.NewProductService(repo.Product), ProductSvc: product.NewProductService(repo.Product),
OrderSvc: order.NewOrderService(repo.Order, repo.Product, repo.Crypto, repo.Midtrans, repo.Payment), OrderSvc: order.NewOrderService(repo.Order, repo.Product,
repo.Crypto, repo.Midtrans, repo.Payment, repo.Trx, repo.Wallet),
OSSSvc: oss.NewOSSService(repo.OSS), OSSSvc: oss.NewOSSService(repo.OSS),
PartnerSvc: partner.NewPartnerService( PartnerSvc: partner.NewPartnerService(
repo.Partner, users.NewUserService(repo.User, repo.Branch), repo.Trx, repo.Wallet), repo.Partner, users.NewUserService(repo.User, repo.Branch), repo.Trx, repo.Wallet),
@ -90,12 +91,14 @@ type Product interface {
Update(ctx mycontext.Context, id int64, productReq *entity.Product) (*entity.Product, error) Update(ctx mycontext.Context, id int64, productReq *entity.Product) (*entity.Product, error)
GetByID(ctx context.Context, id int64) (*entity.Product, error) GetByID(ctx context.Context, id int64) (*entity.Product, error)
GetAll(ctx context.Context, search entity.ProductSearch) ([]*entity.Product, int, error) GetAll(ctx context.Context, search entity.ProductSearch) ([]*entity.Product, int, error)
GetProductPOS(ctx context.Context, search entity.ProductPOS) ([]*entity.Product, error)
Delete(ctx mycontext.Context, id int64) error Delete(ctx mycontext.Context, id int64) error
} }
type Order interface { type Order interface {
CreateOrder(ctx context.Context, req *entity.OrderRequest) (*entity.OrderResponse, error) CreateOrder(ctx context.Context, req *entity.OrderRequest) (*entity.OrderResponse, error)
Execute(ctx context.Context, req *entity.OrderExecuteRequest) (*entity.ExecuteOrderResponse, error) Execute(ctx context.Context, req *entity.OrderExecuteRequest) (*entity.ExecuteOrderResponse, error)
ProcessCallback(ctx context.Context, req *entity.CallbackRequest) error
} }
type OSSService interface { type OSSService interface {

View File

@ -9,7 +9,7 @@ metadata:
nginx.ingress.kubernetes.io/ingress-class: "nginx" # Add this line nginx.ingress.kubernetes.io/ingress-class: "nginx" # Add this line
spec: spec:
rules: rules:
- host: "furtuna-be.app-dev.altru.id" - host: "furtuna-backend.app-dev.altru.id"
http: http:
paths: paths:
- pathType: Prefix - pathType: Prefix
@ -21,5 +21,5 @@ spec:
number: 3300 number: 3300
tls: tls:
- hosts: - hosts:
- "furtuna-be.app-dev.altru.id" - "furtuna-backend.app-dev.altru.id"
secretName: furtuna-be-app-dev-biz-id-tls secretName: furtuna-backend-app-dev-biz-id-tls

View File

@ -1,8 +1,8 @@
CREATE TABLE public.payments CREATE TABLE public.payments
( (
id uuid NOT NULL DEFAULT uuid_generate_v4(), id uuid NOT NULL DEFAULT uuid_generate_v4(),
partner_id varchar NOT NULL, partner_id numeric NOT NULL,
order_id varchar NOT NULL, order_id numeric NOT NULL,
reference_id varchar NOT NULL, reference_id varchar NOT NULL,
channel varchar NOT NULL, channel varchar NOT NULL,
payment_type varchar NOT NULL, payment_type varchar NOT NULL,