Files
vmall/openspec/changes/archive/2026-09-17-replace-mock-api-wave-4/design.md
T
james f6ddfd21cb chore(openspec): archive replace-mock-api-wave-4
The merge creates the storefront-content capability (two requirements, five
scenarios) and updates the mall's home-page requirement so its banner,
promotion, quick-link and floor advert content comes from the content API.

openspec validate --all --strict stays green at 12 passed / 0 failed.
2026-09-17 16:49:35 +00:00

55 lines
4.4 KiB
Markdown

# Design
## Context
See `proposal.md` — Why. The home page renders four local arrays from `~/mock/data` (`pages/index.vue`): `MOCK_BANNERS` and `MOCK_PROMOS` are `{ image, url }`, `MOCK_QUICK_LINKS` adds a bilingual `label` and an inline SVG `glyph`, and each floor's advert art is picked by floor index from `/mock/floor-adv-1..6.svg`. Those assets already exist under `apps/mall/public/mock/`.
The backend has no content concept: the only shared tables are identity, catalog and orders (`apps/api/migrations/0001..0005`). The admin console already manages shops, users and currencies through `/api/admin/*` with `platform_admin` gating (`apps/api/src/routes/admin.rs`).
## Goals / Non-Goals
**Goals:**
- The home page renders every element from the backend, with no local content arrays left in `pages/index.vue`.
- Content is editable by a platform admin through the API, not only by SQL.
- The flip is visually invisible: the seeded content reproduces today's page exactly.
**Non-Goals:**
- No image upload or CDN; rows hold URLs and the seed points at the existing static assets.
- No scheduling, targeting or A/B testing.
- No content for the seckill, collective or integral pages, nor for stores, brands, coupons, favourites or addresses.
## Decisions
**1. Four typed tables, not one JSONB content table.**
`banners`, `promos`, `quick_links` and `floor_adverts` each get explicit columns, so Postgres enforces the shape and `sqlx::query_as` maps rows without hand-written validation.
*Alternative:* a single `content_blocks(kind, payload JSONB)` table is fewer lines, but every read then needs a runtime payload check, which is precisely the `any`-shaped weakness the repo's TS and Rust rules avoid.
**2. Replace a whole kind at a time instead of per-entry CRUD.**
`PUT /api/admin/content/{kind}` takes the ordered list, validates it, and rewrites that kind in one transaction, reindexing positions from array order.
*Alternatives:* twelve REST routes for four small lists is a lot of surface, and per-entry CRUD still needs a separate reorder endpoint to express order. Bulk replace makes reordering, insertion and deletion one operation, which is how the content is actually edited.
*Consequence to state plainly:* submitting an empty list clears that kind. That is intended (it is how a block is removed), and the endpoint returns the stored list so a caller can confirm what it now holds.
**3. Floor adverts are an ordered pool, not rows keyed to categories.**
The page assigns them to floors in order and wraps, matching today's `index % 6` behaviour.
*Alternative:* `floor_adverts.category_id` would let an editor target one specific floor. That is a nicer model, but it changes what the page does today and would leave floors without adverts whenever the tree grows; the pool keeps the current behaviour and the schema smaller.
**4. The quick-link glyph is stored with the row.**
It is a short SVG path string. Keeping it in content means a new quick link renders without a frontend deploy, which is the point of moving this into the database at all.
**5. A migration seeds content from the assets already in use.**
`banner-1..3`, `promo-1..3` and `floor-adv-1..6` become rows, so the page is byte-identical after the flip and the change can be verified as a visual no-op.
## Risks / Trade-offs
- [An empty submission silently clears a content kind] → intended (decision 2); the naive admin UI should confirm, and the response echoes the stored list.
- [Content rows point at frontend asset paths] → temporary coupling, called out in the non-goals; a real upload capability replaces the URLs later without touching the schema.
- [Four extra queries on every home render] → negligible at this size; if it ever matters, the four reads collapse into one query.
- [The home page loses its content if the content API is down] → the page renders the remaining blocks rather than failing (a spec scenario), and the fixed-data rollback still works because the mock client keeps serving the same four lists.
## Migration Plan
1. Ship the migration and the public read; nothing is reading it yet.
2. Flip `pages/index.vue` to the content API and drop the three mock arrays in the same commit as the admin surface, so content is never live-but-uneditable.
3. Verify the page is visually unchanged, then verify rollback with the fixed-data adapter.
4. Rollback: revert `pages/index.vue`. The content tables are additive and harmless if left in place.