Files
vmall/openspec/changes/archive/2026-09-17-replace-mock-api-wave-3/design.md
T
james 2136a48fbe chore(openspec): archive the mock-migration waves and green the spec set
Archive the three completed changes behind replace-mock-api-wave-1/2/3. Each
merge applied cleanly to the main specs:

- catalog gains the Public product browse requirement (subtree filtering and
  price sort)
- frontend-mall picks up the per-domain adapter, the pinned home page, the
  discovery-page changes, the live auth panels and the live transaction flows
- cart's Server-side cart requirement now documents the shop and stock carried
  by every line

Also replace the TBD Purpose placeholder in all eleven specs with a one-line
description of what each capability covers. Those placeholders predate this
work and were the only reason `openspec validate --all --strict` reported
0 passed / 11 failed; it now reports 11 passed / 0 failed.
2026-09-17 16:36:10 +00:00

62 lines
5.3 KiB
Markdown

# Design
## Context
See `proposal.md` — Why. After waves 1 and 2 the mall serves catalog, currency and auth live through the per-domain `liveDomains` switch (`apps/mall/plugins/api.ts`); cart, orders, shipments and invoices remain on fixed data. Facts that shape this design:
- The fixed-data adapter holds `state.cart -> state.orders -> state.shipments` in one localStorage blob (`apps/mall/mock/api.ts:31-43`): `checkout()` reads `state.cart` (`:194`), `requestInvoice()` looks up `state.orders` (`:285`), `listMyShipments()` returns `state.shipments` (`:282`).
- The live cart is Redis-backed per user; `cart_view` already joins products and shops (`apps/api/src/cart.rs:68-74`) but selects only `price_minor` and `currency`.
- The live cart checks purchasability, not stock (`apps/api/src/routes/cart.rs:31-47`); checkout is where stock is enforced, answering 409.
- `packages/shared/src/types.ts` requires `Shipment.items` and a non-null `Invoice.invoice_no`; the API returns neither (`apps/api/src/models.rs:172-182`, `:192-205`).
- Order and payment pages already fall back to a generic store label when `storeById` misses (`pages/user/orders/index.vue:94`, `pages/checkout/pay.vue:42`), so live orders degrade rather than break.
- `pages/goods/[id].vue` is public and is not behind `middleware/auth.ts`.
## Goals / Non-Goals
**Goals:**
- One purchase runs end to end against the backend: cart → per-shop orders → payment → shipment → invoice.
- The cart keeps grouping per shop and capping quantity at real stock.
- The shared contract stops describing fields the API never sends.
**Non-Goals:**
- No stock check on add-to-cart; checkout stays the authority.
- No store names on order or shipment surfaces — the Wave 4 public store read owns that.
- No addresses model, and no deletion of the mock cart/order code, which the rollback path needs.
## Decisions
**1. The four domains flip together, in dependency order.**
They share entities, so any subset leaves the mock half reading state the live half never writes. Cart must precede orders, orders precede shipments and invoices, and the flip lands in a single commit so no shopper can reach a live cart in front of a mock checkout.
*Alternative:* bridge a live cart into the mock's order state — rejected as throwaway code that would still be wrong for invoice lookups.
**2. Add `shop_id`, `shop_name` and `stock` to `CartItemView` rather than reading the mock catalog.**
The join already exists (`cart.rs:68-74`), so this is three columns. Without it every live cart line collapses into one `"unknown"` group (`pages/cart.vue:31-42`) and the quantity stepper falls back to a hard-coded 999 (`pages/cart.vue:60`) — both visible regressions of things the UI currently does correctly.
*Alternative:* derive the shop client-side — impossible, a live cart line carries no shop.
**3. Stock stays advisory in the cart.**
Exposing `stock` lets the stepper cap, matching the mock's behaviour, but the API deliberately does not re-check it when adding: two shoppers can race regardless, and checkout's 409 is the real gate. Recorded explicitly so nobody mistakes the cart for a stock reservation.
**4. An anonymous add-to-cart redirects to `/login?redirect=…`.**
The alternative — an inline "sign in to buy" panel — still leaves the shopper to find sign-in themselves, and a return path is needed either way. The `redirect` value is accepted only as a same-origin path, so it cannot become an open redirect.
**5. Fix the contract by relaxing the types, not by inventing API fields.**
`Shipment.items` becomes optional and `Invoice.invoice_no` becomes nullable. Nothing consumes shipment items, and the API genuinely does not send them, so adding fields nobody reads would be speculative. The invoices table renders a placeholder for a null number, mirroring how it already handles a missing `order_no` (`pages/user/invoices.vue:46`).
**6. Keep the fixed-data cart and order code.**
The `Mock API adapter` requirement promises the adapter can still serve every domain, so deleting it would break the documented rollback. This wave adds nothing to it, and touches no requirement that waves 1 or 2 modify — so archive order cannot clobber their text.
## Risks / Trade-offs
- [Shoppers lose their existing mock cart and orders] → intended and marked BREAKING; the localStorage blob is left untouched, so rolling `liveDomains` back restores it.
- [Exposed stock can go stale between read and checkout] → advisory by design (decision 3); checkout remains authoritative.
- [Order surfaces lose real store names] → pre-existing fallback to a generic label, unchanged here, owned by Wave 4.
- [A live cart needs a token while the product page is public] → decision 4 is the gate, verified explicitly for a signed-out shopper.
- [Four domains moving at once is a large diff] → the milestone order in `tasks.md` keeps the backend additive and each verification step independent.
## Migration Plan
1. Backend and contract first: extend `cart_view`'s selected columns and the shared types. Both are additive and still serve the fixed-data path.
2. Flip the four domains in `liveDomains` in the same commit as the page changes.
3. Verify one purchase end to end, then verify the all-fixed-data rollback with the backend stopped.
4. Rollback: remove the four names from `liveDomains`; no data migration to reverse.