- 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
68 lines
3.9 KiB
Markdown
68 lines
3.9 KiB
Markdown
# 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=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`
|