# 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=&page_size=` - **Purpose:** role-scoped "My bookings" (paginated, projected). `role=all` is admin-only (`403` otherwise). **Success:** `PagedResult`. ### `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=&page_size=` - **Purpose:** the nurse's sessions for a day (default all), with check-in/out CTA state. **Auth:** nurse, tenancy-scoped. **Success:** `PagedResult`. ### `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=&page_size=` - **Purpose:** admin EVV-review queue. **Auth:** admin policy · **Rate-limited:** yes. **Success:** `PagedResult`. ### `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).