13 KiB
Flow — Booking lifecycle & EVV
Last verified: 2026-08-02 against commit
c841bde
Actor(s): nurse (check-in/out, care instructions) · customer (watch the engagement) · Status: partial Client: real · Server: real Business source: product/business/06-evv-and-service-delivery.md · product/business/05-booking-and-scheduling.md Integration: docs/integration/domains/bookings.md
What it does
Once a booking request is paid, a bookings row exists with per-visit sessions. The assigned nurse works the
day: opens the visit, clocks in, reads the gated care instructions, clocks out. The customer watches the same
booking from the other side — a server-truth timeline, the money split, and the address. Check-out is the
event that makes a session payout-eligible; nothing else does.
Screens
| Step | Route | Component / notes |
|---|---|---|
| Nurse day hero | /fa/nurse |
NurseDashboardScreen.tsx:81-133 NextVisitCard picks the first scheduled/in_progress row, CTA → /nurse/visits |
| Nurse «ویزیت امروز» | /fa/nurse/visits |
visits/page.tsx — SessionCard per row, one useEvvController, 60 s poll, EvvStatusBanner (rendered inside SessionCard) once checked in |
| Nurse booking detail | /fa/nurse/visits/[id] |
BookingDetailView viewerRole="nurse" + NurseVisitNotesPanel + BookingSupportEntry. EVV controls + CareInstructionsCard live here. The notes panel is not this flow's data — see gap 12 |
| Customer «رزروها» | /fa/bookings |
BookingsScreen — 3 tabs; فعال = pending_payment|confirmed|in_progress, گذشته = completed|disputed|closed|cancelled |
| Customer booking detail | /fa/bookings/[id] |
The same BookingDetailView, viewerRole="customer" — no EVV controls, care query never fires |
BookingDetailView is one component in two shells (both rows appear in this atlas's route coverage,
index.md); the viewer role drives the UI gate only — the server gates independently.
API
| Call | Endpoint | Notes |
|---|---|---|
| list bookings | GET bookings/list?role= |
clientApi.ts:36-43 → ListBookings. Thin DTO — no patientId |
| booking detail | GET bookings/get/{id} |
:33 → GetBookingDetail; nurse view nulls addressSnapshotJson (BookingMapper.cs:28) |
| today feed | GET booking_sessions/today |
:45-53 → ListSessionsForNurseQuery → BookingRepository.cs:185-214. See gap 1 |
| session EVV | GET booking_sessions/evv/{id} |
:55 → GetVisitVerification; raw GPS gated to owning nurse + admin |
| care instructions | GET bookings/care_instructions/{id} |
:58 → GetCareInstructionsQuery.Handler.cs:35-38 — the stage-2 gate |
| check in | POST booking_sessions/check_in/{id} |
:61-67 → CheckInVisitCommand.Handler.cs. Body is coordinates only; the server timestamps |
| check out | POST booking_sessions/check_out/{id} |
:69-75 → CheckOutVisitCommand.Handler.cs. sensitive 20/min |
Shapes live in bookings.md — not restated here. Client seam
USE_BOOKINGS_MOCK = false (constants.ts:14); all 7 seam
ops map published routes.
The independent EVV-GPS seam. NEXT_PUBLIC_EVV_MOCK_GPS selects ILocationProvider
(evv/locationProvider.ts:76). It resolves as
?? (USE_BOOKINGS_MOCK ? 'in_range' : 'off') — and USE_BOOKINGS_MOCK is now false, so the default is
off = real browser GPS. Flipping the bookings flag does not disable this seam and vice-versa.
Rules that must hold
| Rule | Value / shape | Source |
|---|---|---|
| Forward-only lifecycle | pending_payment → confirmed → in_progress → completed → closed, plus disputed/cancelled; illegal edge ⇒ clean 409 |
INV-1, BookingTransitions. Live: check_in on a completed session → 409 "This session cannot be checked in." |
| EVV is advisory | checkInAddressMatch tri-state true/false/null = no reading, not a failure. A mismatch raises SupportAlertType.EvvLocationMismatch + notifies the family; it never blocks and never cancels |
INV-14, CheckInVisitCommand.Handler.cs:64-110; business/06 |
| Location tolerance | evv_location_tolerance_meters = 200 m (CONFIG), Haversine against the frozen address snapshot |
…Handler.cs:71 |
| Check-out starts the payout clock | session.SetPayoutEligible(now + dispute_window_hours) — the only thing that makes a session payout-eligible; never the completed status alone |
INV-2, CheckOutVisitCommand.Handler.cs:59-62 |
| Dispute window | dispute_window_hours = 72 h (CONFIG), frozen per session and onto Bookings.DisputeWindowEndsAt |
business/10 |
| Two-stage clinical gate | Care instructions decrypt only post-confirmation, only for the assigned nurse or an admin. Never in a list, never logged. Any other caller gets 404, not 403 |
INV-6 / INV-7, GetCareInstructionsQuery.Handler.cs:35-38 |
| Three-amount split | gross = balinyaarCommission + nursePayout, DB CHECK; platformFeeRate snapshotted at conversion |
INV-4 / INV-8 |
| Per-session accrual | equal integer shares, remainder on the last session — Σ = nursePayoutAmount |
INV-19, BookingAmounts.SplitPayout:35-42 |
| No-show | no_show_threshold_minutes = 60 (CONFIG); the hourly no_show_sweep flags an un-checked-in session missed |
business/06 |
| Emergency playbook | The nurse's escalation contact reaches them only inside the gated CareInstructionsCard (emergencyContactName/Phone); LogEmergencyTicket never dials and never exposes a number |
INV-20, business/12 |
Live proof of the money + clock rules on booking 2: 4500000 = 675000 + 3825000, platformFeeRate 0.15,
visitPayoutAmount 765000 × 5, and session 4's payoutEligibleAt is exactly checkOutAt + 72 h.
How to test
- Log in as 09120000001 (nurse زهرا عزیزی) — see testing-setup.md.
Before starting
npm run dev, setNEXT_PUBLIC_EVV_MOCK_GPS=in_rangeinclient/.env.development, or every check-in from your laptop is a mismatch (see gap 6). - Open
/fa/nurse/visits. Expect: a session list — but not today's. The seeded world has no session dated 2026-08-02, yet the screen renders 11 rows from 2026-07-15 to 2026-08-12 (gap 1). OnlysessionId 14(booking 10, 2026-08-12) isscheduled/pending; every other seeded row iscompleted,missedorcancelled. - Tap مشاهدهٔ رزرو on booking 2.
Expect:
BookingDetailViewin the nurse shell — status «در حال انجام», the care-instructions card populated (فشار خون بالا، دیابت نوع دو تحت کنترل./متفورمین 500 (صبح و شب)…/پنی سیلین.), 5 session rows (3completed, 2missed), and the quietaddress_pending_notecopy «آدرس پس از تأیید رزرو در دسترس قرار میگیرد.» instead of the address — REQ-051, not a crash. - Walk a real check-in/out. The seeded world can no longer support this: booking 2's sessions are all
terminal and booking 10's only session is dated 2026-08-12. Workaround — either (a) as 09120000010
create a request for today → accept it as the nurse → convert it with the Development-only capture
simulator
POST /api/v1/bookings/convertinside the 30-minute payment window (paying through the browser cannot finish —MockPaymentProviderredirects to the non-existentmock-psp.local, see checkout-and-payment.md); or (b) probe the guards, which is what proves the wiring:POST /api/v1/booking_sessions/check_in/4→409"This session cannot be checked in.";POST /api/v1/booking_sessions/check_out/5→400"Check-out must follow an open check-in." - Read the out-of-range EVV as 09120000002 (nurse علی کریمی):
GET /api/v1/booking_sessions/evv/9. Expect:checkInAddressMatch: false,checkInDistanceMeters: 1220.00— this is the seededevv_location_mismatchcase. Its admin queue is unreachable (gap 5). - Confirm the clinical gate from both sides:
GET /api/v1/bookings/care_instructions/2as the nurse →200with decrypted Persian text; the same call as 09120000010 (the owning customer) →404"Care instructions not found." - Log in as 09120000010 and open
/fa/bookings→ tab فعال. Expect: bookings 1, 2 and 10 (confirmed,in_progress,confirmed). Open booking 2 → the address is present in the customer view, and no EVV control is rendered.
Known gaps
booking_sessions/todayapplies no date filter whendateis omitted (BookingRepository.cs:191-192), and both callers —visits/page.tsx:26andNurseDashboardScreen.tsx:85— calluseTodaySessions()with no argument. The «ویزیت امروز» screen therefore lists the nurse's whole session history, oldest first (live: 11 rows, 2026-07-15 → 2026-08-12).useTodaySessions.ts:10asserts "dateomitted = the server's today"; that is false. The feed is also a fixedpageSize: 20with no "load more" (useTodaySessions.ts:16), so a nurse past 20 lifetime sessions can never reach the recent ones. The dashboard hero compounds it:NextVisitCardlabels the 2026-08-12 session "next visit" ten days early.- A booking whose remaining sessions are swept to
missednever completes.DetectNoShowSessionsCommand.Handlertransitions sessions only; theallSettled → Completedre-evaluation exists solely inCheckOutVisitCommand.Handler.cs:64-70. Live: booking 2 has all 5 sessions terminal (3completed, 2missed), is stillin_progress, anddisputeWindowEndsAtisnull— so its 3 checked-out sessions can never enter a payout batch (PayoutRepository.cs:31requiresb.Status == BookingStatus.Completed). Nurse money is stranded. completed → closedis unreachable. No job closes a booking and the only writer,POST bookings/transition/{id}, is admin-only, has no UI, and 403s for the seeded admins. Live: bookings 3, 4 and 8 have dispute windows that closed on 2026-07-18/19/28 and are stillcompleted.- No client path produces
disputed. The status renders inBOOKING_STATUS_KINDbut nothing raises a dispute — the customer's only booking write is cancel. - The
evv_location_mismatchalert queue is doubly unreachable:GET admin_evv/listhas no client screen, and it 403s for09120000020/09120000021anyway. The seeded 1220 m mismatch on session 9 can only be seen through the nurse's own EVV read. NEXT_PUBLIC_EVV_MOCK_GPSnow defaults tooff(constants.ts:60-61) because it is derived fromUSE_BOOKINGS_MOCK, which was flipped tofalse. A tester on a laptop away from the seeded Tehran address therefore fires a real mismatch — and aSupportAlert— on every check-in. The constant's own doc-comment still describes the oldin_rangedefault.- REQ-051 — the nurse never sees the address on a confirmed booking.
BookingMapper.ToDetailDtonullsaddressSnapshotJsonfor the nurse, so the nurse must navigate by patient name alone. Ironically the server does read that snapshot to compute the EVV match. - REQ-052 —
BookingSessionListItemDto(BookingDtos.cs:71-80) carries no service/variant field. The client models it as optionalvariantLabel?and it is alwaysundefinedon the real path, so every today-feed row says patient name + visit index only. - REQ-057 —
BookingListItemDtohas nopatientId, so a booking row cannot deep-link to the care record. POST bookings/submit_care_instructions/{id}is unwired — there is no customer form anywhere inclient/src. Care instructions exist only because the seeder wrote them; on a booking a tester creates, the nurse's card will be empty.POST booking_sessions/cancel/{id}is unwired — a nurse cannot cancel a single visit from the UI.- The nurse booking detail is half real.
Client: realabove is a verdict on thebookingsseam only. The same screen mountsNurseVisitNotesPanel, whose task checklist, continuity history and note-save all come fromservices/patientRecords—USE_PATIENT_RECORDS_MOCK = true, module state, lost on navigation. The EVV/care/timeline half is server truth; the notes half is fabricated. → patient-care-records.md client/src/services/bookings/apis/serverApi.tshas no importer anywhere inclient/src— dead code, and its doc-block still claims the domain is mock-primary (as doesconstants.ts:1-13above thefalse).