# 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/06-evv-and-service-delivery.md) ·
[product/business/05-booking-and-scheduling.md](../../product/business/05-booking-and-scheduling.md)
**Integration:** [docs/integration/domains/bookings.md](../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`](../../client/src/app/%5Blocale%5D/(private-routes)/nurse/NurseDashboardScreen.tsx) `NextVisitCard` picks the first `scheduled`/`in_progress` row, CTA → `/nurse/visits` |
| Nurse «ویزیت امروز» | `/fa/nurse/visits` | [`visits/page.tsx`](../../client/src/app/%5Blocale%5D/(private-routes)/nurse/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](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](../integration/domains/bookings.md) — not restated here. Client seam
`USE_BOOKINGS_MOCK = false` ([`constants.ts:14`](../../client/src/services/bookings/constants.ts)); all 7 seam
ops map published routes.

**The independent EVV-GPS seam.** `NEXT_PUBLIC_EVV_MOCK_GPS` selects `ILocationProvider`
([`evv/locationProvider.ts:76`](../../client/src/services/bookings/evv/locationProvider.ts)). 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](../../product/business/06-evv-and-service-delivery.md) |
| 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](../../product/business/10-payouts.md) |
| 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](../../product/business/06-evv-and-service-delivery.md) |
| 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](../../product/business/12-messaging-and-emergencies.md) |

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](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](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."**
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/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](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`).
