diff --git a/services/nginx/app/objects/order_bookings_o.php b/services/nginx/app/objects/order_bookings_o.php index 12159b2b..8888020f 100644 --- a/services/nginx/app/objects/order_bookings_o.php +++ b/services/nginx/app/objects/order_bookings_o.php @@ -369,6 +369,7 @@ class order_bookings_o extends db return; } + $this->requireLinkedOrderMatchesBooking($order); $normalizedSafetySeal = orders_o::normalizeSafetySealValue($safety_seal); if ($normalizedSafetySeal !== null) { $order->setSafetySealValue($normalizedSafetySeal); @@ -467,14 +468,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 737dbca4..d62df2b2 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; @@ -52,8 +56,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([]); } @@ -91,7 +99,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); @@ -104,6 +112,22 @@ 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('uses the linked pos order wash certificate item added during mobile completion', function (): void { $order = new OrderBookingsCompletionOrderDouble(); $order->containsWashCertificate = true;