fix(orders): block cancelling an order once the kitchen has started it
CI/CD / CI · API (dotnet build + test) (push) Successful in 52s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 29s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m8s
CI/CD / CI · Admin Web (tsc) (push) Successful in 39s
CI/CD / CI · Website (tsc) (push) Successful in 45s
CI/CD / CI · Koja (tsc) (push) Successful in 49s
CI/CD / Deploy · all services (push) Successful in 1m40s
CI/CD / CI · API (dotnet build + test) (push) Successful in 52s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 29s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m8s
CI/CD / CI · Admin Web (tsc) (push) Successful in 39s
CI/CD / CI · Website (tsc) (push) Successful in 45s
CI/CD / CI · Koja (tsc) (push) Successful in 49s
CI/CD / Deploy · all services (push) Successful in 1m40s
Anti-fraud / integrity: a cashier could fire an order to the kitchen, take cash without recording a payment, then cancel (soft-delete) the unpaid order to erase it. CancelOrderAsync now only allows cancelling a still-Pending order; once the kitchen has acted on it (Confirmed/Preparing/Ready) it returns ORDER_IN_PREPARATION and a started order can no longer be removed — it must be completed (and refunded through the audited refund flow if needed). Delivered → ORDER_NOT_OPEN; paid → ORDER_HAS_PAYMENTS (unchanged). Orders are never hard-deleted and every cancel is already audited with the actor. Applies to all roles, independent of permissions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -376,6 +376,8 @@ public class OrdersController : CafeApiControllerBase
|
||||
false, null, new ApiError(code, "Order is already cancelled.", field))),
|
||||
"ORDER_HAS_PAYMENTS" => Conflict(new ApiResponse<object>(
|
||||
false, null, new ApiError(code, "Refund the recorded payments before cancelling this order.", field))),
|
||||
"ORDER_IN_PREPARATION" => Conflict(new ApiResponse<object>(
|
||||
false, null, new ApiError(code, "This order has already been sent to the kitchen and cannot be cancelled.", field))),
|
||||
"ITEM_NOT_FOUND" => NotFound(new ApiResponse<object>(
|
||||
false, null, new ApiError(code, "Line item not found.", field))),
|
||||
"ITEM_ALREADY_VOIDED" => BadRequest(new ApiResponse<object>(
|
||||
|
||||
@@ -995,9 +995,18 @@ public class OrderService : IOrderService
|
||||
if (order.Status == OrderStatus.Cancelled)
|
||||
return new OrderServiceResult<OrderDto>(false, null, "ORDER_ALREADY_CANCELLED");
|
||||
|
||||
if (!OpenForPaymentStatuses.Contains(order.Status))
|
||||
if (order.Status == OrderStatus.Delivered)
|
||||
return new OrderServiceResult<OrderDto>(false, null, "ORDER_NOT_OPEN");
|
||||
|
||||
// Integrity / anti-fraud: once the kitchen has acted on the order
|
||||
// (Confirmed / Preparing / Ready) the food has been produced, so the order
|
||||
// can no longer be cancelled/deleted — otherwise a cashier could fire an
|
||||
// order, take cash without recording a payment, then erase it. Only a
|
||||
// not-yet-started (Pending) order may be cancelled; a started one must be
|
||||
// completed (and refunded via the audited refund flow if needed).
|
||||
if (order.Status != OrderStatus.Pending)
|
||||
return new OrderServiceResult<OrderDto>(false, null, "ORDER_IN_PREPARATION");
|
||||
|
||||
// A paid order must be refunded through the payment flow first — cancelling it
|
||||
// here would silently strip the recorded money. Block and surface the reason.
|
||||
if (order.Payments.Any(p => p.DeletedAt == null))
|
||||
|
||||
Reference in New Issue
Block a user