feat(mall): serve catalog and currency from the live API

Wave 1 of replacing the fixed-data mock adapter. The mall now selects its API
adapter per domain, with catalog and currency served live while auth, cart,
orders, shipments and invoices stay on fixed data.

Backend:
- seed the 6 x 2 x 2 category tree as reference data (migration 0005). The API
  exposes no category write route, so this cannot come from the seed script
- filter public product listing by the category subtree with a recursive CTE,
  matching the mock's existing behaviour instead of exact-match
- add sort=price with order=asc|desc, validated by hand so an unsupported value
  returns the project's ApiError 400 shape rather than axum's own rejection

Mall:
- replace the all-or-nothing mockApi boolean with a liveDomains list composed
  through a typed per-domain pick map
- source home floors, the category menu, search and product detail from the
  catalog API; banners, promos, quick links, store card and comment/coupon
  content stay local display-only content
- drop the brand facet and the sales/comments sorts: no backend model backs them
- fix salesOf/commentCountOf, which parsed digits out of the product id and so
  rendered "NaN sold" for live UUID ids; they now hash the id

Seed: 24 products across 4 shops, idempotent on re-run.

Note: the mall defaults to a live catalog, so pnpm dev:mall now expects the API
to be running; set NUXT_PUBLIC_LIVE_DOMAINS to an empty array for all-mock work.

OpenSpec change: openspec/changes/replace-mock-api-wave-1
This commit is contained in:
2026-09-17 15:15:25 +00:00
parent 44466e5e88
commit e0e833d0e5
20 changed files with 909 additions and 223 deletions
@@ -0,0 +1,33 @@
# Proposal
## Why
The mall is the only frontend still on the fixed-data mock adapter; `shop-admin` and `admin` run live. Every mall catalog surface also bypasses the `@vmall/shared` contract — 18 files import `~/mock/data` and no `$api` catalog call exists. The storefront cannot outgrow demo data, and no later domain can flip until real SKU ids come from the live catalog.
## What Changes
- Replace the all-or-nothing `mockApi` boolean with a per-domain switch; Wave 1 flips `catalog` and `currency`, leaving auth, cart, orders, shipments and invoices on mock.
- Rewire the browse surfaces (home floors, `/search`, `/goods/[id]`, category menu) from `~/mock/data` onto `listCategories`, `listProducts` and `getProduct`.
- **BREAKING** (browse UX): remove the brand facet and the `sales`/`comments` sorts, which have no backing model; add `sort=price` with `order=asc|desc`.
- Filter public product listing by category **subtree**, as the mock does today, instead of exact `category_id`.
- Grow `scripts/seed-demo.mjs` to parity: categories with children and grandchildren, ~24 bilingual products, 4 shops (live: 3 childless categories, 4 products, 1 shop).
- Banners, promos, quick links and floor art stay local.
## Capabilities
### New Capabilities
(none)
### Modified Capabilities
- `catalog`: add a public browse requirement — category subtree filtering and an optional price sort on `/api/products`.
- `frontend-mall`: "Mock API adapter" becomes a per-domain switch with catalog and currency live; browse surfaces re-sourced from the API without the brand facet or non-price sorts.
## Impact
`apps/mall/plugins/api.ts`; 18 files under `apps/mall/{pages,components}`; `apps/mall/mock/data.ts` (marketing content only); `packages/shared/src/api.ts` (`ProductListQuery` gains `sort`/`order`); `apps/api/src/routes/catalog.rs`; `scripts/seed-demo.mjs`. The shared contract change means all three frontends must rebuild.
## Non-goals
Auth, cart, orders, shipments and invoices stay mock. No backend capability for brands, storefront content, store directory, favorites, coupons or addresses; those stay mock. No change to money handling, i18n storage or the other apps.