From 27ca80fd54496a54ded08687781baf247ec04433 Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Thu, 25 Jun 2026 10:41:30 +0330 Subject: [PATCH] fix(orders): block cancelling an order once the kitchen has started it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/Meezi.API/Controllers/OrdersController.cs | 2 ++ src/Meezi.API/Services/OrderService.cs | 11 ++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Meezi.API/Controllers/OrdersController.cs b/src/Meezi.API/Controllers/OrdersController.cs index dc642a3..c738df9 100644 --- a/src/Meezi.API/Controllers/OrdersController.cs +++ b/src/Meezi.API/Controllers/OrdersController.cs @@ -376,6 +376,8 @@ public class OrdersController : CafeApiControllerBase false, null, new ApiError(code, "Order is already cancelled.", field))), "ORDER_HAS_PAYMENTS" => Conflict(new ApiResponse( false, null, new ApiError(code, "Refund the recorded payments before cancelling this order.", field))), + "ORDER_IN_PREPARATION" => Conflict(new ApiResponse( + 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( false, null, new ApiError(code, "Line item not found.", field))), "ITEM_ALREADY_VOIDED" => BadRequest(new ApiResponse( diff --git a/src/Meezi.API/Services/OrderService.cs b/src/Meezi.API/Services/OrderService.cs index b996a83..4283204 100644 --- a/src/Meezi.API/Services/OrderService.cs +++ b/src/Meezi.API/Services/OrderService.cs @@ -995,9 +995,18 @@ public class OrderService : IOrderService if (order.Status == OrderStatus.Cancelled) return new OrderServiceResult(false, null, "ORDER_ALREADY_CANCELLED"); - if (!OpenForPaymentStatuses.Contains(order.Status)) + if (order.Status == OrderStatus.Delivered) return new OrderServiceResult(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(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))