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
This commit is contained in:
@@ -2,31 +2,32 @@
|
||||
|
||||
## 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`); verify the table exists after `cargo run -p vmall-api`
|
||||
- [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
|
||||
|
||||
- [ ] 2.1 Add `ShopProfile` (shop identity plus the optional profile fields) and a `ShopProfileInput` to `packages/shared/src/types.ts`; verify all three frontends build
|
||||
- [ ] 2.2 Add `listShops()` / `getShop(slug)` to the `ApiClient` and `createApi`, plus `admin.setShopProfile(id, body)`; give the fixed-data adapter matching implementations built from `MOCK_STORES` so the rollback path still renders; verify the mall build
|
||||
- [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
|
||||
|
||||
- [ ] 3.1 Add `GET /api/shops` returning active shops with their profile, and `GET /api/shops/{slug}` returning one, both unauthenticated and both tolerating a missing profile row; verify a suspended shop is absent from the list and an unknown slug is a 404
|
||||
- [ ] 3.2 Add `PUT /api/admin/shops/{id}/profile` upserting the profile, validating non-empty `en`/`zh` on bilingual fields and gated to `platform_admin`; verify a bad payload is refused without changing the stored row
|
||||
- [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
|
||||
|
||||
- [ ] 4.1 Teach `scripts/seed-demo.mjs` to set a profile for each demo shop using the existing `/mock/store-N.svg` assets and bilingual copy; verify a re-run is idempotent and `GET /api/shops` returns a profile for every shop
|
||||
- [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
|
||||
|
||||
- [ ] 5.1 `pages/stores/index.vue`: list from `GET /api/shops` and drop the distance column and its sort; verify the directory renders every active shop and no "km" text remains
|
||||
- [ ] 5.2 `pages/stores/[id].vue`: load the shop by slug and its products from the catalog API by `shop_id`, drop the mock sales ranking in favour of the same shop-scoped product call; verify a store page renders its profile and products
|
||||
- [ ] 5.3 `pages/goods/[id].vue`: read the store card from the shop API instead of `storeById`, keeping the existing guards for a missing profile
|
||||
- [ ] 5.4 `pages/checkout/pay.vue` and `pages/user/orders/index.vue`: resolve shop names from the cached shop read, removing the generic "Shop" placeholder
|
||||
- [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
|
||||
|
||||
- [ ] 6.1 Run the mall, shop-admin and admin builds, since the shared contract changed; verify all three pass and `cargo test -p vmall-api` stays green
|
||||
- [ ] 6.2 With the backend seeded, verify in a browser that the store directory lists the demo shops, a store home renders its profile and products, the product page's store card shows a real name, and the cart/payment/order surfaces name the shop; confirm no console errors beyond deliberate failed responses
|
||||
- [ ] 6.3 Verify the rollback: with every domain on fixed data and the backend stopped, the store directory and store home still render
|
||||
- [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
|
||||
|
||||
Reference in New Issue
Block a user