fix: prevent race condition on order subtotal calculation
This commit is contained in:
parent
afa1aa5b75
commit
ea9dceb333
@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user