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:
2026-09-17 17:13:32 +00:00
parent 51f8bb7c1d
commit d0b6350d2d
19 changed files with 666 additions and 130 deletions
+8 -2
View File
@@ -9,6 +9,13 @@ const { locale, t } = useI18n();
const { currency } = usePrefs();
const router = useRouter();
const pendingOrderIds = useState<string[]>("checkout-orders", () => []);
// Shared with the store directory; orders carry only a shop id.
const { data: shops } = await useAsyncData("shops", () => $api.listShops(), { default: () => [] });
function shopName(shopId: string): string {
const shop = shops.value.find((entry) => entry.id === shopId);
return shop ? pick(shop.name, locale.value) : t("checkout.shop");
}
const orders = ref<Order[]>([]);
const paymentMethod = ref("balance");
@@ -85,8 +92,7 @@ onMounted(() => void loadOrders());
<article v-for="order in orders" :key="order.id" class="mpanel order-card">
<header class="order-heading">
<span>{{ t("checkout.orderNo") }}: {{ order.order_no }}</span>
<!-- Store names need the public store read; see docs/TBD-migrate-wave.md. -->
<span class="shop-name">{{ t("checkout.shop") }}</span>
<span class="shop-name">{{ shopName(order.shop_id) }}</span>
</header>
<div v-for="item in order.items" :key="item.id" class="order-item">
<img :src="imageFor(item.image)" :alt="pick(item.product_name, locale)" />