From 71ffa20811b368af06bb5ba099195d7cd4ac8b64 Mon Sep 17 00:00:00 2001 From: Jeppe B <2jepp9350@gmail.com> Date: Mon, 1 Jun 2026 20:55:26 +0200 Subject: [PATCH] Validate booking order context before certificates --- .../nginx/app/objects/order_bookings_o.php | 24 ++++++++++++++++- .../OrderBookingsCompletionDedupTest.php | 27 ++++++++++++++++++- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/services/nginx/app/objects/order_bookings_o.php b/services/nginx/app/objects/order_bookings_o.php index 729378e2..7a956e22 100644 --- a/services/nginx/app/objects/order_bookings_o.php +++ b/services/nginx/app/objects/order_bookings_o.php @@ -369,6 +369,8 @@ class order_bookings_o extends db } $order = $this->getOrder(); + $this->requireLinkedOrderMatchesBooking($order); + $normalizedSafetySeal = orders_o::normalizeSafetySealValue($safety_seal); if ($normalizedSafetySeal !== null) { $order->setSafetySealValue($normalizedSafetySeal); @@ -462,14 +464,34 @@ class order_bookings_o extends db return $order; } + /** + * @throws Exception + */ + private function requireLinkedOrderMatchesBooking(orders_o $order): void + { + self::requireSelected(); + + $bookingCustomerNumber = (int)$this->customer_number->value(); + $bookingDepartmentId = (int)$this->department->value(); + $orderCustomerId = (int)$order->customer_id->value(); + $orderDepartmentId = (int)$order->department_id->value(); + + if ($orderCustomerId !== $bookingCustomerNumber || $orderDepartmentId !== $bookingDepartmentId) { + throw new Exception('Linked order does not match booking customer or department'); + } + } + /** * @throws Exception */ protected function attachWashCertificate(int $user_id, ?string $safety_seal = null): void { self::requireSelected(); + $order = $this->getOrder(); + $this->requireLinkedOrderMatchesBooking($order); + // Check if the order already has a wash certificate attached - if ($this->getOrder()->hasWashCertificateAttached()) { + if ($order->hasWashCertificateAttached()) { return; } // Get the operator name diff --git a/services/nginx/app/tests/Unit/Orders/OrderBookingsCompletionDedupTest.php b/services/nginx/app/tests/Unit/Orders/OrderBookingsCompletionDedupTest.php index 810f6172..e6065aa7 100644 --- a/services/nginx/app/tests/Unit/Orders/OrderBookingsCompletionDedupTest.php +++ b/services/nginx/app/tests/Unit/Orders/OrderBookingsCompletionDedupTest.php @@ -15,8 +15,12 @@ if (!class_exists('OrderBookingsCompletionOrderDouble')) { { $this->id = -1; $this->booking_id = new object_property('orders', -1, 'booking_id', 'int', false); + $this->customer_id = new object_property('orders', -1, 'customer_id', 'int', true); + $this->department_id = new object_property('orders', -1, 'department_id', 'int', true); $this->safety_seal = new object_property('orders', -1, 'safety_seal', 'string', false); $this->completed_at = new object_property('orders', -1, 'completed_at', 'timestamp', false); + $this->customer_id->set(111111); + $this->department_id->set(10); } public bool $washCertificateAttached = false; @@ -46,8 +50,12 @@ if (!class_exists('OrderBookingsCompletionDouble')) { { $this->id = -1; $this->linkedOrder = $linkedOrder; + $this->customer_number = new object_property('order_bookings', -1, 'customer_number', 'int', true); + $this->department = new object_property('order_bookings', -1, 'department', 'int', true); $this->order_id = new object_property('order_bookings', -1, 'order_id', 'int', false); $this->items = new object_property('order_bookings', -1, 'items', 'json', false); + $this->customer_number->set(111111); + $this->department->set(10); $this->items->set([]); } @@ -85,7 +93,7 @@ if (!class_exists('OrderBookingsCompletionDouble')) { } } -it('attaches and emails a wash certificate when a booking is already linked to a pos order without one', function (): void { +it('attaches and emails a wash certificate when a booking is already linked to a matching pos order without one', function (): void { $order = new OrderBookingsCompletionOrderDouble(); $booking = new OrderBookingsCompletionDouble($order); $booking->order_id->set(321); @@ -98,6 +106,23 @@ it('attaches and emails a wash certificate when a booking is already linked to a expect($booking->sendCalls)->toBe(1); }); + +it('rejects wash certificate completion when the linked pos order belongs to another booking context', function (): void { + $order = new OrderBookingsCompletionOrderDouble(); + $order->customer_id->set(222222); + $order->department_id->set(99); + + $booking = new OrderBookingsCompletionDouble($order); + $booking->order_id->set(321); + $booking->containsWashCertificate = true; + + expect(fn() => $booking->completeBooking(77, 'LINKED-SEAL')) + ->toThrow(Exception::class, 'Linked order does not match booking customer or department'); + expect($order->getSafetySealValue())->toBeNull(); + expect($booking->attachCalls)->toBe(0); + expect($booking->sendCalls)->toBe(0); +}); + it('does not create or email a duplicate wash certificate when a linked pos order already has one', function (): void { $order = new OrderBookingsCompletionOrderDouble(); $order->washCertificateAttached = true;