Merge pull request 'Add QR Payment' (#8) from customer-order into main

Reviewed-on: https://git.altru.id/Furtuna/furtuna-backend/pulls/8
This commit is contained in:
altru 2024-08-06 09:22:12 +00:00
commit a51563d3f9
10 changed files with 141 additions and 18 deletions

View File

@ -11,3 +11,9 @@ type MidtransRequest struct {
TotalAmount int64 TotalAmount int64
OrderItems []OrderItem OrderItems []OrderItem
} }
type MidtransQrisResponse struct {
QrCodeUrl string
OrderID string
Amount int64
}

View File

@ -45,6 +45,7 @@ type OrderResponse struct {
type ExecuteOrderResponse struct { type ExecuteOrderResponse struct {
Order *Order Order *Order
QRCode string
PaymentToken string PaymentToken string
RedirectURL string RedirectURL string
} }

View File

@ -139,6 +139,7 @@ func MapOrderToExecuteOrderResponse(orderResponse *entity.ExecuteOrderResponse)
OrderItems: orderItems, OrderItems: orderItems,
PaymentToken: orderResponse.PaymentToken, PaymentToken: orderResponse.PaymentToken,
RedirectURL: orderResponse.RedirectURL, RedirectURL: orderResponse.RedirectURL,
QRcode: orderResponse.QRCode,
} }
} }
@ -199,7 +200,7 @@ func (h *Handler) Detail(c *gin.Context) {
} }
ctx := request.GetMyContext(c) ctx := request.GetMyContext(c)
order, err := h.service.GetByID(ctx, req.ID) order, err := h.service.GetByID(ctx, req.ID, req.ReferenceID)
if err != nil { if err != nil {
response.ErrorWrapper(c, err) response.ErrorWrapper(c, err)
return return

View File

@ -98,9 +98,10 @@ func (e Execute) ToOrderExecuteRequest(createdBy int64) *entity.OrderExecuteRequ
} }
type OrderParamCustomer struct { type OrderParamCustomer struct {
ID int64 `form:"id" json:"id" example:"10"` ID int64 `form:"id" json:"id" example:"10"`
Limit int `form:"limit" json:"limit" example:"10"` ReferenceID string `form:"reference_id" json:"reference_id" example:"10"`
Offset int `form:"offset" json:"offset" example:"0"` Limit int `form:"limit" json:"limit" example:"10"`
Offset int `form:"offset" json:"offset" example:"0"`
} }
func (o *OrderParamCustomer) ToOrderEntity(ctx mycontext.Context) entity.OrderSearch { func (o *OrderParamCustomer) ToOrderEntity(ctx mycontext.Context) entity.OrderSearch {

View File

@ -102,6 +102,7 @@ type ExecuteOrderResponse struct {
OrderItems []CreateOrderItemResponse `json:"order_items"` OrderItems []CreateOrderItemResponse `json:"order_items"`
PaymentToken string `json:"payment_token"` PaymentToken string `json:"payment_token"`
RedirectURL string `json:"redirect_url"` RedirectURL string `json:"redirect_url"`
QRcode string `json:"qr_code"`
} }
type CreateOrderItemResponse struct { type CreateOrderItemResponse struct {

View File

@ -39,10 +39,10 @@ func (c *ClientService) CreatePayment(order entity.MidtransRequest) (*entity.Mid
Client: c.client, Client: c.client,
} }
paymentMethod := []midtrans.PaymentType{} var paymentMethod []midtrans.PaymentType
if order.PaymentMethod == "GOPAY" { if order.PaymentMethod == "QRIS" {
paymentMethod = append(paymentMethod, midtrans.SourceGopay) paymentMethod = []midtrans.PaymentType{midtrans.SourceGopay}
} }
snapReq := &midtrans.SnapReq{ snapReq := &midtrans.SnapReq{
@ -83,3 +83,44 @@ func (c ClientService) getProductItems(products []entity.OrderItem) []midtrans.I
return items return items
} }
func (c *ClientService) CreateQrisPayment(order entity.MidtransRequest) (*entity.MidtransQrisResponse, error) {
coreGateway := midtrans.CoreGateway{
Client: c.client,
}
req := &midtrans.ChargeReq{
PaymentType: midtrans.SourceGopay,
TransactionDetails: midtrans.TransactionDetails{
OrderID: order.PaymentReferenceID,
GrossAmt: order.TotalAmount,
},
}
// Request charge and retrieve response
resp, err := coreGateway.Charge(req)
if err != nil {
logger.GetLogger().Error(fmt.Sprintf("error when creating QRIS payment: %v", err))
return nil, err
}
// Extract QR code URL from response actions
var qrCodeURL string
for _, action := range resp.Actions {
if action.Name == "generate-qr-code" {
qrCodeURL = action.URL
break
}
}
if qrCodeURL == "" {
logger.GetLogger().Error("error: QR code URL not provided in response")
return nil, fmt.Errorf("QR code URL not provided in response")
}
return &entity.MidtransQrisResponse{
QrCodeUrl: qrCodeURL,
OrderID: order.PaymentReferenceID,
Amount: order.TotalAmount,
}, nil
}

View File

@ -72,6 +72,10 @@ 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, db *gorm.DB, 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 db == nil {
db = r.db
}
if err := 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

@ -192,6 +192,7 @@ type WalletRepository interface {
type Midtrans interface { type Midtrans interface {
CreatePayment(order entity.MidtransRequest) (*entity.MidtransResponse, error) CreatePayment(order entity.MidtransRequest) (*entity.MidtransResponse, error)
CreateQrisPayment(order entity.MidtransRequest) (*entity.MidtransQrisResponse, error)
} }
type Payment interface { type Payment interface {

View File

@ -5,7 +5,6 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
errors2 "furtuna-be/internal/common/errors"
"furtuna-be/internal/common/logger" "furtuna-be/internal/common/logger"
"furtuna-be/internal/common/mycontext" "furtuna-be/internal/common/mycontext"
order2 "furtuna-be/internal/constants/order" order2 "furtuna-be/internal/constants/order"
@ -155,12 +154,20 @@ func (s *OrderService) Execute(ctx context.Context, req *entity.OrderExecuteRequ
} }
if order.PaymentType != "CASH" { if order.PaymentType != "CASH" {
paymentResponse, err := s.processNonCashPayment(ctx, order, partnerID, req.CreatedBy) if order.PaymentType == "QRIS" {
if err != nil { paymentResponse, err := s.processQRPayment(ctx, order, partnerID, req.CreatedBy)
return nil, err if err != nil {
return nil, err
}
resp.QRCode = paymentResponse.QrCodeUrl
} else {
paymentResponse, err := s.processNonCashPayment(ctx, order, partnerID, req.CreatedBy)
if err != nil {
return nil, err
}
resp.PaymentToken = paymentResponse.Token
resp.RedirectURL = paymentResponse.RedirectURL
} }
resp.PaymentToken = paymentResponse.Token
resp.RedirectURL = paymentResponse.RedirectURL
} }
order.SetExecutePaymentStatus() order.SetExecutePaymentStatus()
@ -217,7 +224,54 @@ func (s *OrderService) processNonCashPayment(ctx context.Context, order *entity.
PartnerID: partnerID, PartnerID: partnerID,
OrderID: order.ID, OrderID: order.ID,
ReferenceID: paymentRequest.PaymentReferenceID, ReferenceID: paymentRequest.PaymentReferenceID,
Channel: "XENDIT", Channel: "MIDTRANS",
PaymentType: order.PaymentType,
Amount: order.Amount,
State: "PENDING",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
RequestMetadata: requestMetadata,
}
_, err = s.payment.Create(ctx, payment)
if err != nil {
logger.ContextLogger(ctx).Error("error when creating payment record", zap.Error(err))
return nil, err
}
return paymentResponse, nil
}
func (s *OrderService) processQRPayment(ctx context.Context, order *entity.Order, partnerID, createdBy int64) (*entity.MidtransQrisResponse, error) {
paymentRequest := entity.MidtransRequest{
PaymentReferenceID: generator.GenerateUUIDV4(),
TotalAmount: int64(order.Amount),
OrderItems: order.OrderItems,
PaymentMethod: order.PaymentType,
}
paymentResponse, err := s.midtrans.CreateQrisPayment(paymentRequest)
if err != nil {
logger.ContextLogger(ctx).Error("error when creating payment", zap.Error(err))
return nil, err
}
requestMetadata, err := json.Marshal(map[string]string{
"partner_id": strconv.FormatInt(partnerID, 10),
"created_by": strconv.FormatInt(createdBy, 10),
"qr_code": paymentResponse.QrCodeUrl,
})
if err != nil {
logger.ContextLogger(ctx).Error("error when marshaling request metadata", zap.Error(err))
return nil, err
}
payment := &entity.Payment{
PartnerID: partnerID,
OrderID: order.ID,
ReferenceID: paymentRequest.PaymentReferenceID,
Channel: "MIDTRANS",
PaymentType: order.PaymentType, PaymentType: order.PaymentType,
Amount: order.Amount, Amount: order.Amount,
State: "PENDING", State: "PENDING",
@ -362,16 +416,29 @@ func (s OrderService) SumAmount(ctx mycontext.Context, req entity.OrderSearch) (
return data, nil return data, nil
} }
func (s *OrderService) GetByID(ctx mycontext.Context, id int64) (*entity.Order, error) { func (s *OrderService) GetByID(ctx mycontext.Context, id int64, referenceID string) (*entity.Order, error) {
if referenceID != "" {
payment, err := s.payment.FindByReferenceID(ctx, nil, referenceID)
if err != nil {
logger.ContextLogger(ctx).Error("error when getting payment by IDs", zap.Error(err))
return nil, err
}
id = payment.OrderID
}
order, err := s.repo.FindByID(ctx, id) order, err := s.repo.FindByID(ctx, id)
if err != nil { if err != nil {
logger.ContextLogger(ctx).Error("error when getting products by IDs", zap.Error(err)) logger.ContextLogger(ctx).Error("error when getting products by IDs", zap.Error(err))
return nil, err return nil, err
} }
if order.CreatedBy != ctx.RequestedBy() { if ctx.IsCasheer() {
return nil, errors2.NewError(errors2.ErrorBadRequest.ErrorType(), "order not found") return order, nil
} }
//if order.CreatedBy != ctx.RequestedBy() {
// return nil, errors2.NewError(errors2.ErrorBadRequest.ErrorType(), "order not found")
//}
return order, nil return order, nil
} }

View File

@ -119,7 +119,7 @@ type Order interface {
SumAmount(ctx mycontext.Context, req entity.OrderSearch) (*entity.Order, error) SumAmount(ctx mycontext.Context, req entity.OrderSearch) (*entity.Order, error)
GetDailySales(ctx mycontext.Context, req entity.OrderSearch) ([]entity.ProductDailySales, error) GetDailySales(ctx mycontext.Context, req entity.OrderSearch) ([]entity.ProductDailySales, error)
GetPaymentDistribution(ctx mycontext.Context, req entity.OrderSearch) ([]entity.PaymentTypeDistribution, error) GetPaymentDistribution(ctx mycontext.Context, req entity.OrderSearch) ([]entity.PaymentTypeDistribution, error)
GetByID(ctx mycontext.Context, id int64) (*entity.Order, error) GetByID(ctx mycontext.Context, id int64, referenceID string) (*entity.Order, error)
} }
type OSSService interface { type OSSService interface {