Files
vmall/openspec/changes/replace-mock-api-wave-6/specs/catalog/spec.md
T
james 65cea42da6 docs(openspec): plan wave 6, brands and real sales
Planning only: proposal, delta specs, design and tasks. No code yet.

This is the last substantive in-scope box in docs/TBD-migrate-wave.md. Wave 1
removed the brand facet and the sales/comments sorts for want of a model; sales
are derivable from order_items and a brand model is a table plus a column, so
both come back for real.

Decisions recorded in the design:

- brands are reference data in their own table with a nullable products.brand_id
  and an ordered replace endpoint, mirroring categories and storefront content
- a "sale" is a unit on an order that reached payment; pending and cancelled
  orders do not count, so an abandoned checkout cannot inflate the figure
- sold_count is computed per read rather than stored, so it cannot drift from
  the orders that produced it
- the review UI is removed rather than relabelled: the mall attributes invented
  comments to named shoppers and shows a "good rate", which a migration that
  makes everything else real has no business keeping on screen. Reviews are
  recorded as a separate future capability

openspec validate --strict passes and the change is ready to apply.
2026-09-17 17:17:52 +00:00

2.4 KiB

Spec Delta

MODIFIED Requirements

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