fix(api): only require notes when the product actually requires them on POST /order/items (#360)

## Bug
PR #345 (order_item_reason_policy wiring) accidentally broadened the
legacy
\`Notes is required for this product\` check to fire for every product
whose
POST body carried an empty/whitespace \`notes\` field.

Mobile POS step 2 always posts the primary product (e.g. Sættevognstræk,
product id 3) with \`notes: ''\` as part of
\`syncCurrentTransactionToOrder\`. After #345, the API started returning
400 for that primary item. The frontend silently swallowed the 400 in
the next-step click handler, and the operator saw **"Fuldfør doesn't
continue"** with no feedback.

## Repro
1. Log in to the mobile POS (e.g. dept 12 / Taulov)
2. Scan / type a customer's plates (e.g. EP68666 + GG1876)
3. Long-press Sættevognstræk to add the service
4. Tap **Fuldfør**

Before this fix: \`POST /order/items\` → 400 \`Notes is required for
this product\`. Frontend catches and logs \`Next-step action was
interrupted: AxiosError: Request failed with status code 400\`. Operator
sees no error in the UI.

After this fix: \`POST /order/items\` → 200 for the primary product; the
order completes normally.

## Fix
Scope the empty-notes rejection to products whose \`requires_note\` flag
(or extraordinary-chemistry special case) is set, matching the existing
PUT handler behaviour. Products that don't require notes can post
\`notes=''\` without rejection.

## Lock-in tests
Two Pest tests under \`Tests\\Api\\OrderItemsApiTest\`:
- \`allows empty notes for primary products that do not require a note\`
— \`requires_note=0\` product with \`notes=''\` returns 200
- \`still rejects empty notes for products whose requires_note flag is
enabled\` — \`requires_note=1\` product with \`notes=' '\` returns 400
with the legacy message

## Verification
PHP API suite: **292/292 passing** (11892 assertions). Local
\`scripts/php-ci-test.sh api\`.

## Companion PR
\`copenhagentruckwash/pleno-vue\` →
\`fix/fuldfor-surface-order-item-error\` will surface order-item API
errors in the UI so silent failures become visible. That PR is a
follow-up; this one is the actual root cause fix.

Co-authored-by: Truck Wash Agent <agent@copenhagentruckwash.local>
This commit is contained in:
Jeppe B
2026-08-10 11:32:11 +02:00
committed by GitHub
co-authored by Truck Wash Agent
parent 7174e3be6c
commit 43df3e4dca
2 changed files with 84 additions and 3 deletions
+11 -3
View File
@@ -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 {