Files
vmall/docs/TBD-migrate-wave.md
T
james d0b6350d2d feat(mall): read the store directory from the API
Wave 5: the store directory, store home and product-page store card stop reading
MOCK_STORES, and the payment and order surfaces name their shop.

- a `shop_profiles` table beside `shops`, so the identity model both consoles
  consume is untouched, with a public `GET /api/shops` and `GET /api/shops/{slug}`
  and an admin `PUT /api/admin/shops/{id}/profile`
- a shop with no profile is still listed, with the fields absent rather than
  invented; the pages guard every block, and a missing logo renders an
  initial-letter placeholder
- `scripts/seed-demo.mjs` upserts a profile per demo shop, since profiles hang
  off shops that script creates
- payment and order pages resolve shop ids to names from one cached shop read,
  retiring the generic "Shop" label
- three things went rather than being faked, following the wave-1 precedent:
  `distanceKm` and its sort (no geo model), the store home's sales/comments
  sorts, and its "best sellers" rail (no sales model)
- `lowestSku` moved out of the fixed-data module into `apps/mall/utils/product.ts`
  and re-exported, so live pages stop importing the mock module for a pure
  helper

Verified: 28 backend tests green including five new shop tests; all three
frontends build; the directory, store home, store card and order cards all render
real data with no distance or sales claims; the fixed-data rollback still renders
the store surfaces with the backend stopped.

Note: `nuxt build` does not typecheck in this repo (no `typescript.typeCheck`,
no `vue-tsc`), which AGENTS.md implies it does. A re-export used here created no
local binding and broke internal callers at runtime while the build stayed green;
`docs/TBD-migrate-wave.md` records the gap.

OpenSpec change: openspec/changes/replace-mock-api-wave-5
2026-09-17 17:13:32 +00:00

79 lines
7.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# TBD — migrate the mall off the mock API (waves 4+)
Waves 13 are captured in `openspec/changes/replace-mock-api-wave-{1,2,3}/`: catalog +
currency, auth, and the transaction chain (cart, orders, shipments, invoices). Every domain
the mall had an API for is now live; what remains in Wave 4 is the mock content that never
had a backend behind it.
**How to use:** check a box only once the behaviour is implemented *and* verified against
the live backend (`cargo run -p vmall-api`, `node scripts/seed-demo.mjs`).
**Delete this file** once every Wave 4 box is checked, or consciously dropped and recorded.
The "deliberately out of scope" list at the bottom does not block deleting it.
---
## Wave 2 — auth (done)
Captured in `openspec/changes/replace-mock-api-wave-2/`; auth is live, with the session
validated through `/auth/me` rather than trusted from `localStorage`.
- [x] Flip `auth` to live and verify `login` / `register` / `me` against `:8080` using the seeded `customer@vmall.local` / `customer123`.
- [x] Confirm bad credentials now produce a real 401 — the mock accepted any input (`apps/mall/mock/api.ts:125`), so this is a deliberate UX change.
- [x] Confirm the JWT round-trips through the `vmall.token` localStorage key, shared with shop-admin and admin, and that logout clears it.
## Wave 3 — the transaction chain: cart + orders + shipments + invoices
These move **together**, not one at a time. The mock adapter keeps
`state.cart -> state.orders -> state.shipments` in a single shared state, so a partial flip
leaves the mock half reading state the live half never populates:
- live cart + mock `checkout()` fails with `EMPTY_CART` (`apps/mall/mock/api.ts:194`),
- mock `requestInvoice()` 404s on any live order id (`mock/api.ts:285`),
- mock `listMyShipments()` returns shipments whose `order_id` matches no live order, so the
shipment block is silently empty (`mock/api.ts:282`, `pages/user/orders/[id].vue:25`).
- [x] Flip `cart`, `orders`, `shipments` and `invoices` in the same change, then verify one purchase end to end: add to cart, check out into per-shop orders, pay, ship, confirm delivery, request an invoice. Done in `replace-mock-api-wave-3`; the merchant half was driven through the API because the mall has no merchant UI.
- [x] Verify cancel restores stock and `payOrder` only accepts `pending_payment`. Cancel and stock restore are asserted by `cancel_rules_and_stock_restore`; `pay_order` is a status-guarded `UPDATE ... AND status = 'pending_payment'` that answers 409 otherwise (`apps/api/src/routes/orders.rs:279-287`).
- [x] Verify a company invoice requires a tax number, and that one order can hold only one active invoice. Asserted by `invoice_lifecycle` (400 without a tax number, 409 on the second invoice).
- [x] Remove cart's mock display coupling. `CartItemView` now carries `shop_id`, `shop_name` and `stock` (`apps/api/src/cart.rs`), and `pages/cart.vue` groups by them.
- [x] Decide what caps cart quantity. `CartItem.stock` is exposed and the stepper caps at it, but the API deliberately does not check stock on add — checkout stays authoritative with its 409.
- [x] Gate add-to-cart for anonymous shoppers: `pages/goods/[id].vue` sends a 401 to `/login?redirect=…`, and `pages/login.vue` honours only same-origin paths.
- [x] Checkout keeps sourcing `shipping_address` from `MOCK_ADDRESSES` — intentional; see the out-of-scope note below.
- [x] Fix contract debt: `Shipment.items` is optional and `Invoice.invoice_no` is nullable, matching what the API returns. `Invoice` was declared twice in `packages/shared/src/types.ts` and TypeScript merges duplicate interfaces, so the duplicate had to go for the change to take effect.
- [x] Remove the remaining `storeById` mock usage on the order pages. They animate the generic store label instead; the real names need the public store read below.
- [x] Confirm the mall still renders when the live API is down, with every domain configured to fixed data.
## Wave 4 — the mock content that never had an API
Each is a new backend capability rather than a domain flip.
- [x] **Storefront content** — banners, promos, quick links and floor advert art. Done in `replace-mock-api-wave-4`: four tables seeded from the existing assets, a public `GET /api/content/home`, and an admin read/replace pair.
- [x] **Public store read** — a buyer-facing shop endpoint so `stores/index`, `stores/[id]` and the cart's shop grouping leave mock. Done in `replace-mock-api-wave-5`: a `shop_profiles` table, `GET /api/shops` + `GET /api/shops/{slug}`, an admin upsert, and the order surfaces now name their shop. Two things went rather than being faked: `distanceKm` (no geo model) and the store home's "best sellers" rail plus its sales/comments sorts (no sales model).
- [ ] **Brand model + sales/comments sorts** — restores the brand facet and the sorts removed in Wave 1. Needs a `brands` table (`products.brand_id` + i18n) plus sales and comments data, neither of which exists today.
- [ ] Extend the `ORDER BY` whitelist if more sorts are wanted beyond the `sort=price` added in Wave 1.
- [ ] *(adjacent, not part of the migration)* Move the session token to a cookie so SSR knows whether anyone is signed in. Today a full page load of a guarded route renders the page and then redirects on the client, which logs a hydration mismatch; it is pre-existing (verified identical before Wave 2) and harmless, but it is the real fix for the `ClientOnly` workarounds in `components/shell/TopBar.vue` and `pages/user.vue`.
---
## Deliberately out of scope — not tracked here, and they do not block deleting this file
These have no API contract and no backend model. Leaving them on `~/mock/data` is a decision, not a backlog item.
- **Addresses** — never a blocker: `Address` is embedded in the order and live checkout takes it in the request body, so no addresses table is needed. `MOCK_ADDRESSES` can stay behind checkout indefinitely.
- **Favorites, coupons, account stats** — pure presentation, no transactional impact.
- **seckill / collective / integral marketing pages** — display-only mock content.
## Decisions already made (do not relitigate)
- Category filtering is by **subtree**; the backend exact-match filter was the bug (fixed in Wave 1).
- A migration wave must not change what the UI claims: facets without a backing model are removed rather than left matching nothing.
- The mall is the last mock holdout; `shop-admin` and `admin` already run live against the same backend.
- Auth flips independently, but the transaction domains do not: the mock's shared cart/order/shipment state makes any partial flip fail loudly.
## Known gap: the frontend builds do not typecheck
`pnpm --filter @vmall/<app> build` runs `nuxt build`, which does **not** typecheck: `nuxt.config.ts` sets no `typescript.typeCheck` and `vue-tsc` is not installed. AGENTS.md describes the build as the type gate, so a passing build has been read as type-safe across waves 15; it is not. Wave 5 found this the hard way — `export { lowestSku } from "~/utils/product"` creates no local binding, so every internal caller broke at runtime while the build stayed green.
Cheap fix when someone wants it: add `vue-tsc` + `typescript` as devDependencies, set `typescript: { typeCheck: true }`, and expect a backlog of pre-existing errors on first run. Until then treat the browser check, not the build, as the thing that proves a page works.