Files
baya-monorepo/archive/docs/flows/booking-lifecycle-evv.md
T
2026-08-02 20:01:31 +03:30

119 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`).