backend phase 9
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
# 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 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).
|
||||
Reference in New Issue
Block a user