The merge creates the brand capability (two requirements) and extends the catalog browse requirement with the brand filter, the sales sort and a real sold count. openspec validate --all --strict stays green at 14 specs. With every box now checked, docs/TBD-migrate-wave.md is deleted, as its own instructions require. Its two durable notes move to the README instead of disappearing with it: - `nuxt build` does not typecheck, so a green build is not the type gate the verification section claimed. AGENTS.md told me to treat it as one, which is why waves 1-5 over-reported "builds pass" as evidence. - the list of what stays deliberately mock: addresses, coupons, favourites, account stats, the seckill/collective/integral pages, reviews, and the fixed-data adapter itself, which the adapter spec requires for rollback. The README's verification block also gains the real test count (29, not 16).
3.9 KiB
catalog Specification
Purpose
The public product and category catalogue: localized content, publish lifecycle, shop isolation, SKU pricing and public browse.
Requirements
Requirement: Localized product content
Product and category names/descriptions SHALL be stored as JSONB maps keyed by locale (en, zh). The API MUST return the full map; clients pick the display locale.
Scenario: bilingual round-trip
- WHEN a shop owner creates a product with name
{"en": "Mug", "zh": "马克杯"} - THEN both public detail and shop-admin GET return the identical map
Requirement: Publish lifecycle
Products SHALL have status draft | published | unpublished. Only published products appear in public list/detail.
Scenario: publish then unpublish
- WHEN a product is published
- THEN it appears in
GET /api/products - WHEN it is unpublished
- THEN public detail returns 404 and it disappears from listings
Scenario: publish requires sellable SKU
- WHEN publishing a product with no active SKU having price > 0
- THEN the API returns 400
Requirement: Shop isolation
Shop-role users SHALL only see and mutate their own shop's products under /api/shop/products.
Scenario: cross-shop access denied
- WHEN shop owner A requests
/api/shop/products/{id}of shop B - THEN the API returns 404
Requirement: SKU pricing
Each SKU SHALL carry price_minor (integer minor units) and an ISO currency code; stock is a non-negative integer.
Scenario: negative stock rejected
- WHEN upserting a SKU with stock < 0
- THEN the API returns 400
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=priceandorder=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=salesandorder=desc - THEN products come back ordered by their
sold_countdescending, 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_idalongside acategory_id - THEN only products matching both filters are returned, and
totalreflects the combined filter
Scenario: unsupported sort is rejected
- WHEN a client requests a
sortvalue that is neitherpricenorsales - THEN the API responds 400 with an
ApiErrorbody 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 bothitemsandtotal