From 43eac0ced45116ff7f1f001531f9f8831b6b639a Mon Sep 17 00:00:00 2001 From: efrilm Date: Wed, 30 Sep 2026 11:49:49 +0700 Subject: [PATCH] feat(loyalty): pay own orders with EnakPoint from the app Adds POST /customer/orders/:id/pay-with-points (docs/prd-point-coin.md F9, PC-306) for the customer app and self-order. It uses the same payment path as the cashier, approved by the customer's PIN instead of a code: the session alone is not enough (K8), and a wrong PIN takes nothing and counts toward the lock. A customer can pay only their own order; any other order, and one that does not exist, answer 404 alike, so the endpoint does not reveal other customers' orders. The method is the organization's EnakPoint method, no cashier is recorded, and settling the order triggers earning through the same onOrderPaid hook as every other payment. Co-Authored-By: Claude Opus 5.5 --- internal/app/app.go | 5 +- internal/contract/customer_pin_contract.go | 6 +++ .../handler/customer_order_payment_handler.go | 35 +++++++++++++ internal/processor/order_processor.go | 52 +++++++++++++++++-- internal/processor/point_payment_db_test.go | 44 +++++++++++++++- internal/processor/point_payment_processor.go | 15 ++++++ .../repository/point_payment_repository.go | 18 +++++++ internal/router/router.go | 5 +- internal/router/router_test.go | 1 + .../service/customer_order_payment_service.go | 34 ++++++++++++ internal/service/order_service_table_test.go | 8 +++ 11 files changed, 217 insertions(+), 6 deletions(-) create mode 100644 internal/handler/customer_order_payment_handler.go create mode 100644 internal/service/customer_order_payment_service.go diff --git a/internal/app/app.go b/internal/app/app.go index 23a9f90..c207899 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -158,6 +158,7 @@ func (a *App) Initialize(cfg *config.Config) error { services.loyaltySettingsService, services.customerPinService, services.pointPaymentService, + services.customerOrderPaymentService, a.redisClient, ) @@ -409,7 +410,7 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor orderProcessor.SetLoyalty(earningProcessor) // Pay orders with EnakPoint, approved by the customer's one-time code (docs/prd-point-coin.md F9) pointPaymentProcessor := processor.NewPointPaymentProcessor(repository.NewPointPaymentRepository(a.db), loyaltySettingsProcessor, repos.walletQueryRepo, processor.NewWalletProcessor(repos.walletRepo), repos.txManager) - orderProcessor.SetPointPayments(pointPaymentProcessor, paymentCodeProcessor) + orderProcessor.SetPointPayments(pointPaymentProcessor, paymentCodeProcessor, customerPinProcessor) return &processors{ userProcessor: processor.NewUserProcessor(repos.userRepo, repos.organizationRepo, repos.outletRepo), @@ -509,6 +510,7 @@ type services struct { loyaltySettingsService *service.LoyaltySettingsServiceImpl customerPinService *service.CustomerPinServiceImpl pointPaymentService *service.PointPaymentServiceImpl + customerOrderPaymentService *service.CustomerOrderPaymentServiceImpl } func (a *App) initServices(processors *processors, repos *repositories, cfg *config.Config) *services { @@ -594,6 +596,7 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con loyaltySettingsService: service.NewLoyaltySettingsService(processors.loyaltySettingsProcessor, repos.walletQueryRepo), customerPinService: service.NewCustomerPinService(processors.customerPinProcessor, processors.paymentCodeProcessor), pointPaymentService: service.NewPointPaymentService(processors.pointPaymentProcessor), + customerOrderPaymentService: service.NewCustomerOrderPaymentService(processors.orderProcessor), } } diff --git a/internal/contract/customer_pin_contract.go b/internal/contract/customer_pin_contract.go index 8140bd3..cb550cc 100644 --- a/internal/contract/customer_pin_contract.go +++ b/internal/contract/customer_pin_contract.go @@ -31,3 +31,9 @@ type RemoveCustomerPinRequest struct { type IssuePaymentCodeRequest struct { Pin string `json:"pin" binding:"required"` } + +// PayWithPointsRequest is POST /customer/orders/:id/pay-with-points. +type PayWithPointsRequest struct { + Points int64 `json:"points" binding:"required,min=1"` + Pin string `json:"pin" binding:"required"` +} diff --git a/internal/handler/customer_order_payment_handler.go b/internal/handler/customer_order_payment_handler.go new file mode 100644 index 0000000..29f7071 --- /dev/null +++ b/internal/handler/customer_order_payment_handler.go @@ -0,0 +1,35 @@ +package handler + +import ( + "github.com/gin-gonic/gin" + + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/service" + "apskel-pos-be/internal/util" +) + +// CustomerOrderPaymentHandler serves POST /customer/orders/:id/pay-with-points +// (docs/prd-point-coin.md F9). The body holds the PIN, so it is never logged. +type CustomerOrderPaymentHandler struct { + payments service.CustomerOrderPaymentService +} + +func NewCustomerOrderPaymentHandler(payments service.CustomerOrderPaymentService) *CustomerOrderPaymentHandler { + return &CustomerOrderPaymentHandler{payments: payments} +} + +func (h *CustomerOrderPaymentHandler) PayWithPoints(c *gin.Context) { + customerID, ok := customerIDFromGin(c, "CustomerOrderPaymentHandler::PayWithPoints") + if !ok { + return + } + orderID, ok := parseUUIDParam(c, "id", "CustomerOrderPaymentHandler::PayWithPoints") + if !ok { + return + } + var req contract.PayWithPointsRequest + if !bindPinRequest(c, &req, "CustomerOrderPaymentHandler::PayWithPoints") { + return + } + util.HandleResponse(c.Writer, c.Request, h.payments.PayWithPoints(c.Request.Context(), customerID, orderID, &req, pinRequestInfo(c)), "CustomerOrderPaymentHandler::PayWithPoints") +} diff --git a/internal/processor/order_processor.go b/internal/processor/order_processor.go index 701633c..7d1ff50 100644 --- a/internal/processor/order_processor.go +++ b/internal/processor/order_processor.go @@ -21,6 +21,9 @@ type OrderProcessor interface { CreateOrder(ctx context.Context, req *models.CreateOrderRequest, organizationID uuid.UUID) (*models.OrderResponse, error) AddToOrder(ctx context.Context, orderID uuid.UUID, req *models.AddToOrderRequest) (*models.AddToOrderResponse, error) UpdateOrder(ctx context.Context, id uuid.UUID, req *models.UpdateOrderRequest) (*models.OrderResponse, error) + // PayWithPointsInApp pays the customer's own order with EnakPoint from the app or a + // self-order, approved by their PIN (docs/prd-point-coin.md F9). + PayWithPointsInApp(ctx context.Context, customerID, orderID uuid.UUID, points int64, pin string, info models.CustomerPinRequestInfo) (*models.PaymentResponse, error) GetOrderByID(ctx context.Context, id uuid.UUID) (*models.OrderResponse, error) ListOrders(ctx context.Context, req *models.ListOrdersRequest) (*models.ListOrdersResponse, error) VoidOrder(ctx context.Context, req *models.VoidOrderRequest, voidedBy uuid.UUID) error @@ -114,6 +117,7 @@ type OrderProcessorImpl struct { loyalty OrderLoyalty pointPayments *PointPaymentProcessor paymentCodes paymentCodeRedeemer + pins pinVerifier } // OrderLoyalty is what the order flow tells and asks the loyalty program @@ -144,11 +148,13 @@ type paymentCodeRedeemer interface { Redeem(ctx context.Context, code string, customerID uuid.UUID) error } -// SetPointPayments enables paying with the EnakPoint method: CreatePayment hands such -// payments to pointPayments, approved by the code the customer shows (F9). -func (p *OrderProcessorImpl) SetPointPayments(pointPayments *PointPaymentProcessor, codes paymentCodeRedeemer) { +// SetPointPayments enables paying with the EnakPoint method. CreatePayment hands such +// payments to pointPayments approved by the code the customer shows at the cashier, +// and PayWithPointsInApp approved by the customer's PIN (F9). +func (p *OrderProcessorImpl) SetPointPayments(pointPayments *PointPaymentProcessor, codes paymentCodeRedeemer, pins pinVerifier) { p.pointPayments = pointPayments p.paymentCodes = codes + p.pins = pins } // createPointPayment is CreatePayment for the EnakPoint method. It never uses the @@ -1787,3 +1793,43 @@ func (p *OrderProcessorImpl) prepareRefundedIngredientRecipeItem(ctx context.Con func stringPtr(s string) *string { return &s } + +// PayWithPointsInApp pays an order with EnakPoint on the customer's own request, in the +// app or a self-order. The session alone is not enough: the customer's PIN approves +// it (K8). An order that is not the customer's own is reported as not found, so the +// endpoint does not reveal other customers' orders. +func (p *OrderProcessorImpl) PayWithPointsInApp(ctx context.Context, customerID, orderID uuid.UUID, points int64, pin string, info models.CustomerPinRequestInfo) (*models.PaymentResponse, error) { + if p.pointPayments == nil || p.pins == nil { + return nil, fmt.Errorf("%w: paying with EnakPoint is not available", ErrPointPaymentRejected) + } + organizationID, owner, err := p.pointPayments.OrderOwner(ctx, orderID) + if err != nil { + return nil, err + } + if owner == nil || *owner != customerID { + return nil, repository.ErrPointPaymentOrderNotFound + } + methodID, err := p.pointPayments.PointMethodID(ctx, organizationID) + if err != nil { + return nil, err + } + result, err := p.pointPayments.Pay(ctx, PointPaymentInput{ + OrderID: orderID, + PaymentMethodID: methodID, + Points: points, + Authorize: func(ctx context.Context, customerID uuid.UUID) error { + return p.pins.VerifyPin(ctx, customerID, pin, PinActionPay, info) + }, + }) + if err != nil { + return nil, err + } + if result.Completed { + p.onOrderPaid(ctx, orderID) + } + payment, err := p.paymentRepo.GetByID(ctx, result.Payment.ID) + if err != nil { + return nil, fmt.Errorf("failed to retrieve created payment: %w", err) + } + return mappers.PaymentEntityToResponse(payment), nil +} diff --git a/internal/processor/point_payment_db_test.go b/internal/processor/point_payment_db_test.go index cd846ac..7f72d3a 100644 --- a/internal/processor/point_payment_db_test.go +++ b/internal/processor/point_payment_db_test.go @@ -91,7 +91,7 @@ func newPointPaymentEnv(t *testing.T) *pointPaymentEnv { txManager: txm, } e.orders.SetLoyalty(NewEarningProcessor(repository.NewEarningRepository(db), settings, wallet, txm)) - e.orders.SetPointPayments(e.payments, e.codes) + e.orders.SetPointPayments(e.payments, e.codes, pinVerifierFake{good: "482913"}) // The outlet earns 1 EnakPoint per Rp 100 and accepts EnakPoint. s, err := settings.Outlet(context.Background(), e.outlet) @@ -332,3 +332,45 @@ func TestPointPayment_ConcurrentForOneCustomer(t *testing.T) { assert.NotEqual(t, customer, d.CustomerID, d.Check) } } + +func TestPointPayment_InApp(t *testing.T) { + e := newPointPaymentEnv(t) + owner := e.customerWith(100000) + stranger := e.customerWith(100000) + order := e.order(owner, 60000) + info := models.CustomerPinRequestInfo{} + + // Another customer cannot pay it, and is not told it exists. + _, err := e.orders.PayWithPointsInApp(e.ctx, stranger, order, 1000, "482913", info) + assert.ErrorIs(t, err, repository.ErrPointPaymentOrderNotFound) + _, err = e.orders.PayWithPointsInApp(e.ctx, owner, uuid.New(), 1000, "482913", info) + assert.ErrorIs(t, err, repository.ErrPointPaymentOrderNotFound) + + // The session alone is not enough: a wrong PIN takes nothing. + _, err = e.orders.PayWithPointsInApp(e.ctx, owner, order, 1000, "000000", info) + var pe *PinError + require.ErrorAs(t, err, &pe) + assert.Equal(t, PinErrInvalid, pe.Code) + assert.Equal(t, int64(100000), e.balance(owner)) + + // The owner pays part, then the rest, with the same rules as at the cashier. + payment, err := e.orders.PayWithPointsInApp(e.ctx, owner, order, 10000, "482913", info) + require.NoError(t, err) + assert.Equal(t, int64(10000), *payment.PointsUsed) + status, remaining := e.orderState(order) + assert.Equal(t, "partial", status) + assert.Equal(t, 50000.0, remaining) + + _, err = e.orders.PayWithPointsInApp(e.ctx, owner, order, 50001, "482913", info) + assert.ErrorIs(t, err, ErrPointPaymentRejected, "not more than what is left") + _, err = e.orders.PayWithPointsInApp(e.ctx, owner, order, 50000, "482913", info) + require.NoError(t, err) + status, _ = e.orderState(order) + assert.Equal(t, "completed", status) + assert.Equal(t, int64(40000), e.balance(owner)) + assert.Equal(t, int64(100000), e.balance(stranger)) + + var createdBy *string + require.NoError(t, e.db.Raw(`SELECT created_by_user::text FROM wallet_transactions WHERE customer_id = ? AND type = 'PAYMENT' LIMIT 1`, owner).Scan(&createdBy).Error) + assert.Nil(t, createdBy, "no cashier took an in-app payment") +} diff --git a/internal/processor/point_payment_processor.go b/internal/processor/point_payment_processor.go index 144ebf1..0e989cd 100644 --- a/internal/processor/point_payment_processor.go +++ b/internal/processor/point_payment_processor.go @@ -319,3 +319,18 @@ func formatRupiah(n int64) string { } return string(out) } + +// PointMethodID returns the organization's EnakPoint payment method. +func (p *PointPaymentProcessor) PointMethodID(ctx context.Context, organizationID uuid.UUID) (uuid.UUID, error) { + return p.repo.PointMethodID(ctx, organizationID) +} + +// OrderOwner returns the organization and customer of an order, for checking that a +// customer pays only their own order. +func (p *PointPaymentProcessor) OrderOwner(ctx context.Context, orderID uuid.UUID) (organizationID uuid.UUID, customerID *uuid.UUID, err error) { + order, err := p.repo.GetOrder(ctx, orderID, false) + if err != nil { + return uuid.Nil, nil, err + } + return order.OrganizationID, order.CustomerID, nil +} diff --git a/internal/repository/point_payment_repository.go b/internal/repository/point_payment_repository.go index c98fdd1..eb6cbfa 100644 --- a/internal/repository/point_payment_repository.go +++ b/internal/repository/point_payment_repository.go @@ -44,6 +44,8 @@ type PointPaymentRepository interface { TotalPaid(ctx context.Context, orderID uuid.UUID) (float64, error) // PaidWithPoints sums the rupiah of the order's completed EnakPoint payments. PaidWithPoints(ctx context.Context, orderID uuid.UUID) (float64, error) + // PointMethodID returns the organization's EnakPoint payment method. + PointMethodID(ctx context.Context, organizationID uuid.UUID) (uuid.UUID, error) InsertPayment(ctx context.Context, payment *entities.Payment) error // UpdateOrderAfterPayment stores what is left to pay and marks the order paid when // nothing is. @@ -176,3 +178,19 @@ func (r *pointPaymentRepository) UpdateOrderAfterPayment(ctx context.Context, or } return nil } + +func (r *pointPaymentRepository) PointMethodID(ctx context.Context, organizationID uuid.UUID) (uuid.UUID, error) { + var ids []string + err := DBFromContext(ctx, r.db).WithContext(ctx). + Table("payment_methods"). + Where("organization_id = ? AND type = ?", organizationID, constants.PaymentMethodTypePoint). + Limit(1). + Pluck("id::text", &ids).Error + if err != nil { + return uuid.Nil, fmt.Errorf("failed to find the EnakPoint payment method: %w", err) + } + if len(ids) == 0 { + return uuid.Nil, fmt.Errorf("the organization has no EnakPoint payment method") + } + return uuid.Parse(ids[0]) +} diff --git a/internal/router/router.go b/internal/router/router.go index 23cb25c..ffd18f5 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -58,12 +58,13 @@ type Router struct { loyaltySettingsHandler *handler.LoyaltySettingsHandler customerPinHandler *handler.CustomerPinHandler pointPaymentHandler *handler.PointPaymentHandler + customerOrderPaymentHandler *handler.CustomerOrderPaymentHandler authMiddleware *middleware.AuthMiddleware customerAuthMiddleware *middleware.CustomerAuthMiddleware redisClient *redis.Client } -func NewRouter(cfg *config.Config, healthHandler *handler.HealthHandler, authService service.AuthService, authMiddleware *middleware.AuthMiddleware, userService *service.UserServiceImpl, userValidator *validator.UserValidatorImpl, organizationService service.OrganizationService, organizationValidator validator.OrganizationValidator, outletService service.OutletService, outletValidator validator.OutletValidator, outletSettingService service.OutletSettingService, categoryService service.CategoryService, categoryValidator validator.CategoryValidator, productService service.ProductService, productValidator validator.ProductValidator, productVariantService service.ProductVariantService, productVariantValidator validator.ProductVariantValidator, inventoryService service.InventoryService, inventoryValidator validator.InventoryValidator, orderService service.OrderService, orderValidator validator.OrderValidator, fileService service.FileService, fileValidator validator.FileValidator, customerService service.CustomerService, customerValidator validator.CustomerValidator, paymentMethodService service.PaymentMethodService, paymentMethodValidator validator.PaymentMethodValidator, analyticsService *service.AnalyticsServiceImpl, reportService service.ReportService, tableService *service.TableServiceImpl, tableValidator *validator.TableValidator, unitService handler.UnitService, ingredientService handler.IngredientService, productRecipeService service.ProductRecipeService, vendorService service.VendorService, vendorValidator validator.VendorValidator, purchaseOrderService service.PurchaseOrderService, purchaseOrderValidator validator.PurchaseOrderValidator, purchaseCategoryService service.PurchaseCategoryService, purchaseCategoryValidator validator.PurchaseCategoryValidator, unitConverterService service.IngredientUnitConverterService, unitConverterValidator validator.IngredientUnitConverterValidator, chartOfAccountTypeService service.ChartOfAccountTypeService, chartOfAccountTypeValidator validator.ChartOfAccountTypeValidator, chartOfAccountService service.ChartOfAccountService, chartOfAccountValidator validator.ChartOfAccountValidator, accountService service.AccountService, accountValidator validator.AccountValidator, orderIngredientTransactionService service.OrderIngredientTransactionService, orderIngredientTransactionValidator validator.OrderIngredientTransactionValidator, gamificationService service.GamificationService, gamificationValidator validator.GamificationValidator, rewardService service.RewardService, rewardValidator validator.RewardValidator, campaignService service.CampaignService, campaignValidator validator.CampaignValidator, customerAuthService service.CustomerAuthService, customerAuthValidator validator.CustomerAuthValidator, customerPointsService service.CustomerPointsService, spinGameService service.SpinGameService, customerAuthMiddleware *middleware.CustomerAuthMiddleware, userDeviceService service.UserDeviceService, userDeviceValidator validator.UserDeviceValidator, notificationService service.NotificationService, notificationValidator validator.NotificationValidator, productOutletPriceService service.ProductOutletPriceService, productOutletPriceValidator validator.ProductOutletPriceValidator, selfOrderHandler *handler.SelfOrderHandler, expenseService *service.ExpenseServiceImpl, expenseValidator *validator.ExpenseValidatorImpl, cashAdvanceService service.CashAdvanceService, cashAdvanceValidator validator.CashAdvanceValidator, walletAdminService service.WalletAdminService, walletValidator validator.WalletValidator, loyaltySettingsService service.LoyaltySettingsService, customerPinService service.CustomerPinService, pointPaymentService service.PointPaymentService, redisClient *redis.Client) *Router { +func NewRouter(cfg *config.Config, healthHandler *handler.HealthHandler, authService service.AuthService, authMiddleware *middleware.AuthMiddleware, userService *service.UserServiceImpl, userValidator *validator.UserValidatorImpl, organizationService service.OrganizationService, organizationValidator validator.OrganizationValidator, outletService service.OutletService, outletValidator validator.OutletValidator, outletSettingService service.OutletSettingService, categoryService service.CategoryService, categoryValidator validator.CategoryValidator, productService service.ProductService, productValidator validator.ProductValidator, productVariantService service.ProductVariantService, productVariantValidator validator.ProductVariantValidator, inventoryService service.InventoryService, inventoryValidator validator.InventoryValidator, orderService service.OrderService, orderValidator validator.OrderValidator, fileService service.FileService, fileValidator validator.FileValidator, customerService service.CustomerService, customerValidator validator.CustomerValidator, paymentMethodService service.PaymentMethodService, paymentMethodValidator validator.PaymentMethodValidator, analyticsService *service.AnalyticsServiceImpl, reportService service.ReportService, tableService *service.TableServiceImpl, tableValidator *validator.TableValidator, unitService handler.UnitService, ingredientService handler.IngredientService, productRecipeService service.ProductRecipeService, vendorService service.VendorService, vendorValidator validator.VendorValidator, purchaseOrderService service.PurchaseOrderService, purchaseOrderValidator validator.PurchaseOrderValidator, purchaseCategoryService service.PurchaseCategoryService, purchaseCategoryValidator validator.PurchaseCategoryValidator, unitConverterService service.IngredientUnitConverterService, unitConverterValidator validator.IngredientUnitConverterValidator, chartOfAccountTypeService service.ChartOfAccountTypeService, chartOfAccountTypeValidator validator.ChartOfAccountTypeValidator, chartOfAccountService service.ChartOfAccountService, chartOfAccountValidator validator.ChartOfAccountValidator, accountService service.AccountService, accountValidator validator.AccountValidator, orderIngredientTransactionService service.OrderIngredientTransactionService, orderIngredientTransactionValidator validator.OrderIngredientTransactionValidator, gamificationService service.GamificationService, gamificationValidator validator.GamificationValidator, rewardService service.RewardService, rewardValidator validator.RewardValidator, campaignService service.CampaignService, campaignValidator validator.CampaignValidator, customerAuthService service.CustomerAuthService, customerAuthValidator validator.CustomerAuthValidator, customerPointsService service.CustomerPointsService, spinGameService service.SpinGameService, customerAuthMiddleware *middleware.CustomerAuthMiddleware, userDeviceService service.UserDeviceService, userDeviceValidator validator.UserDeviceValidator, notificationService service.NotificationService, notificationValidator validator.NotificationValidator, productOutletPriceService service.ProductOutletPriceService, productOutletPriceValidator validator.ProductOutletPriceValidator, selfOrderHandler *handler.SelfOrderHandler, expenseService *service.ExpenseServiceImpl, expenseValidator *validator.ExpenseValidatorImpl, cashAdvanceService service.CashAdvanceService, cashAdvanceValidator validator.CashAdvanceValidator, walletAdminService service.WalletAdminService, walletValidator validator.WalletValidator, loyaltySettingsService service.LoyaltySettingsService, customerPinService service.CustomerPinService, pointPaymentService service.PointPaymentService, customerOrderPaymentService service.CustomerOrderPaymentService, redisClient *redis.Client) *Router { return &Router{ config: cfg, @@ -113,6 +114,7 @@ func NewRouter(cfg *config.Config, healthHandler *handler.HealthHandler, authSer loyaltySettingsHandler: handler.NewLoyaltySettingsHandler(loyaltySettingsService), customerPinHandler: handler.NewCustomerPinHandler(customerPinService), pointPaymentHandler: handler.NewPointPaymentHandler(pointPaymentService), + customerOrderPaymentHandler: handler.NewCustomerOrderPaymentHandler(customerOrderPaymentService), redisClient: redisClient, } } @@ -167,6 +169,7 @@ func (r *Router) addAppRoutes(rg *gin.Engine) { customer.GET("/wallet", r.customerPointsHandler.GetCustomerWallet) customer.GET("/wallet/transactions", r.customerPointsHandler.GetCustomerWalletTransactions) customer.POST("/wallet/payment-code", r.customerPinHandler.IssuePaymentCode) + customer.POST("/orders/:id/pay-with-points", r.customerOrderPaymentHandler.PayWithPoints) // PIN that approves moving EnakPoint and EnakCoin (docs/prd-point-coin.md F11) customer.GET("/pin/status", r.customerPinHandler.Status) customer.POST("/pin/otp", r.customerPinHandler.RequestOtp) diff --git a/internal/router/router_test.go b/internal/router/router_test.go index 9feaf26..1baa348 100644 --- a/internal/router/router_test.go +++ b/internal/router/router_test.go @@ -37,6 +37,7 @@ func TestAllRoutesRegister(t *testing.T) { "GET /api/v1/marketing/loyalty-settings/history", "POST /api/v1/customer/wallet/payment-code", "GET /api/v1/orders/:id/point-payment/preview", + "POST /api/v1/customer/orders/:id/pay-with-points", "GET /api/v1/customer/pin/status", "POST /api/v1/customer/pin/otp", "POST /api/v1/customer/pin", diff --git a/internal/service/customer_order_payment_service.go b/internal/service/customer_order_payment_service.go new file mode 100644 index 0000000..b7e57c4 --- /dev/null +++ b/internal/service/customer_order_payment_service.go @@ -0,0 +1,34 @@ +package service + +import ( + "context" + + "github.com/google/uuid" + + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/models" + "apskel-pos-be/internal/processor" + "apskel-pos-be/internal/transformer" +) + +// CustomerOrderPaymentService lets customers pay their own orders with EnakPoint in the +// app or a self-order (docs/prd-point-coin.md F9). +type CustomerOrderPaymentService interface { + PayWithPoints(ctx context.Context, customerID, orderID uuid.UUID, req *contract.PayWithPointsRequest, info models.CustomerPinRequestInfo) *contract.Response +} + +type CustomerOrderPaymentServiceImpl struct { + orders processor.OrderProcessor +} + +func NewCustomerOrderPaymentService(orders processor.OrderProcessor) *CustomerOrderPaymentServiceImpl { + return &CustomerOrderPaymentServiceImpl{orders: orders} +} + +func (s *CustomerOrderPaymentServiceImpl) PayWithPoints(ctx context.Context, customerID, orderID uuid.UUID, req *contract.PayWithPointsRequest, info models.CustomerPinRequestInfo) *contract.Response { + payment, err := s.orders.PayWithPointsInApp(ctx, customerID, orderID, req.Points, req.Pin, info) + if err != nil { + return PointPaymentErrorResponse(err) + } + return contract.BuildSuccessResponse(transformer.PaymentModelToContract(payment)) +} diff --git a/internal/service/order_service_table_test.go b/internal/service/order_service_table_test.go index 66fd43b..adbc497 100644 --- a/internal/service/order_service_table_test.go +++ b/internal/service/order_service_table_test.go @@ -42,6 +42,14 @@ func (m *MockOrderProcessor) UpdateOrder(ctx context.Context, id uuid.UUID, req return args.Get(0).(*models.OrderResponse), args.Error(1) } +func (m *MockOrderProcessor) PayWithPointsInApp(ctx context.Context, customerID, orderID uuid.UUID, points int64, pin string, info models.CustomerPinRequestInfo) (*models.PaymentResponse, error) { + args := m.Called(ctx, customerID, orderID, points, pin, info) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*models.PaymentResponse), args.Error(1) +} + func (m *MockOrderProcessor) GetOrderByID(ctx context.Context, id uuid.UUID) (*models.OrderResponse, error) { args := m.Called(ctx, id) if args.Get(0) == nil {