Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
021ec152e9 | ||
|
|
ea9dceb333 |
@@ -138,6 +138,7 @@ func (a *App) Initialize(cfg *config.Config) error {
|
|||||||
selfOrderHandler,
|
selfOrderHandler,
|
||||||
services.expenseService,
|
services.expenseService,
|
||||||
validators.expenseValidator,
|
validators.expenseValidator,
|
||||||
|
a.redisClient,
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
IdempotencyKeyHeader = "X-Idempotency-Key"
|
||||||
|
idempotencyTTL = 24 * time.Hour
|
||||||
|
idempotencyPrefix = "idempotency:"
|
||||||
|
)
|
||||||
|
|
||||||
|
type cachedResponse struct {
|
||||||
|
StatusCode int `json:"status_code"`
|
||||||
|
Headers map[string]string `json:"headers"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IdempotencyMiddleware returns a Gin middleware that ensures idempotent processing
|
||||||
|
// for mutating operations. Client must send X-Idempotency-Key header.
|
||||||
|
func IdempotencyMiddleware(redisClient *redis.Client) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
key := c.GetHeader(IdempotencyKeyHeader)
|
||||||
|
if key == "" {
|
||||||
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"errors": []gin.H{
|
||||||
|
{
|
||||||
|
"code": "missing_idempotency_key",
|
||||||
|
"entity": "IdempotencyMiddleware",
|
||||||
|
"cause": "X-Idempotency-Key header is required",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
redisKey := fmt.Sprintf("%s%s", idempotencyPrefix, key)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
fmt.Printf("[DEBUG] IdempotencyMiddleware: key=%s redisKey=%s\n", key, redisKey)
|
||||||
|
|
||||||
|
// Check if key already exists (request was already processed)
|
||||||
|
cached, err := redisClient.Get(ctx, redisKey).Result()
|
||||||
|
if err == nil {
|
||||||
|
// Key exists — return cached response
|
||||||
|
fmt.Printf("[DEBUG] IdempotencyMiddleware: cache HIT for key=%s\n", key)
|
||||||
|
var resp cachedResponse
|
||||||
|
if err := json.Unmarshal([]byte(cached), &resp); err == nil {
|
||||||
|
for k, v := range resp.Headers {
|
||||||
|
c.Writer.Header().Set(k, v)
|
||||||
|
}
|
||||||
|
c.Writer.Header().Set("X-Idempotent-Replay", "true")
|
||||||
|
c.Data(resp.StatusCode, "application/json", []byte(resp.Body))
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Printf("[DEBUG] IdempotencyMiddleware: cache MISS for key=%s err=%v\n", key, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark key as in-progress to prevent concurrent duplicates
|
||||||
|
set, err := redisClient.SetNX(ctx, redisKey, "processing", idempotencyTTL).Result()
|
||||||
|
if err != nil {
|
||||||
|
// Redis error — proceed without idempotency (fail open)
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !set {
|
||||||
|
// Another request with the same key is being processed
|
||||||
|
c.AbortWithStatusJSON(http.StatusConflict, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"errors": []gin.H{
|
||||||
|
{
|
||||||
|
"code": "request_in_progress",
|
||||||
|
"entity": "IdempotencyMiddleware",
|
||||||
|
"cause": "A request with this idempotency key is already being processed",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture response using a custom writer
|
||||||
|
writer := &responseCapture{
|
||||||
|
ResponseWriter: c.Writer,
|
||||||
|
body: &bytes.Buffer{},
|
||||||
|
}
|
||||||
|
c.Writer = writer
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
|
||||||
|
// After handler completes, cache the response only if successful (2xx)
|
||||||
|
statusCode := writer.Status()
|
||||||
|
if statusCode >= 200 && statusCode < 300 {
|
||||||
|
resp := cachedResponse{
|
||||||
|
StatusCode: statusCode,
|
||||||
|
Headers: map[string]string{
|
||||||
|
"Content-Type": writer.Header().Get("Content-Type"),
|
||||||
|
},
|
||||||
|
Body: writer.body.String(),
|
||||||
|
}
|
||||||
|
|
||||||
|
respJSON, err := json.Marshal(resp)
|
||||||
|
if err == nil {
|
||||||
|
redisClient.Set(ctx, redisKey, string(respJSON), idempotencyTTL)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remove the in-progress key so the client can retry with the same key
|
||||||
|
redisClient.Del(ctx, redisKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// responseCapture wraps gin.ResponseWriter to capture the response body
|
||||||
|
type responseCapture struct {
|
||||||
|
gin.ResponseWriter
|
||||||
|
body *bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *responseCapture) Write(b []byte) (int, error) {
|
||||||
|
w.body.Write(b)
|
||||||
|
return w.ResponseWriter.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *responseCapture) WriteString(s string) (int, error) {
|
||||||
|
w.body.WriteString(s)
|
||||||
|
return w.ResponseWriter.WriteString(s)
|
||||||
|
}
|
||||||
@@ -338,7 +338,7 @@ func (p *OrderProcessorImpl) AddToOrder(ctx context.Context, orderID uuid.UUID,
|
|||||||
ProductID: itemReq.ProductID,
|
ProductID: itemReq.ProductID,
|
||||||
ProductVariantID: itemReq.ProductVariantID,
|
ProductVariantID: itemReq.ProductVariantID,
|
||||||
Quantity: itemReq.Quantity,
|
Quantity: itemReq.Quantity,
|
||||||
UnitPrice: unitPrice, // Use price from database
|
UnitPrice: unitPrice,
|
||||||
TotalPrice: itemTotalPrice,
|
TotalPrice: itemTotalPrice,
|
||||||
UnitCost: unitCost,
|
UnitCost: unitCost,
|
||||||
TotalCost: itemTotalCost,
|
TotalCost: itemTotalCost,
|
||||||
@@ -594,6 +594,10 @@ func (p *OrderProcessorImpl) VoidOrder(ctx context.Context, req *models.VoidOrde
|
|||||||
return fmt.Errorf("order item does not belong to this order")
|
return fmt.Errorf("order item does not belong to this order")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if orderItem.Status == entities.OrderItemStatusCancelled {
|
||||||
|
return fmt.Errorf("order item %s is already cancelled", orderItemID)
|
||||||
|
}
|
||||||
|
|
||||||
if itemVoid.Quantity > orderItem.Quantity {
|
if itemVoid.Quantity > orderItem.Quantity {
|
||||||
return fmt.Errorf("void quantity cannot exceed original quantity for item %d", itemVoid.OrderItemID)
|
return fmt.Errorf("void quantity cannot exceed original quantity for item %d", itemVoid.OrderItemID)
|
||||||
}
|
}
|
||||||
@@ -614,9 +618,15 @@ func (p *OrderProcessorImpl) VoidOrder(ctx context.Context, req *models.VoidOrde
|
|||||||
return fmt.Errorf("outlet not found: %w", err)
|
return fmt.Errorf("outlet not found: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reload order to get latest state
|
||||||
|
order, err = p.orderRepo.GetByID(ctx, req.OrderID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to reload order: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
order.Subtotal -= totalVoidedAmount
|
order.Subtotal -= totalVoidedAmount
|
||||||
order.TotalCost -= totalVoidedCost
|
order.TotalCost -= totalVoidedCost
|
||||||
order.TaxAmount = order.Subtotal * outlet.TaxRate // Recalculate tax using outlet's tax rate
|
order.TaxAmount = order.Subtotal * outlet.TaxRate
|
||||||
order.TotalAmount = order.Subtotal + order.TaxAmount - order.DiscountAmount
|
order.TotalAmount = order.Subtotal + order.TaxAmount - order.DiscountAmount
|
||||||
|
|
||||||
if err := p.orderRepo.Update(ctx, order); err != nil {
|
if err := p.orderRepo.Update(ctx, order); err != nil {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package repository
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@@ -37,3 +38,15 @@ func (m *TxManager) WithTransaction(ctx context.Context, fn func(ctx context.Con
|
|||||||
return fn(ctxTx)
|
return fn(ctxTx)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithTransactionOptions runs fn inside a DB transaction with custom TxOptions (e.g. isolation level).
|
||||||
|
func (m *TxManager) WithTransactionOptions(ctx context.Context, opts *sql.TxOptions, fn func(ctx context.Context) error) error {
|
||||||
|
if m == nil || m.db == nil {
|
||||||
|
return fn(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
ctxTx := context.WithValue(ctx, txKey, tx)
|
||||||
|
return fn(ctxTx)
|
||||||
|
}, opts)
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"apskel-pos-be/internal/validator"
|
"apskel-pos-be/internal/validator"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Router struct {
|
type Router struct {
|
||||||
@@ -53,9 +54,10 @@ type Router struct {
|
|||||||
expenseHandler *handler.ExpenseHandler
|
expenseHandler *handler.ExpenseHandler
|
||||||
authMiddleware *middleware.AuthMiddleware
|
authMiddleware *middleware.AuthMiddleware
|
||||||
customerAuthMiddleware *middleware.CustomerAuthMiddleware
|
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, 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) *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, 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, redisClient *redis.Client) *Router {
|
||||||
|
|
||||||
return &Router{
|
return &Router{
|
||||||
config: cfg,
|
config: cfg,
|
||||||
@@ -99,6 +101,7 @@ func NewRouter(cfg *config.Config, healthHandler *handler.HealthHandler, authSer
|
|||||||
selfOrderHandler: selfOrderHandler,
|
selfOrderHandler: selfOrderHandler,
|
||||||
productOutletPriceHandler: handler.NewProductOutletPriceHandler(productOutletPriceService, productOutletPriceValidator),
|
productOutletPriceHandler: handler.NewProductOutletPriceHandler(productOutletPriceService, productOutletPriceValidator),
|
||||||
expenseHandler: handler.NewExpenseHandler(expenseService, expenseValidator),
|
expenseHandler: handler.NewExpenseHandler(expenseService, expenseValidator),
|
||||||
|
redisClient: redisClient,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,19 +277,19 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
|||||||
orders.GET("", r.orderHandler.ListOrders)
|
orders.GET("", r.orderHandler.ListOrders)
|
||||||
orders.GET("/:id", r.orderHandler.GetOrderByID)
|
orders.GET("/:id", r.orderHandler.GetOrderByID)
|
||||||
orders.POST("", r.orderHandler.CreateOrder)
|
orders.POST("", r.orderHandler.CreateOrder)
|
||||||
orders.POST("/:id/add-items", r.orderHandler.AddToOrder)
|
orders.POST("/:id/add-items", middleware.IdempotencyMiddleware(r.redisClient), r.orderHandler.AddToOrder)
|
||||||
orders.PUT("/:id", r.orderHandler.UpdateOrder)
|
orders.PUT("/:id", r.orderHandler.UpdateOrder)
|
||||||
orders.PUT("/:id/customer", r.orderHandler.SetOrderCustomer)
|
orders.PUT("/:id/customer", r.orderHandler.SetOrderCustomer)
|
||||||
orders.POST("/void", r.orderHandler.VoidOrder)
|
orders.POST("/void", middleware.IdempotencyMiddleware(r.redisClient), r.orderHandler.VoidOrder)
|
||||||
orders.POST("/:id/refund", r.orderHandler.RefundOrder)
|
orders.POST("/:id/refund", middleware.IdempotencyMiddleware(r.redisClient), r.orderHandler.RefundOrder)
|
||||||
orders.POST("/split-bill", r.orderHandler.SplitBill)
|
orders.POST("/split-bill", r.orderHandler.SplitBill)
|
||||||
}
|
}
|
||||||
|
|
||||||
payments := protected.Group("/payments")
|
payments := protected.Group("/payments")
|
||||||
payments.Use(r.authMiddleware.RequireAdminOrManager())
|
payments.Use(r.authMiddleware.RequireAdminOrManager())
|
||||||
{
|
{
|
||||||
payments.POST("", r.orderHandler.CreatePayment)
|
payments.POST("", middleware.IdempotencyMiddleware(r.redisClient), r.orderHandler.CreatePayment)
|
||||||
payments.POST("/:id/refund", r.orderHandler.RefundPayment)
|
payments.POST("/:id/refund", middleware.IdempotencyMiddleware(r.redisClient), r.orderHandler.RefundPayment)
|
||||||
}
|
}
|
||||||
|
|
||||||
paymentMethods := protected.Group("/payment-methods")
|
paymentMethods := protected.Group("/payment-methods")
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package service
|
|||||||
import (
|
import (
|
||||||
"apskel-pos-be/internal/appcontext"
|
"apskel-pos-be/internal/appcontext"
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -228,7 +229,9 @@ func (s *OrderServiceImpl) AddToOrder(ctx context.Context, orderID uuid.UUID, re
|
|||||||
var response *models.AddToOrderResponse
|
var response *models.AddToOrderResponse
|
||||||
var ingredientTransactions []*contract.CreateOrderIngredientTransactionRequest
|
var ingredientTransactions []*contract.CreateOrderIngredientTransactionRequest
|
||||||
|
|
||||||
err := s.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
err := s.txManager.WithTransactionOptions(ctx, &sql.TxOptions{
|
||||||
|
Isolation: sql.LevelSerializable,
|
||||||
|
}, func(txCtx context.Context) error {
|
||||||
addResp, err := s.orderProcessor.AddToOrder(txCtx, orderID, req)
|
addResp, err := s.orderProcessor.AddToOrder(txCtx, orderID, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to add items to order: %w", err)
|
return fmt.Errorf("failed to add items to order: %w", err)
|
||||||
@@ -305,9 +308,17 @@ func (s *OrderServiceImpl) VoidOrder(ctx context.Context, req *models.VoidOrderR
|
|||||||
return fmt.Errorf("invalid user ID")
|
return fmt.Errorf("invalid user ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.orderProcessor.VoidOrder(ctx, req, voidedBy); err != nil {
|
err := s.txManager.WithTransactionOptions(ctx, &sql.TxOptions{
|
||||||
|
Isolation: sql.LevelSerializable,
|
||||||
|
}, func(txCtx context.Context) error {
|
||||||
|
if err := s.orderProcessor.VoidOrder(txCtx, req, voidedBy); err != nil {
|
||||||
return fmt.Errorf("failed to void order: %w", err)
|
return fmt.Errorf("failed to void order: %w", err)
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if err := s.handleTableReleaseOnVoid(ctx, req.OrderID); err != nil {
|
if err := s.handleTableReleaseOnVoid(ctx, req.OrderID); err != nil {
|
||||||
fmt.Printf("Warning: failed to handle table release for voided order %s: %v\n", req.OrderID, err)
|
fmt.Printf("Warning: failed to handle table release for voided order %s: %v\n", req.OrderID, err)
|
||||||
@@ -561,9 +572,14 @@ func (s *OrderServiceImpl) validateCreatePaymentRequest(req *models.CreatePaymen
|
|||||||
return fmt.Errorf("payment item amount must be greater than zero for item %d", i+1)
|
return fmt.Errorf("payment item amount must be greater than zero for item %d", i+1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fmt.Printf("[DEBUG] CreatePayment order_id=%s item[%d] order_item_id=%s amount=%.10f\n",
|
||||||
|
req.OrderID, i, item.OrderItemID, item.Amount)
|
||||||
totalItemAmount += item.Amount
|
totalItemAmount += item.Amount
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fmt.Printf("[DEBUG] CreatePayment order_id=%s total_amount=%.10f sum_items=%.10f diff=%.10f\n",
|
||||||
|
req.OrderID, req.Amount, totalItemAmount, req.Amount-totalItemAmount)
|
||||||
|
|
||||||
if totalItemAmount != req.Amount {
|
if totalItemAmount != req.Amount {
|
||||||
return fmt.Errorf("sum of payment item amounts must equal total payment amount")
|
return fmt.Errorf("sum of payment item amounts must equal total payment amount")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user