Files
vmall/openspec/specs/catalog/spec.md
T
james 2136a48fbe chore(openspec): archive the mock-migration waves and green the spec set
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.
2026-09-17 16:36:10 +00:00

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=price and order=asc
  • THEN products come back ordered by their lowest active SKU price ascending

Scenario: unsupported sort is rejected

  • WHEN a client requests a sort value that is not price
  • 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