Files
baya-monorepo/dev/contracts/domains/bookings-evv.md
T
2026-07-06 19:23:44 +03:30

133 lines
9.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<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=&page_size=`
- **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=&page_size=`
- **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 0100, 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).