Files
vmall/openspec/specs/product/spec.md
T
Chengdong Zhang 4d2ee3b0bf Archive add-favorites and split-catalog-into-product-category-brand
- 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
2026-09-22 16:27:42 +08:00

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=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