# Contract — Bookings, Sessions, EVV & Cancellation (backend phase b9)

> One-line: the post-payment engagement — convert a paid request into a booking + N sessions, the two-stage
> care-instructions boundary, per-session EVV check-in/out, dispute-window gating, and cancellation. Assumes
> [`../conventions/api-conventions.md`](../conventions/api-conventions.md) +
> [`../conventions/money-and-types.md`](../conventions/money-and-types.md). Machine schema:
> [`../openapi/swagger.v1.json`](../openapi/README.md).

**Status:** live as of backend-phase-b9 · **Frontend consumer:** frontend-phase-f8-b9

All money is **IRR Rials, integer, on the wire as a string of digits** (`"15000000"`). The three booking
amounts always satisfy `gross_price_irr = balinyaar_commission_irr + nurse_payout_amount`. Timestamps are UTC
ISO-8601. Enums cross as their stable string codes.

## Enums used
- `BookingStatus`: `pending_payment` | `confirmed` | `in_progress` | `completed` | `disputed` | `closed` | `cancelled`.
- `BookingSessionStatus`: `scheduled` | `in_progress` | `completed` | `missed` | `cancelled`.
- `VisitVerificationStatus`: `pending` | `checked_in` | `completed`.
- `CancellationActor` (`applies_to` / `cancelled_by`): `customer` | `nurse` | `admin`.

## Endpoints

### `POST api/v1/bookings/convert`
- **Purpose:** The (mock) payment-capture conversion — creates the booking 1:1 from an
  `accepted_awaiting_payment` request. In b10 the real card capture calls this path directly.
- **Auth:** authenticated (owning customer or admin) · **Rate-limited:** yes (sensitive) · **Idempotent:** yes
  (replay returns the existing booking).
- **Request body:** `{ "bookingRequestId": 123 }`
- **Success `200` (`data`):** the `Booking` shape (below), `status = confirmed`.
- **Failure:** `400` bad id, `401` unauth, `404` request not found / not the caller's, `409` request not
  awaiting payment / no longer convertible, `400` capture failed (no booking created).
- **Notes:** computes the three amounts (commission = round(`gross × platform_fee_rate`), rate snapshotted),
  writes `variant_snapshot_json` + **encrypted** `address_snapshot_json`, generates ≥ 1 session with
  `Σ visit_payout_amount = nurse_payout_amount`, flips the request → `converted`, notifies both parties.

### `GET api/v1/bookings/get/{id}`
- **Purpose:** Booking header + money summary + sessions + timeline. **Auth:** authenticated, tenancy-scoped
  (customer own / nurse assigned / admin all). The **nurse** view omits `addressSnapshotJson`. Never includes
  care-instruction clinical fields. **Failure:** `401`, `404` (not found / not a party → no leak).

### `GET api/v1/bookings/list?role=customer|nurse|all&status=&page=&pageSize=`
- **Purpose:** role-scoped "My bookings" (paginated, projected). `role=all` is admin-only (`403` otherwise).
  **Success:** `PagedResult<BookingListItem>`.

### `POST api/v1/bookings/transition/{id}`
- **Purpose:** admin/dispute status move. **Auth:** admin (`403` otherwise). **Body:**
  `{ "targetStatus": "disputed", "reason": "…" }`. **Failure:** `409` (illegal edge, or contradicts EVV —
  e.g. `in_progress` with no session checked in, `completed` with a live session), `400` (use `cancel` for
  cancellation). Completing here also opens the dispute window.

### `POST api/v1/bookings/cancel/{id}`
- **Purpose:** cancel a whole booking. **Auth:** owning customer / assigned nurse / admin · **Rate-limited:** yes.
  **Body:** `{ "reason": "…" }`. **Success:** `CancellationResult`. Resolves + **snapshots** the policy
  (`code` + `refund_percentage`) onto the booking, cancels only un-started (`scheduled`) sessions, computes the
  refundable amount. **No refund ledger is posted (b11).** **Failure:** `400` no reason, `409` not cancellable.

### `POST api/v1/bookings/submit_care_instructions/{id}`
- **Purpose:** write/update the encrypted stage-2 `booking_care_instructions`. **Auth:** owning customer or
  admin, booking must be `confirmed`+. **Body:** `CareInstructions` (all optional strings). **Failure:** `409`
  not confirmed, `404` not found.

### `GET api/v1/bookings/care_instructions/{id}`
- **Purpose:** the **gated** stage-2 read. **Auth:** **assigned nurse or admin only, post-confirmation.** Any
  other caller (customer, unassigned nurse, pre-confirmation) → `404` (never leaks). Returns decrypted
  `CareInstructions`. **This is the two-stage disclosure boundary.**

### `POST api/v1/booking_sessions/check_in/{id}`
- **Purpose:** assigned nurse clocks in. **Auth:** assigned nurse. **Body:** `{ "latitude": 35.6892,
  "longitude": 51.389 }` (both nullable — GPS-denied still checks in, flagged). Moves the session + booking to
  `in_progress`; computes the **advisory** address match against `evv_location_tolerance_meters`. A mismatch
  raises a `location_mismatch` support alert + notifies **without blocking**. **Success:** `VisitVerification`.
  **Failure:** `401`, `403` (not a nurse), `404` (not the nurse's session), `409` (not startable).

### `POST api/v1/booking_sessions/check_out/{id}`
- **Purpose:** assigned nurse clocks out — must follow an open check-in. Completes the session's EVV, sets its
  `payout_eligible_at`, and — when all sessions are settled — completes the booking + sets
  `dispute_window_ends_at`. **Failure:** `400` no open check-in, `409` not checkout-able.

### `GET api/v1/booking_sessions/today?date=&page=&pageSize=`
- **Purpose:** the nurse's sessions for a day (default all), with check-in/out CTA state. **Auth:** nurse,
  tenancy-scoped. **Success:** `PagedResult<BookingSessionListItem>`.

### `GET api/v1/booking_sessions/evv/{id}`
- **Purpose:** per-session EVV detail. **Auth:** owning nurse + admin only (raw GPS gated); others → `404`.

### `POST api/v1/booking_sessions/cancel/{id}`
- **Purpose:** cancel a single un-started session · **Rate-limited:** yes. Snapshots the policy + computes the
  session's refundable share. **Failure:** `409` if the session already started.

### `GET api/v1/admin_evv/list?type=mismatch|no_show&page=&pageSize=`
- **Purpose:** admin EVV-review queue. **Auth:** admin policy · **Rate-limited:** yes. **Success:**
  `PagedResult<AdminEvvItem>`.

### `POST api/v1/admin_evv/detect_no_shows`
- **Purpose:** the manual no-show sweep trigger (the recurring cron is DEFERRED). **Auth:** admin. Marks
  overdue scheduled sessions `missed`, raises `no_show` alerts + notifies. **Success:** `{ "missed": N }`.

### `POST api/v1/admin_cancellation_policies/upsert` · `GET api/v1/admin_cancellation_policies/list`
- **Purpose:** admin CRUD of cancellation tiers (keyed by unique `code`). **Auth:** admin policy. Editing a
  policy never mutates an already-snapshotted cancellation. **Failure:** `400` (percentage not 0–100, bad
  actor, min ≥ max).

## Shared shapes
- **`Booking`** (`bookings/get`, `convert`, `transition`): `id`, `bookingRequestId`, `status` (`BookingStatus`),
  `nurseId`, `nurseName`, `patientId`, `patientName`, `variantId`, `variantSnapshotJson` (string),
  `customerAddressId`, `addressSnapshotJson` (string, **null for the nurse view**), `grossPriceIrr`,
  `balinyaarCommissionIrr`, `nursePayoutAmount`, `pspFeeAmount` (money strings; psp nullable),
  `platformFeeRate` (decimal), `sessionCount` (int), `scheduledDate`/`scheduledTimeStart`/`scheduledTimeEnd`,
  `confirmedAt`/`completedAt`/`cancelledAt` (nullable), `cancelledBy`/`cancellationReason`/
  `cancellationPolicyCode` (nullable), `cancellationRefundPercentage` (nullable decimal), `refundableAmountIrr`
  (nullable money string), `disputeWindowEndsAt` (nullable), `createdAt`, `sessions[]`.
- **`BookingSessionSummary`** (embedded): `id`, `sessionIndex`, schedule, `status` (`BookingSessionStatus`),
  `visitPayoutAmount` (money string), `payoutEligibleAt` (nullable), `evvStatus` (`VisitVerificationStatus`),
  `checkInAt`/`checkOutAt` (nullable), `checkInAddressMatch` (nullable bool).
- **`BookingListItem`**: `id`, `status`, `counterpartyName`, `scheduledDate`, `sessionCount`, `amountIrr`
  (gross for customer / payout for nurse), `disputeWindowEndsAt`, `createdAt`.
- **`BookingSessionListItem`**: `sessionId`, `bookingId`, `sessionIndex`, `patientName`, schedule, `status`,
  `evvStatus`.
- **`CareInstructions`**: `bookingId` + `currentConditions`/`medications`/`allergies`/`specialInstructions`/
  `emergencyContactName`/`emergencyContactPhone` (all nullable). **Encrypted at rest; gated read.**
- **`VisitVerification`**: `id`, `bookingSessionId`, `status`, `checkInAt`/`checkInLat`/`checkInLng`,
  `checkOutAt`/`checkOutLat`/`checkOutLng`, `checkInAddressMatch`, `checkInDistanceMeters` (raw GPS gated).
- **`AdminEvvItem`**: `sessionId`, `bookingId`, `nurseId`, `sessionStatus`, `scheduledDate`,
  `scheduledTimeStart`, `checkInAt`, `checkInAddressMatch`, `checkInDistanceMeters`.
- **`CancellationResult`**: `bookingId`, `sessionId` (nullable), `bookingStatus`, `policyCode`,
  `refundPercentage`, `refundableAmountIrr` (money string).
- **`CancellationPolicy`**: `id`, `code`, `appliesTo`, `hoursBeforeStartMin`/`Max` (nullable), `refundPercentage`,
  `feeAmountIrr` (money string), `feeRate` (nullable), `isActive`.

## Changelog
- b9 — initial contract (bookings + sessions + care instructions + EVV + cancellation; capture mocked via
  `IPaymentCaptureSimulator`, real trigger arrives with b10).
