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:
@@ -21,6 +21,7 @@ import type {
|
||||
import {
|
||||
BASE_CURRENCY,
|
||||
MOCK_BANNERS,
|
||||
MOCK_BRANDS,
|
||||
MOCK_CATEGORIES,
|
||||
MOCK_CURRENCIES,
|
||||
MOCK_PROMOS,
|
||||
@@ -162,7 +163,10 @@ export function createMockApi(): ApiClient {
|
||||
searchMockProducts({
|
||||
q: q.q,
|
||||
categoryId: q.category_id,
|
||||
brandId: q.brand_id,
|
||||
shopId: q.shop_id,
|
||||
sort: q.sort,
|
||||
order: q.order,
|
||||
page: q.page,
|
||||
perPage: q.per_page,
|
||||
}),
|
||||
@@ -175,6 +179,7 @@ export function createMockApi(): ApiClient {
|
||||
},
|
||||
|
||||
listCategories: () => Promise.resolve(MOCK_CATEGORIES),
|
||||
listBrands: () => Promise.resolve(MOCK_BRANDS),
|
||||
listCurrencies: () => Promise.resolve(MOCK_CURRENCIES),
|
||||
convert: (amountMinor, from, to) =>
|
||||
Promise.resolve({ amount_minor: mockConvertMinor(amountMinor, from, to), currency: to }),
|
||||
@@ -375,8 +380,7 @@ export function createMockApi(): ApiClient {
|
||||
})),
|
||||
}),
|
||||
|
||||
listShops: (): Promise<ShopProfile[]> => Promise.resolve(MOCK_STORES.map(toShopProfile)),
|
||||
getShop: (slug) => {
|
||||
listShops: (): Promise<ShopProfile[]> => Promise.resolve(MOCK_STORES.map(toShopProfile)), getShop: (slug) => {
|
||||
const store = storeById(slug);
|
||||
if (!store) return Promise.reject(new ApiError(404, "NOT_FOUND", "Shop not found"));
|
||||
return Promise.resolve(toShopProfile(store));
|
||||
@@ -412,6 +416,8 @@ export function createMockApi(): ApiClient {
|
||||
getContent: () => unsupported(),
|
||||
replaceContent: () => unsupported(),
|
||||
setShopProfile: () => unsupported(),
|
||||
getBrands: () => unsupported(),
|
||||
replaceBrands: () => unsupported(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user