Files
baya-monorepo/docs/flows/booking-lifecycle-evv.md
T
2026-08-02 17:18:36 +03:30

13 KiB
Raw Blame History

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.tsxSessionCard 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-43ListBookings. Thin DTO — no patientId
booking detail GET bookings/get/{id} :33GetBookingDetail; nurse view nulls addressSnapshotJson (BookingMapper.cs:28)
today feed GET booking_sessions/today :45-53ListSessionsForNurseQueryBookingRepository.cs:185-214. See gap 1
session EVV GET booking_sessions/evv/{id} :55GetVisitVerification; raw GPS gated to owning nurse + admin
care instructions GET bookings/care_instructions/{id} :58GetCareInstructionsQuery.Handler.cs:35-38 — the stage-2 gate
check in POST booking_sessions/check_in/{id} :61-67CheckInVisitCommand.Handler.cs. Body is coordinates only; the server timestamps
check out POST booking_sessions/check_out/{id} :69-75CheckOutVisitCommand.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

  1. Log in as 09120000001 (nurse زهرا عزیزی) — see testing-setup.md. Before starting npm run dev, set NEXT_PUBLIC_EVV_MOCK_GPS=in_range in client/.env.development, or every check-in from your laptop is a mismatch (see gap 6).
  2. 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). Only sessionId 14 (booking 10, 2026-08-12) is scheduled/pending; every other seeded row is completed, missed or cancelled.
  3. Tap مشاهدهٔ رزرو on booking 2. Expect: BookingDetailView in the nurse shell — status «در حال انجام», the care-instructions card populated (فشار خون بالا، دیابت نوع دو تحت کنترل. / متفورمین 500 (صبح و شب)… / پنی سیلین.), 5 session rows (3 completed, 2 missed), and the quiet address_pending_note copy «آدرس پس از تأیید رزرو در دسترس قرار می‌گیرد.» instead of the address — REQ-051, not a crash.
  4. 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/convert inside the 30-minute payment window (paying through the browser cannot finish — MockPaymentProvider redirects to the non-existent mock-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/4409 "This session cannot be checked in."; POST /api/v1/booking_sessions/check_out/5400 "Check-out must follow an open check-in."
  5. 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 seeded evv_location_mismatch case. Its admin queue is unreachable (gap 5).
  6. Confirm the clinical gate from both sides: GET /api/v1/bookings/care_instructions/2 as the nurse → 200 with decrypted Persian text; the same call as 09120000010 (the owning customer) → 404 "Care instructions not found."
  7. 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/today applies no date filter when date is omitted (BookingRepository.cs:191-192), and both callers — visits/page.tsx:26 and NurseDashboardScreen.tsx:85 — call useTodaySessions() 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:10 asserts "date omitted = the server's today"; that is false. The feed is also a fixed pageSize: 20 with no "load more" (useTodaySessions.ts:16), so a nurse past 20 lifetime sessions can never reach the recent ones. The dashboard hero compounds it: NextVisitCard labels the 2026-08-12 session "next visit" ten days early.
  • A booking whose remaining sessions are swept to missed never completes. DetectNoShowSessionsCommand.Handler transitions sessions only; the allSettled → Completed re-evaluation exists solely in CheckOutVisitCommand.Handler.cs:64-70. Live: booking 2 has all 5 sessions terminal (3 completed, 2 missed), is still in_progress, and disputeWindowEndsAt is null — so its 3 checked-out sessions can never enter a payout batch (PayoutRepository.cs:31 requires b.Status == BookingStatus.Completed). Nurse money is stranded.
  • completed → closed is 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 still completed.
  • No client path produces disputed. The status renders in BOOKING_STATUS_KIND but nothing raises a dispute — the customer's only booking write is cancel.
  • The evv_location_mismatch alert queue is doubly unreachable: GET admin_evv/list has no client screen, and it 403s for 09120000020/09120000021 anyway. The seeded 1220 m mismatch on session 9 can only be seen through the nurse's own EVV read.
  • NEXT_PUBLIC_EVV_MOCK_GPS now defaults to off (constants.ts:60-61) because it is derived from USE_BOOKINGS_MOCK, which was flipped to false. A tester on a laptop away from the seeded Tehran address therefore fires a real mismatch — and a SupportAlert — on every check-in. The constant's own doc-comment still describes the old in_range default.
  • REQ-051 — the nurse never sees the address on a confirmed booking. BookingMapper.ToDetailDto nulls addressSnapshotJson for 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 optional variantLabel? and it is always undefined on the real path, so every today-feed row says patient name + visit index only.
  • REQ-057 — BookingListItemDto has no patientId, 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 in client/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: real above is a verdict on the bookings seam only. The same screen mounts NurseVisitNotesPanel, whose task checklist, continuity history and note-save all come from services/patientRecordsUSE_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.ts has no importer anywhere in client/src — dead code, and its doc-block still claims the domain is mock-primary (as does constants.ts:1-13 above the false).