6.4 KiB
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 commitd3ec723and 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.
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 (cancel and refund, REQ-019) |
| GET | /api/v1/bookings/{id}/cancellation_policy |
customer | wired — by refunds (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) andPOST 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
checkInAddressMatchis 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). Tolerance isevv_location_tolerance_metersconfig. REQ-015 confirmed the tri-state. BookingDetailDto.variantSnapshotJsonandaddressSnapshotJsonare 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.platformFeeRateis snapshotted onto the row at compute time, so a later rate change is not retroactive. Never recompute any of these. payoutEligibleAtper session is the payout clock, started by check-out and resolved against the holiday calendar server-side. See payouts.md.disputeWindowEndsAtgatescompleted → closed;dispute_window_hoursis config.BookingListItemDtois deliberately thin:id, status, counterpartyName, scheduledDate, sessionCount, amountIrr, disputeWindowEndsAt, createdAt. NopatientId— which is what blocks REQ-057's care teaser.NEXT_PUBLIC_EVV_MOCK_GPSoverrides the GPS reading for local testing (off= real capture). See ../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 |