backend phase 15 & frontend phase 8
This commit is contained in:
@@ -12,6 +12,30 @@ One block per completed backend phase. Newest at the top. Backend lane writes he
|
||||
- **Notes for frontend:** <anything load-bearing>
|
||||
-->
|
||||
|
||||
## backend-phase-15 — Messaging (tickets), partner centers & admin backoffice — 2026-07-10
|
||||
- **Shipped (FINAL backend phase):** new `messaging` schema — `Tickets` (`UNIQUE(reference_code)`, status/
|
||||
category, nullable `booking_id`/`refund_id`), `TicketParticipants` (`UNIQUE(ticket_id, user_id)`, soft-remove
|
||||
via `removed_at`), `TicketMessages` (`is_internal` hard boundary) — and new `partner` schema — `PartnerCenters`
|
||||
(`IAuditable`, encrypted+masked `settlement_iban`, `commission_rate` separate from `platform_fee_rate`). Added
|
||||
the `nurse_profiles.partner_center_id` FK in place. One migration (`MessagingAndPartnerCenters`). CQRS:
|
||||
OpenTicket / AutoCreateCoordinationTicket / PostMessage / Add+RemoveParticipant / Close+ReopenTicket /
|
||||
LogEmergencyTicket / GetTicketThread (user vs admin view) / ListMyTickets / ListTicketsForAdmin; CreatePartnerCenter
|
||||
/ UpdatePartnerCenter / VerifyPartnerCenter / SponsorNurse / GetCenterForBooking / ListPartnerCenters /
|
||||
GetPartnerCenterById / GetCenterDashboard. 5 new controllers (Tickets/AdminTickets/AdminPartnerCenters/Centers/
|
||||
InternalCenters). Wired: b11 `IssueInvoice` now resolves issuer/settlement via `GetCenterForBooking`; b11
|
||||
`CreateRefund` auto-opens a `refund` ticket (so `refunds.ticket_id` is always non-null); the card confirm + BNPL
|
||||
settle handlers auto-create the coordination ticket. Support-alert worklist + audit viewer reused from b1 (not
|
||||
rebuilt). The 4 DEFERRED tables were **not** created.
|
||||
- **Contracts:** `dev/contracts/domains/messaging-notifications-admin.md` + openapi snapshot refreshed (yes).
|
||||
- **Mocked:** `ILicenseVerificationService` (eNamad / MoH permit — manual-approve at MVP) → 🟡 (see reports/mocks-registry.md).
|
||||
- **Gate:** build clean (0 new code warnings) / tests green (358 total: 4 identity + 240 foundation + 114 API,
|
||||
incl. 4 new merchant-of-record resolver tests + 8 new ticket/partner-center API tests).
|
||||
- **Handoff:** backend/handoff/after-backend-phase-15.md
|
||||
- **Notes for frontend:** `is_internal` is stripped from the user thread view server-side (never trust the UI);
|
||||
no direct nurse↔customer channel / no phone numbers; ticket↔booking/refund links are optional (nullable);
|
||||
duplicate participant add = 409; `settlement_iban` is only ever returned masked (last 4); merchant-of-record
|
||||
(invoice issuer + settlement) follows `partner_centers`, resolved by `GET /internal/bookings/{id}/center`.
|
||||
|
||||
## backend-phase-14 — Reviews, ratings & patient care records — 2026-07-09
|
||||
- **Shipped:** new `reviews` schema, 4 tables — `Reviews` (`UNIQUE(booking_id)`, `CHECK(rating 1–5)`, guarded
|
||||
`moderation_status`, `IAuditable`), `ReviewTagsMaster` (seeded 5-tag vocab, `UNIQUE(code)`), `ReviewTagLinks`
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
# Handoff — after backend phase 15 (Messaging, partner centers & admin backoffice)
|
||||
|
||||
**This is the final backend phase. The backend chain is complete.** Every domain the admin backoffice acts on
|
||||
now exists and is wired together.
|
||||
|
||||
## What is now live (frontend can build against it)
|
||||
|
||||
### Tickets — the post-booking channel (f14 messaging)
|
||||
- `POST /api/v1/tickets` — open a ticket (`category` ∈ `support|coordination|refund|emergency`; optional
|
||||
`bookingId`/`refundId`; body optional). Opener is auto-added as the first participant. Returns
|
||||
`{ ticketId, referenceCode, status, category }`.
|
||||
- `POST /api/v1/tickets/{id}/messages` — post a message. **`isInternal` is staff-only**; a non-staff caller
|
||||
sending `true` → `403`; posting to a closed ticket as non-staff → `403`.
|
||||
- `POST /api/v1/tickets/{id}/participants` (add) / `DELETE …/participants/{userId}` (soft-remove) — staff or
|
||||
ticket owner. A **duplicate add is `409`** (backed by `UNIQUE(ticket_id, user_id)`), never a 500.
|
||||
- `POST /api/v1/tickets/{id}/close` · `/reopen` — participant or staff (idempotent).
|
||||
- `POST /api/v1/tickets/emergency` — assigned nurse (or staff) logs an emergency (+ optional support alert).
|
||||
- `GET /api/v1/tickets` — my tickets (paginated, filter `status`, search `referenceCode`).
|
||||
- `GET /api/v1/tickets/{id}` — **user thread view: internal notes are stripped** in the projection.
|
||||
- `GET /api/v1/admin/tickets` + `GET /api/v1/admin/tickets/{id}` — admin queue + **admin thread view: internal
|
||||
notes included** (`support`/`admin`).
|
||||
|
||||
**The rule f14 must respect:** never build a direct nurse↔customer channel, never surface a phone number, and
|
||||
never rely on the UI to hide internal notes — the backend already strips them from the user payload. The
|
||||
coordination ticket for a booking is auto-created on confirmation (you don't create it).
|
||||
|
||||
### Partner centers + merchant-of-record (f15 admin + partner consoles)
|
||||
- `POST /api/v1/admin/partner-centers` (create, inactive) · `PATCH …/{id}` (update) ·
|
||||
`POST …/{id}/verify` (activate) · `POST …/{id}/sponsor-nurse` · `GET …` (list) · `GET …/{id}` (detail) —
|
||||
`admin`/`super_admin`. **`settlementIbanMasked` (last 4) is the only IBAN ever returned** — never plaintext.
|
||||
`commissionRate ∈ [0,1)`; a merchant-of-record center requires a `settlementIban`.
|
||||
- `GET /api/v1/centers/{id}/dashboard` — the center's own account (or staff): sponsored nurses + booking/invoice
|
||||
counts + masked settlement summary.
|
||||
- `GET /api/v1/internal/bookings/{bookingId}/center` — the issuer/settlement resolver
|
||||
(`platform` | `partner_center`).
|
||||
|
||||
### Admin backoffice (surfaced, not rebuilt)
|
||||
- Support-alert worklist: `GET support_alerts/get_support_alerts`, `POST …/assign_support_alert`,
|
||||
`POST …/resolve_support_alert` (built b1). Audit viewer: `GET audit/get_audit_trail` (built b1). Both
|
||||
`DynamicPermission`. Verification queue / refunds / payout dashboard / moderation queue are their own phases'
|
||||
routes — surface them under the admin console with the right RBAC scope.
|
||||
|
||||
## RBAC the frontend must respect (per route)
|
||||
- Authenticated (own) ticket routes: any logged-in user; participation is enforced server-side.
|
||||
- Admin ticket queue + admin thread: `support`/`admin`. Partner centers: `admin`/`super_admin`. Center
|
||||
dashboard: the center's `adminUserId` (or staff). Support alerts: `support`/`admin`. Audit: `super_admin`/`admin`.
|
||||
- The admin role passes every `DynamicPermission` check; narrower staff scopes (`support`/`finance`/`moderation`)
|
||||
are granted via seeded role claims.
|
||||
|
||||
## What's mocked
|
||||
- **`ILicenseVerificationService`** (eNamad / MoH establishment-permit) — `MockLicenseVerificationService`,
|
||||
manual-approve at MVP (`NeedsManualReview`); `VerifyPartnerCenter` records the human decision. Config
|
||||
`Seams:LicenseVerification:AutoApprove` forces `Valid`. See the mock registry (🟡). There is **no** telephony
|
||||
seam — the emergency call is an out-of-platform `tel:` link by design.
|
||||
|
||||
## Contracts
|
||||
- `dev/contracts/domains/messaging-notifications-admin.md` (this phase). `swagger.v1.json` refreshed (now includes
|
||||
`/tickets`, `/admin/tickets`, `/admin/partner-centers`, `/centers`, `/internal/bookings/{id}/center`).
|
||||
|
||||
## Types / wire notes
|
||||
- Envelope unchanged (camelCase body, snake_case URL tokens where `[action]`-based; the new controllers use
|
||||
explicit REST routes). Pagination `page`/`pageSize` (default 50, max 100). `sentAt`/`closedAt`/`verifiedAt` are
|
||||
UTC ISO-8601; ids are numbers; the settlement IBAN is a masked string (`"••••0001"`).
|
||||
@@ -12,6 +12,39 @@ for awareness.
|
||||
- **Requests filed:** frontend/requests/for-backend.md (yes/no)
|
||||
-->
|
||||
|
||||
## frontend-phase-8-b9 — Booking detail, sessions & nurse EVV — 2026-07-10
|
||||
- **Shipped:** the post-payment engagement — a **new** `services/bookings` domain (the sibling of
|
||||
`bookingRequests`, NOT a rename): types/keys/constants/apis[client(1:1 b9)+mock+serverApi]/8 hooks +
|
||||
barrel, plus the `evv/locationProvider.ts` **ILocationProvider** GPS seam. Screens: customer **رزروها
|
||||
list** `/bookings` + **booking detail** `/bookings/[id]` (BookingDetailView, customer view), nurse
|
||||
**ویزیت امروز** `/nurse/visits` (today-sessions EVV feed) + **nurse booking detail** `/nurse/visits/[id]`
|
||||
(EVV controls + gated care). Seven shared tested composites under `src/components/booking/`:
|
||||
`BookingDetailView`, `BookingStatusTimeline` (server-truth 7-status), `SessionList`→`SessionCard`
|
||||
(per-session schedule/status/EVV CTA), `EvvStatusBanner` (advisory in/out-of-range/no-gps), `CareInstructionsCard`,
|
||||
`BookingMoneySummary`, + `useEvvController`. i18n `booking` extended (`bstatus_*`/`sstatus_*`/`evv_*`/`care_*`/
|
||||
`money_*`/`list_*`) both locales; new icons (check_in/out, gps, clinical, medication, emergency, lock) +
|
||||
`--bal-secondary-soft` token.
|
||||
- **Load-bearing rules honored:** **two-stage disclosure is a UI gate** — `useCareInstructions` is
|
||||
`enabled` only for the assigned-nurse view on a `confirmed`+ booking; the customer NEVER fires it (proven
|
||||
by test). **EVV mismatch/GPS-denial is advisory, never a block** — out-of-range check-in still succeeds
|
||||
(warning-tokened banner, not error); denial still submits. **Timeline = server truth** (never advanced
|
||||
client-side); **money display-only** (gross/commission/payout rendered as sent, never summed/re-split;
|
||||
`payoutEligibleAt` never recomputed); single-visit renders one session row through the same card; EVV
|
||||
mutations **invalidate** detail+sessionEvv+today+list.
|
||||
- **Consumes:** dev/contracts/domains/bookings-evv.md (b9) + swagger `BookingDetailDto`/`BookingSessionSummaryDto`/
|
||||
`VisitVerificationDto`/`CareInstructionsDto` — `services/bookings/types.ts` derives from these 1:1.
|
||||
- **Mocked client-side:** `services/bookings` via `bookingsMockApi` (**USE_BOOKINGS_MOCK=true, primary**) —
|
||||
seeds 2 confirmed bookings (one 3-session, one single-visit) + care + a check-in/out EVV state machine,
|
||||
because a real 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. Real `bookingsClientApi` maps the
|
||||
routes 1:1; swap is one flag. Also the **ILocationProvider** GPS seam (`NEXT_PUBLIC_EVV_MOCK_GPS`
|
||||
in_range|out_of_range|denied|off) — the first frontend seam recorded in mocks-registry.
|
||||
- **Gate:** npm run check green · npm run test:ci green (195 tests, +22). Added a committed
|
||||
`NEXT_PUBLIC_API_URL` default in `jest.setup.ts` (first test to render a service-hook component pulled
|
||||
`@/config` at import).
|
||||
- **Requests filed:** frontend/requests/for-backend.md — yes (REQ-015: confirm the booking/session/EVV enum
|
||||
string codes + the `checkInAddressMatch` tri-state semantics the client unions assume).
|
||||
|
||||
## frontend-phase-7-b8 — Booking request flow (customer request + nurse inbox) — 2026-07-09
|
||||
- **Shipped:** the money-free request phase — `services/bookingRequests` (types/keys/constants/apis[client+
|
||||
mock]/hooks + barrel) and screens **C4** `/bookings/request` (patient/variant/address/date+time + a
|
||||
|
||||
@@ -195,3 +195,22 @@ delivers fixes in its own change. **Frontend never edits backend code to "fix" a
|
||||
`variantLabel` on the row makes the inbox self-describing; a coarse age is a nice-to-have for triage.
|
||||
- **Proposed shape:** `BookingRequestListItemDto { …, variantLabel: string, patientAge?: int }`.
|
||||
- **Status:** open
|
||||
|
||||
## REQ-015 — Confirm the booking/session/EVV enum codes + `checkInAddressMatch` tri-state — filed by frontend-phase-8-b9 — 2026-07-10
|
||||
- **Need:** Two confirmations so the f8 `services/bookings/types.ts` client unions stay wire-accurate:
|
||||
1. **Enum string codes.** The b9 swagger types `status`/`evvStatus`/session `status` as bare `string`
|
||||
(no enum constraint). Confirm the stable wire codes match the client unions: `BookingStatus`
|
||||
= `pending_payment|confirmed|in_progress|completed|disputed|closed|cancelled`; `BookingSessionStatus`
|
||||
= `scheduled|in_progress|completed|missed|cancelled`; `VisitVerificationStatus`
|
||||
= `pending|checked_in|completed`. (They match the contract doc's "Enums used" — this just asks that the
|
||||
serialized JSON emits these exact snake_case codes, not PascalCase/int.)
|
||||
2. **`checkInAddressMatch` tri-state semantics.** The EVV banner keys off it as: `true` = in range
|
||||
(«موقعیت تایید شد»), `false` = out-of-range/advisory-under-review («موقعیت خارج از محدوده»), `null` =
|
||||
GPS unavailable/denied («موقعیت ثبت نشد»). Confirm the server returns `null` (not `false`) when the
|
||||
nurse checked in **without** coordinates (GPS denied), so the UI can distinguish "flagged mismatch"
|
||||
from "no position captured". A mismatch stays advisory server-side (support alert, never a block) — the
|
||||
UI mirrors that.
|
||||
- **Why:** f8 renders the status timeline, per-session chips, and the EVV banner strictly off these codes;
|
||||
a casing/int drift or a `false`-vs-`null` conflation would mislabel a visit. Low-risk (mock-primary now),
|
||||
but worth locking before f9/f13 consume the same shapes.
|
||||
- **Status:** open
|
||||
|
||||
@@ -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