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:
parent
9f27e9d4f1
commit
afa4c070b3
1 changed files with 5 additions and 4 deletions
|
|
@ -147,13 +147,14 @@ Behaviours:
|
||||||
```
|
```
|
||||||
|
|
||||||
Reject 400 when:
|
Reject 400 when:
|
||||||
|
- Request body is not valid JSON.
|
||||||
- `updates` is empty, missing, or not an array.
|
- `updates` is empty, missing, or not an array.
|
||||||
- `updates.length > 100` (matches `bulk_update_quote_items` cap; the page slices if more).
|
- `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.
|
- Any `update` has `id` empty or not 15-char PB id.
|
||||||
- Numeric fields not finite / negative.
|
|
||||||
- `actor` missing or `actor.id` empty.
|
- `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.
|
Idempotent: 200 on retry with the same body, same per-row outcomes.
|
||||||
|
|
||||||
### Page behaviour (`/stock/page.tsx`)
|
### 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.
|
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.
|
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).
|
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
|
### 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.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. |
|
| `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 — 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 — 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. |
|
| `/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. |
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue