Files
vmall/openspec/changes/replace-mock-api-wave-6/tasks.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

3.3 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; verify the column and table exist after cargo run -p vmall-api
  • 1.2 Seed the six demo brands and assign them to the demo products from scripts/seed-demo.mjs; verify a re-run is idempotent and GET /api/brands returns them in order

2. Shared contract

  • 2.1 Add Brand and BrandInput to packages/shared/src/types.ts, add the optional brand_id to the product payload and ProductUpsertBody, and add brand_id to ProductListQuery plus "sales" to its sort; verify all three frontends build
  • 2.2 Add listBrands(), admin.getBrands() / admin.replaceBrands(list) to the ApiClient and createApi, and give the fixed-data adapter matching implementations so the rollback path still serves a brand list and a brand filter; register a brands domain in the per-domain switch

3. Catalog: brands

  • 3.1 Add GET /api/brands (public, position-ordered) and PUT /api/admin/brands (admin, transactional replace, validating slug and non-empty en/zh); verify a rejected list changes nothing and a non-admin is refused
  • 3.2 Add the brand_id filter to listProducts, composing with the category, shop and keyword filters, and return brand_id on the product payload; verify a brand plus category filter narrows correctly
  • 3.3 Accept brand_id in the shop product upsert so a merchant, and the seed, can set it; verify a merchant can set and clear it on their own product only

4. Catalog: real sales

  • 4.1 Compute sold_count per product from order_items joined to orders that reached payment (paid, fulfilling, shipped, completed), exposed on the product list and detail payloads; verify a product with no paid orders reports zero
  • 4.2 Accept sort=sales with order=asc|desc, keeping the 400 for any other sort value; verify descending order matches the reported counts and that an unpaid order does not move a product
  • 4.3 Extend apps/api/tests/catalog.rs with brand filtering, the sales order and the unpaid-order exclusion; verify cargo test -p vmall-api is green and repeatable

5. Mall surfaces

  • 5.1 pages/search.vue: restore the brand facet from listBrands() and add the sales sort; verify the facet renders only when brands exist and that both filters compose
  • 5.2 components/ui/ProductCard.vue: show the product's real sold_count and drop the review figure; verify no fabricated number remains
  • 5.3 pages/goods/[id].vue: show the real sold count, and remove the reviews tab, its summary and reply blocks along with their mock imports; verify the page renders detail and after-sale tabs only

6. Verification

  • 6.1 Run all three frontend builds and cargo test -p vmall-api; verify green. Treat the browser check as the real gate, since nuxt build does not typecheck (recorded in docs/TBD-migrate-wave.md)
  • 6.2 With the backend seeded, verify in a browser: the search page filters by brand and sorts by sales, a product card shows a real sold count, and the product page has no reviews while keeping its after-sale copy
  • 6.3 Verify the rollback: with every domain on fixed data and the backend stopped, the search facet and sorts still work from the fixed-data brand list