# 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