feat(mall): read the store directory from the API
Wave 5: the store directory, store home and product-page store card stop reading
MOCK_STORES, and the payment and order surfaces name their shop.
- a `shop_profiles` table beside `shops`, so the identity model both consoles
consume is untouched, with a public `GET /api/shops` and `GET /api/shops/{slug}`
and an admin `PUT /api/admin/shops/{id}/profile`
- a shop with no profile is still listed, with the fields absent rather than
invented; the pages guard every block, and a missing logo renders an
initial-letter placeholder
- `scripts/seed-demo.mjs` upserts a profile per demo shop, since profiles hang
off shops that script creates
- payment and order pages resolve shop ids to names from one cached shop read,
retiring the generic "Shop" label
- three things went rather than being faked, following the wave-1 precedent:
`distanceKm` and its sort (no geo model), the store home's sales/comments
sorts, and its "best sellers" rail (no sales model)
- `lowestSku` moved out of the fixed-data module into `apps/mall/utils/product.ts`
and re-exported, so live pages stop importing the mock module for a pure
helper
Verified: 28 backend tests green including five new shop tests; all three
frontends build; the directory, store home, store card and order cards all render
real data with no distance or sales claims; the fixed-data rollback still renders
the store surfaces with the backend stopped.
Note: `nuxt build` does not typecheck in this repo (no `typescript.typeCheck`,
no `vue-tsc`), which AGENTS.md implies it does. A re-export used here created no
local binding and broke internal callers at runtime while the build stayed green;
`docs/TBD-migrate-wave.md` records the gap.
OpenSpec change: openspec/changes/replace-mock-api-wave-5
This commit is contained in:
@@ -15,6 +15,7 @@ import type {
|
||||
Order,
|
||||
Product,
|
||||
Shipment,
|
||||
ShopProfile,
|
||||
User,
|
||||
} from "@vmall/shared";
|
||||
import {
|
||||
@@ -87,6 +88,28 @@ function unsupported(): never {
|
||||
throw new ApiError(501, "MOCK_UNSUPPORTED", "This admin/shop endpoint is not part of the mall mock.");
|
||||
}
|
||||
|
||||
type MockStoreRecord = NonNullable<ReturnType<typeof storeById>>;
|
||||
|
||||
/** Mock stores carry a nested `rate`; the API exposes flat score fields. */
|
||||
function toShopProfile(store: MockStoreRecord): ShopProfile {
|
||||
return {
|
||||
id: store.id,
|
||||
slug: store.slug,
|
||||
name: store.name,
|
||||
company: store.company,
|
||||
region: store.region,
|
||||
address: store.address,
|
||||
logo: store.logo,
|
||||
banner: store.banner,
|
||||
notice: store.notice,
|
||||
after_sale: store.afterSale,
|
||||
score_rating: store.rate.score,
|
||||
score_agreement: store.rate.agree,
|
||||
score_service: store.rate.service,
|
||||
score_speed: store.rate.speed,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function skuIndex(): Record<string, { product: Product; skuId: string }> {
|
||||
const index: Record<string, { product: Product; skuId: string }> = {};
|
||||
@@ -352,6 +375,13 @@ export function createMockApi(): ApiClient {
|
||||
})),
|
||||
}),
|
||||
|
||||
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));
|
||||
},
|
||||
|
||||
shop: {
|
||||
getMyShop: () => unsupported(),
|
||||
listMyProducts: () => unsupported(),
|
||||
@@ -381,6 +411,7 @@ export function createMockApi(): ApiClient {
|
||||
setRate: () => unsupported(),
|
||||
getContent: () => unsupported(),
|
||||
replaceContent: () => unsupported(),
|
||||
setShopProfile: () => unsupported(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user