diff --git a/internal/common/mycontext/kinoscontext.go b/internal/common/mycontext/kinoscontext.go index 353f9d8..fb19d02 100644 --- a/internal/common/mycontext/kinoscontext.go +++ b/internal/common/mycontext/kinoscontext.go @@ -15,6 +15,7 @@ type Context interface { IsSuperAdmin() bool IsCasheer() bool GetPartnerID() *int64 + GetSiteID() *int64 } type MyContextImpl struct { @@ -24,6 +25,7 @@ type MyContextImpl struct { requestID string partnerID int64 roleID int + siteID int64 } func (m *MyContextImpl) RequestedBy() int64 { @@ -45,12 +47,20 @@ func (m *MyContextImpl) GetPartnerID() *int64 { 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) { return &MyContextImpl{ Context: parent, requestedBy: claims.UserID, partnerID: claims.PartnerID, roleID: claims.Role, + siteID: claims.SiteID, }, nil } diff --git a/internal/entity/auth.go b/internal/entity/auth.go index 716bf9c..a134f78 100644 --- a/internal/entity/auth.go +++ b/internal/entity/auth.go @@ -25,6 +25,7 @@ type UserDB struct { RoleID int64 `gorm:"column:role_id" json:"role_id"` RoleName string `gorm:"column:role_name" json:"role_name"` 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"` PartnerStatus string `gorm:"column:partner_status" json:"partner_status"` CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` @@ -50,6 +51,7 @@ func (u *UserDB) ToUser() *User { RoleName: u.RoleName, PartnerID: u.PartnerID, PartnerName: u.PartnerName, + SiteID: u.SiteID, } return userEntity @@ -67,6 +69,7 @@ func (u *UserDB) ToUserRoleDB() *UserRoleDB { PartnerID: u.PartnerID, CreatedAt: u.CreatedAt, UpdatedAt: u.UpdatedAt, + SiteID: u.SiteID, } return userRole diff --git a/internal/entity/jwt.go b/internal/entity/jwt.go index 44f9061..ae31eb8 100644 --- a/internal/entity/jwt.go +++ b/internal/entity/jwt.go @@ -8,6 +8,7 @@ type JWTAuthClaims struct { Email string `json:"email"` Role int `json:"role"` PartnerID int64 `json:"partner_id"` + SiteID int64 `json:"site_id"` jwt.StandardClaims } diff --git a/internal/entity/order.go b/internal/entity/order.go index ca046e0..57a1b21 100644 --- a/internal/entity/order.go +++ b/internal/entity/order.go @@ -44,6 +44,7 @@ type OrderItem struct { UpdatedAt time.Time `gorm:"autoUpdateTime;column:updated_at"` CreatedBy int64 `gorm:"type:int;column:created_by"` UpdatedBy int64 `gorm:"type:int;column:updated_by"` + Product *Product `gorm:"foreignKey:ItemID;references:ID"` } func (OrderItem) TableName() string { @@ -75,3 +76,8 @@ func (o *Order) SetExecutePaymentStatus() { } o.Status = "PENDING" } + +type CallbackRequest struct { + TransactionStatus string `json:"transaction_status"` + TransactionID string `json:"transaction_id"` +} diff --git a/internal/entity/payment.go b/internal/entity/payment.go index 9cc1acb..39c21be 100644 --- a/internal/entity/payment.go +++ b/internal/entity/payment.go @@ -8,8 +8,8 @@ import ( type Payment struct { ID uuid.UUID `gorm:"type:uuid;default:uuid_generate_v4();primaryKey;column:id"` - PartnerID string `gorm:"type:varchar;not null;column:partner_id"` - OrderID string `gorm:"type:varchar;not null;column:order_id"` + PartnerID int64 `gorm:"type:numeric;not null;column:partner_id"` + OrderID int64 `gorm:"type:numeric;not null;column:order_id"` ReferenceID string `gorm:"type:varchar;not null;column:reference_id"` Channel string `gorm:"type:varchar;not null;column:channel"` PaymentType string `gorm:"type:varchar;not null;column:payment_type"` diff --git a/internal/entity/product.go b/internal/entity/product.go index fb261d4..0cb94e9 100644 --- a/internal/entity/product.go +++ b/internal/entity/product.go @@ -37,6 +37,11 @@ type ProductSearch struct { Offset int } +type ProductPOS struct { + PartnerID int64 + SiteID int64 +} + type ProductList []*ProductDB type ProductDB struct { diff --git a/internal/entity/user.go b/internal/entity/user.go index 03d5f54..4ae55b3 100644 --- a/internal/entity/user.go +++ b/internal/entity/user.go @@ -21,6 +21,7 @@ type User struct { RoleID role.Role RoleName string PartnerID *int64 + SiteID *int64 PartnerName string } @@ -39,6 +40,7 @@ type UserRoleDB struct { UserID int64 `gorm:"column:user_id"` RoleID int64 `gorm:"column:role_id"` PartnerID *int64 `gorm:"column:partner_id"` + SiteID *int64 `gorm:"column:site_id"` CreatedAt time.Time `gorm:"column:created_at"` UpdatedAt time.Time `gorm:"column:updated_at"` } @@ -66,6 +68,7 @@ func (u *User) ToUserDB(createdBy int64) (*UserDB, error) { PartnerID: u.PartnerID, Status: userstatus.Active, CreatedBy: createdBy, + SiteID: u.SiteID, }, nil } diff --git a/internal/handlers/http/midtrans/order.go b/internal/handlers/http/midtrans/order.go new file mode 100644 index 0000000..ae31f44 --- /dev/null +++ b/internal/handlers/http/midtrans/order.go @@ -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", + }) + +} diff --git a/internal/handlers/http/order/order.go b/internal/handlers/http/order/order.go index 92e23ec..fcd1ee0 100644 --- a/internal/handlers/http/order/order.go +++ b/internal/handlers/http/order/order.go @@ -108,6 +108,7 @@ func MapOrderToCreateOrderResponse(orderResponse *entity.OrderResponse) response ItemID: item.ItemID, Quantity: item.Quantity, Price: item.Price, + Name: item.Product.Name, } } @@ -133,6 +134,7 @@ func MapOrderToExecuteOrderResponse(orderResponse *entity.ExecuteOrderResponse) ItemID: item.ItemID, Quantity: item.Quantity, Price: item.Price, + Name: item.Product.Name, } } diff --git a/internal/handlers/http/product/product.go b/internal/handlers/http/product/product.go index 91a833a..8661454 100644 --- a/internal/handlers/http/product/product.go +++ b/internal/handlers/http/product/product.go @@ -22,6 +22,7 @@ func (h *Handler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) { route := group.Group("/product") route.POST("/", jwt, h.Create) + route.GET("/pos", jwt, h.GetPOSProduct) route.GET("/list", jwt, h.GetAll) route.PUT("/:id", jwt, h.Update) 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. // @Summary Delete a product by ID // @Description Delete a product based on the provided ID. @@ -240,14 +272,18 @@ func (h *Handler) GetByID(c *gin.Context) { func (h *Handler) toProductResponse(resp *entity.Product) response.Product { return response.Product{ - ID: resp.ID, - Name: resp.Name, - Type: resp.Type, - Price: resp.Price, - Status: resp.Status, - Description: resp.Description, - CreatedAt: resp.CreatedAt.Format(time.RFC3339), - UpdatedAt: resp.CreatedAt.Format(time.RFC3339), + ID: resp.ID, + Name: resp.Name, + Type: resp.Type, + Price: resp.Price, + Status: resp.Status, + Description: resp.Description, + CreatedAt: resp.CreatedAt.Format(time.RFC3339), + UpdatedAt: resp.CreatedAt.Format(time.RFC3339), + PartnerID: resp.PartnerID, + SiteID: resp.SiteID, + IsSeasonTicket: resp.IsSeasonTicket, + IsWeekendTicket: resp.IsWeekendTicket, } } diff --git a/internal/handlers/http/user/user.go b/internal/handlers/http/user/user.go index 9d500b7..cdaf440 100644 --- a/internal/handlers/http/user/user.go +++ b/internal/handlers/http/user/user.go @@ -1,6 +1,7 @@ package user import ( + "furtuna-be/internal/constants/role" "net/http" "strconv" "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()) if err != nil { response.ErrorWrapper(c, err) diff --git a/internal/handlers/request/midtrans.go b/internal/handlers/request/midtrans.go new file mode 100644 index 0000000..f73a865 --- /dev/null +++ b/internal/handlers/request/midtrans.go @@ -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, + } +} diff --git a/internal/handlers/request/user.go b/internal/handlers/request/user.go index 47de17c..1170347 100644 --- a/internal/handlers/request/user.go +++ b/internal/handlers/request/user.go @@ -8,14 +8,15 @@ import ( ) type User struct { - Name string `json:"name" validate:"required"` - Email string `json:"email" validate:"required"` - Password string `json:"password" validate:"required"` - PartnerID *int64 `json:"partner_id"` - RoleID int64 `json:"role_id" validate:"required"` - NIK string `json:"nik"` - UserType string `json:"user_type"` - PhoneNumber string `json:"phone_number"` + Name string `json:"name" validate:"required"` + Email string `json:"email" validate:"required"` + Password string `json:"password" validate:"required"` + PartnerID *int64 `json:"partner_id"` + SiteID *int64 `json:"site_id"` + RoleID role.Role `json:"role_id" validate:"required"` + NIK string `json:"nik"` + UserType string `json:"user_type"` + PhoneNumber string `json:"phone_number"` } func (e *User) Validate() error { @@ -34,6 +35,7 @@ func (u *User) ToEntity() *entity.User { Password: u.Password, RoleID: role.Role(u.RoleID), PartnerID: u.PartnerID, + SiteID: u.SiteID, } } diff --git a/internal/handlers/response/order.go b/internal/handlers/response/order.go index 6fceeb3..419a867 100644 --- a/internal/handlers/response/order.go +++ b/internal/handlers/response/order.go @@ -82,4 +82,5 @@ type CreateOrderItemResponse struct { ItemID int64 `json:"item_id"` Quantity int64 `json:"quantity"` Price float64 `json:"price"` + Name string `json:"name"` } diff --git a/internal/repository/auth/init.go b/internal/repository/auth/init.go index 9d462bc..105da98 100644 --- a/internal/repository/auth/init.go +++ b/internal/repository/auth/init.go @@ -26,7 +26,7 @@ func (r *AuthRepository) CheckExistsUserAccount(ctx context.Context, email strin err := r.db. 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). Joins("left join user_roles on users.id = user_roles.user_id"). Joins("left join roles on user_roles.role_id = roles.role_id"). diff --git a/internal/repository/crypto/init.go b/internal/repository/crypto/init.go index 4f310b9..3934da0 100644 --- a/internal/repository/crypto/init.go +++ b/internal/repository/crypto/init.go @@ -51,6 +51,11 @@ func (c *CryptoImpl) GenerateJWT(user *entity.User) (string, error) { partnerID = *user.PartnerID } + siteID := int64(0) + if user.SiteID != nil { + siteID = *user.SiteID + } + claims := &entity.JWTAuthClaims{ StandardClaims: jwt.StandardClaims{ Subject: strconv.FormatInt(user.ID, 10), @@ -63,6 +68,7 @@ func (c *CryptoImpl) GenerateJWT(user *entity.User) (string, error) { Email: user.Email, Role: int(user.RoleID), PartnerID: partnerID, + SiteID: siteID, } token, err := jwt. diff --git a/internal/repository/orders/order.go b/internal/repository/orders/order.go index 319ace5..24757bb 100644 --- a/internal/repository/orders/order.go +++ b/internal/repository/orders/order.go @@ -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)) 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) { @@ -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) { 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)) return nil, err } + 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) { if err := r.db.WithContext(ctx).Save(order).Error; err != nil { logger.ContextLogger(ctx).Error("error when updating order", zap.Error(err)) diff --git a/internal/repository/payment/payment.go b/internal/repository/payment/payment.go index 5291799..de4a9dc 100644 --- a/internal/repository/payment/payment.go +++ b/internal/repository/payment/payment.go @@ -38,6 +38,14 @@ func (r *PaymentRepository) Update(ctx context.Context, payment *entity.Payment) 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 func (r *PaymentRepository) FindByID(ctx context.Context, id uuid.UUID) (*entity.Payment, error) { 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 -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) - 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)) return nil, err } diff --git a/internal/repository/products/product.go b/internal/repository/products/product.go index 3f91df5..60a8496 100644 --- a/internal/repository/products/product.go +++ b/internal/repository/products/product.go @@ -45,6 +45,16 @@ func (b *ProductRepository) GetProductByID(ctx context.Context, id int64) (*enti 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) { var products []*entity.ProductDB var total int64 diff --git a/internal/repository/repository.go b/internal/repository/repository.go index 47e0c2b..23be698 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -112,6 +112,7 @@ type Product interface { CreateProduct(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) + GetProductByPartnerIDAndSiteID(ctx context.Context, partnerID, siteID int64) (entity.ProductList, error) GetAllProducts(ctx context.Context, req entity.ProductSearch) (entity.ProductList, int, error) DeleteProduct(ctx context.Context, id int64) 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) FindByID(ctx context.Context, id int64) (*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 { @@ -154,6 +156,8 @@ type TransactionManager interface { type WalletRepository interface { 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 { @@ -163,5 +167,7 @@ type Midtrans interface { type Payment interface { Create(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) + FindByReferenceID(ctx context.Context, db *gorm.DB, referenceID string) (*entity.Payment, error) } diff --git a/internal/repository/wallet/wallet.go b/internal/repository/wallet/wallet.go index ec2e75e..df25945 100644 --- a/internal/repository/wallet/wallet.go +++ b/internal/repository/wallet/wallet.go @@ -28,14 +28,23 @@ func (r *WalletRepository) Create(ctx context.Context, tx *gorm.DB, wallet *enti return wallet, nil } -func (r *WalletRepository) Update(ctx context.Context, wallet *entity.Wallet) (*entity.Wallet, error) { - if err := r.db.Save(wallet).Error; err != nil { +func (r *WalletRepository) Update(ctx context.Context, db *gorm.DB, wallet *entity.Wallet) (*entity.Wallet, error) { + if err := db.Save(wallet).Error; err != nil { logger.ContextLogger(ctx).Error("error when updating wallet", zap.Error(err)) return nil, err } 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) { wallet := new(entity.Wallet) if err := r.db.First(wallet, id).Error; err != nil { diff --git a/internal/routes/routes.go b/internal/routes/routes.go index 39f502b..1f00cee 100644 --- a/internal/routes/routes.go +++ b/internal/routes/routes.go @@ -2,6 +2,7 @@ package routes import ( "furtuna-be/internal/handlers/http/branch" + mdtrns "furtuna-be/internal/handlers/http/midtrans" "furtuna-be/internal/handlers/http/order" "furtuna-be/internal/handlers/http/oss" "furtuna-be/internal/handlers/http/partner" @@ -54,6 +55,7 @@ func RegisterPrivateRoutes(app *app.Server, serviceManager *services.ServiceMana oss.NewOssHandler(serviceManager.OSSSvc), partner.NewHandler(serviceManager.PartnerSvc), site.NewHandler(serviceManager.SiteSvc), + mdtrns.NewHandler(serviceManager.OrderSvc), } for _, handler := range serverRoutes { diff --git a/internal/services/order/order.go b/internal/services/order/order.go index 62c02a1..2b075e2 100644 --- a/internal/services/order/order.go +++ b/internal/services/order/order.go @@ -1,8 +1,10 @@ package order import ( + "database/sql" "encoding/json" "errors" + "fmt" "furtuna-be/internal/common/logger" order2 "furtuna-be/internal/constants/order" "furtuna-be/internal/entity" @@ -16,21 +18,29 @@ import ( ) type OrderService struct { - repo repository.Order - crypt repository.Crypto - product repository.Product - midtrans repository.Midtrans - payment repository.Payment + repo repository.Order + crypt repository.Crypto + product repository.Product + midtrans repository.Midtrans + payment repository.Payment + txmanager repository.TransactionManager + wallet repository.WalletRepository } -func NewOrderService(repo repository.Order, product repository.Product, crypt repository.Crypto, - midtrans repository.Midtrans, payment repository.Payment) *OrderService { +func NewOrderService( + repo repository.Order, + product repository.Product, crypt repository.Crypto, + midtrans repository.Midtrans, payment repository.Payment, + txmanager repository.TransactionManager, + wallet repository.WalletRepository) *OrderService { return &OrderService{ - repo: repo, - product: product, - crypt: crypt, - midtrans: midtrans, - payment: payment, + repo: repo, + product: product, + crypt: crypt, + midtrans: midtrans, + 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, Quantity: item.Quantity, 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 } - // Check for existing payment to handle idempotency payment, err := s.payment.FindByOrderAndPartnerID(ctx, orderID, partnerID) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { 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{ - PartnerID: strconv.FormatInt(partnerID, 10), - OrderID: strconv.FormatInt(order.ID, 10), + PartnerID: partnerID, + OrderID: order.ID, ReferenceID: paymentRequest.PaymentReferenceID, - Channel: "xendit", + Channel: "XENDIT", PaymentType: order.PaymentType, Amount: order.Amount, - State: "pending", + State: "PENDING", CreatedAt: time.Now(), UpdatedAt: time.Now(), RequestMetadata: requestMetadata, @@ -219,3 +229,70 @@ func (s *OrderService) processNonCashPayment(ctx context.Context, order *entity. 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 +} diff --git a/internal/services/product/product.go b/internal/services/product/product.go index 4e1dd6e..bd7ce23 100644 --- a/internal/services/product/product.go +++ b/internal/services/product/product.go @@ -70,6 +70,16 @@ func (s *ProductService) GetAll(ctx context.Context, search entity.ProductSearch 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 { productDB, err := s.repo.GetProductByID(ctx, id) if err != nil { diff --git a/internal/services/service.go b/internal/services/service.go index 0610fd0..8255e2f 100644 --- a/internal/services/service.go +++ b/internal/services/service.go @@ -41,8 +41,9 @@ func NewServiceManagerImpl(cfg *config.Config, repo *repository.RepoManagerImpl) BranchSvc: branch.NewBranchService(repo.Branch), StudioSvc: studio.NewStudioService(repo.Studio), ProductSvc: product.NewProductService(repo.Product), - OrderSvc: order.NewOrderService(repo.Order, repo.Product, repo.Crypto, repo.Midtrans, repo.Payment), - OSSSvc: oss.NewOSSService(repo.OSS), + OrderSvc: order.NewOrderService(repo.Order, repo.Product, + repo.Crypto, repo.Midtrans, repo.Payment, repo.Trx, repo.Wallet), + OSSSvc: oss.NewOSSService(repo.OSS), PartnerSvc: partner.NewPartnerService( repo.Partner, users.NewUserService(repo.User, repo.Branch), repo.Trx, repo.Wallet), SiteSvc: site.NewSiteService(repo.Site), @@ -90,12 +91,14 @@ type Product interface { Update(ctx mycontext.Context, id int64, productReq *entity.Product) (*entity.Product, error) GetByID(ctx context.Context, id int64) (*entity.Product, 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 } type Order interface { CreateOrder(ctx context.Context, req *entity.OrderRequest) (*entity.OrderResponse, error) Execute(ctx context.Context, req *entity.OrderExecuteRequest) (*entity.ExecuteOrderResponse, error) + ProcessCallback(ctx context.Context, req *entity.CallbackRequest) error } type OSSService interface { diff --git a/k8s/staging/ingress.yaml b/k8s/staging/ingress.yaml index aae7d0a..adb9376 100644 --- a/k8s/staging/ingress.yaml +++ b/k8s/staging/ingress.yaml @@ -9,7 +9,7 @@ metadata: nginx.ingress.kubernetes.io/ingress-class: "nginx" # Add this line spec: rules: - - host: "furtuna-be.app-dev.altru.id" + - host: "furtuna-backend.app-dev.altru.id" http: paths: - pathType: Prefix @@ -21,5 +21,5 @@ spec: number: 3300 tls: - hosts: - - "furtuna-be.app-dev.altru.id" - secretName: furtuna-be-app-dev-biz-id-tls + - "furtuna-backend.app-dev.altru.id" + secretName: furtuna-backend-app-dev-biz-id-tls diff --git a/migrations/000010_add-payment-table.up.sql b/migrations/000010_add-payment-table.up.sql index 2b242a0..4d9b800 100644 --- a/migrations/000010_add-payment-table.up.sql +++ b/migrations/000010_add-payment-table.up.sql @@ -1,8 +1,8 @@ CREATE TABLE public.payments ( id uuid NOT NULL DEFAULT uuid_generate_v4(), - partner_id varchar NOT NULL, - order_id varchar NOT NULL, + partner_id numeric NOT NULL, + order_id numeric NOT NULL, reference_id varchar NOT NULL, channel varchar NOT NULL, payment_type varchar NOT NULL,