Files
vmall/docs/TBD-migrate-wave.md
T
james e1a0a5dbdb feat(mall): run the transaction chain against the live API
Wave 3 of replacing the fixed-data mock adapter: cart, orders, shipments and
invoices flip together, so one purchase runs end to end against the backend.

- cart: CartItemView carries the line's shop and the SKU's stock, so the cart
  keeps grouping per shop and the quantity stepper caps at real stock instead
  of a hard-coded 999
- contract: Shipment.items is optional and Invoice.invoice_no nullable, both
  matching what the API actually returns. Invoice was declared twice in
  types.ts and TypeScript merges duplicate interfaces, so the duplicate had to
  go for the change to take effect at all
- an anonymous add-to-cart redirects to /login?redirect=..., and sign-in
  honours only same-origin paths
- the fixed-data adapter learns the new cart fields, and its persisted state
  key moves to v2 because a cart saved by an older build is no longer valid
- order surfaces drop their storeById lookups and keep the generic store label
  until the public store read arrives

Verified end to end: two-shop cart grouping with live shop names, stock caps
read from the API, checkout, payment, shipment, delivery confirmation and an
issued invoice. Rollback re-verified with every domain on fixed data and the
backend stopped.

Also checks off Wave 3 in docs/TBD-migrate-wave.md and re-points that file at
the mock content that remains.

OpenSpec change: openspec/changes/replace-mock-api-wave-3
2026-09-17 16:33:22 +00:00

6.3 KiB
Raw Blame History

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.

  • Flip auth to live and verify login / register / me against :8080 using the seeded customer@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.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).

  • 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.

  • 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).

  • 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. CartItemView now carries shop_id, shop_name and stock (apps/api/src/cart.rs), and pages/cart.vue groups by them.

  • 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.

  • 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.

  • Checkout keeps sourcing shipping_address from MOCK_ADDRESSES — intentional; see the out-of-scope note below.

  • 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.

  • 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.

  • 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. Needs real tables, admin CRUD and i18n JSONB. Do this first if you want the home page fully live; it is the most visible remaining mock surface.
  • 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.