docs(openspec): plan wave 6, brands and real sales
Planning only: proposal, delta specs, design and tasks. No code yet. This is the last substantive in-scope box in docs/TBD-migrate-wave.md. Wave 1 removed the brand facet and the sales/comments sorts for want of a model; sales are derivable from order_items and a brand model is a table plus a column, so both come back for real. Decisions recorded in the design: - brands are reference data in their own table with a nullable products.brand_id and an ordered replace endpoint, mirroring categories and storefront content - a "sale" is a unit on an order that reached payment; pending and cancelled orders do not count, so an abandoned checkout cannot inflate the figure - sold_count is computed per read rather than stored, so it cannot drift from the orders that produced it - the review UI is removed rather than relabelled: the mall attributes invented comments to named shoppers and shows a "good rate", which a migration that makes everything else real has no business keeping on screen. Reviews are recorded as a separate future capability openspec validate --strict passes and the change is ready to apply.
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
# Spec Delta
|
||||
|
||||
## Purpose
|
||||
|
||||
The product brand registry: a bilingual, admin-managed list of manufacturers that products reference, shoppers filter listings by, and the storefront shows on a product.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Public brand list
|
||||
`GET /api/brands` SHALL return the brands without authentication, ordered by their stored position, each with an id, slug and bilingual name. Products SHALL carry a nullable `brand_id` referring to that list, and `GET /api/products` SHALL accept a `brand_id` filter that composes with the existing category, shop and keyword filters.
|
||||
|
||||
#### Scenario: filter a listing by brand
|
||||
- **WHEN** a shopper filters the catalog by a brand
|
||||
- **THEN** only that brand's published products are returned, and the filter combines with a category filter rather than replacing it
|
||||
|
||||
#### Scenario: a product without a brand
|
||||
- **WHEN** a product has no brand assigned
|
||||
- **THEN** it is still listed and its `brand_id` is null rather than pointing at an invented brand
|
||||
|
||||
### Requirement: Brand management
|
||||
A platform admin SHALL replace the brand list with `PUT /api/admin/brands`, which validates each entry, applies as one transaction and reindexes positions from the submitted order; the write SHALL require the `platform_admin` role. Each entry SHALL carry a slug and non-empty `en` and `zh` names, and a rejected list SHALL leave the stored brands untouched.
|
||||
|
||||
#### Scenario: replace round-trips
|
||||
- **WHEN** an admin replaces the brands and then reads them publicly
|
||||
- **THEN** the public list matches, in the submitted order
|
||||
|
||||
#### Scenario: incomplete bilingual name is refused
|
||||
- **WHEN** an admin submits a brand with only `en` text
|
||||
- **THEN** the request fails and the stored brands are unchanged
|
||||
@@ -0,0 +1,30 @@
|
||||
# Spec Delta
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Public product browse
|
||||
Public `GET /api/products` SHALL return only `published` products whose shop is active, and SHALL remain readable without authentication. When `category_id` is supplied, the filter SHALL match that category **and every category beneath it**, so requesting a parent category returns products assigned to its child and grandchild categories. When `brand_id` is supplied the filter SHALL match that brand and compose with the other filters. The listing SHALL accept an optional `sort` of `price` or `sales`: `price` orders by each product's lowest active SKU price, and `sales` orders by units sold across orders that reached payment, which SHALL also be reported per product as `sold_count`. Any other `sort` value SHALL be rejected with a 400 `ApiError` rather than silently ignored. An unsorted listing SHALL order newest first. Paging SHALL keep returning `page` and `per_page` alongside the filtered `total`.
|
||||
|
||||
#### Scenario: parent category includes descendant products
|
||||
- **WHEN** a shopper requests products for a category that has child categories holding published products
|
||||
- **THEN** the response contains the products assigned to those descendant categories, not only those assigned directly to the requested category
|
||||
|
||||
#### Scenario: sort by lowest active SKU price
|
||||
- **WHEN** a shopper requests the product list with `sort=price` and `order=asc`
|
||||
- **THEN** products come back ordered by their lowest active SKU price ascending
|
||||
|
||||
#### Scenario: sort by units sold
|
||||
- **WHEN** a shopper requests the product list with `sort=sales` and `order=desc`
|
||||
- **THEN** products come back ordered by their `sold_count` descending, and a product with no paid orders reports zero rather than being omitted
|
||||
|
||||
#### Scenario: filter by brand
|
||||
- **WHEN** a shopper requests products with a `brand_id` alongside a `category_id`
|
||||
- **THEN** only products matching both filters are returned, and `total` reflects the combined filter
|
||||
|
||||
#### Scenario: unsupported sort is rejected
|
||||
- **WHEN** a client requests a `sort` value that is neither `price` nor `sales`
|
||||
- **THEN** the API responds 400 with an `ApiError` body instead of ignoring the parameter
|
||||
|
||||
#### Scenario: unpublished products never appear
|
||||
- **WHEN** any public listing or filter is applied
|
||||
- **THEN** products that are not `published`, or whose shop is not active, are absent from both `items` and `total`
|
||||
@@ -0,0 +1,18 @@
|
||||
# Spec Delta
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Product discovery pages
|
||||
The mall SHALL provide `/search` with breadcrumb, category, brand and sort controls, a five-column desktop product grid, pagination and an empty state, listing products from the catalog API filtered by the selected category's subtree and brand. The sort control SHALL offer newest-first, price and sales. It SHALL provide `/goods/[id]` rendering product and SKU data from the catalog API with image gallery/zoom, bilingual name/subtitle, integer-minor-unit prices, attribute and SKU selection, stock-aware quantity, store card, and detail/after-sale tabs. Product cards and the product detail page SHALL show the product's real sold count, and the mall SHALL NOT present reviews, ratings or reviewer comments while no reviews capability exists.
|
||||
|
||||
#### Scenario: filter and inspect a product
|
||||
- **WHEN** a shopper filters the search page by a parent category and a brand, then opens a product
|
||||
- **THEN** products from that category's subtree matching the brand are listed, and selecting an in-stock SKU updates the displayed price, stock and cart target from the catalog API
|
||||
|
||||
#### Scenario: sort by sales
|
||||
- **WHEN** a shopper sorts the search results by sales
|
||||
- **THEN** the order follows the products' reported sold counts
|
||||
|
||||
#### Scenario: no invented reviews
|
||||
- **WHEN** a shopper opens a product
|
||||
- **THEN** the page shows the shop's own after-sale copy and no rating, review count or reviewer comment
|
||||
Reference in New Issue
Block a user