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
6.3 KiB
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.
- Flip
authto live and verifylogin/register/meagainst:8080using the seededcustomer@vmall.local/customer123. - 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. - Confirm the JWT round-trips through the
vmall.tokenlocalStorage 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 withEMPTY_CART(apps/mall/mock/api.ts:194), -
mock
requestInvoice()404s on any live order id (mock/api.ts:285), -
mock
listMyShipments()returns shipments whoseorder_idmatches no live order, so the shipment block is silently empty (mock/api.ts:282,pages/user/orders/[id].vue:25). -
Flip
cart,orders,shipmentsandinvoicesin 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 inreplace-mock-api-wave-3; the merchant half was driven through the API because the mall has no merchant UI. -
Verify cancel restores stock and
payOrderonly acceptspending_payment. Cancel and stock restore are asserted bycancel_rules_and_stock_restore;pay_orderis a status-guardedUPDATE ... AND status = 'pending_payment'that answers 409 otherwise (apps/api/src/routes/orders.rs:279-287). -
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). -
Remove cart's mock display coupling.
CartItemViewnow carriesshop_id,shop_nameandstock(apps/api/src/cart.rs), andpages/cart.vuegroups by them. -
Decide what caps cart quantity.
CartItem.stockis exposed and the stepper caps at it, but the API deliberately does not check stock on add — checkout stays authoritative with its 409. -
Gate add-to-cart for anonymous shoppers:
pages/goods/[id].vuesends a 401 to/login?redirect=…, andpages/login.vuehonours only same-origin paths. -
Checkout keeps sourcing
shipping_addressfromMOCK_ADDRESSES— intentional; see the out-of-scope note below. -
Fix contract debt:
Shipment.itemsis optional andInvoice.invoice_nois nullable, matching what the API returns.Invoicewas declared twice inpackages/shared/src/types.tsand TypeScript merges duplicate interfaces, so the duplicate had to go for the change to take effect. -
Remove the remaining
storeByIdmock usage on the order pages. They animate the generic store label instead; the real names need the public store read below. -
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.
- 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 publicGET /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 carryshop_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
brandstable (products.brand_id+ i18n) plus sales and comments data, neither of which exists today. - Extend the
ORDER BYwhitelist if more sorts are wanted beyond thesort=priceadded 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
ClientOnlyworkarounds incomponents/shell/TopBar.vueandpages/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:
Addressis embedded in the order and live checkout takes it in the request body, so no addresses table is needed.MOCK_ADDRESSEScan 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-adminandadminalready 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.