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
34 lines
3.9 KiB
Markdown
34 lines
3.9 KiB
Markdown
# Tasks
|
|
|
|
## 1. Schema
|
|
|
|
- [x] 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
|
|
|
|
- [x] 2.1 Add `ShopProfile` and `ShopProfileInput` to `packages/shared/src/types.ts`, with every profile field nullable; verified all three frontends build
|
|
- [x] 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
|
|
|
|
- [x] 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
|
|
- [x] 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
|
|
|
|
- [x] 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
|
|
|
|
- [x] 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
|
|
- [x] 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
|
|
- [x] 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
|
|
- [x] 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
|
|
- [x] 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
|
|
|
|
- [x] 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
|
|
- [x] 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
|
|
- [x] 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
|