add build development phases

This commit is contained in:
hamid
2026-06-28 21:59:59 +03:30
parent 1df3cd9f64
commit 53a40dc51d
52 changed files with 12379 additions and 0 deletions
+331
View File
@@ -0,0 +1,331 @@
# Frontend Phase 4 — Catalog browse & nurse service builder
> **Mission:** light up the two faces of the configurable service catalog. For the **family/customer**
> this is the **Home (A5)** screen — greeting, the search bar, the four-tile service-category grid
> (مراقبت سالمند / پرستار کودک / تزریقات و سرم / فیزیوتراپی), and the complete-patient-record nudge —
> the front door of the whole app. For the **nurse** this is the **"add a service" builder (B7 services)**:
> a stepper that walks pick-category → answer required/optional option groups → set price + price unit,
> producing a priced **variant** (the atomic bookable unit), plus the list/edit/deactivate surface for a
> nurse's own offerings. This is what makes nurses *appear* in search (f6) and what a customer browses, so
> it must be exactly right about money, required-option validation, and reference-data caching.
>
> **Track:** frontend · **Depends on:** [`frontend-phase-3-b4.md`](./frontend-phase-3-b4.md) (addresses & geo, cascading province/city/district, nurse coverage editor) + backend **phase b5** contract (`catalog`) · **Unlocks:** search & discovery ([`frontend-phase-6-b7.md`](./frontend-phase-6-b7.md))
> **Before you start, read [`../_shared/agent-operating-rules.md`](../_shared/agent-operating-rules.md).** It is not optional.
---
## 1. Context — where this sits
We are at the hinge between *identity/geo* (f1f3, done) and *discovery/booking* (f6+). A nurse cannot
be found until she has at least one **active, priced variant**; a customer has nowhere to start until the
**Home category grid** exists. This phase builds both, against the backend's **Catalog & pricing** domain
(b5): admin-seeded `service_categories``service_option_groups``service_option_values`, and the nurse
layers `nurse_service_variants``nurse_service_variant_options`. The bookable unit is the **variant**, never
the nurse and never the category — search, booking, and pricing all operate on variants downstream.
The product framing: transparent, **nurse-set** pricing per variant is a deliberate differentiator versus the
opaque "توافقی/negotiable" incumbents — so the price the nurse enters and the way we *display* it (price +
unit, with session count) is brand-load-bearing, not a detail.
**What already exists (do not rebuild) — built by prior frontend phases:**
- **f0** ([`frontend-phase-0.md`](./frontend-phase-0.md)) — the three actor app **shells** and route groups
(customer mobile shell with the **5-tab bottom nav** خانه/رزروها/بیماران/کیف‌پول/پروفایل · nurse shell ·
admin shell); the **`services/{domain}` + TanStack Query** reference pattern (`types.ts` / `keys.ts` /
`apis/clientApi.ts` / `hooks/use*.ts` / `index.ts`); the **types-from-contract** convention; the
**shared composite components** including the **stepper/progress header**, **status chip**, OTP input,
phone field; the **money/format util** (`formatIrrToToman`, integer-safe IRR parse, Shamsi date display)
in `src/utils/`; the i18n namespace conventions in both `messages/en.json` and `messages/fa.json`; the
RTL baseline and `tokens.css` brand colours. **Reuse all of it — do not re-create the shell, the stepper,
the money util, or the services pattern.**
- **f1-b2** — phone-OTP login, the role router, and roles in `AuthContext`. You read the current role to
decide *customer Home* vs *nurse builder* chrome; you do **not** touch auth.
- **f2-b3** — onboarding, patient CRUD, customer & nurse profiles, nurse bank-account settings. The
**patient list/record state** is what the Home "complete patient record" nudge points at; the **nurse
profile** is the parent the variant builder hangs off (B7 = "تکمیل پروفایل و خدمات"; this phase owns the
**services part**, the profile-bio/photo part is already in f2).
- **f3-b4** — address book + map picker + cascading **province → city → district** dropdowns, and the **nurse
coverage-area editor** (`nurse_service_areas`). You do **not** rebuild geo; the *service areas* a nurse
declares there are what fan the variant into search (f6) — out of scope here, just don't regress it.
> The variant builder produces *pricing*; the coverage editor (f3) produces *geography*; **search (f6)**
> joins them. This phase ships neither search nor the index — wiring the Home search bar to results is f6.
## 2. Required reading (do this first)
**Product / domain (business truth — read before designing any screen):**
- [`../../../product/business/03-service-catalog-and-pricing.md`](../../../product/business/03-service-catalog-and-pricing.md)
— the catalog model in plain language: admin defines categories + configurable option groups/values; each
**nurse defines variants** (category + chosen option values + own `price` + `price_unit`); the five price
units (`per_hour`/`per_session`/`per_half_day`/`per_day`/`per_24h`); `display_name` auto-generates from
option labels but is nurse-editable; **deactivate not delete**; catalog snapshotted onto bookings. This is
the *why* behind every validation rule below.
- [`../../../product/wireframes/index.html`](../../../product/wireframes/index.html) — the visual baseline.
Study **A5 (خانه / Home)**: greeting + avatar, search bar (`جستجوی خدمت یا پرستار…`), the **service-category
grid** (مراقبت سالمند / پرستار کودک / تزریقات و سرم / فیزیوتراپی), the **complete-patient-record nudge card**,
and the **bottom tab nav** (Home active). And **B7 (تکمیل پروفایل و خدمات)**: the services-and-prices list
(`مراقبت سالمند — ساعتی ۲۸۰٬۰۰۰ تومان/ساعت`, `+ افزودن خدمت`) — this phase builds the *services* half of B7.
Note the legend (green=verified, amber=pending) and the deep-green brand / cream surface / Vazirmatn font.
**Contract to consume (the source of truth for shapes — do not guess):**
- [`../../contracts/domains/catalog.md`](../../contracts/domains/catalog.md) — written by **backend-phase-5**.
This is where the real routes, request/response payloads, enums, and pagination live. Read it end-to-end
before writing a single type. If it is not yet published or a needed shape is missing, follow the seam
procedure in §4 (mock behind `services/catalog` and append a request for backend).
- [`../../contracts/conventions/api-conventions.md`](../../contracts/conventions/api-conventions.md) — the
envelope (`OperationResult`/`ApiResult`, already unwrapped by `clientFetch`), `snake_case` routes/JSON,
status codes, **mandatory pagination** (`page`/`page_size``items`+`total`) on the variant list,
`name_fa`/`name_en` reference-data localisation.
- [`../../contracts/conventions/money-and-types.md`](../../contracts/conventions/money-and-types.md) —
**money is IRR Rials as an integer string on the wire**, parsed integer-safe and rendered via the f0
money util; **Toman is display-only**; enums cross as stable string codes (`per_hour`/`per_session`/
`per_half_day`/`per_day`/`per_24h`) mirrored as string-literal unions with **i18n labels, never a label
hardcoded off the code**.
**Engineering & design rules:**
- [`../_shared/frontend-conventions-checklist.md`](../_shared/frontend-conventions-checklist.md) and
[`../../../client/CLAUDE.md`](../../../client/CLAUDE.md) — RSC/client boundary, `services/{domain}` +
Query caching + invalidate-on-mutation, one-hook-per-file, minimal re-renders, MUI v9 primitives reused,
both locales in sync, tokens-based colours, RTL.
- **Invoke the `frontend-designer` skill** for *all* visual work here (Home, the category grid/tiles, the
builder stepper UI, the variant list/cards, every empty/loading/error/success state). It is the brand/
design contract — palette, tokens, typography, the `App*` library, layout shells, the hard UI rules. Do
not hand-craft visuals outside it.
- The existing **f0 `services/auth/*`** and the **f3 `services/geo` + `services/address`** services — copy
their exact structure for the new `services/catalog`. Reuse the f0 **stepper** and **status chip**; do not
fork them.
## 3. Scope — build this
Two surfaces (customer Home, nurse builder) + the supporting `services/catalog` domain. Build every state
(loading / empty / error / validation / success) the digest's "Notes for UI" calls for.
### 3.1 `services/catalog` domain (the data layer the screens consume)
Create `client/src/services/catalog/` mirroring the f0 pattern:
- `types.ts` — string-literal unions + DTOs derived from [`catalog.md`](../../contracts/domains/catalog.md):
`PriceUnit = 'per_hour' | 'per_session' | 'per_half_day' | 'per_day' | 'per_24h'`; `ServiceCategory`
(`id`, `name_fa`, `name_en`, `sort_order`, optional `icon`/`slug`); `ServiceOptionGroup` (`id`,
`service_category_id` nullable = cross-category, `name_fa`/`name_en`, **`is_required`**, `sort_order`);
`ServiceOptionValue` (`id`, `option_group_id`, `name_fa`/`name_en`, `sort_order`); `NurseServiceVariant`
(`id`, `service_category_id`, `price` as **IRR digit-string**, `price_unit`, `session_count` nullable,
`display_name`, `is_active`, the chosen `options: { option_group_id, option_value_id }[]`). Mirror the
exact casing/nullability from the published swagger — do not invent.
- `keys.ts` — a query-key factory: `catalogKeys.categories()`, `catalogKeys.categoryOptionGroups(categoryId)`,
`catalogKeys.myVariants()` (nurse), `catalogKeys.variant(id)`.
- `apis/clientApi.ts` — wrappers over `clientFetch` for each route in the contract (see §3.4). Add
`apis/serverApi.ts` (`serverFetch`) **only** if the Home category grid is prefetched in an RSC (see 3.2).
- `hooks/` (one hook per file): `useServiceCategories.ts`, `useCategoryOptionGroups.ts` (query),
`useMyVariants.ts` (paginated nurse list), `useCreateVariant.ts`, `useUpdateVariant.ts`,
`useDeactivateVariant.ts` (mutations). **Reference data (categories, option groups/values) is cached with
a long `staleTime`/`gcTime`** — it changes rarely; do not refetch it on every screen. Variant mutations
**invalidate `catalogKeys.myVariants()`** (and `setQueryData` the edited row where it avoids a refetch).
- `index.ts` — barrel export.
### 3.2 Customer **Home (A5)** — `app/[locale]/(private-routes)/<customer>/home`
The primary landing screen, inside the customer shell with the **bottom tab nav (Home active)**:
- **Greeting + avatar** — `سلام، {firstName}` from `AuthContext`/`useCurrentUser` (f1); avatar from the
customer profile (f2). RSC where it cleanly removes a round-trip; no `next/headers` in client components.
- **Search bar** — `جستجوی خدمت یا پرستار…`. **Render the input here, but search execution is f6** — tapping
it navigates toward the (future) search route / sets the query; do **not** implement results, the index,
or filters in this phase. Tag results/filters **(DEFERRED → [`frontend-phase-6-b7.md`](./frontend-phase-6-b7.md))**.
- **Service-category grid** — a tile per `service_category` from `useServiceCategories()`, ordered by
`sort_order`, label by locale (`name_fa`/`name_en`), seed-matching the wireframe's four
(مراقبت سالمند / پرستار کودک / تزریقات و سرم / فیزیوتراپی) but **data-driven** (never hardcode the category
list — EAV configurability is load-bearing). Tapping a tile carries the chosen `service_category_id` into
the (future) search flow. Build **loading skeleton tiles**, **empty** (no categories seeded → friendly
message) and **error/retry** states.
- **Complete-patient-record nudge card** — shown when the signed-in customer has no patient *or* an incomplete
record (derive from the f2 patient state already in cache — do **not** add a new fetch if f2's query
already holds it); CTA routes to add/complete a patient (f2 screens). Hide when the record is complete.
- A small reusable **`CategoryTile`** and **`HomeNudgeCard`** composite live at the shared level if reused;
page-only composition stays in the page.
### 3.3 Nurse **"add a service" builder + offerings list** — `app/[locale]/(private-routes)/<nurse>/services`
The services half of **B7**, inside the nurse shell:
**(a) Offerings list (`ListMyVariants`)** — `useMyVariants()` (paginated):
- Each row/card shows `display_name`, the **price rendered via the f0 money util** as
`{formatIrrToToman(price)} تومان {unitLabel}` (unit label is an i18n key off `price_unit`), and an
**active vs deactivated** visual distinction (reuse the f0 **status chip**) with a "deactivated can't be
booked" hint on inactive rows. Row actions: **Edit**, **Deactivate** (with a confirm).
- States: **loading** (skeleton rows), **empty** (no offerings yet → prominent `+ افزودن خدمت` CTA), populated.
**(b) Create/Edit variant builder (`CreateVariant` / `UpdateVariant`)** — a **stepper** (reuse the f0
stepper/progress header), launched by `+ افزودن خدمت` or Edit:
1. **Step 1 — category.** Single-select from `useServiceCategories()`. On select, fetch that category's
option groups via `useCategoryOptionGroups(categoryId)` (cached). (On *edit*, category is fixed — changing
it would change identity; lock it and explain.)
2. **Step 2 — options.** Render each `service_option_group` for the category (plus any cross-category group
where `service_category_id` is null) as a single-select of its `service_option_value`s. **Mark required
groups** (`is_required`) and **block advancing until every required group is answered** (one value per
group — `UNIQUE(variant_id, option_group_id)` is enforced server-side; the UI enforces single-select).
Optional groups may be left unanswered.
3. **Step 3 — price + unit (+ duration).** A **price** field (Toman input → store/submit as IRR digit-string
via the f0 integer-safe parse; **never a float**), a **`price_unit`** select (the five units, i18n
labels), and an optional **`session_count`/duration**. Show a **live estimated total** computed from
**price + unit + session_count together** — e.g. for `per_hour` with a duration, surface the
`formatIrrToToman(price × hours)` estimate — **do not compute or display a total from price alone**. Show
the auto-generated **`display_name`** (from the chosen option labels) as an editable field.
- **Submit:** `useCreateVariant()` / `useUpdateVariant()`. On the **duplicate-listing conflict** (server `409`
on `(nurse_id, category, option-set)`), show a **friendly inline warning** ("شما قبلاً خدمتی با همین مشخصات
دارید") and let the nurse adjust — don't silently fail or generic-toast it. Success → invalidate
`myVariants`, route back to the list with the new/edited row visible.
- States to build: loading (fetching catalog), per-step validation errors (missing required option, missing/
invalid price), the duplicate warning, success.
**(c) Deactivate (`DeactivateVariant`)** — `useDeactivateVariant()`; a confirm dialog explaining the variant
becomes **unbookable and drops out of search**; **soft only — never a hard delete**. On success, flip the
row to the deactivated visual state (`setQueryData`/invalidate).
### 3.4 Catalog browse (categories) — the read surface
The category-browse query the Home grid and the (future) search filters both consume: `ListCategories` and
`GetCategoryOptionGroups` via `services/catalog`. Build the **catalog-browse view** as the data-driven grid
in 3.2 (Home) reusing `CategoryTile`; a standalone "all categories" browse screen is optional and may be
**(DEFERRED → f6)** if the Home grid + search cover it — your call, but if you build it, reuse the same
hooks/components.
**Endpoints consumed (final names from [`catalog.md`](../../contracts/domains/catalog.md) — these mirror the
b5 capabilities; use the contract's exact `snake_case` routes):**
- `GET api/v1/catalog/categories` → categories (reference data, cached).
- `GET api/v1/catalog/categories/{id}/option_groups` (with values) → the skeleton the builder renders.
- `POST api/v1/nurse/variants` → CreateVariant.
- `PUT api/v1/nurse/variants/{id}` → UpdateVariant / EditDisplayName.
- `POST api/v1/nurse/variants/{id}/deactivate` → Deactivate (soft).
- `GET api/v1/nurse/variants` (paginated) → ListMyVariants (active + inactive).
**Out of scope (tag explicitly):**
- Search results / filters / the nurse-result cards / the search index — **(DEFERRED → [`frontend-phase-6-b7.md`](./frontend-phase-6-b7.md))**.
- The **admin catalog manager** (category/option-group/value CRUD) — **(DEFERRED → admin console [`frontend-phase-15-b15.md`](./frontend-phase-15-b15.md))**; the admin seeds the catalog server-side for now.
- Nurse **availability** slots/calendar — **(DEFERRED**, soft-constraint, not on this path).
- The **public nurse profile** services rows a customer sees — **(DEFERRED → f6 C3)**.
- Holiday/surge pricing, companionship tier, per-category commission — **(DEFERRED** per product doc).
## 4. Mocks & seams in this phase
This is a **frontend** phase: the only seam is the data seam behind `services/catalog`.
- **Reuse the `services/{domain}` seam pattern from f0.** Every catalog call goes through
`services/catalog/apis/clientApi.ts` (over `clientFetch`) — never a raw `fetch()`.
- **If the b5 `catalog` contract is published and merged**, derive `types.ts` from it and call the real
endpoints — no mock.
- **If a needed shape is missing or the contract isn't live yet**, build a **mock `clientApi`** behind the
same `services/catalog` seam (returning realistically-shaped data: a few seeded categories, an option
group with `is_required` true/false, a couple of variants across price units, and a `409` path to exercise
the duplicate warning), **and**:
- append the gap to [`../../shared-working-context/frontend/requests/for-backend.md`](../../shared-working-context/frontend/requests/for-backend.md)
(per operating-rules §6 — you request, backend delivers; never edit backend files), and
- record the mock in [`../../shared-working-context/reports/mocks-registry.md`](../../shared-working-context/reports/mocks-registry.md)
+ your phase report (per operating-rules §7) with exactly how f-next swaps it for the real endpoint.
- No new external-service seam is introduced here (Elasticsearch / `INurseSearch` belongs to **search**,
f6/b7 — do not pull it forward).
## 5. Critical rules you must not get wrong
- **Money correctness — IRR is integer, never a float.** Money is **IRR Rials as an integer string on the
wire**; parse it integer-safe and render it **only** through the f0 money util (`formatIrrToToman`). Toman
is **display-only**; convert Toman input → IRR digit-string at the field boundary. **No floating-point**
anywhere on the price path (input, state, or submit) — float coercion is a defect.
- **price + unit + session_count drive the displayed total — never compute the total from price alone.** The
estimated total = `price` interpreted by `price_unit` combined with `session_count`/duration. A bare
`price` is a unit rate, not an engagement total. Render the unit label from an **i18n key off the
`price_unit` code**, never a label hardcoded in the component.
- **Reference data is cached.** Categories and option groups/values are admin-seeded reference data — fetch
once with a long `staleTime`/`gcTime` and reuse from cache; do not refetch them per screen or per step.
Variant mutations invalidate `myVariants` (and `setQueryData` the edited row) so you never needlessly
refetch the list.
- **Validate every required option group.** The builder must not submit until **every `is_required` group has
exactly one value chosen**; optional groups may be empty; one value per group (single-select mirrors the
server's `UNIQUE(variant_id, option_group_id)`).
- **Deactivate is soft — never hard-delete.** A deactivated variant must read as unbookable and is understood
to drop out of search; there is no delete affordance.
- **Duplicate-listing is a friendly conflict, not a crash.** Surface the server `409` on
`(nurse_id, category, option-set)` as inline, actionable copy.
- **Data-driven catalog (no hardcoded enums).** Categories, option groups, and option values come from the
API and render by `sort_order` + locale label — **never** hardcode the category/option list as constants.
The **only** closed enum is `price_unit` (the five units).
- **RTL-first, both locales.** `fa` is default and RTL; every user-visible string is a key in **both**
`en.json` and `fa.json`, in sync. Persian unit labels (ساعتی / روزانه / شبانه‌روزی, …) must read correctly.
- **RSC/client boundary & re-renders.** No `next/headers`/`next-intl/server` in client components; keep
builder step state colocated (low) so typing in the price field doesn't re-render the whole stepper;
stable references where it pays.
- **MUI primitives stay MUI; reuse the shared stepper & status chip** from f0 — do not fork a new root
primitive or a second stepper.
- **Tenancy is server-enforced, but don't leak it in UI:** the nurse only ever sees/edits *her own* variants
(`GET api/v1/nurse/variants` is self-scoped) — never build a UI that lists or edits another nurse's
offerings.
## 6. Definition of Done
The shared [definition-of-done.md](../_shared/definition-of-done.md), plus these phase specifics:
- [ ] `services/catalog` exists (types/keys/apis/hooks/index) mirroring the f0 pattern; reference data
cached with deliberate `staleTime`/`gcTime`; variant mutations invalidate/`setQueryData` `myVariants`.
- [ ] **Home (A5)** renders inside the customer shell with the bottom nav: greeting + avatar, the search bar
(navigates toward f6, results not built here), the **data-driven category grid** (loading/empty/error
states), and the complete-patient-record nudge (derived from cached f2 state, hidden when complete).
- [ ] The **nurse builder** (stepper category → required/optional options → price+unit+duration) enforces all
required groups, shows the live unit-aware estimated total, auto-generates an editable `display_name`,
submits as IRR digit-string, and handles the **duplicate `409`** with friendly inline copy.
- [ ] The **offerings list** shows active vs deactivated distinctly, supports **edit** and **soft
deactivate** (confirm dialog), with empty/loading states.
- [ ] All money rendered via the f0 money util; the **`price_unit`** labels are i18n keys in **both** locales,
RTL-correct; `en.json`/`fa.json` in sync.
- [ ] Types derive from [`catalog.md`](../../contracts/domains/catalog.md); any gap is logged in
`for-backend.md` and mocked behind the `services/catalog` seam (recorded in the mock registry).
- [ ] `npm run check` green; `npm run test:ci` green for any shared component added (e.g. `CategoryTile`,
a price-unit display, the variant card) with co-located `*.test.tsx`.
- [ ] `client/CLAUDE.md` *Project Structure* updated for the new route segments (customer `home`, nurse
`services`) and the `services/catalog` domain + any new shared component.
## 7. How to test (what a human can verify after this phase)
Run `npm run dev` (and ensure the b5 endpoints are reachable, or the `services/catalog` mock is active).
1. **Home renders.** Sign in as a customer → the Home screen shows the greeting, avatar, search bar, and the
**category grid** (مراقبت سالمند / پرستار کودک / تزریقات و سرم / فیزیوتراپی) ordered by `sort_order`. With
no patient on file, the **complete-patient-record nudge** is visible; after completing a patient (f2) it
disappears (no extra refetch — verify in React Query Devtools that the patient query is reused).
2. **Locale + RTL.** Switch `fa``en` → labels translate, `dir` flips, the grid and tiles mirror correctly;
Persian unit labels read right.
3. **Build a variant.** Sign in as a nurse → `+ افزودن خدمت` → step through: pick a category; in the options
step, try to advance **without** answering a required group → **blocked** with a clear message; answer it →
advance; set a **price (in Toman)** and a **unit** (e.g. ساعتی) with a duration → the **estimated total**
updates from price × duration (not from price alone); edit the auto-generated `display_name`; submit →
the new variant appears in the offerings list, price shown as `… تومان ساعتی`.
4. **Duplicate warning.** Create a second variant with the **same category + same option set** → the builder
shows the friendly duplicate-listing warning (server `409`) and lets you change it, without a crash or a
generic error toast.
5. **Edit + deactivate.** Edit a variant's price/`display_name` → list reflects it without a full refetch
(Devtools: `setQueryData`/single invalidation). Deactivate a variant → it flips to the deactivated visual
state with the "can't be booked" hint; there is **no delete** option.
6. **Caching.** In React Query Devtools confirm `catalogKeys.categories()` / `categoryOptionGroups` are
served from cache across Home and the builder (no repeated network calls per step); a variant mutation
invalidates only `myVariants`.
7. **Gate.** `npm run check` and `npm run test:ci` pass.
## 8. Hand off & document (close the phase)
- **Docs to update (same change):**
- [`client/CLAUDE.md`](../../../client/CLAUDE.md) *Project Structure* — add the customer `home` and nurse
`services` route segments, the `services/catalog` domain, and any new shared component (`CategoryTile`,
price-unit display, variant card). Note the reference-data caching convention if it's the first long-lived
cached domain.
- If you discover or decide a catalog/pricing rule the product docs don't capture (e.g. how the estimated
total is presented for each unit), record it in
[`../../../product/business/03-service-catalog-and-pricing.md`](../../../product/business/03-service-catalog-and-pricing.md)
— don't invent rules; record decisions, and flag any open question in your report.
- **Contract to consume:** [`../../contracts/domains/catalog.md`](../../contracts/domains/catalog.md) (b5) —
types/services derive from it; do not guess shapes. Any missing/ambiguous shape → append to
[`../../shared-working-context/frontend/requests/for-backend.md`](../../shared-working-context/frontend/requests/for-backend.md)
(you request; backend delivers — never edit backend files). The frontend produces no contract.
- **Handoff & report (per operating-rules §6–§7):**
- Append your phase summary to
[`../../shared-working-context/frontend/STATUS.md`](../../shared-working-context/frontend/STATUS.md).
- Write `dev/shared-working-context/reports/frontend-phase-4-report.md`: what shipped (Home A5, the nurse
variant builder + offerings list, `services/catalog`), **what is now testable and exactly how** (the §7
steps), what is mocked vs live behind the catalog seam and **how f6 swaps it**, the contract consumed,
and the follow-ups handed to **f6** (the Home search bar now hands a `service_category_id` to search; the
variant builder is what populates the index f6 reads).
- Update [`../../shared-working-context/reports/mocks-registry.md`](../../shared-working-context/reports/mocks-registry.md)
if you mocked any catalog endpoint (seam, what's faked, config, how to make it real).
- **Memory (per operating-rules §8):** save a `project`-type memory note for the non-obvious decisions —
the price/unit/session_count display rule (total never from price alone), the IRR-string-in/Toman-display
money handling on the builder, and the reference-data caching choice — with a one-line `MEMORY.md` pointer.
Don't record what the code/docs already make obvious.