Wave 6, the last substantive piece of the mock-API migration. Wave 1 removed the brand facet and the sales/comments sorts for want of a model; sales turn out to be derivable from order_items and a brand model is a table plus a column. - a `brands` table with a nullable `products.brand_id` and an ordered admin replace, mirroring categories and storefront content; a public `GET /api/brands` and a `brand_id` filter on the catalog, which the search page's facet uses - `sold_count` per product, computed from `order_items` joined to orders that reached payment, so an abandoned or cancelled checkout cannot count as a sale. It is computed per read rather than stored, so it cannot drift from the orders that produced it - `sort=sales` alongside `sort=price`; anything else is still a 400 - merchants can set a product's brand through the existing product upsert - the review UI is gone: the card's review figure and the product detail page's reviews tab, summary and replies. There is no reviews model, and the mall attributed invented comments to named shoppers and showed a "good rate". The now-unreferenced fabrication helpers went with it (`salesOf`, `commentCountOf`, `commentsFor`, `commentStats`, `salesRankFor`, `productDetail`, `storeDetail`) Two bugs found by checking rather than trusting: the fixed-data `listProducts` had silently ignored `brand_id`, `sort` and `order`, so the restored facet rendered but filtered nothing until the rollback check caught it; and the seed's brand lookup read back through the shared `r` variable the product loop reassigns, working once and then throwing. Verified: 29 backend tests green including a new brand-and-sales case; all three frontends build; searching filters by brand (24 to 6) and sorts by sales with counts matching the API; a product page offers detail and after-sale tabs only, with a real sold count; the fixed-data rollback filters by brand too. OpenSpec change: openspec/changes/replace-mock-api-wave-6
4.1 KiB
4.1 KiB
Tasks
1. Schema and seed
- 1.1 Add a migration creating
brands(bilingual name, slug, position, active) andproducts.brand_idnullable withON DELETE SET NULL; verified the table and column exist aftercargo 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
rvariable worked once and then broke, because the product loop reassigns it
2. Shared contract
- 2.1 Add
BrandandBrandInput, the optionalbrand_idon the product payload andProductUpsertBody,brand_idonProductListQuery,"sales"on itssort, andsold_countonProduct; verified all three frontends build - 2.2 Add
listBrands(),admin.getBrands()/admin.replaceBrands(list)and fixed-data implementations, and register abrandsdomain in the per-domain switch. Also taught the fixed-datalistProductsto passbrand_id,sortandorderthrough — 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/brandsand adminPUT /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_idfilter tolistProducts, composing with the other filters, and returnbrand_idon the payload; verified a brand filter narrows 24 products to 6 and composes with the shop filter - 3.3 Accept
brand_idin 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_countper product fromorder_itemsjoined to orders inpaid,fulfilling,shippedorcompleted, exposed on the list and detail payloads; verified a product with no paid orders reports zero - 4.2 Accept
sort=saleswithorder, keeping the 400 for any other value; verified the order matches the reported counts and that apending_paymentorder moves nothing - 4.3 Extend
apps/api/tests/catalog.rswithbrand_filter_and_real_sales, covering the brand filter, the unpaid exclusion, the paid count and the sales order; verifiedcargo test -p vmall-apiis green at 29 tests and repeatable
5. Mall surfaces
- 5.1
pages/search.vue: restored the brand facet fromlistBrands()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 realsold_countand 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.ts—salesOf,commentCountOf,commentsFor,commentStats,salesRankFor,productDetail,storeDetailand their types. Nothing imported them once the review UI went, and the fixed-data sales sort now orders bysold_count, which is zero there rather than an invented number
6. Verification
- 6.1 All three frontends build and
cargo test -p vmall-apiis green at 29 tests. The browser check remains the real gate, sincenuxt builddoes 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