# catalog — service categories, option groups, nurse pricing variants > Client seam `client/src/services/catalog/` · `USE_CATALOG_MOCK = false` (**real**) · 14 server ops > Last verified: 2026-07-30 against commit `d3ec723` and swagger.v1.json (2026-07-29). An EAV catalogue: admin defines categories and their option groups; each nurse composes **variants** — a category + a chosen set of option values + a price. A variant is what a customer actually books. ## Endpoints | Method | Path | Auth | Verdict | | --- | --- | --- | --- | | GET | `/api/v1/catalog/categories` | **anonymous** | wired · paginated | | GET | `/api/v1/catalog/option_groups` | **anonymous** | wired | | GET | `/api/v1/nurse_variants/list` | `[Authorize]` | wired · paginated | | GET | `/api/v1/nurse_variants/get/{id}` | **anonymous** | wired | | POST | `/api/v1/nurse_variants/create` | `[Authorize]` | wired | | POST | `/api/v1/nurse_variants/update/{id}` | `[Authorize]` | wired | | POST | `/api/v1/nurse_variants/set_active/{id}` | `[Authorize]` | wired | | POST | `/api/v1/admin_catalog/create_category` | admin | **unwired** — no console screen | | POST | `/api/v1/admin_catalog/update_category/{id}` | admin | **unwired** | | POST | `/api/v1/admin_catalog/set_category_active/{id}` | admin | **unwired** | | POST | `/api/v1/admin_catalog/create_option_group` | admin | **unwired** | | POST | `/api/v1/admin_catalog/update_option_group/{id}` | admin | **unwired** | | POST | `/api/v1/admin_catalog/create_option_value` | admin | **unwired** | | POST | `/api/v1/admin_catalog/update_option_value/{id}` | admin | **unwired** | No phantoms. The seven `admin_catalog` routes are `DynamicPermission` + `sensitive`; the catalogue is seeded and managed out of band today, so the console has no editor. That is a UI gap, not a contract gap. > Mutations are **action-style, not REST**: `POST admin_catalog/create_category`, never > `POST admin/catalog/categories`. The old contract doc calls this out explicitly and it still holds. ## Shape rules the JSON does not express - **`GET nurse_variants/get/{id}` is anonymous** while `list` requires auth. That asymmetry is deliberate — a public nurse profile links to a specific variant. - **A duplicate variant is rejected by `option_set_hash`.** The server hashes the chosen option-value set per (nurse, category) and enforces uniqueness, so a nurse cannot list the same configuration twice at two prices. The client surfaces the resulting `409`, it does not pre-check. - **The variant snapshot is serialised at booking time** (`IVariantSnapshotSerializer`) onto the booking row — see [bookings.md](bookings.md). Editing or deactivating a variant never changes a past booking. - **`set_active` is the only way to retire a variant.** There is no delete; a variant referenced by bookings must remain resolvable. - **Prices are IRR digit strings** outbound. The nurse enters Toman in the UI and the client converts at the input boundary — the wire is always IRR. - Reference names come as **both** `nameFa` and `nameEn`; the client picks by locale. `OptionGroupDto` carries its `values` inline, so the variant builder needs one round trip, not one per group. - `isRequired` + `sortOrder` on an option group drive the builder's validation and layout — the client does not hardcode either. ## Enums | Vocabulary | Values | | --- | --- | | `PriceUnit` | `per_hour` `per_session` `per_half_day` `per_day` `per_24h` | `PriceUnit` is a label vocabulary, never a multiplier — the client must not derive a total from it. Display strings are i18n keys, never the code. ## Open REQs None. The variant builder (b7) and the Home category grid (A5) both read the contract as served.