9.1 KiB
9.1 KiB
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/money-and-types.md. Machine schema:../openapi/swagger.v1.json.
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_paymentrequest. 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): theBookingshape (below),status = confirmed. - Failure:
400bad id,401unauth,404request not found / not the caller's,409request not awaiting payment / no longer convertible,400capture failed (no booking created). - Notes: computes the three amounts (commission = round(
gross × platform_fee_rate), rate snapshotted), writesvariant_snapshot_json+ encryptedaddress_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=allis admin-only (403otherwise). Success:PagedResult<BookingListItem>.
POST api/v1/bookings/transition/{id}
- Purpose: admin/dispute status move. Auth: admin (
403otherwise). Body:{ "targetStatus": "disputed", "reason": "…" }. Failure:409(illegal edge, or contradicts EVV — e.g.in_progresswith no session checked in,completedwith a live session),400(usecancelfor 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:400no reason,409not 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 beconfirmed+. Body:CareInstructions(all optional strings). Failure:409not confirmed,404not 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 decryptedCareInstructions. 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 toin_progress; computes the advisory address match againstevv_location_tolerance_meters. A mismatch raises alocation_mismatchsupport 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 + setsdispute_window_ends_at. Failure:400no open check-in,409not 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:
409if 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, raisesno_showalerts + 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).