feat(mall): restore real brand and sales facets
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
This commit is contained in:
+36
-10
@@ -222,24 +222,50 @@ const products = [
|
||||
{ shop: "terra-grocery", slug: "terra-leather-tote", category: "fashion-bags", price: 21900, stock: 26, name: { en: "Terra Leather Tote", zh: "大地真皮托特包" }, description: { en: "Full-grain leather tote with laptop sleeve.", zh: "头层牛皮托特包,含电脑隔层。" }, img: "terra-tote" },
|
||||
];
|
||||
|
||||
// 5b. brands (admin-managed reference data), then assign one per product
|
||||
const BRANDS = [
|
||||
{ slug: "aurora", name: { en: "Aurora", zh: "极光" } },
|
||||
{ slug: "nordwind", name: { en: "Nordwind", zh: "北风" } },
|
||||
{ slug: "hexon", name: { en: "Hexon", zh: "赫克森" } },
|
||||
{ slug: "mikado", name: { en: "Mikado", zh: "御门" } },
|
||||
{ slug: "solace", name: { en: "Solace", zh: "索莱斯" } },
|
||||
{ slug: "terra", name: { en: "Terra", zh: "大地" } },
|
||||
];
|
||||
r = await call("PUT", "/admin/brands", { token: admin, body: BRANDS });
|
||||
if (r.status !== 200) fail("replace brands", r);
|
||||
// Captured, not read back through `r`: the product loop reassigns `r`.
|
||||
const brandList = r.data;
|
||||
const brandBy = (slug) => brandList.find((b) => b.slug === slug)?.id ?? null;
|
||||
const SHOP_BRAND = {
|
||||
"demo-store": "solace",
|
||||
"aurora-digital": "aurora",
|
||||
"nordwind-home": "nordwind",
|
||||
"terra-grocery": "terra",
|
||||
};
|
||||
// A couple of deliberate exceptions so more than four brands are in use.
|
||||
const PRODUCT_BRAND = { "mechanical-keyboard": "hexon", "terra-leather-tote": "mikado" };
|
||||
console.log(`brands ready: ${r.data.length}`);
|
||||
|
||||
for (const p of products) {
|
||||
const token = ownerTokens.get(p.shop);
|
||||
r = await call("POST", "/shop/products", {
|
||||
token,
|
||||
body: {
|
||||
slug: p.slug,
|
||||
name: p.name,
|
||||
description: p.description,
|
||||
category_id: catBy(p.category),
|
||||
images: [`https://picsum.photos/seed/${p.img}/600/600`],
|
||||
},
|
||||
});
|
||||
const body = {
|
||||
slug: p.slug,
|
||||
name: p.name,
|
||||
description: p.description,
|
||||
category_id: catBy(p.category),
|
||||
brand_id: brandBy(PRODUCT_BRAND[p.slug] ?? SHOP_BRAND[p.shop]),
|
||||
images: [`https://picsum.photos/seed/${p.img}/600/600`],
|
||||
};
|
||||
r = await call("POST", "/shop/products", { token, body });
|
||||
let id;
|
||||
if (r.status === 201) id = r.data.id;
|
||||
else if (r.status === 409) {
|
||||
const list = await call("GET", "/shop/products?per_page=100", { token });
|
||||
id = list.data.items.find((x) => x.slug === p.slug)?.id;
|
||||
if (!id) fail(`lookup product ${p.slug}`, r);
|
||||
// Re-apply the body so a re-run converges the brand assignment too.
|
||||
r = await call("PUT", `/shop/products/${id}`, { token, body });
|
||||
if (r.status !== 200) fail(`update product ${p.slug}`, r);
|
||||
} else fail(`create product ${p.slug}`, r);
|
||||
|
||||
r = await call("POST", `/shop/products/${id}/skus`, {
|
||||
|
||||
Reference in New Issue
Block a user