# After backend-phase-5 — the service catalog & nurse pricing variants are live

The two-tier service model the whole marketplace is priced and searched on now exists. Admins curate a
catalog skeleton (categories → configurable option groups → option values) that ships as **data, not
migrations**, and each nurse turns that skeleton into **variants** — the atomic bookable unit: a category +
one chosen value per dimension, at the nurse's own price and price unit. Contract:
[`dev/contracts/domains/catalog.md`](../../../contracts/domains/catalog.md); machine schema:
`dev/contracts/openapi/swagger.v1.json` (refreshed for b5).

## What the frontend (f4-b5) can now build
- **Category grid / browse** — `GET api/v1/catalog/categories` (active, ordered, **paginated**, cached).
  Five categories are seeded (Elderly, Post-Surgery, Infant, Chronic, Companionship) with `nameFa`+`nameEn`.
- **The nurse service-builder** — for a chosen category, `GET api/v1/catalog/option_groups?category_id=`
  returns the **applicable** dimensions (the category's own groups **plus** every cross-category group),
  each with `isRequired` and its active values. The nurse then `POST api/v1/nurse_variants/create`
  `{ serviceCategoryId, options: [{ optionGroupId, optionValueId }], price, priceUnit, sessionCount?, displayName? }`.
  Manage with `update/{id}`, `set_active/{id}`, `list`, `get/{id}`. Requires a nurse profile first
  (b3 `nurse_profiles/upsert`).
- **Admin catalog console** — `admin_catalog/create_category|update_category/{id}|set_category_active/{id}`,
  `create_option_group|update_option_group/{id}`, `create_option_value|update_option_value/{id}` (admin
  token / dynamic-permission).
- **Public variant view** — `GET api/v1/nurse_variants/get/{id}` returns the public (active-only) projection
  for anyone, backing a nurse-profile offerings list.

## Rules baked into the API (don't fight them client-side)
- **The bookable unit is the variant, not the nurse.** Price/book against a specific variant.
- **`price` is a string of IRR-Rial digits** (e.g. `"8000000"`) — integer money, no floats, no Toman. The
  **total is `price` + `priceUnit` + `sessionCount`**; never compute it from `price` alone. Parse with a
  BigInt-safe helper, format for display, map `priceUnit` to an i18n label (never off the code).
- **A NULL-category option group is cross-category** — it applies to every category. Render the cross-
  category groups in the builder for *every* category; the required ones must be answered.
- **All required dimensions must be answered**, **one value per dimension**, a value must belong to its
  group. A missing required dimension → **400** (names it); a duplicate identical listing → **409**; surface
  both cleanly.
- **`displayName` auto-generates** (category + chosen value labels) — show it, let the nurse override it.
- **Deactivate, never delete.** A deactivated variant stays in the nurse's own `list` (flagged
  `isActive:false`) but is unbookable and 404s on the public `get`.
- **Every catalog row has `nameFa` (primary) + `nameEn`** — pick by locale, never label off a code.
- **Tenancy** — variants are strictly owner-scoped; another nurse's id on write → **404** (existence not
  leaked). Catalog skeleton writes are **admin-only**.
- **Refresh after `select_role`** still applies (nurse scoping reads the role claim in the token).
- **Routes are action-style** (`admin_catalog/create_category`, `nurse_variants/create`, …), POST for
  mutations, camelCase bodies — see the contract for the full list.

## What b7 (search & matching) must read from the variant
The variant is a clean, projection-friendly source. b7 owns the denormalized `nurse_search_index` and the
`INurseSearch` seam (**not built here**). Its fan-out reads, per active variant: `service_category_id`,
`price`, `price_unit`, `is_active`, `nurse_id` — joined to the nurse's `nurse_service_areas` (b4) to emit one
index row per covered city/district, and to `nurse_profiles` for `is_verified`/accepting/gender/rating. The
**single write trigger points** to maintain that index are `CreateVariantCommand`, `SetVariantActiveCommand`
(and later option/price edits) — hook the index maintenance there.

## What b8 (booking) consumes
`IVariantSnapshotSerializer` (`Application/Contracts/Common`, single real impl in `Application/Common`) is
ready: `string Serialize(VariantSnapshot)` emits the canonical `variant_snapshot_json` (category id + labels,
each `(group label, value label)`, `price` as a digit string, `price_unit`, `session_count`, `display_name`,
`variant_id`). b8 owns the `booking_requests.variant_snapshot_json` **column** and calls the serializer at
booking time so later variant edits/deactivation never mutate past bookings. The snapshot column is **not**
added here.

## Schema / migration
Migration **`20260702132758_ServiceCatalogAndNurseVariants`** (applies on startup), new **`catalog`** schema:
`ServiceCategories`, `ServiceOptionGroups` (nullable `ServiceCategoryId` = cross-category),
`ServiceOptionValues`, `NurseServiceVariants` (`Price` **BIGINT**, `PriceUnit` code, `SessionCount?`,
`DisplayName`, `OptionSetHash`), `NurseServiceVariantOptions`. Constraints: `UNIQUE(VariantId, OptionGroupId)`
(one value per dimension); filtered `UNIQUE(NurseId, ServiceCategoryId, OptionSetHash) WHERE DeletedAt IS
NULL` (duplicate-listing backstop); soft-delete query filters. Seed: five categories (`nameFa`+`nameEn`),
ids 1–5. Option groups/values are admin-authored data (not seeded).

## Deferred to later phases (do not build against these yet)
- **`nurse_search_index`, `INurseSearch`, the search query & index fan-out** → **b7** (variant writes are the
  clean trigger point; the variant shape is projection-ready).
- **`variant_snapshot_json` persistence** → **b8** (the serializer is shipped and unit-tested here).
- **`nurse_availability_slots` / `nurse_availability_exceptions`** → **deferred** (soft scheduling guidance,
  not on the money/safety path) — not built.
- **Holiday/surge pricing, a Companionship *tier* pricing model, tiered per-category commission** →
  **deferred**. Companionship ships only as a seeded category (data), not a special pricing path.

## What's mocked
**Nothing.** Catalog and variant data are fully owned by Balinyaar's DB — this phase introduces **no**
cross-cutting seam and adds **no** row to `reports/mocks-registry.md`. It reuses `ICacheService` (b0) for the
public catalog reads with invalidate-on-mutation.
