docs(stock): reconcile spec with shipped design

Two spec-vs-implementation drifts caught by the final review:
- Move 'per-row validation → 200 with reason in results[]' out of the
  'Reject 400' list. The route enforces body shape (json / empty /
  too-many / bad id / missing actor) as 400; the service enforces
  forbidden_field / non-finite / not_found / pb_error per row so one
  bad sibling never aborts the rest.
- Move the 'realtime-conflict banner' from Page-behaviour to Out of
  scope (the implementation deferred it; the spec still listed it).
This commit is contained in:
Claude 2026-08-31 17:19:12 +02:00
parent 9f27e9d4f1
commit afa4c070b3

View file

@ -147,13 +147,14 @@ Behaviours:
```
Reject 400 when:
- Request body is not valid JSON.
- `updates` is empty, missing, or not an array.
- `updates.length > 100` (matches `bulk_update_quote_items` cap; the page slices if more).
- Any `update` has keys outside `{ id, costPrice, sellingPrice, markup }`.
- Any `update` has `id` empty or not 15-char PB id.
- Numeric fields not finite / negative.
- `actor` missing or `actor.id` empty.
Per-row rejections surface **inside** the 200 response, not as 400s: forbidden-field / non-finite / non-numeric / `not_found` / `pb_error` failures each land in `results[]` with `{ ok: false, reason, message }` so one bad row never aborts the siblings. The route relies on the service for those checks (per-row independence). Clients should inspect `summary.failed` and walk `results[]` for the per-row failure reasons.
Idempotent: 200 on retry with the same body, same per-row outcomes.
### Page behaviour (`/stock/page.tsx`)
@ -169,7 +170,7 @@ Idempotent: 200 on retry with the same body, same per-row outcomes.
5. `StockRow` branches on `editPricing` for its pricing cells.
6. New `<PricingApplyBar>` rendered sticky at the bottom of the table container when `pricingDraft.size > 0`. Shows count + Discard + Apply buttons. Reappears below the existing page padding.
7. `useBeforeUnload` warning when `pricingDraft.size > 0` (refresh-confirmation).
8. Realtime subscribe to `stock_items` stays unchanged; on a remote write while in edit-mode, conflicts are surfaced as a small banner *"Changes by another operator may have landed — review before apply"*.
8. Realtime subscribe to `stock_items` stays unchanged. Realtime-edit-conflict detection (a banner warning when another operator's writes land while you're editing) is **deferred** — see `Out of scope` below.
### Edge cases
@ -186,7 +187,7 @@ Idempotent: 200 on retry with the same body, same per-row outcomes.
|---|---|---|
| `stock-pricing-service.updateStockItemsPricing` | Unit (mock PB) | Happy path (cost + selling + markup), only-selling-update, only-cost-update writing a ledger row, idempotency, validation errors (negative, NaN, forbidden field), per-row independence, not_found. |
| `stock-pricing-service` ledger side-effect | Unit (assert PB calls) | When `costPrice` changes, exactly one stock_ledger row is created with `reason="adjustment"`, `unit_cost=new`, `change_qty=0`, `reference_type="manual"`. When `costPrice` is omitted or unchanged, no ledger row. |
| `POST /api/v1/stock/items/set-pricing` | Integration (Next route) | 200 on valid body; 400 on missing/empty updates; 400 on forbidden field; 400 on missing actor; per-row failures surfaced in `results`. |
| `POST /api/v1/stock/items/set-pricing` | Integration (Next route) | 200 on valid body; 400 on malformed JSON / empty updates / `>100` updates / malformed PB id / missing `actor.id`. Per-row forbidden-field / non-finite / not_found / pb_error failures surface inside `results[].reason` — never as route-level 400s (see route contract note above). |
| `/stock` page — read-only pricing visibility | Component (`page.test.tsx`) | Three columns hidden by default; visible when `showPricingColumns=true`; rows render read cells. |
| `/stock` page — missing-pricing filter | Component | Filter toggle adds PB filter; row count in subtitle reflects `selling=0 AND cost=0`. |
| `/stock` page — edit mode | Component | Toggling Edit replaces cells with inputs; dirty rows tint yellow; footer count reflects staged changes; Apply is disabled while empty. |