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
This commit is contained in:
@@ -17,3 +17,4 @@ dist/
|
|||||||
.omp/
|
.omp/
|
||||||
.opencode/
|
.opencode/
|
||||||
.omc/
|
.omc/
|
||||||
|
.superpowers/
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# category Specification
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
The product category tree: localized names and the subtree-matching filter behavior used when browsing products by category.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Requirement: Localized category content
|
||||||
|
Category names 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** an admin creates a category with name `{"en": "Mugs", "zh": "马克杯"}`
|
||||||
|
- **THEN** `GET /api/categories` returns the identical map for that category
|
||||||
|
|
||||||
|
### Requirement: Category subtree browse filtering
|
||||||
|
`GET /api/categories` SHALL return the full category tree as a flat list, each with `id`, `parent_id`, `name`, `slug`, and `position`, ordered by `position` then `slug`. When a product listing is filtered by `category_id`, the filter SHALL match that category **and every category beneath it** in the tree, so requesting a parent category returns products assigned to its child and grandchild categories.
|
||||||
|
|
||||||
|
#### 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
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# favorites Specification
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Customer-owned favorites over products and shops: idempotent add/remove and paginated, target-aware listing with live public target data.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Requirement: Customer-owned favorite targets
|
||||||
|
A favorite SHALL belong to one authenticated customer and reference exactly one product or one shop. The database SHALL enforce target foreign keys and no more than one favorite per customer and target. Customers SHALL never read or mutate another customer's favorites.
|
||||||
|
|
||||||
|
#### Scenario: target shape is enforced
|
||||||
|
- **WHEN** a favorite row would reference both a product and a shop or neither target
|
||||||
|
- **THEN** the database rejects the row
|
||||||
|
|
||||||
|
#### Scenario: ownership filters every operation
|
||||||
|
- **WHEN** one customer lists or removes favorites
|
||||||
|
- **THEN** only that customer's rows are read or changed
|
||||||
|
|
||||||
|
### Requirement: Idempotent favorite mutation
|
||||||
|
An authenticated customer SHALL add or remove a published product belonging to an active shop or an active shop as a favorite. Repeating the same add SHALL return the existing favorite without creating a duplicate, and repeating the same remove SHALL succeed with the target still unfavorited. A missing or unavailable target SHALL return 404 on add.
|
||||||
|
|
||||||
|
#### Scenario: repeated product add
|
||||||
|
- **WHEN** a customer adds the same published product twice
|
||||||
|
- **THEN** both requests succeed and exactly one favorite row exists
|
||||||
|
|
||||||
|
#### Scenario: repeated shop removal
|
||||||
|
- **WHEN** a customer removes the same shop favorite twice
|
||||||
|
- **THEN** both requests succeed and no favorite row remains
|
||||||
|
|
||||||
|
### Requirement: Paginated target-aware favorite listing
|
||||||
|
An authenticated customer SHALL list favorites filtered by product or shop kind, with optional target ID and pagination. Each result SHALL be a discriminated favorite containing current public target summary data; product summaries SHALL include the current lowest active SKU price and currency. Unpublished products, products of inactive shops, and inactive shops SHALL be absent, and `total` SHALL count only visible results.
|
||||||
|
|
||||||
|
#### Scenario: buyer center loads product favorites
|
||||||
|
- **WHEN** a customer lists product favorites
|
||||||
|
- **THEN** each visible row contains product card data without additional per-product requests
|
||||||
|
|
||||||
|
#### Scenario: unavailable target is hidden
|
||||||
|
- **WHEN** a favorited product becomes unpublished
|
||||||
|
- **THEN** it is absent from the favorite list and total until it becomes publicly available again
|
||||||
@@ -211,3 +211,22 @@ The mall SHALL render active group-buying activities and open groups from the sh
|
|||||||
#### Scenario: product detail stays on catalog price
|
#### Scenario: product detail stays on catalog price
|
||||||
- **WHEN** a shopper opens a catalog product that also has an active group-buying activity
|
- **WHEN** a shopper opens a catalog product that also has an active group-buying activity
|
||||||
- **THEN** the product-detail page still shows catalog SKU prices and does not require a group-buy overlay in this change
|
- **THEN** the product-detail page still shows catalog SKU prices and does not require a group-buy overlay in this change
|
||||||
|
|
||||||
|
### Requirement: Live customer favorites
|
||||||
|
The mall SHALL use the shared selected API adapter for product and shop favorite state. Product and store detail controls SHALL load persisted state, require customer authentication, prevent duplicate in-flight mutations, and survive reloads. The buyer-center favorites page and dashboard preview/counts SHALL render paginated live favorite results and remove targets through the API instead of reading `MOCK_FAVORITES` or mutating local-only state.
|
||||||
|
|
||||||
|
#### Scenario: product favorite survives reload
|
||||||
|
- **WHEN** a signed-in shopper favorites a product and reloads its detail page
|
||||||
|
- **THEN** the favorite control remains selected from backend state
|
||||||
|
|
||||||
|
#### Scenario: anonymous favorite requires sign-in
|
||||||
|
- **WHEN** a signed-out shopper uses a product or store favorite control
|
||||||
|
- **THEN** the mall sends the shopper to sign in with the current detail URL as the return destination
|
||||||
|
|
||||||
|
#### Scenario: remove from buyer center
|
||||||
|
- **WHEN** a shopper removes a product or shop from the favorites page
|
||||||
|
- **THEN** the API state, visible list, dashboard preview, and visible count reflect the removal without fixture mutation
|
||||||
|
|
||||||
|
#### Scenario: fixed adapter remains functional
|
||||||
|
- **WHEN** the favorites domain is configured to fixed data
|
||||||
|
- **THEN** detail controls and buyer-center favorite flows behave deterministically through the same shared client methods
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
# catalog Specification
|
# product Specification
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
The public product and category catalogue: localized content, publish lifecycle, shop isolation, SKU pricing and public browse.
|
Shop-owned products and their SKUs: localized content, publish lifecycle, shop isolation, SKU pricing and public browse.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
### Requirement: Localized product content
|
### 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.
|
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
|
#### Scenario: bilingual round-trip
|
||||||
- **WHEN** a shop owner creates a product with name `{"en": "Mug", "zh": "马克杯"}`
|
- **WHEN** a shop owner creates a product with name `{"en": "Mug", "zh": "马克杯"}`
|
||||||
@@ -40,7 +40,7 @@ Each SKU SHALL carry `price_minor` (integer minor units) and an ISO `currency` c
|
|||||||
- **THEN** the API returns 400
|
- **THEN** the API returns 400
|
||||||
|
|
||||||
### Requirement: Public product browse
|
### 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`.
|
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
|
#### Scenario: parent category includes descendant products
|
||||||
- **WHEN** a shopper requests products for a category that has child categories holding published products
|
- **WHEN** a shopper requests products for a category that has child categories holding published products
|
||||||
Reference in New Issue
Block a user