Archive the three completed changes behind replace-mock-api-wave-1/2/3. Each merge applied cleanly to the main specs: - catalog gains the Public product browse requirement (subtree filtering and price sort) - frontend-mall picks up the per-domain adapter, the pinned home page, the discovery-page changes, the live auth panels and the live transaction flows - cart's Server-side cart requirement now documents the shop and stock carried by every line Also replace the TBD Purpose placeholder in all eleven specs with a one-line description of what each capability covers. Those placeholders predate this work and were the only reason `openspec validate --all --strict` reported 0 passed / 11 failed; it now reports 11 passed / 0 failed.
3.3 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. The listing SHALL accept an optional sort of price together with an order of asc or desc, ordering by each product's lowest active SKU price; 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: unsupported sort is rejected
- WHEN a client requests a
sortvalue that is notprice - 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