# Domain contracts

One file per client `services/` domain — **22 files, 22 domains, one-to-one.** Each names every server
endpoint that belongs to it, verdicted against the live swagger and the real client code.

> Last verified: 2026-07-30 against commit `d3ec723` and
> [`../openapi/swagger.v1.json`](../openapi/swagger.v1.json) (2026-07-29).

Read [`../api-contract.md`](../api-contract.md) first — the envelope, casing, pagination, errors, auth,
idempotency and money rules hold everywhere and are not restated per domain.

---

## How to read a domain file

Every endpoint carries one verdict:

| Verdict | Means |
| --- | --- |
| **wired** | In swagger **and** called by the domain's real `apis/clientApi.ts` |
| **unwired** | In swagger, no real client caller. Server-only, admin-only, or superseded — the reason is given |
| **phantom** | The client calls it; **the server has no such route.** It 404s. Always carries its REQ |

`phantom` rows are the frontend's proposed routes, filed as REQs and mocked behind the domain seam
meanwhile. They are not bugs in the client — they are the contract's open edge — but on a domain whose
mock is **off** they are live 404s, and that is called out where it happens.

## The census

186 operations, each in exactly one file below. 184 belong to a domain; 2 (`ping`) are platform
endpoints and live in [`../api-contract.md`](../api-contract.md#platform-endpoints-outside-every-domain).

| Domain file | `client/src/services/` | Seam | Server ops | Phantom |
| --- | --- | --- | --- | --- |
| [addresses.md](addresses.md) | `addresses` | real | 5 | — |
| [admin.md](admin.md) | `admin` | **mock** | 14 | 5 |
| [auth.md](auth.md) | `auth` | real | 7 | — |
| [bnpl.md](bnpl.md) | `bnpl` | **mock** | 9 | 3 |
| [booking-requests.md](booking-requests.md) | `bookingRequests` | real | 7 | — |
| [bookings.md](bookings.md) | `bookings` | real | 16 | — |
| [catalog.md](catalog.md) | `catalog` | real | 14 | — |
| [geography.md](geography.md) | `geography` | real | 13 | — |
| [notifications.md](notifications.md) | `notifications` | real | 4 | — |
| [nurse.md](nurse.md) | `nurse` | real | 4 | — |
| [partner-center.md](partner-center.md) | `partnerCenter` | **mock** | 9 | 6 |
| [patient-records.md](patient-records.md) | `patientRecords` | **mock** | 5 | — |
| [patients.md](patients.md) | `patients` | real | 5 | — |
| [payment.md](payment.md) | `payment` | real | 3 | 1 |
| [payouts.md](payouts.md) | `payouts` | **mock** | 13 | 1 |
| [profiles.md](profiles.md) | `profiles` | real | 7 | — |
| [refunds.md](refunds.md) | `refunds` | **mock** | 8 | 4 |
| [reviews.md](reviews.md) | `reviews` | real | 8 | — |
| [search.md](search.md) | `search` | real | 2 | — |
| [service-areas.md](service-areas.md) | `serviceAreas` | real | 3 | — |
| [tickets.md](tickets.md) | `tickets` | real | 11 | 1 |
| [verification.md](verification.md) | `verification` | **mock** | 17 | 3 |
| | | | **184** | **24** |

"Seam" is the domain's `constants.ts` flag (`USE_<DOMAIN>_MOCK`): **15 real, 7 mock.** A mocked domain
still has a complete real client — flipping the flag is one line in `apis/index.ts`.

**Two phantoms sit on a domain whose mock is off**, so they are reachable and they 404:
`GET /api/v1/bookings/payment_history` ([payment.md](payment.md), REQ-047) and
`POST /api/v1/tickets/{id}/assign` ([tickets.md](tickets.md), REQ-063). Both are guarded in the client —
the first renders an empty state, the second is behind a default-off capability flag.

## Route-shape exceptions

The routing convention is snake_case segments generated from `[controller]`/`[action]` tokens. **Four
controllers hardcode a route string instead**, and three of those introduce hyphens:

| Route | Controller | Note |
| --- | --- | --- |
| `api/v1/admin/partner-centers` | `AdminPartnerCentersController` | hyphens **and** a nested `admin/` segment; children add `/set-active`, `/sponsor-nurse` |
| `api/v1/admin/tickets` | `AdminTicketsController` | nested `admin/` segment |
| `api/v1/admin/reviews/moderation_queue` | `AdminReviewsController` | nested `admin/`, then snake_case |
| `api/v1/internal/bookings/{bookingId}/center` | `InternalCentersController` | an `internal/` namespace |

Every other admin controller uses a flat `admin_*` prefix (`admin_geo`, `admin_catalog`, `admin_refunds`,
…). The split is historical, not meaningful. Since the route also derives the dynamic-permission key,
normalising it is a breaking change to permissions as well as URLs — it is recorded here, not fixed.

## Enum vocabularies

Swagger declares **no** string enums (see
[`../api-contract.md`](../api-contract.md#enums)), so each domain file carries its own vocabulary. Every
one was cross-checked against the server's `Baya.Domain` code set **and** the client's string-literal
union. All match except one, noted in [tickets.md](tickets.md).

| Vocabulary | Domain file | Server source |
| --- | --- | --- |
| `BookingRequestStatus` · `RequiredCaregiverGender` | [booking-requests.md](booking-requests.md) | `Entities/Booking/BookingRequestStatus.cs` |
| `BookingStatus` · `BookingSessionStatus` · `VisitVerificationStatus` | [bookings.md](bookings.md) | `Entities/Booking/*.cs` |
| `PriceUnit` | [catalog.md](catalog.md) | catalog config rows |
| `BnplStatus` · `BnplEligibilityStatus` · `ProviderCode` | [bnpl.md](bnpl.md) | `Entities/Bnpl/*.cs` |
| `PaymentTransactionStatus` · `MoadianStatus` | [payment.md](payment.md) | `Entities/Payments/`, `Entities/Invoices/` |
| `RefundStatus` · `RefundChannel` · `ClawbackStatus` · cancellation codes | [refunds.md](refunds.md) | `Entities/Refunds/*.cs` |
| `PayoutStatus` · `PayoutBatchStatus` · `EarningsState` | [payouts.md](payouts.md) | `Entities/Payouts/*.cs` |
| `VerificationStatus` · `VerificationStepStatus` · `StepTypeCode` | [verification.md](verification.md) | `Entities/Verification/*.cs` |
| `ModerationStatus` · `ModerationAction` | [reviews.md](reviews.md) | `Entities/Reviews/ReviewModerationStatus.cs` |
| `TicketStatus` · `TicketCategory` · `TicketAuthorRole` | [tickets.md](tickets.md) | `Entities/Messaging/TicketCodes.cs` |
| `CenterOnboardingState` | [partner-center.md](partner-center.md) | `Entities/PartnerCenters/` |
| `BankAccountStatus` | [nurse.md](nurse.md) | bank-account entity |
| roles · `Gender` | [auth.md](auth.md) | identity seed |
| config/audit/holiday/alert codes | [admin.md](admin.md) | `Entities/Configuration/`, `Audit/`, `Holidays/`, `SupportAlerts/` |

## What replaced what

These files supersede [`archive/build-chain/contracts/domains/`](../../../archive/build-chain/contracts/domains/) — 17 hand-written files
frozen 2026-07-13, plus the two `conventions/` files. Route-level content there held up well: an audit of
every route those files name found **zero** that the live swagger lacks. What did not hold up:

| Was | Now |
| --- | --- |
| `conventions/api-conventions.md`: body casing is "typically `snake_case` … derive from swagger" | **camelCase**, proven mechanically. [`../api-contract.md`](../api-contract.md#casing) |
| `conventions/api-conventions.md`: server default `https://localhost:5002` | `http://localhost:5002` — plain HTTP (contradiction **C-3**) |
| The envelope has 5 fields | It has **6** — `code` was added for machine-readable errors (REQ-003) |
| Enum vocabularies spread across 17 files and the REQ ledger | One vocabulary block per domain file, cross-checked both ways |
| `messaging.md` (851 B, headerless) silently amending `messaging-notifications-admin.md` | Merged: [tickets.md](tickets.md) + [notifications.md](notifications.md) + [admin.md](admin.md) (contradiction **C-8**) |
| 17 files whose names matched *backend phases* | 22 files whose names match the **client's domains**, which is how the seam is actually consumed |
| The REQ ledger as the change log you had to read to know the current shape | Each domain file states the current shape and lists only its **open** REQs |
