docs(openspec): plan wave 5, the store directory
Planning only: proposal, delta specs, design and tasks for replacing the store surfaces. No code yet. The batch adds a public shop read (profile + products) so the store directory, store home and product-page store card stop reading MOCK_STORES, and so the cart, payment and order surfaces can name a shop instead of printing "Shop". Decisions recorded in the design: - a `shop_profiles` table beside `shops`, so the existing admin and shop-admin contracts are untouched - the four store scores stay stored profile values, because there is no review model and deriving them would dress fabricated numbers as computed ones - `distanceKm` is dropped rather than faked: there is no geo model behind it - profiles are filled by seed-demo.mjs, since they hang off shops that script creates - order surfaces resolve names from one cached shop read rather than growing a denormalised shop name on every order payload openspec validate --strict passes and the change is ready to apply.
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.
|
||||||
@@ -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
|
||||||
@@ -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,32 @@
|
|||||||
|
# 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`); verify 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
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
## 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
|
||||||
Reference in New Issue
Block a user