chore(openspec): archive replace-mock-api-wave-5
The merge creates the store-directory capability (two requirements, six scenarios) and updates the mall's store-and-marketing requirement so the store directory and store home read live shops and order surfaces name their shop. openspec validate --all --strict stays green at 13 specs.
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-09-17
|
||||
@@ -0,0 +1,57 @@
|
||||
# Design
|
||||
|
||||
## Context
|
||||
|
||||
See `proposal.md` — Why. Today the store surfaces read `MOCK_STORES` (`pages/stores/index.vue`, `pages/stores/[id].vue`, and the store card in `pages/goods/[id].vue`), whose shape is `{ id, slug, name, company, logo, banner, region, address, distanceKm, rate{score,agree,service,speed}, notice, afterSale }`.
|
||||
|
||||
The live `shops` table holds only `(id, name, slug, status, created_at)` (`apps/api/migrations/*`), and `Shop` is the shared model returned by `create_shop` and `set_shop_status` to both the admin console and shop-admin. Products already carry `shop_id`, and `listProducts({ shop_id })` already filters by it.
|
||||
|
||||
After wave 3 the cart line carries `shop_name`, but `pages/checkout/pay.vue` and `pages/user/orders/index.vue` still resolve a name through `storeById` and fall back to a generic "Shop" label, because an order carries only `shop_id`.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- The store directory, store home and product-page store card render real shops with real products.
|
||||
- Order surfaces show real shop names.
|
||||
- No field is invented to fill a UI slot.
|
||||
|
||||
**Non-Goals:**
|
||||
- No reviews model. The four scores are stored profile values set by the platform, not derived ratings.
|
||||
- No geo data: a shop has no coordinates, so distance cannot be computed.
|
||||
- No merchant self-service profile editing, and no work on the seckill, collective or integral pages.
|
||||
|
||||
## Decisions
|
||||
|
||||
**1. A `shop_profiles` table rather than new columns on `shops`.**
|
||||
Keeps identity (`name`, `slug`, `status`) separate from marketing content, and leaves the `Shop` model — and therefore the admin and shop-admin contracts — untouched. The public read composes the two with one join.
|
||||
*Alternative:* widening `shops` would ripple through both consoles' payloads for data neither of them uses.
|
||||
|
||||
**2. The four scores are stored profile values.**
|
||||
The store pages show a score row and the product page shows a store rating. Nothing derives them: there is no review model, and inventing a formula over order counts would dress fabricated numbers as computed ones. Storing them as platform-set profile fields is the honest option, and it is named as such in the non-goals so nobody mistakes them for review aggregates.
|
||||
|
||||
**3. Distance is dropped, not faked.**
|
||||
`distanceKm` sorts the directory and prints "x.x km away". There is no user location and no shop coordinates, so any number is fiction. The distance column and its sort option go, and the tracker records it.
|
||||
*Alternative:* store a distance per shop — meaningless without a reference point.
|
||||
|
||||
**4. `seed-demo.mjs` fills the profiles, not a migration.**
|
||||
Profiles hang off shops that the seed script creates, so a migration cannot reference them. Schema stays in the migration; demo profile content goes where the shops are made.
|
||||
*Consequence to accept:* the mall must render a shop with no profile row rather than assume one exists, which the spec makes explicit.
|
||||
|
||||
**5. Order surfaces resolve names from one cached public read.**
|
||||
`pages/checkout/pay.vue` and `pages/user/orders/index.vue` share a `useAsyncData` key over `GET /api/shops` and map `shop_id` to a name, so no order or shipment payload grows a denormalised shop name.
|
||||
*Alternative:* add `shop_name` to every order view — more backend surface, and it would duplicate data the shop read already owns.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [The directory now lists the four real demo shops, not the mock's fictional ones] → expected: this is real data. The layout is unchanged; the names and logos differ from the mock.
|
||||
- [Removing the distance sort removes a UI feature] → deliberate and recorded; it can return with a geo model.
|
||||
- [Stored scores could be read as real ratings] → named as profile values in the capability spec and the non-goals.
|
||||
- [A shop without a profile must not render blank boxes] → the spec requires the fields to be absent rather than invented, and the pages guard each block.
|
||||
- [One extra request on cart, payment and order pages] → cached under a single key, and it replaces lookups that currently miss and print a placeholder.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Ship the migration (schema only) and the two endpoints; nothing reads them yet.
|
||||
2. Teach `seed-demo.mjs` to set each shop's profile, then re-run it.
|
||||
3. Flip the store pages, the store card and the order surfaces in one commit.
|
||||
4. Rollback: revert those pages. `shop_profiles` is additive and harmless if left in place.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Proposal
|
||||
|
||||
## Why
|
||||
|
||||
The store directory and store home still render four hard-coded shops, and the cart, payment and order surfaces fall back to a generic "Shop" label because nothing exposes a buyer-facing shop read. Products already carry `shop_id`; the missing piece is the profile.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add a `shop_profiles` table keyed to shops — logo, banner, company, region, address, notice, after-sale text and four scores — kept separate from `shops` so the admin and shop-admin contracts do not change.
|
||||
- Add public `GET /api/shops` and `GET /api/shops/{slug}` returning a profile composed with its shop; the store directory, store home and product-page store card read them, taking products from the catalog API by `shop_id`.
|
||||
- Add `PUT /api/admin/shops/{id}/profile` for platform admins, and fill the four demo shops from `seed-demo.mjs`, which is the only place that knows the shops exist.
|
||||
- Retire the mock's `distanceKm`: there is no geo model behind it, so the distance column and its sort are removed rather than filled with an invented number.
|
||||
- Let the cart, payment and order surfaces resolve `shop_id` to a real name from that read, retiring the generic "Shop" label.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `store-directory`: the buyer-facing view of a shop — its profile and its products — read publicly and maintained by platform admins.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `frontend-mall`: the store directory and store home read live shops instead of fixed mock content, and order surfaces show real shop names.
|
||||
|
||||
## Impact
|
||||
|
||||
A new migration and route module; `packages/shared/src/{api,types}.ts`; `pages/stores/{index,[id]}.vue`, the store card in `pages/goods/[id].vue`, `pages/checkout/pay.vue` and `pages/user/orders/index.vue`; `scripts/seed-demo.mjs`; the mall's fixed-data adapter. The contract change rebuilds all three frontends.
|
||||
|
||||
## Non-goals
|
||||
|
||||
No reviews model: the four scores stay stored profile values, not derived ratings. No geo data or distance. No merchant self-service editing — platform admin only. No work on the seckill, collective or integral pages, nor coupons, favourites or addresses.
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
# Spec Delta
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Store and marketing pages
|
||||
The mall SHALL provide a store directory and store home reading live shops from the shop API, with each store's products coming from the catalog API, and SHALL keep the timed seckill page, collective-buy list and points-mall home on fixed bilingual content for now. The cart, payment and order surfaces SHALL show each order's shop by name from the shop API rather than a generic label. Marketing pages MAY be display-only except navigation to product detail.
|
||||
|
||||
#### Scenario: navigate storefront discovery channels
|
||||
- **WHEN** a shopper opens stores, seckill, collective or integral routes
|
||||
- **THEN** each page renders the appropriate B2B2C mall-style banner/filter/session/card layout, the store directory and store home list real shops, and product links resolve to product detail
|
||||
|
||||
#### Scenario: order surfaces name the shop
|
||||
- **WHEN** a shopper views a cart line, a payment group or an order card
|
||||
- **THEN** the shop is shown by its name, not a placeholder
|
||||
|
||||
#### Scenario: a shop without a profile still renders
|
||||
- **WHEN** a shop has no profile row
|
||||
- **THEN** the directory and store home render it without inventing logo, scores or copy
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
# Spec Delta
|
||||
|
||||
## Purpose
|
||||
|
||||
The buyer-facing view of a shop: its public profile — identity, contact and service copy — alongside the products it sells, so the store directory, a store's home page and the store card on a product page all read real shops.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Public shop directory
|
||||
`GET /api/shops` SHALL list the active shops without authentication, and `GET /api/shops/{slug}` SHALL return one shop by slug, answering 404 for an unknown slug or a suspended shop. Each entry SHALL carry the shop's id, slug, bilingual name and its profile: logo and banner URLs, company, region, bilingual address, bilingual notice and after-sale copy, and four score values. A shop with no profile row SHALL still be returned, with the profile fields absent rather than invented.
|
||||
|
||||
#### Scenario: directory lists active shops only
|
||||
- **WHEN** a suspended shop exists alongside active ones
|
||||
- **THEN** the directory lists only the active shops
|
||||
|
||||
#### Scenario: store home by slug
|
||||
- **WHEN** a shopper opens a shop's slug
|
||||
- **THEN** the profile and the shop's products are available, with the products filtered by that shop through the catalog API
|
||||
|
||||
#### Scenario: unknown slug is a 404
|
||||
- **WHEN** a shopper opens a slug that does not exist
|
||||
- **THEN** the API answers 404 rather than an empty profile
|
||||
|
||||
### Requirement: Shop profile management
|
||||
A platform admin SHALL set a shop's profile with `PUT /api/admin/shops/{id}/profile`, which upserts the profile row and returns the composed shop. Bilingual fields SHALL carry non-empty `en` and `zh` text, and the write SHALL require the `platform_admin` role.
|
||||
|
||||
#### Scenario: profile upsert round-trips
|
||||
- **WHEN** an admin sets a profile and then reads the shop publicly
|
||||
- **THEN** the public read returns those values
|
||||
|
||||
#### Scenario: incomplete bilingual text is refused
|
||||
- **WHEN** an admin submits a notice with only `en` text
|
||||
- **THEN** the request is refused and the stored profile is unchanged
|
||||
|
||||
#### Scenario: only platform admins may write
|
||||
- **WHEN** a shop owner or customer submits a profile
|
||||
- **THEN** the API refuses the write
|
||||
@@ -0,0 +1,33 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user