- Sync favorites capability spec and frontend-mall live-favorites requirement into openspec/specs/ - Split catalog spec into new category and product capability specs; remove now-empty catalog spec - Move both completed changes to openspec/changes/archive/ - Ignore .superpowers/ scratch directory
3.9 KiB
product Specification
Purpose
Shop-owned products and their SKUs: localized content, publish lifecycle, shop isolation, SKU pricing and public browse.
Requirements
Requirement: Localized product content
Product 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 (per the category capability's subtree matching). 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