Wave 4 of replacing the fixed-data mock adapter, and the first capability the
mall never had a backend for: the home page's banners, promo tiles, quick links
and floor advert art move out of local arrays.
- four explicit tables (`banners`, `promos`, `quick_links`, `floor_adverts`)
rather than one JSONB payload table, so Postgres enforces each shape
- a migration seeds them from the assets the page already rendered, so the flip
is visually a no-op. Destinations are real routes now: the mock's promo links
pointed at dangling `?category=c1` ids and its first banner used `sort=sales`,
which the catalog API rejects
- `GET /api/content/home` is public and returns the four active, ordered lists,
always including a key so a page can render a missing block
- `GET /api/admin/content` and `PUT /api/admin/content/{kind}` let a platform
admin read everything and replace one kind transactionally, with positions
reindexed from the submitted order and a rejected list changing nothing
- the mall's fixed-data adapter learns `getHomeContent`, and a `content` domain
joins the per-domain switch so the rollback path still renders the page
Verified: 23 backend tests green including six new content tests; all three
frontends build; the home page renders the same four blocks as before, an admin
reorder and deactivation change the rendered carousel, and the fixed-data
rollback renders every block with the backend stopped.
OpenSpec change: openspec/changes/replace-mock-api-wave-4
73 lines
6.3 KiB
Markdown
73 lines
6.3 KiB
Markdown
# TBD — migrate the mall off the mock API (waves 4+)
|
||
|
||
Waves 1–3 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.
|
||
- [ ] **Public store read** — a buyer-facing shop endpoint so `stores/index`, `stores/[id]` and the cart's shop grouping leave mock. Small: products already carry `shop_id`, and the public catalog already joins shops for the active check.
|
||
- [ ] **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.
|