backend phase 15 & frontend phase 8
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
# Backend Phase 15 report — Messaging (tickets), partner centers & admin backoffice
|
||||
|
||||
**The final backend phase.** It closes the operational loop: the ticket system, the licensed partner centers
|
||||
(merchant-of-record), and the consolidated admin backoffice. The backend chain is now complete.
|
||||
|
||||
## What was built
|
||||
|
||||
### Messaging (tickets) — new `messaging` schema
|
||||
- Entities `Domain/Entities/Messaging/`: `Ticket`, `TicketParticipant`, `TicketMessage` + `TicketStatus` /
|
||||
`TicketCategory` / `TicketParticipantRole` code sets. Configs in `Persistence/Configuration/MessagingConfig/`.
|
||||
- `ITicketRepository` (+ `TicketRepository`) on `IUnitOfWork`.
|
||||
- Features `Application/Features/Messaging/`: `OpenTicket`, `AutoCreateCoordinationTicket`, `PostMessage`,
|
||||
`AddParticipant`, `RemoveParticipant`, `CloseTicket`, `ReopenTicket`, `LogEmergencyTicket`, `GetTicketThread`
|
||||
(role-aware user/admin view), `ListMyTickets`, `ListTicketsForAdmin`. Shared helpers `TicketReferenceCode`
|
||||
(collision-checked mint) + `TicketRoleResolver` + `StaffRoles` (Application/Common).
|
||||
- Controllers `TicketsController` (authenticated) + `AdminTicketsController` (`support`/`admin`).
|
||||
|
||||
### Partner centers — new `partner` schema
|
||||
- Entity `Domain/Entities/PartnerCenters/PartnerCenter` (`IAuditable`; `settlement_iban` `[AuditRedacted]` +
|
||||
encrypted converter in `ApplicationDbContext`). Config in `Persistence/Configuration/PartnerCentersConfig/`
|
||||
(also adds the `nurse_profiles.partner_center_id` FK in place). `IPartnerCenterRepository` (+ impl).
|
||||
- Features: `CreatePartnerCenter`, `UpdatePartnerCenter`, `VerifyPartnerCenter`, `SponsorNurse`,
|
||||
`GetCenterForBooking` (the merchant-of-record resolver), `ListPartnerCenters`, `GetPartnerCenterById`,
|
||||
`GetCenterDashboard`. Controllers `AdminPartnerCentersController`, `CentersController` (portal),
|
||||
`InternalCentersController` (resolver).
|
||||
|
||||
### Seam
|
||||
- **`ILicenseVerificationService`** (`Application/Contracts/Common`) + `MockLicenseVerificationService`
|
||||
(`CrossCutting/Seams/`, registered in `AddCrossCuttingSeams`, config `Seams:LicenseVerification:AutoApprove`).
|
||||
|
||||
### Cross-phase wiring
|
||||
- b11 `IssueInvoiceCommand` now sets `invoices.issuing_entity_type` + `partner_center_id` from
|
||||
`ResolveCenterForBookingAsync` (the single merchant-of-record resolver).
|
||||
- b11 `CreateRefundCommand` auto-opens a `category=refund` ticket via `OpenTicketCommand` when the caller passes
|
||||
none, so `refunds.ticket_id` is always non-null (replaces the old config-gated "ticket required" check).
|
||||
- The card `ConfirmPaymentAndPostLedger` and BNPL `SettleBnplOrder` handlers dispatch
|
||||
`AutoCreateCoordinationTicketCommand` after a booking is confirmed (idempotent, one per booking).
|
||||
|
||||
### Reused, not rebuilt (admin backoffice consolidation)
|
||||
- Support-alert worklist (`ISupportAlertService` List/Assign/Resolve — `SupportAlertsController`) and the audit
|
||||
viewer (`GetAuditTrail` — `AuditController`) already existed since b1; verified as the backoffice surface.
|
||||
Verification/refund/payout/moderation queues are their own phases' endpoints.
|
||||
|
||||
## What is now testable and exactly how (the §7 steps)
|
||||
1. **Open + message:** `POST /api/v1/tickets` (no links) → 200 with a `TKT-…` `referenceCode` + opener as
|
||||
participant; `POST /api/v1/tickets/{id}/messages` → the message appears in the thread.
|
||||
2. **Internal boundary (proven by a test):** admin `POST …/messages {isInternal:true}` → 200; user
|
||||
`GET /api/v1/tickets/{id}` omits it; admin `GET /api/v1/admin/tickets/{id}` includes it; a non-staff
|
||||
`isInternal:true` → 403. (`MessagingApiTests.InternalNote_IsHiddenInUserView_ShownInAdminView` + `NonAdmin_CannotSetInternal`.)
|
||||
3. **Participant uniqueness:** add a user → 200; add again → **409** (not 500); delete → 200.
|
||||
(`MessagingApiTests.AddParticipant_DuplicateIsConflict_NotServerError`.)
|
||||
4. **Partner center + masked IBAN:** `POST /api/v1/admin/partner-centers {isMerchantOfRecord:true, settlementIban}`
|
||||
→ 200 with `settlementIbanMasked` (last 4), never plaintext; `GET …/{id}` masks it too; created inactive.
|
||||
(`PartnerCentersApiTests.CreateMerchantOfRecord_MasksSettlementIban`, `Verify_ActivatesTheCenter`.)
|
||||
5. **Merchant-of-record resolution:** `GET /api/v1/internal/bookings/{id}/center` → `partner_center` (+ id) for a
|
||||
nurse sponsored by a merchant-of-record center, `platform` otherwise. (`CenterForBookingTests`, 4 cases.)
|
||||
6. **Refund anchors a ticket:** `CreateRefund` yields a non-null `refunds.ticket_id` (foundation refund tests
|
||||
pass with the auto-open wired via `TestSenders.WithTicketHooks()`).
|
||||
7. **Admin worklists / RBAC:** support alerts + audit reachable under admin scope; a non-admin token on an admin
|
||||
route → 403 (`PartnerCentersApiTests.NonAdmin_IsForbidden`), unauthenticated → 401.
|
||||
8. **Audit:** admin state changes (e.g. `VerifyPartnerCenter`) append an `audit_logs` row (`PartnerCenter` is
|
||||
`IAuditable`; `settlement_iban` is redacted in the diff).
|
||||
|
||||
## What is mocked / waiting on a real service
|
||||
- `ILicenseVerificationService` → manual-approve at MVP (no public eNamad/MoH B2B API). Make-it-real steps in
|
||||
`reports/mocks-registry.md` (🟡). No telephony seam — the emergency call is out-of-platform by design.
|
||||
|
||||
## Contracts produced
|
||||
- `dev/contracts/domains/messaging-notifications-admin.md`; `dev/contracts/openapi/swagger.v1.json` refreshed
|
||||
(now includes tickets, partner centers, the center resolver).
|
||||
|
||||
## Gate
|
||||
- `dotnet build Baya.sln` — 0 new code warnings. `dotnet test Baya.sln` — green: 4 identity + 240 foundation +
|
||||
114 API (12 new tests this phase). Migration `MessagingAndPartnerCenters` scaffolds cleanly.
|
||||
|
||||
## Decisions / notes for the future
|
||||
- **Merchant-of-record** = `partner_center` issuer only when the sponsoring center `is_merchant_of_record`; a
|
||||
non-MoR sponsor leaves the platform as issuer (so a sponsored-but-platform-billed nurse is representable).
|
||||
- **Participant removal** is a soft `removed_at` stamp (not a hard delete / not `deleted_at`), so the
|
||||
`UNIQUE(ticket_id, user_id)` row survives and a re-add resurrects it.
|
||||
- **SQLite gotcha (again):** messages are ordered by the monotonic `Id` (== send order), never `ORDER BY sent_at`
|
||||
(`DateTimeOffset`), which the SQLite test provider can't translate.
|
||||
- **Follow-ups:** the invoice-issuer wire sets the columns but the downstream settlement rail (paying a center's
|
||||
IBAN when it is MoR) is not exercised end-to-end here; the center dashboard caps the sponsored-nurse list at 50
|
||||
(count is exact) — paginate it if a center grows large. Bookings/invoices `partner_center_id` columns exist
|
||||
without a DB FK (only `nurse_profiles` got the FK, per the DoD).
|
||||
@@ -0,0 +1,122 @@
|
||||
# Frontend Phase 8 (f8-b9) — Booking detail, sessions & nurse EVV — report
|
||||
|
||||
**Date:** 2026-07-10 · **Lane:** frontend · **Consumes:** [bookings-evv.md](../../contracts/domains/bookings-evv.md) (b9)
|
||||
· **Unlocks:** f9 (checkout/pay), f13 (reviews & patient records)
|
||||
|
||||
## What was built
|
||||
|
||||
The post-payment engagement — the hinge between "I asked for a nurse" and "a nurse is delivering care."
|
||||
|
||||
### Data layer — a **new** `services/bookings` domain
|
||||
The **sibling** of `services/bookingRequests` (b8), **not** a rename — a distinct b9 contract, distinct
|
||||
routes (`/api/v1/bookings/*` + `/api/v1/booking_sessions/*`), distinct shapes. Same `services/{domain}`
|
||||
shape as every other domain:
|
||||
- `types.ts` — derived 1:1 from the b9 swagger (camelCase): `BookingDetailDto`, `BookingSessionDto`
|
||||
(`BookingSessionSummaryDto`), `VisitVerificationDto`, `CareInstructionsDto`, `BookingListItemDto`,
|
||||
`BookingSessionListItemDto`, `CheckInVisitInput`/`CheckOutVisitInput`, the `BookingsApi` seam, the three
|
||||
enum unions (`BookingStatus`/`BookingSessionStatus`/`VisitVerificationStatus`), and pure helpers
|
||||
(`isBookingConfirmedOrBeyond`, `isBookingTerminalBranch`, `bookingTimelineActiveIndex`, `BOOKING_TIMELINE_ORDER`).
|
||||
- `keys.ts` — `bookingDetail(id)`, `bookingSessions(id)` (alias of `bookingDetail` — sessions are embedded),
|
||||
`today(params)`, `sessionEvv(id)`, `careInstructions(id)`, `list(params)`.
|
||||
- `apis/` — real `clientApi` (maps the routes 1:1), `mockApi` (the seeded state machine, **primary**),
|
||||
`serverApi.getBookingDetail` (the RSC-prefetch seam for the real path), and a config-selecting `index`.
|
||||
- `evv/locationProvider.ts` — the **`ILocationProvider`** GPS seam (real `navigator.geolocation` vs a canned
|
||||
mock; `getCurrentPosition()` resolves `null` on denial, never rejects).
|
||||
- `hooks/` (one per file): `useBookingDetail`, `useBookingSessions` (a `select` over the detail query, no
|
||||
second fetch), `useBookingList`, `useTodaySessions`, `useSessionEvv`, `useCareInstructions` (**enabled-gated**),
|
||||
`useCheckInVisit`, `useCheckOutVisit` (both invalidate detail+sessionEvv+today+list on success).
|
||||
|
||||
### Shared composites — `src/components/booking/` (each with a co-located `*.test.tsx`)
|
||||
- `BookingDetailView` — the both-roles smart container (role-conditioned EVV + gated care).
|
||||
- `BookingStatusTimeline` — the server-truth 7-status timeline over the f0 `StepperHeader` + status chip.
|
||||
- `SessionList` → `SessionCard` — per-session Shamsi schedule, status chip, EVV CTA, elapsed/payout.
|
||||
- `EvvStatusBanner` — advisory banner (in-range success / out-of-range warning / no-gps neutral).
|
||||
- `CareInstructionsCard` — the decrypted clinical read (conditions/meds/allergies/instructions/emergency).
|
||||
- `BookingMoneySummary` — gross / commission (کارمزد) / payout, display-only via the money util.
|
||||
- `useEvvController` — GPS-capture + check-in/out orchestration (one instance per surface, per-session busy).
|
||||
- `format.ts` (clock/duration) + `statusKind.ts` (status → StatusChip kind) helpers.
|
||||
|
||||
### Screens
|
||||
- **Customer:** `/bookings` (رزروها list) → `/bookings/[id]` (detail, customer view: timeline + sessions +
|
||||
money; the care record shows the "visible to your nurse only" affordance and the query never fires).
|
||||
- **Nurse:** `/nurse/visits` (ویزیت امروز — today's sessions with inline EVV check-in/out) →
|
||||
`/nurse/visits/[id]` (detail, nurse view: EVV controls + the gated care card).
|
||||
|
||||
### Cross-cutting
|
||||
- i18n `booking` namespace **extended** (both locales, key-synced): `bstatus_*`, `sstatus_*`, `evv_*`
|
||||
(banner variants + CTAs + GPS copy), `care_*` (+ the customer lock copy), `money_*`, `list_*`, dispute note.
|
||||
- 8 new registry icons (`check_in`/`check_out`/`gps`/`schedule`/`clinical`/`medication`/`emergency`/`lock`)
|
||||
and one new token `--bal-secondary-soft` (the نمای پرستار chip / EVV affordance), both schemes.
|
||||
|
||||
## What is now testable, and exactly how
|
||||
|
||||
Run `npm run dev` (mock is primary — no backend needed). The b9 endpoints are also live if you flip the flag.
|
||||
1. **Confirmed booking (customer):** open the رزروها tab → the list shows the two seeded bookings; open one
|
||||
→ **status timeline** at `confirmed`, the **session schedule** (booking #5002 shows exactly **one**
|
||||
session; #5001 shows **3**), and the **money summary** in Toman. Toggle `/en`↔`/fa` → strings + `dir`
|
||||
flip; the timeline reads RTL.
|
||||
2. **Care gate:** open a booking **as the nurse** (`/nurse/visits` → a session → view booking) → the
|
||||
**care-instructions card** is visible (conditions/meds/allergies/instructions/emergency). As the
|
||||
**customer**, the card is absent and the Network tab shows the care request **was never made** (proven by
|
||||
the `BookingDetailView` test too).
|
||||
3. **Nurse check-in:** on `/nurse/visits` (or in the nurse booking detail), tap **«ثبت ورود (EVV)»** →
|
||||
"در حال دریافت موقعیت…" → the **«ورود ثبت شد … موقعیت تایید شد (EVV)»** banner; the session chip →
|
||||
`in_progress`; the timeline → `in_progress`. Set `NEXT_PUBLIC_EVV_MOCK_GPS=out_of_range` → the **advisory**
|
||||
«موقعیت خارج از محدوده (در حال بررسی)» banner and the check-in **still succeeds**. `=denied` → the nurse
|
||||
still checks in (no block; advisory toast + no-gps banner).
|
||||
4. **Nurse check-out:** tap **«ثبت خروج (EVV)»** → the session chip → `completed` with elapsed duration; for
|
||||
the single-visit booking (#5002) the timeline advances to `completed` + the dispute-window note appears —
|
||||
all from the server response, no client-side step jump.
|
||||
5. **Caching:** in React Query Devtools, an EVV mutation invalidates `bookingDetail`/`sessionEvv`/`today`/
|
||||
`list` and the UI re-renders from the refetch; revisiting within `staleTime` does not refetch.
|
||||
6. `npm run check` green · `npm run test:ci` green (195 tests, +22).
|
||||
|
||||
## What is mocked / waiting on a real service
|
||||
|
||||
- **`services/bookings` — mock-primary** (`USE_BOOKINGS_MOCK=true`). A booking only exists after
|
||||
`bookings/convert` runs on a **paid** request, and both upstreams (`bookingRequests` mock, card capture
|
||||
b10) aren't real client-side yet, so a real `bookings/list` returns nothing. The mock seeds confirmed
|
||||
bookings + sessions + care + the EVV state machine. Real `bookingsClientApi` maps the routes 1:1; the swap
|
||||
is one flag (see mocks-registry). `serverApi.getBookingDetail` is ready for the RSC prefetch on the real path.
|
||||
- **`ILocationProvider`** (`NEXT_PUBLIC_EVV_MOCK_GPS`) — GPS capture seam; the real path is
|
||||
`navigator.geolocation`. Server-side address-match math stays behind the backend geocoding seam.
|
||||
- Both are recorded in [mocks-registry.md](./mocks-registry.md).
|
||||
|
||||
## Contract consumed + gaps filed
|
||||
|
||||
- **Consumed:** [bookings-evv.md](../../contracts/domains/bookings-evv.md) + the b9 swagger shapes — types
|
||||
derive 1:1. No shape was guessed.
|
||||
- **Filed:** **REQ-015** — confirm the booking/session/EVV enum **string codes** (bare `string` in swagger)
|
||||
match the client unions, and the **`checkInAddressMatch` tri-state** (`true`/`false`/`null`) so the banner
|
||||
can distinguish an advisory mismatch from "no GPS captured." Low-risk (mock-primary now); worth locking
|
||||
before f9/f13 reuse the shapes.
|
||||
|
||||
## Deliberate design decisions (non-obvious)
|
||||
|
||||
- **Two-stage disclosure is a UI gate, not just a server check.** `useCareInstructions` is `enabled` only for
|
||||
the assigned-nurse view on a `confirmed`+ booking; the customer/unassigned viewer **never fires** the
|
||||
request (a 403/404 is treated as a defect path). The `BookingDetailView` test asserts the customer never
|
||||
calls `getCareInstructions` and the nurse does.
|
||||
- **EVV mismatch/denial is advisory, never a block.** Out-of-range check-in succeeds with a **warning**-tokened
|
||||
banner (never the error token); GPS denial still submits. Check-out is never gated on the match.
|
||||
- **Server-truth timeline + display-only money.** The timeline renders `BookingDetailDto.status` exactly (no
|
||||
client step advance); money is rendered as-sent (no sum/derive/re-split); `payoutEligibleAt` is never
|
||||
recomputed. Sessions are **embedded** in the detail (no standalone list endpoint) — `useBookingSessions`
|
||||
is a `select` over the one detail query, so invalidating `bookingDetail(id)` refreshes both.
|
||||
- **`ILocationProvider`** is the single new client seam; the `NEXT_PUBLIC_EVV_MOCK_GPS` default is `in_range`
|
||||
while mock-primary so the happy path is demoable without a device (real GPS would never fall near the
|
||||
seeded Tehran address).
|
||||
|
||||
## Follow-ups for later phases
|
||||
|
||||
- **f9 (checkout/pay):** the money summary here shows the confirmed **split only**; the **tax (مالیات) line**,
|
||||
escrow notice, and invoice are the checkout surface — b9's `BookingDetailDto` has **no tax field** (flagged;
|
||||
f9 owns it). The C5 accept CTA still lands on `/bookings/checkout?request_id=…` (f7 stub).
|
||||
- **f13 (reviews & records):** the E3 **visit-note authoring** (bottom half) and the full **E2 patient-record
|
||||
viewer** are deferred here; the booking-detail/EVV/care pattern (timeline + sessions + gated care + EVV
|
||||
banner) is the template they extend. The customer-side **care-details authoring** (`submit_care_instructions`)
|
||||
write form is also f13 — f8 only reads the gated record.
|
||||
- **f15 (admin):** the EVV-review queue (mismatch / no-show worklist) is the admin console; f8 raises no
|
||||
alerts client-side (no-show detection is a server job).
|
||||
- **Swap to real:** flip `USE_BOOKINGS_MOCK=false` once `bookings/convert` is reachable client-side (b10
|
||||
card capture) — `bookingsClientApi` + `bookingsServerApi` are wired; no hook/component change.
|
||||
@@ -47,6 +47,8 @@ Status legend: 🔴 not built · 🟡 mocked (seam + fake impl in place) · 🟢
|
||||
| `ICurrencyNormalizer` | backend-phase-12 | Toman↔IRR at the provider boundary — `MockCurrencyNormalizer` (`Baya.Infrastructure.CrossCutting/Seams/`): `ToIrr(amount,"TOMAN")` = `amount × TomanToIrrMultiplier`, IRR passes through; `ToDisplayToman` divides back. **Conversion happens ONLY here, never internally.** Registered singleton in `AddCrossCuttingSeams` | `Seams:Currency:TomanToIrrMultiplier` (default `10`) | Read the multiplier (or a per-provider unit) from provider config; the interface stays — a currency redenomination is a config change | 🟡 |
|
||||
| `INursePayoutStatus` | backend-phase-11 (interim) → **backend-phase-13 (authoritative)** | "Was the nurse already paid for this booking?" — **b13 shipped the real `NursePayoutLinkStatusService`** (`Persistence/Services/Payments/`): a booking is paid iff a `nurse_payout_booking_links` row ties it to a `nurse_payouts` row in status `paid`. This **supersedes** the interim `NursePayoutStatusService` (dispute-window derivation, now deleted); the `refund_assume_nurse_paid` config override still forces the paid answer for ops/testing. Not a mock of an external — a real ledger-backed derivation. Registered scoped in `AddPersistenceServices`. The refund pre-payout/clawback fork is unchanged | `refund_assume_nurse_paid` (`platform_configs`, default `false`) | Nothing further — this is the real implementation. (A future on-demand-withdrawal model would extend the "paid?" definition, not replace it.) | 🟢 |
|
||||
|
||||
| `ILicenseVerificationService` | **backend-phase-15** | Partner-center licensing (eNamad / MoH establishment-permit پروانه تأسیس) — `MockLicenseVerificationService` (`Baya.Infrastructure.CrossCutting/Seams/`), **no external call**: `VerifyEstablishmentPermitAsync`/`VerifyENamadAsync` return `NeedsManualReview` (no automated registry → a human admin decides), so `VerifyPartnerCenterCommand` records the manual approval and activates the center. A config toggle makes clean checks return `Valid` (auto-approve path); an explicit `Invalid` verdict blocks activation. Registered singleton in `AddCrossCuttingSeams` | `Seams:LicenseVerification:AutoApprove` (default `false`) | 1) obtain access to a real eNamad status endpoint and/or the MoH establishment-permit registry (no public B2B API today — likely a manual/partner data feed at launch); 2) implement the two methods to look up the permit/eNamad code and return `Valid`/`Invalid` + a reason; 3) swap the registration (config-selected) — `VerifyPartnerCenter` is unchanged (it keeps the human-override decision authority) | 🟡 |
|
||||
|
||||
> Exact config keys and file paths get filled in by the phase that builds each seam. Keep the
|
||||
> "Make it real →" column actionable enough that a developer can pick up any single row and ship it.
|
||||
|
||||
@@ -67,3 +69,5 @@ the frontend can build before the backend phase merges, and swap to the real HTT
|
||||
| `AddressMapPicker` (map stand-in) | `client/src/components/geography/AddressMapPicker.tsx` | **Not a real map** — a bounded, tappable/draggable marker canvas (CSS grid, no Neshan/Google tiles, no network) that maps the pointer position to `{ latitude, longitude }` around the chosen city's centroid (`CITY_CENTROIDS`/`IRAN_CENTROID` in `services/geography/constants.ts`). Emits real coordinates for the create/update request | _none (component boundary)_ | Replace the canvas internals with a real map widget (Neshan/Google, inlined per the client CSP) that emits the same `{ latitude, longitude }` via `onChange` — `AddressForm` and every caller stay unchanged | 🟡 |
|
||||
| `CatalogApi` | `client/src/services/catalog/apis/mockApi.ts` (+ `apis/seed.ts`) | The catalog skeleton + nurse pricing layer. **Categories mirror the b5 seed exactly** (5 categories, ids 1–5, `sortOrder` 0–4). Seeds representative **option groups/values** the fresh backend does **not** (an admin authors them per category) — incl. required + optional groups and one **cross-category** (`serviceCategoryId=null`) group — so the builder's required-option gate + cross-category rendering demo. Enforces the server's create validation in-memory: `400` missing required dimension / bad price, and the `(nurse, category, option-set)` duplicate **`409`** (via `optionSetSignature`). Variant store seeded **empty** so the offerings empty-state demos; the nurse builds variants live (across price units). `create`/`update`/`set_active`/`list`(active-first, paginated)/`get`. Money stays an **IRR digit-string** end-to-end | `USE_CATALOG_MOCK` (`services/catalog/constants.ts`, default `true`) | b5 `catalog/*` + `nurse_variants/*` are live; set flag `false` — `catalogClientApi` is wired to the action-style routes (camelCase bodies, `pageSize` pagination per REQ-010, `category_id` snake_case filter). **When swapped, categories will have NO option groups until an admin authors them** (the mock's groups were illustrative). No hook/component change | 🟡 |
|
||||
| `VerificationApi` | `client/src/services/verification/apis/mockApi.ts` | The whole nurse trust journey (b6). Seeds the six required steps on `start` (idempotent); `runIdentityKyc` passes any well-formed 10-digit id **except** `0000000000` (→ `failed`/`kyc_no_match`, matches backend `MockIdentityKycProvider`); `runShahkarMatch` requires identity passed, fails **shared-SIM** when the bound national id is `1111111111` (→ `failed`/`shared_sim`); `runBankVerification` passes (assumes a primary bank account); `uploadStepDocument` simulates signed-URL PUT progress then moves the step to `in_review` (metadata only); `submitCredentialDetails` validates the INO number. Re-aggregates like the server (`approved` only when every step passes). **Dev-only** `__mockApproveAll()`/`__mockRejectStep(code,reason)` stand in for the deferred (f15) admin review queue so a human can watch `is_verified`/the trust badge/the publish gate flip — reachable from B3/B6 only while the flag is true | `USE_VERIFICATION_MOCK` (`services/verification/constants.ts`, default `true`) | b6 `nurse_verification/*` + `nurses/{id}/trust_badge` are live; set flag `false` — `verificationClientApi` is wired (action-style routes, camelCase, XHR signed-URL PUT for upload progress + SHA-256 integrity hash). **Caveat:** the real `submitCredentialDetails` no-ops pending REQ-011 (no nurse-facing endpoint for the structured INO/specialties fields yet) — the document uploads it accompanies are contract-backed. No hook/component change | 🟡 |
|
||||
| `BookingsApi` | `client/src/services/bookings/apis/mockApi.ts` | The post-payment engagement (b9). Seeds **2 confirmed bookings** (one 3-session multi-day, one single-visit) + `booking_care_instructions` + a per-session **EVV state machine** — `checkInVisit` flips the session→`in_progress`/`checked_in` (booking→`in_progress`) and computes the **advisory** `checkInAddressMatch` (haversine vs the seeded address ± `MOCK_EVV_TOLERANCE_METERS`, `null` when GPS was absent); `checkOutVisit` requires an open check-in (**`400 no_open_check_in`** otherwise), completes the session (stamps `payoutEligibleAt`), and completes the booking + opens the dispute window once **all** sessions settle. `getCareInstructions` **404s any viewer but the assigned nurse** (the two-stage-disclosure boundary; the UI `enabled` gate means the customer never even calls it). Money stays IRR digit-strings with `gross = commission + payout` and `Σ visitPayout = payout` | `USE_BOOKINGS_MOCK` (`services/bookings/constants.ts`, default `true`) | b9 `bookings/*` + `booking_sessions/*` are live, but a booking only exists after `bookings/convert` runs on a **paid** request — both upstreams (`bookingRequests` mock, card capture b10) aren't real client-side yet. Once conversion is live, set flag `false` — `bookingsClientApi` maps the routes 1:1 (+ `bookingsServerApi` for the RSC prefetch). No hook/component change | 🟡 |
|
||||
| `ILocationProvider` | `client/src/services/bookings/evv/locationProvider.ts` | **EVV GPS capture** — the only client seam f8 introduces. `getCurrentPosition()` never rejects (denied/unavailable → `null`, so a GPS problem is **advisory, never a block**). The **real** provider wraps `navigator.geolocation.getCurrentPosition`; the **mock** returns canned coordinates per mode so the in-range / advisory-out-of-range / denied paths are all demoable without a device (the mock `BookingsApi` computes the match against the same seeded reference point) | `NEXT_PUBLIC_EVV_MOCK_GPS` = `in_range` \| `out_of_range` \| `denied` \| `off` (default `in_range` while `USE_BOOKINGS_MOCK`, else `off`) | Set `NEXT_PUBLIC_EVV_MOCK_GPS=off` (or flip `USE_BOOKINGS_MOCK`) → the real `navigator.geolocation` provider is selected. Real **address-match math** stays server-side (backend geocoding seam), not here — this seam only *captures* the position | 🟡 |
|
||||
|
||||
Reference in New Issue
Block a user