From afa4c070b39c79beacb90b457c76496b9b5ec12c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 17:19:12 +0200 Subject: [PATCH] docs(stock): reconcile spec with shipped design MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- .../specs/2026-08-31-stock-pricing-inline-edit-design.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/superpowers/specs/2026-08-31-stock-pricing-inline-edit-design.md b/docs/superpowers/specs/2026-08-31-stock-pricing-inline-edit-design.md index b33e65c..6307854 100644 --- a/docs/superpowers/specs/2026-08-31-stock-pricing-inline-edit-design.md +++ b/docs/superpowers/specs/2026-08-31-stock-pricing-inline-edit-design.md @@ -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 `` 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. |