diff --git a/services/nginx/app/routes/orderItemsRoute.php b/services/nginx/app/routes/orderItemsRoute.php index b306b040..69b4fe89 100644 --- a/services/nginx/app/routes/orderItemsRoute.php +++ b/services/nginx/app/routes/orderItemsRoute.php @@ -104,18 +104,26 @@ class orderItemsRoute } // Validation order for audited products: // 1. If reason_code is present, run reason validation first (most specific messages). - // 2. If notes is provided but empty/whitespace, return "Notes is required" (skip reason). - // 3. Otherwise run reason validation (covers missing reason_code and invalid combos). + // 2. If the product requires an order-item note and notes are provided but + // empty/whitespace, return "Notes is required" (the legacy message). Other products + // may carry an empty notes field without rejecting the request. + // 3. Otherwise run reason validation (covers missing reason_code on affected products). $reasonFields = ['reason_code' => null, 'reason_label_snapshot' => null, 'reason_comment' => null]; $reasonCodeProvided = array_key_exists('reason_code', (array)$data) || array_key_exists('order_item_reason_code', (array)$data); + $productRequiresOrderItemNote = $product->requiresOrderItemNote(); + if ($reasonCodeProvided) { try { $reasonFields = \classes\order_item_reason_policy::validateForProduct((int)$data['product_id'], $data); } catch (\InvalidArgumentException $e) { $response->error($e->getMessage(), 400); } - } elseif (array_key_exists('notes', (array)$data) && trim((string)($data['notes'] ?? '')) === '') { + } elseif ( + $productRequiresOrderItemNote + && array_key_exists('notes', (array)$data) + && trim((string)($data['notes'] ?? '')) === '' + ) { $response->error('Notes is required for this product', 400); } else { try { diff --git a/services/nginx/app/tests/Api/OrderItemsApiTest.php b/services/nginx/app/tests/Api/OrderItemsApiTest.php index f30bd41e..98bea313 100644 --- a/services/nginx/app/tests/Api/OrderItemsApiTest.php +++ b/services/nginx/app/tests/Api/OrderItemsApiTest.php @@ -419,6 +419,79 @@ it('does not allow clearing notes for order items whose product requires notes', ->assertMessage('Notes is required for this product'); }); +it('allows empty notes for primary products that do not require a note', function (): void { + api_test_covers('POST /order/items', 'validation'); + + $customer = api_fixtures()->createUser(['display_name' => 'Empty Notes Customer']); + $department = api_fixtures()->createDepartment(); + $cashier = api_fixtures()->createUser(['display_name' => 'Empty Notes Cashier']); + $order = api_fixtures()->createOrder([ + 'customer_id' => $customer['customer_number'], + 'department_id' => $department['id'], + 'cashier_id' => $cashier['id'], + 'reference' => 'EMPTY-NOTES', + ]); + // Primary product with requires_note = 0 and not the extraordinary chemistry product. + $product = api_fixtures()->createProduct([ + 'id' => 1701, + 'name' => 'Standard Wash', + 'price' => 929, + 'requires_note' => 0, + ]); + $session = api_fixtures()->createUserSession(['add_order_items', 'list_order_items', 'department_access_' . $department['id']]); + + $response = api_client()->post('/order/items', [ + 'order_id' => $order['id'], + 'product_id' => $product['id'], + 'quantity' => 1, + 'price' => 929, + 'notes' => '', + ], $session['headers']); + + $response + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + expect($response->data()['product_id'] ?? null)->toBe(1701); + expect($response->data()['notes'] ?? null)->toBe(''); + expect($response->data()['reason_code'] ?? 'not_set')->toBe('not_set'); +}); + +it('still rejects empty notes for products whose requires_note flag is enabled', function (): void { + api_test_covers('POST /order/items', 'validation'); + + $customer = api_fixtures()->createUser(['display_name' => 'Notes Required Customer']); + $department = api_fixtures()->createDepartment(); + $cashier = api_fixtures()->createUser(['display_name' => 'Notes Required Cashier']); + $order = api_fixtures()->createOrder([ + 'customer_id' => $customer['customer_number'], + 'department_id' => $department['id'], + 'cashier_id' => $cashier['id'], + 'reference' => 'NOTES-REQUIRED', + ]); + $product = api_fixtures()->createProduct([ + 'id' => 1702, + 'name' => 'Requires-Note Wash', + 'price' => 199, + 'requires_note' => 1, + ]); + $session = api_fixtures()->createUserSession(['add_order_items', 'list_order_items', 'department_access_' . $department['id']]); + + api_client() + ->post('/order/items', [ + 'order_id' => $order['id'], + 'product_id' => $product['id'], + 'quantity' => 1, + 'price' => 199, + 'notes' => ' ', + ], $session['headers']) + ->assertStatus(400) + ->assertEnvelope() + ->assertSuccess(false) + ->assertMessage('Notes is required for this product'); +}); + it('returns the extraordinary chemistry product with requires_note enabled', function (): void { api_test_covers('GET /products', 'happy');