# bookings — the post-payment engine, sessions and EVV

> Client seam `client/src/services/bookings/` · `USE_BOOKINGS_MOCK = false` (**real**) · 16 server ops
> Last verified: 2026-07-30 against commit `d3ec723` and swagger.v1.json (2026-07-29).

Stage two: a paid booking, its per-visit sessions, and electronic visit verification. A `bookings` row is
created **only on payment capture**, from an accepted [booking request](booking-requests.md).

## Endpoints

| Method | Path | Caller | Verdict |
| --- | --- | --- | --- |
| GET | `/api/v1/bookings/list` | both | wired · paginated · `role=customer\|nurse\|all` |
| GET | `/api/v1/bookings/get/{id}` | both | wired |
| GET | `/api/v1/bookings/care_instructions/{id}` | nurse, admin | wired · **stage-2 disclosure gate** |
| POST | `/api/v1/bookings/submit_care_instructions/{id}` | customer | **unwired** — the client has no care-instructions form |
| POST | `/api/v1/bookings/convert` | — | **unwired** — Development-only capture simulator; the PSP webhook supersedes it |
| POST | `/api/v1/bookings/transition/{id}` | admin | **unwired** — a raw state-machine escape hatch, no UI |
| POST | `/api/v1/bookings/cancel/{id}` | — | **unwired, superseded** by `{id}/cancel` |
| POST | `/api/v1/bookings/{id}/cancel` | customer | wired — by **[refunds](refunds.md)** (cancel *and* refund, REQ-019) |
| GET | `/api/v1/bookings/{id}/cancellation_policy` | customer | wired — by **[refunds](refunds.md)** (REQ-020) |
| GET | `/api/v1/booking_sessions/today` | nurse | wired · paginated |
| GET | `/api/v1/booking_sessions/evv/{id}` | nurse | wired |
| POST | `/api/v1/booking_sessions/check_in/{id}` | nurse | wired |
| POST | `/api/v1/booking_sessions/check_out/{id}` | nurse | wired · `sensitive` 20/min — **this is what releases the payout clock** |
| POST | `/api/v1/booking_sessions/cancel/{id}` | nurse | **unwired** — no per-session cancel in the UI |
| GET | `/api/v1/admin_evv/list` | admin | **unwired** — no console screen; the mock demonstrates it |
| POST | `/api/v1/admin_evv/detect_no_shows` | admin | **unwired** — an ops one-shot; the scheduler runs it |

All `[Authorize]`; the two `admin_evv` routes are `DynamicPermission` + `sensitive`. No phantoms.

> **Two cancel routes exist on the same controller.** `POST bookings/cancel/{id}` (action-style, b9) and
> `POST bookings/{id}/cancel` (REST-style, b11 cancel-and-refund). Only the second is wired. They are
> not aliases — the second also drives the refund. Treat the first as legacy.

## The lifecycle

```
pending_payment ──capture──▸ confirmed ──first check-in──▸ in_progress
                                 │                              │
                                 │                       all sessions out
                                 ▼                              ▼
                             cancelled  ◂──cancel──         completed ──dispute window──▸ closed
                                                                 └──dispute──▸ disputed
```

Forward-only, through the transition table. `status` has a private setter; only cohesive domain methods
mutate it and the handler pre-checks, returning a clean `409`.

Sessions run their own machine: `scheduled → in_progress → completed`, or `missed` / `cancelled`.

## Shape rules the JSON does not express

- **Two-stage clinical disclosure is enforced server-side.** `care_instructions/{id}` decrypts and returns
  the care plan **only** post-confirmation and **only** to the assigned nurse or an admin. It is never
  projected into a list and never logged. The client's UI gate mirrors this; it does not create it.
- **EVV is advisory, and `checkInAddressMatch` is a tri-state**: `true` (inside tolerance), `false`
  (outside), `null` (**no reading** — permission denied or unavailable). Null is not a failure. A
  mismatch does not block check-in; it raises a support alert
  (`evv_location_mismatch`, see [admin.md](admin.md)). Tolerance is
  `evv_location_tolerance_meters` config. REQ-015 confirmed the tri-state.
- **`BookingDetailDto.variantSnapshotJson` and `addressSnapshotJson` are strings containing JSON**, not
  typed objects — the point of a snapshot is that later edits to the variant or address cannot rewrite
  history. The client parses defensively across multiple key spellings (REQ-045 open).
- **The three-amount split is guaranteed by a DB CHECK**:
  `grossPriceIrr = balinyaarCommissionIrr + nursePayoutAmount`. `platformFeeRate` is **snapshotted onto
  the row** at compute time, so a later rate change is not retroactive. Never recompute any of these.
- **`payoutEligibleAt` per session** is the payout clock, started by check-out and resolved against the
  holiday calendar server-side. See [payouts.md](payouts.md).
- **`disputeWindowEndsAt`** gates `completed → closed`; `dispute_window_hours` is config.
- `BookingListItemDto` is deliberately thin: `id, status, counterpartyName, scheduledDate, sessionCount,
  amountIrr, disputeWindowEndsAt, createdAt`. **No `patientId`** — which is what blocks REQ-057's care
  teaser.
- `NEXT_PUBLIC_EVV_MOCK_GPS` overrides the GPS reading for local testing (`off` = real capture). See
  [../config-matrix.md](../config-matrix.md).

## Enums

| Vocabulary | Values |
| --- | --- |
| `BookingStatus` | `pending_payment` `confirmed` `in_progress` `completed` `disputed` `closed` `cancelled` |
| `BookingSessionStatus` | `scheduled` `in_progress` `completed` `missed` `cancelled` |
| `VisitVerificationStatus` (`evvStatus`) | `pending` `checked_in` `completed` |
| `BookingListRole` *(query param)* | `customer` `nurse` `all` |
| `EvvGpsMode` *(client test knob)* | `off` `in_range` `out_of_range` `denied` |

Verified identical to `Baya.Domain/Entities/Booking/*.cs`.

## Open REQs

| REQ | Status | Effect |
| --- | --- | --- |
| REQ-045 | open | `variantSnapshot`/`addressSnapshot` are untyped JSON strings. The client keeps a defensive multi-key parse; no user-facing defect |
| REQ-051 | open | The nurse view of a confirmed+ booking still masks the address. The nurse sees a quiet fallback note, not a crash |
| REQ-052 | open | The today feed carries no service label — it renders patient name + visit index only |
| REQ-054 | deferred | No web push for new requests; a 15 s poll remains the only signal |
| REQ-057 | open | `BookingListItemDto` has no `patientId` and the patient read has no `lastVisitAt`, so the card renders no care teaser. See [patients.md](patients.md) |
