# 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.