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
3.9 KiB
3.9 KiB
Tasks
1. Schema
- 1.1 Add a migration creating
shop_profileskeyed 1:1 toshops(shop_idprimary key,logo,banner,company,region,addressJSONB,noticeJSONB,after_saleJSONB, four score columns,updated_at); verified the table exists aftercargo run -p vmall-api
2. Shared contract
- 2.1 Add
ShopProfileandShopProfileInputtopackages/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 fromMOCK_STORES; also registered ashopsdomain 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/shopsandGET /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}/profileupserting the profile, refusing bilingual fields without non-emptyen/zhand gated toplatform_admin; verified a rejected payload leaves the stored profile untouched
4. Demo profiles
- 4.1 Teach
scripts/seed-demo.mjsto upsert a profile per demo shop; verified a re-run is idempotent andGET /api/shopsreturns all four with logos, companies and scores
5. Mall store surfaces
- 5.1
pages/stores/index.vue: list fromGET /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 byshop_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 ofstoreById, guarding every field and dropping the card entirely when a shop has no profile - 5.4
pages/checkout/pay.vueandpages/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
lowestSkuhelper out of the fixed-data module intoapps/mall/utils/product.ts, re-exported for the mock-era pages, so the live store and product pages no longer import~/mock/datafor it. A first attempt usedexport { lowestSku } from "~/utils/product", which creates no local binding and broke every internal caller at runtime
6. Verification
- 6.1
cargo test -p vmall-apigreen at 28 tests (five new shop tests), and all three frontends build. Caveat worth recording:nuxt builddoes not typecheck — there is notypescript.typeCheckinnuxt.config.tsand novue-tscinstalled — so a build passing is not the type gate AGENTS.md describes. ThelowestSku is not definedbug 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