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.
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
# Design
|
||||
|
||||
## Context
|
||||
|
||||
See `proposal.md` — Why. Wave 1 removed three things the catalog could not answer: the brand facet, and the sales and comments sorts (`search.vue` keeps only newest and price; `catalog.rs` accepts only `sort=price`).
|
||||
|
||||
Two of them now have real backing:
|
||||
|
||||
- `order_items` (`apps/api/migrations/0004_orders.sql`) records `sku_id` and `qty` per order line, and `skus.product_id` ties those to products — so units sold is derivable.
|
||||
- `products` has no brand column, and `ProductUpsertBody` (`packages/shared/src/api.ts`) carries `category_id`, `slug`, `name`, `description` and `images` only, so merchants cannot set a brand today either.
|
||||
|
||||
The mall's current figures are hashes of the product id: `salesOf` and `commentCountOf` in `apps/mall/mock/data.ts`. `ProductCard.vue` renders both as "N sold · N reviews", and `pages/goods/[id].vue` renders a reviews tab with invented reviewers, ratings and replies.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Both remaining facets answer from real data: the brand filter from a brand table, the sales sort from order lines.
|
||||
- Nothing in the product surfaces presents an invented figure as fact.
|
||||
|
||||
**Non-Goals:**
|
||||
- No reviews capability. Writing, moderating and displaying reviews is a feature with its own lifecycle.
|
||||
- No admin or shop-admin console UI for brands this wave — the API and the seed are enough to make them real.
|
||||
- No change to the seckill, collective or integral pages, nor to coupons, favourites, addresses or the session cookie.
|
||||
|
||||
## Decisions
|
||||
|
||||
**1. Brands are reference data in their own table, with a nullable `products.brand_id`.**
|
||||
Mirrors `categories`: a flat list with a position, administered centrally, referenced by products.
|
||||
*Alternative:* a free-text brand on each product — no filterable vocabulary, and every shop would spell the same manufacturer differently.
|
||||
|
||||
**2. `PUT /api/admin/brands` replaces the ordered list, as storefront content does.**
|
||||
Small ordered lists are edited whole; replacing reindexes positions and makes reordering, adding and deleting one operation.
|
||||
|
||||
**3. A "sale" is a unit on an order that reached payment.**
|
||||
`sold_count` counts `order_items.qty` joined to orders whose status is `paid`, `fulfilling`, `shipped` or `completed`. Orders still `pending_payment` and `cancelled` orders do not count: a cart that was abandoned is not a sale, and a cancelled one was refunded by stock restore.
|
||||
*Alternative:* counting every order line inflates the figure with abandoned checkouts.
|
||||
|
||||
**4. `sold_count` is computed per read, not stored.**
|
||||
A grouped aggregate over `order_items` joins the existing product queries, so the number cannot drift from the orders that produced it.
|
||||
*Alternative:* a `products.sold_count` column maintained when payment or cancellation happens — faster, but it makes correctness depend on every future order-path change, and this is cheap at MVP scale.
|
||||
|
||||
**5. The review UI goes, and reviews become declared future work.**
|
||||
The product cards drop their review figure and the product detail page drops its reviews tab, summary and reply blocks. The mall attributes comments to named shoppers and shows a "good rate" percentage; a migration that turns everything else real has no business keeping invented opinions on screen. The after-sale tab stays, because it shows the shop's own `after_sale` copy.
|
||||
*Alternative:* keep the reviews as sample content labelled as such — rejected: a demo label on a review list does not survive the page being shown to anyone else, and the data is attributed to customers.
|
||||
|
||||
**6. Merchants set a product's brand through the existing product upsert.**
|
||||
`ProductUpsertBody` gains an optional `brand_id`, so `seed-demo.mjs` can assign brands to the demo products and merchants can do the same from their console.
|
||||
*Alternative:* assign brands by direct SQL in the seed — bypasses the API the console uses, and leaves the field unsettable in the product form.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [Removing the reviews tab is a visible feature loss] → deliberate, recorded in the proposal and the tracker, with a reviews capability named as the way back.
|
||||
- [The sales aggregate runs on every listing query] → acceptable at this scale; a stored counter is the escape hatch if it ever matters, and decision 4 says why it is not the default.
|
||||
- [A brand can be deleted while products reference it] → `products.brand_id` is `ON DELETE SET NULL`, so products survive with no brand rather than disappearing.
|
||||
- [The brand facet reappears with an empty list until brands are seeded] → the seed assigns the demo brands, and the facet renders only when brands exist.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Ship the migration (brands table plus the nullable column), the brand endpoints and the `sold_count` aggregate; nothing in the mall reads them yet.
|
||||
2. Seed brands and assign them to the demo products.
|
||||
3. Flip the search facet, the sorts, the card figure and the detail page in one commit.
|
||||
4. Rollback: revert those pages. The brand tables are additive, and the fixed-data adapter keeps serving `brandId` from `PRODUCT_BRAND`.
|
||||
Reference in New Issue
Block a user