Files
vmall/openspec/changes/archive/2026-09-17-replace-mock-api-wave-6/tasks.md
T
james e457339847 chore(openspec): archive wave 6 and close the migration tracker
The merge creates the brand capability (two requirements) and extends the
catalog browse requirement with the brand filter, the sales sort and a real
sold count. openspec validate --all --strict stays green at 14 specs.

With every box now checked, docs/TBD-migrate-wave.md is deleted, as its own
instructions require. Its two durable notes move to the README instead of
disappearing with it:

- `nuxt build` does not typecheck, so a green build is not the type gate the
  verification section claimed. AGENTS.md told me to treat it as one, which is
  why waves 1-5 over-reported "builds pass" as evidence.
- the list of what stays deliberately mock: addresses, coupons, favourites,
  account stats, the seckill/collective/integral pages, reviews, and the
  fixed-data adapter itself, which the adapter spec requires for rollback.

The README's verification block also gains the real test count (29, not 16).
2026-09-17 17:36:22 +00:00

4.1 KiB

Tasks

1. Schema and seed

  • 1.1 Add a migration creating brands (bilingual name, slug, position, active) and products.brand_id nullable with ON DELETE SET NULL; verified the table and column exist after cargo run -p vmall-api
  • 1.2 Seed the six demo brands and assign them to the demo products; verified a re-run is idempotent and all 24 products end up with a brand. The seed re-applies each product body on the 409 path so a re-run converges the assignment, and the brand list is captured in a local const — reading it back through the shared r variable worked once and then broke, because the product loop reassigns it

2. Shared contract

  • 2.1 Add Brand and BrandInput, the optional brand_id on the product payload and ProductUpsertBody, brand_id on ProductListQuery, "sales" on its sort, and sold_count on Product; verified all three frontends build
  • 2.2 Add listBrands(), admin.getBrands() / admin.replaceBrands(list) and fixed-data implementations, and register a brands domain in the per-domain switch. Also taught the fixed-data listProducts to pass brand_id, sort and order through — it had silently ignored all three, so the restored facet rendered but filtered nothing until the rollback check caught it

3. Catalog: brands

  • 3.1 Add public GET /api/brands and admin PUT /api/admin/brands (transactional replace, slug and bilingual-name validation); verified a non-admin is refused and a duplicate slug is a 400
  • 3.2 Add the brand_id filter to listProducts, composing with the other filters, and return brand_id on the payload; verified a brand filter narrows 24 products to 6 and composes with the shop filter
  • 3.3 Accept brand_id in the shop product upsert, on both create and update, so a merchant — and the seed — can set or clear it

4. Catalog: real sales

  • 4.1 Compute sold_count per product from order_items joined to orders in paid, fulfilling, shipped or completed, exposed on the list and detail payloads; verified a product with no paid orders reports zero
  • 4.2 Accept sort=sales with order, keeping the 400 for any other value; verified the order matches the reported counts and that a pending_payment order moves nothing
  • 4.3 Extend apps/api/tests/catalog.rs with brand_filter_and_real_sales, covering the brand filter, the unpaid exclusion, the paid count and the sales order; verified cargo test -p vmall-api is green at 29 tests and repeatable

5. Mall surfaces

  • 5.1 pages/search.vue: restored the brand facet from listBrands() and the sales sort; verified the facet renders only when brands exist and that brand plus category compose
  • 5.2 components/ui/ProductCard.vue: shows the product's real sold_count and no review figure; verified the card reads "N sold" only
  • 5.3 pages/goods/[id].vue: shows the real sold count, and the reviews tab, its summary and reply blocks are gone; verified the page offers only the detail and after-sale tabs, keeps the store card, and still shows the shop's after-sale copy
  • 5.4 Removed the now-unreferenced fabrication cluster from apps/mall/mock/data.tssalesOf, commentCountOf, commentsFor, commentStats, salesRankFor, productDetail, storeDetail and their types. Nothing imported them once the review UI went, and the fixed-data sales sort now orders by sold_count, which is zero there rather than an invented number

6. Verification

  • 6.1 All three frontends build and cargo test -p vmall-api is green at 29 tests. The browser check remains the real gate, since nuxt build does not typecheck
  • 6.2 Verified live in a browser: the search page shows the brand facet and a Sales sort, filtering by a brand narrows 24 products to 6, descending sales puts the sold products first with counts matching the API, product cards show only a real sold count, and the product page has no reviews while keeping its after-sale copy and store card
  • 6.3 Verified the rollback: with every domain on fixed data and the backend stopped, the facet renders the fixed-data brands and filtering by one narrows 24 products to 2