Files
vmall/openspec/changes/replace-mock-api-wave-5/tasks.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

3.9 KiB

Tasks

1. Schema

  • 1.1 Add a migration creating shop_profiles keyed 1:1 to shops (shop_id primary key, logo, banner, company, region, address JSONB, notice JSONB, after_sale JSONB, four score columns, updated_at); verified the table exists after cargo run -p vmall-api

2. Shared contract

  • 2.1 Add ShopProfile and ShopProfileInput to packages/shared/src/types.ts, with every profile field nullable; verified all three frontends build
  • 2.2 Add listShops() / getShop(slug), admin.setShopProfile(id, body) and fixed-data implementations built from MOCK_STORES; also registered a shops domain in the per-domain switch. Unlike wave 4, where that registration was missed and only the browser caught it, here the domain was added alongside the methods

3. Public shop read

  • 3.1 Add unauthenticated GET /api/shops and GET /api/shops/{slug}, both tolerating a missing profile row; verified a suspended shop is absent from the list and that a suspended or unknown slug is a 404
  • 3.2 Add PUT /api/admin/shops/{id}/profile upserting the profile, refusing bilingual fields without non-empty en/zh and gated to platform_admin; verified a rejected payload leaves the stored profile untouched

4. Demo profiles

  • 4.1 Teach scripts/seed-demo.mjs to upsert a profile per demo shop; verified a re-run is idempotent and GET /api/shops returns all four with logos, companies and scores

5. Mall store surfaces

  • 5.1 pages/stores/index.vue: list from GET /api/shops, dropping the distance column, its sort and the whole two-option sort control that existed only to offer it; verified four shops render with their logos and no "km" text. A shop with no logo renders an initial-letter placeholder rather than an invented image
  • 5.2 pages/stores/[id].vue: load the shop by slug and its products from the catalog API by shop_id; verified the profile, its score rows and six products render. Two removals were needed beyond the plan, both following the wave-1 precedent that a UI must not claim what no model backs: the sort is now default + price only (sales and comments have no model), and the "best sellers" rail is gone because nothing ranks by sales
  • 5.3 pages/goods/[id].vue: read the store card from the shared shop read instead of storeById, guarding every field and dropping the card entirely when a shop has no profile
  • 5.4 pages/checkout/pay.vue and pages/user/orders/index.vue: resolve shop names from the same cached shop read; verified order cards name Terra Grocery, Aurora Digital and Demo Store instead of the placeholder
  • 5.5 Moved the pure lowestSku helper out of the fixed-data module into apps/mall/utils/product.ts, re-exported for the mock-era pages, so the live store and product pages no longer import ~/mock/data for it. A first attempt used export { lowestSku } from "~/utils/product", which creates no local binding and broke every internal caller at runtime

6. Verification

  • 6.1 cargo test -p vmall-api green at 28 tests (five new shop tests), and all three frontends build. Caveat worth recording: nuxt build does not typecheck — there is no typescript.typeCheck in nuxt.config.ts and no vue-tsc installed — so a build passing is not the type gate AGENTS.md describes. The lowestSku is not defined bug above passed the build and only failed in the browser, which is how it was found
  • 6.2 Verified in a browser: the directory lists the four demo shops with real logos and no distance text, a store home shows its profile and six products with default/price sorts, the product page's store card names Aurora Digital and links to its store, and order cards name their shop. Console showed only the header's signed-out cart 401, once per page load
  • 6.3 Verified the rollback: with every domain on fixed data and the backend stopped, the directory renders the four mock stores with their logos and a mock store home renders its profile and products