diff --git a/client/CLAUDE.md b/client/CLAUDE.md index 3daad0c..f04908b 100644 --- a/client/CLAUDE.md +++ b/client/CLAUDE.md @@ -136,8 +136,7 @@ client/ │ │ │ │ ├── [id]/review/page.tsx # /bookings/[id]/review — f13 leave-a-review (b14): RatingInput + body + ReviewTagSelector; gated on completed/closed + server can_review + 1:1; on submit → persistent "under review" (pending_moderation, never public here); already-reviewed shows the review state, never a 2nd form (services/reviews) │ │ │ │ └── checkout/ # f9 checkout flow (C5 accept CTA lands on page.tsx with ?request_id=) │ │ │ │ ├── page.tsx # C6 خلاصه و پرداخت — acceptance badge, served reconciling breakdown (PriceBreakdown), EscrowNotice, payment-window countdown, «ادامه پرداخت ←» (idempotency-key-per-attempt) + «پرداخت اقساطی» → f11 BNPL wizard - │ │ │ │ ├── gateway/page.tsx # dev mock-gateway page — TEST HARNESS standing in for the PSP redirect (mock redirectUrl points here; success/failure buttons drive both return branches) - │ │ │ │ ├── return/page.tsx # return-from-gateway — confirm return → pending-callback poll (backoff, stops on terminal) → succeeded (invalidate + hand off) / failed retry / window-expired + │ │ │ │ ├── return/page.tsx # return-from-gateway — confirm return → pending-callback poll (backoff, stops on terminal) → succeeded (invalidate + hand off) / failed retry / window-expired (the payment mock-gateway harness was removed in refinement-phase-4 when USE_PAYMENT_MOCK flipped; on the real path the PSP redirectUrl is absolute) │ │ │ │ ├── confirmation/page.tsx # payment success — «مشاهده رزرو» (booking detail) + «دانلود فاکتور» (invoice); REUSED by f11 (?method=bnpl adds «پرداخت‌شده با اقساط» — a settled BNPL order is a card payment net-of-fee) │ │ │ │ └── bnpl/ # f11 BNPL installment checkout (the alternate branch off C6, reached with ?request_id=) │ │ │ │ ├── page.tsx # D1→D4 stateful wizard (StepperHeader): D1 method/provider · D2 plan · D3 eligibility · D4 schedule+contract → provider handoff; card fall-back → C6 everywhere @@ -659,6 +658,19 @@ Every domain follows the same shape: `types.ts` (wire types + the domain's `Api` twice — a real `clientApi.ts` and an in-memory `mockApi.ts` — and select in `apis/index.ts` by a config flag (`USE_{DOMAIN}_MOCK`). Hooks import the selected `api`; the swap is one line. Record every mock in `dev/shared-working-context/reports/mocks-registry.md`. +- **De-mock status (refinement-phase-4):** **14 domains are now REAL** (`USE_*_MOCK = false`): `auth`, + `geography`, `patients`, `profiles`, `nurse` (bank), `addresses`, `serviceAreas`, `catalog`, `search`, + `bookingRequests`, `bookings`, `payment`, `reviews`, `notifications`, `tickets`. Flipping them required + updating each `clientApi.ts` to **consume the fields Phase-3 delivered** (search name/avatar/distance + + `nurses/{id}/profile`; patient relation/conditions; address `provinceId`; booking-request + `variantPrice`/`bookingId`; ticket `unreadCount`/`lastMessageAt`/`clientMessageId`; review `my_review` + mapper; profile `avatarUrl`/`preferredLanguage` + a **multipart avatar upload** now that `clientFetch` + passes `FormData` bodies through). **7 domains stay mocked** because a precondition REQ is deferred/unsafe: + `verification` (REQ-034 admin queue), `refunds` (REQ-035 admin preview), `payouts` (REQ-036 admin preview), + `admin` (REQ-031 RBAC roles), `bnpl` (REQ-022/024 options/schedule/wallet), `partnerCenter` (REQ-032/033/038 + portal reads + `/me` signal), `patientRecords` (REQ-027 endpoints exist but the client family-record + `id` model is `string` vs the wire's `int` — the customer-edit PUT is write-unsafe until reconciled). Note: + the `EVV_GPS_MODE` seam auto-selects `off` (real `navigator.geolocation`) once `USE_BOOKINGS_MOCK=false`. - **The wire envelope:** the server wraps responses in `ApiEnvelope` (`{ isSuccess, statusCode, message, requestId, data }`, camelCase — see `lib/api/types.ts`). `clientFetch` returns the raw body, so a real `clientApi` reads the payload via `unwrap()`. Types are derived from `dev/contracts/` + diff --git a/client/src/app/[locale]/(private-routes)/(customer)/bookings/checkout/gateway/page.tsx b/client/src/app/[locale]/(private-routes)/(customer)/bookings/checkout/gateway/page.tsx deleted file mode 100644 index f648efc..0000000 --- a/client/src/app/[locale]/(private-routes)/(customer)/bookings/checkout/gateway/page.tsx +++ /dev/null @@ -1,76 +0,0 @@ -'use client'; -import { Suspense } from 'react'; -import { useLocale, useTranslations } from 'next-intl'; -import { useRouter, useSearchParams } from 'next/navigation'; -import { Box, Paper, Stack, Typography } from '@mui/material'; -import { AppButton, AppIcon, AppLoading } from '@/components'; -import { ROUTES } from '@/constants'; -import { - CHECKOUT_QUERY_OUTCOME, - CHECKOUT_QUERY_REQUEST_ID, - CHECKOUT_QUERY_TRANSACTION_ID, -} from '@/services/payment/constants'; -import type { GatewayReturnOutcome } from '@/services/payment/types'; - -/** - * Dev mock-gateway page — a **test harness, not a product feature**. It stands in for the PSP so the - * initiate → redirect → return round-trip is exercisable without a real gateway: the payment mock's - * `redirectUrl` points here, and the success/failure buttons drive both outcome branches of the return - * surface (a real PSP redirects back after the cardholder pays or cancels). On the real path the - * `redirectUrl` is the PSP's absolute URL and this page is never reached. - */ -export default function MockGatewayPage() { - return ( - }> - - - ); -} - -function MockGatewayScreen() { - const t = useTranslations('payment'); - const locale = useLocale(); - const router = useRouter(); - const params = useSearchParams(); - - const requestId = params.get(CHECKOUT_QUERY_REQUEST_ID) ?? ''; - const transactionId = params.get(CHECKOUT_QUERY_TRANSACTION_ID) ?? ''; - - const returnWith = (outcome: GatewayReturnOutcome) => { - const query = new URLSearchParams({ - [CHECKOUT_QUERY_REQUEST_ID]: requestId, - [CHECKOUT_QUERY_TRANSACTION_ID]: transactionId, - [CHECKOUT_QUERY_OUTCOME]: outcome, - }); - router.replace(`/${locale}${ROUTES.CHECKOUT_RETURN}?${query.toString()}`); - }; - - return ( - - - - - {t('gateway_title')} - - - {t('gateway_hint')} - - {transactionId ? ( - - {/* dir scoped to the code only — the label is Persian and must keep the RTL base direction. */} - {t('gateway_reference_label')}:{' '} - - #{transactionId} - - - ) : null} - returnWith('success')} sx={{ m: 0 }}> - {t('gateway_pay_success')} - - returnWith('failure')} sx={{ m: 0 }}> - {t('gateway_pay_fail')} - - - - ); -} diff --git a/client/src/app/[locale]/(private-routes)/(customer)/profile/page.tsx b/client/src/app/[locale]/(private-routes)/(customer)/profile/page.tsx index 333263c..c481acf 100644 --- a/client/src/app/[locale]/(private-routes)/(customer)/profile/page.tsx +++ b/client/src/app/[locale]/(private-routes)/(customer)/profile/page.tsx @@ -8,16 +8,25 @@ import { isIranianMobile } from '@/components/PhoneNumberField'; import { ROUTES } from '@/constants'; import { digitsOnly } from '@/utils'; import { useCustomerProfile, useUpsertCustomerProfile } from '@/services/profiles'; +import { useMe } from '@/services/auth'; import type { CustomerProfile } from '@/services/profiles/types'; /** Customer profile — name, preferred language, and the emergency contact. No national-ID KYC. */ export default function CustomerProfilePage() { const { data: profile, isLoading } = useCustomerProfile(); + const { data: me } = useMe(); if (isLoading) return ; - return ; + // The customer name is owned by `/me` (REQ-007), not `CustomerProfileDto` — prefill it from there so + // editing the emergency contact never blanks (and re-saves as null) the existing name. + return ( + + ); } -const CustomerProfileForm: FunctionComponent<{ initial: CustomerProfile | null }> = ({ initial }) => { +const CustomerProfileForm: FunctionComponent<{ + initial: CustomerProfile | null; + nameFallback: { firstName: string | null; lastName: string | null }; +}> = ({ initial, nameFallback }) => { const t = useTranslations('profile'); const ta = useTranslations('address'); const tc = useTranslations('common'); @@ -25,8 +34,8 @@ const CustomerProfileForm: FunctionComponent<{ initial: CustomerProfile | null } const { enqueueSnackbar } = useSnackbar(); const upsert = useUpsertCustomerProfile(); - const [firstName, setFirstName] = useState(initial?.firstName ?? ''); - const [lastName, setLastName] = useState(initial?.lastName ?? ''); + const [firstName, setFirstName] = useState(initial?.firstName ?? nameFallback.firstName ?? ''); + const [lastName, setLastName] = useState(initial?.lastName ?? nameFallback.lastName ?? ''); const [language, setLanguage] = useState(initial?.preferredLanguage ?? 'fa'); const [emergencyName, setEmergencyName] = useState(initial?.defaultEmergencyContactName ?? ''); const [emergencyPhone, setEmergencyPhone] = useState(digitsOnly(initial?.defaultEmergencyContactPhone ?? '')); diff --git a/client/src/components/booking/BookingDetailView/BookingDetailView.test.tsx b/client/src/components/booking/BookingDetailView/BookingDetailView.test.tsx index 6b9f434..0f4f666 100644 --- a/client/src/components/booking/BookingDetailView/BookingDetailView.test.tsx +++ b/client/src/components/booking/BookingDetailView/BookingDetailView.test.tsx @@ -8,6 +8,13 @@ jest.mock('next-intl', () => ({ })); jest.mock('notistack', () => ({ useSnackbar: () => ({ enqueueSnackbar: jest.fn() }) })); +// This is a behavioural test of the two-stage-disclosure gate; it uses the in-memory bookings mock as +// its data fixture (seeded booking 5001 + care instructions). Pin the seam to the mock so the test is +// independent of the production `USE_BOOKINGS_MOCK` flag (flipped to real in refinement-phase-4). +jest.mock('@/services/bookings/apis', () => ({ + bookingsApi: jest.requireActual('@/services/bookings/apis/mockApi').bookingsMockApi, +})); + import BookingDetailView from './BookingDetailView'; import { bookingsApi } from '@/services/bookings/apis'; diff --git a/client/src/lib/api/client.ts b/client/src/lib/api/client.ts index 5b31c6f..cc25a2f 100644 --- a/client/src/lib/api/client.ts +++ b/client/src/lib/api/client.ts @@ -36,9 +36,13 @@ export async function clientFetch(path: string, options?: RequestInit, isRetr const locale = window.location.pathname.split('/')[1] || 'fa'; const reqHeaders: Record = { - 'Content-Type': 'application/json', 'Accept-Language': locale, }; + // Let the browser set `Content-Type` (with the multipart boundary) for FormData bodies — a manual + // JSON content-type there breaks the upload. JSON bodies still declare it explicitly. + if (!(options?.body instanceof FormData)) { + reqHeaders['Content-Type'] = 'application/json'; + } if (token) { reqHeaders['Authorization'] = `Bearer ${token}`; } diff --git a/client/src/services/addresses/apis/clientApi.ts b/client/src/services/addresses/apis/clientApi.ts index 722a6b9..e7fb3c7 100644 --- a/client/src/services/addresses/apis/clientApi.ts +++ b/client/src/services/addresses/apis/clientApi.ts @@ -5,11 +5,14 @@ import type { CreateAddressInput, CustomerAddress, CustomerAddressDto, Addresses const BASE = '/api/v1/customer_addresses'; -// The wire `CustomerAddressDto` has no `provinceId` (REQ-009). Reads default it to null; writes -// echo the caller's chosen province onto the returned row so the just-saved address can be -// re-edited with its cascade prefilled (not yet persisted server-side). -function toAddress(dto: CustomerAddressDto, provinceId?: number | null): CustomerAddress { - return { ...dto, provinceId: provinceId ?? null }; +// REQ-009 (delivered): the wire `CustomerAddressDto` now carries `provinceId` (joined from +// `cities.province_id`), so a freshly-fetched address prefills the cascade. The caller's chosen +// province is kept as a fallback for the optimistic just-saved echo. +interface AddressWire extends CustomerAddressDto { + provinceId: number; +} +function toAddress(dto: AddressWire, provinceId?: number | null): CustomerAddress { + return { ...dto, provinceId: dto.provinceId ?? provinceId ?? null }; } // Only the contract fields cross the wire. `latitude`/`longitude` are the picked pin (REQ-008 — @@ -43,7 +46,7 @@ export const addressesClientApi: AddressesApi = { // geo lookups' explicit `province_id`/`city_id`); match the patients template's `pageSize`. query.set('pageSize', String(params?.pageSize ?? ADDRESSES_PAGE_SIZE)); const page = unwrap( - await clientFetch>>(`${BASE}/list?${query.toString()}`), + await clientFetch>>(`${BASE}/list?${query.toString()}`), ); return { ...page, items: page.items.map((dto) => toAddress(dto)) }; }, @@ -51,7 +54,7 @@ export const addressesClientApi: AddressesApi = { create: async (input) => toAddress( unwrap( - await clientFetch>(`${BASE}/create`, { + await clientFetch>(`${BASE}/create`, { method: 'POST', body: JSON.stringify(toBody(input)), }), @@ -62,7 +65,7 @@ export const addressesClientApi: AddressesApi = { update: async (id, input) => toAddress( unwrap( - await clientFetch>(`${BASE}/update/${id}`, { + await clientFetch>(`${BASE}/update/${id}`, { method: 'POST', body: JSON.stringify(toBody(input)), }), diff --git a/client/src/services/addresses/constants.ts b/client/src/services/addresses/constants.ts index 9b4dcb4..19f37bf 100644 --- a/client/src/services/addresses/constants.ts +++ b/client/src/services/addresses/constants.ts @@ -7,7 +7,7 @@ * `api/v1/customer_addresses/*` routes — no hook/component changes * (see dev/shared-working-context/reports/mocks-registry.md). */ -export const USE_ADDRESSES_MOCK = true; +export const USE_ADDRESSES_MOCK = false; /** Address lists change only on mutation; keep them warm across screen visits. */ export const ADDRESSES_STALE_TIME = 60_000; diff --git a/client/src/services/bookingRequests/apis/clientApi.ts b/client/src/services/bookingRequests/apis/clientApi.ts index 4e42e49..747a69c 100644 --- a/client/src/services/bookingRequests/apis/clientApi.ts +++ b/client/src/services/bookingRequests/apis/clientApi.ts @@ -12,43 +12,27 @@ import type { const BASE = '/api/v1/booking_requests'; -/** - * The b8 wire `BookingRequestDto` — identical to our app DTO minus the client-augmented `variantPrice` - * (REQ-013: the contract returns `variantLabel` + `variantPriceUnit` but no price) and `bookingId` - * (REQ-017: a `converted` request gives no way to reach the booking it became). - */ -type BookingRequestWireDto = Omit; - -/** Map the wire DTO to the app DTO, defaulting the not-yet-contracted fields to `null`. */ -function toDto(wire: BookingRequestWireDto): BookingRequestDto { - return { ...wire, variantPrice: null, bookingId: null }; -} - /** * Real HTTP implementation of the `BookingRequestsApi` seam (b8 contract * `dev/contracts/domains/booking-requests.md`). Routes are action-style + snake_case; ids for * accept/reject/cancel/get come from the **route**, never the body; JSON bodies/fields are camelCase and * `clientFetch` returns the raw envelope, so we `unwrap()`. Mutations use POST. * - * NOT the primary implementation this phase (`USE_BOOKING_REQUESTS_MOCK = true`): every input id (nurse, - * patient, address) comes from a mock-primary upstream domain today, and the DTO omits `variantPrice` - * (REQ-013). This client maps everything b8 provides — the `context` arg (a mock-only display aid) is - * ignored here, and the nurse-view masking is done server-side (so `role` is ignored too). Selected once - * the upstream domains are live and REQ-013 lands (a single config flip; no hook/component change). + * PRIMARY once `USE_BOOKING_REQUESTS_MOCK = false` (refinement-phase-4; REQ-013/014/017 delivered): the + * DTO now carries `variantPrice` + `nurseAvatarUrl` (REQ-013) + `bookingId` (REQ-017), and the list item + * carries `variantLabel` + `patientAge` (REQ-014), so the wire maps 1:1 to the app DTO. The `context` arg + * (a mock-only display aid) is ignored here, and the nurse-view masking is done server-side. */ export const bookingRequestsClientApi: BookingRequestsApi = { create: async (payload: CreateBookingRequestPayload) => - toDto( - unwrap( - await clientFetch>(`${BASE}/create`, { - method: 'POST', - body: JSON.stringify(payload), - }), - ), + unwrap( + await clientFetch>(`${BASE}/create`, { + method: 'POST', + body: JSON.stringify(payload), + }), ), - get: async (id: number) => - toDto(unwrap(await clientFetch>(`${BASE}/get/${id}`))), + get: async (id: number) => unwrap(await clientFetch>(`${BASE}/get/${id}`)), list: async (params: BookingRequestListParams): Promise> => { const query = new URLSearchParams(); @@ -62,22 +46,16 @@ export const bookingRequestsClientApi: BookingRequestsApi = { }, accept: async (id: number) => - toDto( - unwrap(await clientFetch>(`${BASE}/accept/${id}`, { method: 'POST' })), - ), + unwrap(await clientFetch>(`${BASE}/accept/${id}`, { method: 'POST' })), reject: async (id: number, payload: RejectBookingRequestPayload) => - toDto( - unwrap( - await clientFetch>(`${BASE}/reject/${id}`, { - method: 'POST', - body: JSON.stringify(payload), - }), - ), + unwrap( + await clientFetch>(`${BASE}/reject/${id}`, { + method: 'POST', + body: JSON.stringify(payload), + }), ), cancel: async (id: number) => - toDto( - unwrap(await clientFetch>(`${BASE}/cancel/${id}`, { method: 'POST' })), - ), + unwrap(await clientFetch>(`${BASE}/cancel/${id}`, { method: 'POST' })), }; diff --git a/client/src/services/bookingRequests/constants.ts b/client/src/services/bookingRequests/constants.ts index 3ddfef7..6f92023 100644 --- a/client/src/services/bookingRequests/constants.ts +++ b/client/src/services/bookingRequests/constants.ts @@ -11,7 +11,7 @@ * (REQ-013). Flip to `false` once the upstream domains are live and REQ-013 lands — no hook/component * change (see `dev/shared-working-context/reports/frontend-phase-7-report.md`). */ -export const USE_BOOKING_REQUESTS_MOCK = true; +export const USE_BOOKING_REQUESTS_MOCK = false; /** * The customer's C5 and the nurse inbox **poll** while a request is non-terminal so a transition diff --git a/client/src/services/bookings/constants.ts b/client/src/services/bookings/constants.ts index 639dd8e..4412b1c 100644 --- a/client/src/services/bookings/constants.ts +++ b/client/src/services/bookings/constants.ts @@ -11,7 +11,7 @@ * `clientApi` maps the routes 1:1; flip to `false` once conversion (b10) is live client-side — a single * config change, no hook/component edits (see `dev/shared-working-context/reports/frontend-phase-8-report.md`). */ -export const USE_BOOKINGS_MOCK = true; +export const USE_BOOKINGS_MOCK = false; /** * The booking detail changes on status transitions (payment → confirmed → in_progress → completed) and diff --git a/client/src/services/catalog/constants.ts b/client/src/services/catalog/constants.ts index 062b63e..10db9f9 100644 --- a/client/src/services/catalog/constants.ts +++ b/client/src/services/catalog/constants.ts @@ -5,7 +5,7 @@ * demo standalone before the backend is reachable in this environment. Flip to false to hit the * live endpoints — no hook/component changes (see dev/shared-working-context/reports/mocks-registry.md). */ -export const USE_CATALOG_MOCK = true; +export const USE_CATALOG_MOCK = false; /** * Categories and a category's option groups/values are **admin-seeded reference data** that changes diff --git a/client/src/services/geography/constants.ts b/client/src/services/geography/constants.ts index 3edc037..aaef6c9 100644 --- a/client/src/services/geography/constants.ts +++ b/client/src/services/geography/constants.ts @@ -5,7 +5,7 @@ * Flip to false to hit the live `api/v1/geo/*` lookups — no hook/component changes * (see dev/shared-working-context/reports/mocks-registry.md). */ -export const USE_GEOGRAPHY_MOCK = true; +export const USE_GEOGRAPHY_MOCK = false; /** * Reference data almost never changes, so it is cached **for the whole session**: an Infinite diff --git a/client/src/services/notifications/constants.ts b/client/src/services/notifications/constants.ts index 4f87a6d..06c869a 100644 --- a/client/src/services/notifications/constants.ts +++ b/client/src/services/notifications/constants.ts @@ -10,7 +10,7 @@ * `__mockPushNotification` to simulate a fresh notification arriving (the bell increments within the poll * interval — phase §7 step 4). Flip to `false` once the upstreams are real — no hook/component change. */ -export const USE_NOTIFICATIONS_MOCK = true; +export const USE_NOTIFICATIONS_MOCK = false; /** Notification-center page size (api-conventions `pageSize`). */ export const NOTIFICATIONS_PAGE_SIZE = 20; diff --git a/client/src/services/nurse/constants.ts b/client/src/services/nurse/constants.ts index c40bc1f..b45b33a 100644 --- a/client/src/services/nurse/constants.ts +++ b/client/src/services/nurse/constants.ts @@ -5,7 +5,7 @@ * the pending→verified/mismatch UI transition behind the client mock. Flip to false to use * the real endpoints — no hook/component changes (mocks-registry.md). */ -export const USE_NURSE_BANK_MOCK = true; +export const USE_NURSE_BANK_MOCK = false; /** Bank accounts change rarely; keep them warm across screen visits. */ export const BANK_STALE_TIME = 30_000; diff --git a/client/src/services/patients/apis/clientApi.ts b/client/src/services/patients/apis/clientApi.ts index d497bc6..a63c39f 100644 --- a/client/src/services/patients/apis/clientApi.ts +++ b/client/src/services/patients/apis/clientApi.ts @@ -1,17 +1,27 @@ import { clientFetch } from '@/lib/api/client'; import { unwrap, type ApiEnvelope, type Paginated } from '@/lib/api/types'; -import type { CreatePatientInput, Patient, PatientDto, PatientsApi } from '../types'; +import type { + ConditionCode, + CreatePatientInput, + Patient, + PatientDto, + PatientsApi, + Relation, +} from '../types'; const BASE = '/api/v1/patients'; -// The wire `PatientDto` has no relation/conditions yet (REQ-005). Reads default them; writes -// echo the caller's choice onto the returned row so the just-edited card reflects it (not -// yet persisted server-side). -function toPatient(dto: PatientDto, augment?: Pick): Patient { - return { ...dto, relation: augment?.relation ?? null, conditions: augment?.conditions ?? [] }; +/** REQ-005 (delivered): the wire `PatientDto` now carries `relation`/`conditions` (both nullable). */ +interface PatientWire extends PatientDto { + relation: Relation | null; + conditions: ConditionCode[] | null; } -// Only the wire fields cross the boundary — relation/conditions are client-augmented (REQ-005). +function toPatient(dto: PatientWire): Patient { + return { ...dto, relation: dto.relation ?? null, conditions: dto.conditions ?? [] }; +} + +// REQ-005 (delivered): relation/conditions now cross the wire and are persisted server-side. function toBody(input: CreatePatientInput) { const { displayName, firstName, lastName, birthDate, gender } = input; return { @@ -22,13 +32,15 @@ function toBody(input: CreatePatientInput) { gender, bloodType: input.bloodType ?? null, initialMedicalNotes: input.initialMedicalNotes ?? null, + relation: input.relation, + conditions: input.conditions, }; } /** * Real HTTP implementation of the PatientsApi seam (b3 action-style routes). `clientFetch` - * returns the raw envelope, so each call reads its payload via `unwrap`. Selected once - * USE_PATIENTS_MOCK is false and the relation/conditions fields land. + * returns the raw envelope, so each call reads its payload via `unwrap`. Primary once + * USE_PATIENTS_MOCK is false (refinement-phase-4; REQ-005 delivered). */ export const patientsClientApi: PatientsApi = { list: async (params) => { @@ -36,32 +48,30 @@ export const patientsClientApi: PatientsApi = { if (params?.page) query.set('page', String(params.page)); if (params?.pageSize) query.set('pageSize', String(params.pageSize)); const qs = query.toString(); - const page = unwrap(await clientFetch>>(`${BASE}/list${qs ? `?${qs}` : ''}`)); + const page = unwrap(await clientFetch>>(`${BASE}/list${qs ? `?${qs}` : ''}`)); return { ...page, items: page.items.map((dto) => toPatient(dto)) }; }, - get: async (id) => toPatient(unwrap(await clientFetch>(`${BASE}/get/${id}`))), + get: async (id) => toPatient(unwrap(await clientFetch>(`${BASE}/get/${id}`))), create: async (input) => toPatient( unwrap( - await clientFetch>(`${BASE}/create`, { + await clientFetch>(`${BASE}/create`, { method: 'POST', body: JSON.stringify(toBody(input)), }), ), - input, ), update: async (id, input) => toPatient( unwrap( - await clientFetch>(`${BASE}/update/${id}`, { + await clientFetch>(`${BASE}/update/${id}`, { method: 'POST', body: JSON.stringify(toBody(input)), }), ), - input, ), archive: async (id) => { diff --git a/client/src/services/patients/constants.ts b/client/src/services/patients/constants.ts index 8a6d666..4a33e59 100644 --- a/client/src/services/patients/constants.ts +++ b/client/src/services/patients/constants.ts @@ -5,7 +5,7 @@ * mock. Flip to false once those fields land — no hook/component changes are needed * (see dev/shared-working-context/reports/mocks-registry.md). */ -export const USE_PATIENTS_MOCK = true; +export const USE_PATIENTS_MOCK = false; export const PATIENTS_STALE_TIME = 60_000; diff --git a/client/src/services/payment/constants.ts b/client/src/services/payment/constants.ts index bdfe716..2d7b32a 100644 --- a/client/src/services/payment/constants.ts +++ b/client/src/services/payment/constants.ts @@ -14,7 +14,7 @@ * store, and issues the invoice — so C5 → C6 → gateway → confirmation → booking detail demos end-to-end. * Flip to `false` once the upstream domains are real and REQ-016/017 land — no hook/component change. */ -export const USE_PAYMENT_MOCK = true; +export const USE_PAYMENT_MOCK = false; /** * f11 wired the BNPL method screens (D1–D5): C6's «پرداخت اقساطی» seam now navigates into the installment diff --git a/client/src/services/profiles/apis/clientApi.ts b/client/src/services/profiles/apis/clientApi.ts index c200a80..2390cbb 100644 --- a/client/src/services/profiles/apis/clientApi.ts +++ b/client/src/services/profiles/apis/clientApi.ts @@ -25,23 +25,34 @@ async function orNull(promise: Promise): Promise { } } -// The wire DTOs carry no avatar/name yet (REQ-006/007); reads default the augmented fields. -function toNurseProfile(dto: NurseProfileDto): NurseProfile { - return { ...dto, avatarUrl: null }; +/** REQ-006 (delivered): both profile DTOs now carry `avatarUrl`; the customer DTO also carries `preferredLanguage`. */ +interface NurseProfileWire extends NurseProfileDto { + avatarUrl: string | null; } -function toCustomerProfile(dto: CustomerProfileDto): CustomerProfile { - return { ...dto, firstName: null, lastName: null, preferredLanguage: null }; +interface CustomerProfileWire extends CustomerProfileDto { + avatarUrl: string | null; + preferredLanguage: string | null; +} + +function toNurseProfile(dto: NurseProfileWire): NurseProfile { + return { ...dto, avatarUrl: dto.avatarUrl ?? null }; +} +// The customer name lives on `/me` (REQ-007 design), not on `CustomerProfileDto` — the profile screen +// sources first/last name from `useMe`. Here we carry the served `preferredLanguage`; name stays null. +function toCustomerProfile(dto: CustomerProfileWire): CustomerProfile { + return { ...dto, firstName: null, lastName: null, preferredLanguage: dto.preferredLanguage ?? null }; } /** - * Real HTTP implementation of the ProfilesApi seam (b3 action-style routes). Selected once - * USE_PROFILES_MOCK is false and the avatar/name gaps land. `uploadAvatar` has no route yet - * (REQ-006) and the JSON-only fetch layer can't send multipart — it stays mock-only. + * Real HTTP implementation of the ProfilesApi seam (b3 action-style routes). PRIMARY once + * USE_PROFILES_MOCK is false (refinement-phase-4; REQ-006/007 delivered). Avatar upload posts multipart + * to the b3 `nurse_profiles/avatar` route (the JSON-only default is bypassed for `FormData` bodies — + * see `lib/api/client.ts`); the customer name/language reach the server via the upsert body. */ export const profilesClientApi: ProfilesApi = { getCustomerProfile: async () => orNull( - clientFetch>(`${BASE}/customer_profiles/me`).then((env) => + clientFetch>(`${BASE}/customer_profiles/me`).then((env) => toCustomerProfile(unwrap(env)), ), ), @@ -49,11 +60,15 @@ export const profilesClientApi: ProfilesApi = { upsertCustomerProfile: async (input: UpsertCustomerProfileInput) => toCustomerProfile( unwrap( - await clientFetch>(`${BASE}/customer_profiles/upsert`, { + await clientFetch>(`${BASE}/customer_profiles/upsert`, { method: 'POST', body: JSON.stringify({ defaultEmergencyContactName: input.defaultEmergencyContactName, defaultEmergencyContactPhone: input.defaultEmergencyContactPhone, + // REQ-007 (delivered): name + preferred language are accepted on the upsert command. + firstName: input.firstName ?? null, + lastName: input.lastName ?? null, + preferredLanguage: input.preferredLanguage ?? null, }), }), ), @@ -61,13 +76,13 @@ export const profilesClientApi: ProfilesApi = { getNurseProfile: async () => orNull( - clientFetch>(`${BASE}/nurse_profiles/me`).then((env) => toNurseProfile(unwrap(env))), + clientFetch>(`${BASE}/nurse_profiles/me`).then((env) => toNurseProfile(unwrap(env))), ), upsertNurseProfile: async (input: UpsertNurseProfileInput) => toNurseProfile( unwrap( - await clientFetch>(`${BASE}/nurse_profiles/upsert`, { + await clientFetch>(`${BASE}/nurse_profiles/upsert`, { method: 'POST', body: JSON.stringify({ bio: input.bio, @@ -80,7 +95,16 @@ export const profilesClientApi: ProfilesApi = { ), ), - uploadAvatar: async (): Promise => { - throw new ApiError(501, 'Avatar upload has no backend route yet (REQ-006); served by the mock.'); + // REQ-006 (delivered): only the nurse profile screen uploads an avatar today; it persists immediately + // via the dedicated multipart route and the returned URL is echoed for display + read back on reload. + uploadAvatar: async (file: File): Promise => { + const form = new FormData(); + form.append('file', file); + return unwrap( + await clientFetch>(`${BASE}/nurse_profiles/avatar`, { + method: 'POST', + body: form, + }), + ); }, }; diff --git a/client/src/services/profiles/constants.ts b/client/src/services/profiles/constants.ts index 93bec3a..66cda21 100644 --- a/client/src/services/profiles/constants.ts +++ b/client/src/services/profiles/constants.ts @@ -5,7 +5,7 @@ * (REQ-006 / REQ-007), so this phase demos behind the mock. Flip to false once those land — * no hook/component changes (see dev/shared-working-context/reports/mocks-registry.md). */ -export const USE_PROFILES_MOCK = true; +export const USE_PROFILES_MOCK = false; /** Profiles are stable within a session; revisiting a screen shouldn't refetch. */ export const PROFILE_STALE_TIME = 60_000; diff --git a/client/src/services/reviews/apis/clientApi.ts b/client/src/services/reviews/apis/clientApi.ts index 05e1d0e..9964708 100644 --- a/client/src/services/reviews/apis/clientApi.ts +++ b/client/src/services/reviews/apis/clientApi.ts @@ -24,11 +24,17 @@ interface NurseReviewsWire { reviews: Paginated; } -/** - * Wire `ModerationQueueItemDto`. Per the b14 contract it does **not** carry `tagCodes` (REQ-037 — the admin - * card can't show the review's tags); the client defaults it to `[]` on map. - */ -type ModerationQueueItemWire = Omit; +/** Wire `MyReviewDto` (REQ-026) — keys the moderation state as `moderationStatus` (`'none'` when unreviewed). */ +interface MyReviewDto { + moderationStatus: MyReviewState['status']; + rating: number | null; + body: string | null; + tagCodes: string[]; + createdAt: string | null; +} + +/** Wire `ModerationQueueItemDto` — carries `tagCodes` since REQ-037 (delivered in refinement-phase-3). */ +type ModerationQueueItemWire = ModerationQueueItem; /** * Real HTTP implementation of the `ReviewsApi` seam (b14 contract `dev/contracts/domains/reviews-records.md`, @@ -57,14 +63,30 @@ export const reviewsClientApi: ReviewsApi = { return { aggregate: wire.aggregate, reviews: wire.reviews }; }, - // REQ-026: proposed owner-scoped read (no wire endpoint yet). 404s until delivered — never called while - // the domain is mock-primary. Kept symmetric so the swap stays a one-line config flip. - getReviewEligibility: async (bookingId: number): Promise => - unwrap(await clientFetch>(`${API}/bookings/${bookingId}/review_eligibility`)), + // REQ-026 (delivered): owner-scoped eligibility read. Wire `reason` is nullable; normalise to `undefined`. + getReviewEligibility: async (bookingId: number): Promise => { + const wire = unwrap( + await clientFetch>( + `${API}/bookings/${bookingId}/review_eligibility`, + ), + ); + return { canReview: wire.canReview, reason: wire.reason ?? undefined }; + }, - // REQ-026: proposed owner-scoped read of the caller's own review for this booking. - getMyReviewForBooking: async (bookingId: number): Promise => - unwrap(await clientFetch>(`${API}/bookings/${bookingId}/my_review`)), + // REQ-026 (delivered): the caller's own review for this booking + its moderation state. The wire dto keys + // the state as `moderationStatus` (incl. `'none'` when unreviewed); the client model calls it `status`. + getMyReviewForBooking: async (bookingId: number): Promise => { + const wire = unwrap( + await clientFetch>(`${API}/bookings/${bookingId}/my_review`), + ); + return { + status: wire.moderationStatus, + rating: wire.rating, + body: wire.body, + tagCodes: wire.tagCodes ?? [], + createdAt: wire.createdAt, + }; + }, createReview: async (bookingId: number, body: CreateReviewRequest): Promise => unwrap( @@ -84,8 +106,8 @@ export const reviewsClientApi: ReviewsApi = { `${API}/admin/reviews/moderation_queue?${query.toString()}`, ), ); - // REQ-037: the wire dto omits tagCodes — default to [] so the admin card renders without them. - return { ...wire, items: wire.items.map((item) => ({ ...item, tagCodes: [] })) }; + // REQ-037 (delivered): the wire dto carries tagCodes; default to [] only if the server omits it. + return { ...wire, items: wire.items.map((item) => ({ ...item, tagCodes: item.tagCodes ?? [] })) }; }, moderateReview: async ( diff --git a/client/src/services/reviews/constants.ts b/client/src/services/reviews/constants.ts index 1cead9a..7bb549f 100644 --- a/client/src/services/reviews/constants.ts +++ b/client/src/services/reviews/constants.ts @@ -11,7 +11,7 @@ * review appear on the profile (the f15 moderation UI is deferred). Flip to `false` once REQ-026 lands — no * hook/component change (only `clientApi.ts`'s two gap methods start returning real data). */ -export const USE_REVIEWS_MOCK = true; +export const USE_REVIEWS_MOCK = false; /** Page size for the public nurse-reviews list (api-conventions `pageSize`). */ export const REVIEWS_PAGE_SIZE = 5; diff --git a/client/src/services/search/apis/clientApi.ts b/client/src/services/search/apis/clientApi.ts index f022d8c..dfe15be 100644 --- a/client/src/services/search/apis/clientApi.ts +++ b/client/src/services/search/apis/clientApi.ts @@ -1,11 +1,12 @@ import { clientFetch } from '@/lib/api/client'; import { unwrap, type ApiEnvelope, type Paginated } from '@/lib/api/types'; import type { PriceUnit } from '@/services/catalog/types'; -import type { TrustBadge } from '@/services/verification/types'; import { SEARCH_PAGE_SIZE } from '../constants'; import type { NurseGender, NurseProfile, + NurseProfileServiceRow, + NurseReviewSnippet, NurseSearchFilters, NurseSearchResult, SearchApi, @@ -27,23 +28,44 @@ interface NurseSearchResultDto { totalCompletedBookings: number; cityId: number; districtId: number | null; + /** REQ-012 — the C2 card identity, now denormalized into the index row. */ + nurseName: string | null; + avatarUrl: string | null; + distanceKm: number | null; } -/** The INO-membership credential type code (see b6 verification). */ -const INO_MEMBERSHIP_CODE = 'ino_membership'; +/** The b6/b7 aggregated `NursePublicProfileDto` (REQ-012) — the C3 profile payload. */ +interface NursePublicProfileDto { + nurseId: number; + nurseName: string; + avatarUrl: string | null; + bio: string; + yearsExperience: number; + averageRating: number; + totalReviews: number; + totalCompletedBookings: number; + isVerified: boolean; + inoMembership: boolean; + attributeChips: string[]; + services: { + variantId: number; + displayName: string; + priceIrr: string; + priceUnit: PriceUnit; + sessionCount: number | null; + }[]; + latestReview: { rating: number; body: string | null; authorMasked: string | null; createdAt: string } | null; +} /** - * Real HTTP implementation of the `SearchApi` seam (b7 `search/nurses`, b6 trust badge). Routes are + * Real HTTP implementation of the `SearchApi` seam (b7 `search/nurses`, b6/b7 public profile). Routes are * action-style + snake_case; query params are snake_case per the contract; JSON fields are camelCase and * `clientFetch` returns the raw envelope, so we `unwrap()`. * - * NOT the primary implementation this phase (`USE_SEARCH_MOCK = true`): b7's index row omits the nurse - * **display name, avatar, and distance** the C2 card renders, and there is **no** aggregated - * nurse-profile endpoint (name/bio/specialties/full services list/latest review) for C3 — only the b6 - * trust badge is public. Both gaps are filed in - * `dev/shared-working-context/frontend/requests/for-backend.md`. This client maps everything b7/b6 - * currently provide (leaving the missing fields blank) so the swap is a single config flip once the - * backend lands the join + profile route. + * The PRIMARY implementation once `USE_SEARCH_MOCK = false` (refinement-phase-4). REQ-012 delivered the + * discovery enrichment the C2 card + C3 profile need: `nurseName`/`avatarUrl`/`distanceKm` are now + * denormalized onto `NurseSearchResultDto`, and `GET nurses/{id}/profile` aggregates identity + bio + + * specialties + the full services list + the latest review. This client maps both 1:1. */ export const searchClientApi: SearchApi = { searchNurses: async (filters: NurseSearchFilters): Promise> => { @@ -70,16 +92,15 @@ export const searchClientApi: SearchApi = { nurseId: dto.nurseId, variantId: dto.variantId, serviceCategoryId: dto.serviceCategoryId, - // Gap (filed): b7 does not yet join the nurse's name/avatar; the card falls back to a label. - nurseName: '', - avatarUrl: null, + // REQ-012 — identity denormalized onto the index row; card falls back to a label only when null. + nurseName: dto.nurseName ?? '', + avatarUrl: dto.avatarUrl, // Every returned row is searchable by the index invariant. isVerified: true, averageRating: dto.averageRating, totalReviews: dto.totalReviews, totalCompletedBookings: dto.totalCompletedBookings, - // Gap (filed): no geo-distance in the index row yet. - distanceKm: null, + distanceKm: dto.distanceKm, priceFromIrr: dto.price, priceUnit: dto.priceUnit, nurseGender: dto.nurseGender, @@ -90,26 +111,43 @@ export const searchClientApi: SearchApi = { }, getNurseProfile: async (nurseId: number): Promise => { - // Only the public trust badge is available today; the aggregated profile (name/bio/specialties/ - // services list/latest review) is filed for the backend. Compose what b6 exposes; leave the rest blank. - const badge = unwrap( - await clientFetch>(`${NURSES_BASE}/${nurseId}/trust_badge`), + const dto = unwrap( + await clientFetch>(`${NURSES_BASE}/${nurseId}/profile`), ); + const services: NurseProfileServiceRow[] = dto.services.map((s) => ({ + variantId: s.variantId, + displayName: s.displayName, + priceIrr: s.priceIrr, + priceUnit: s.priceUnit, + sessionCount: s.sessionCount, + })); + + const latestReview: NurseReviewSnippet | null = dto.latestReview + ? { + rating: dto.latestReview.rating, + body: dto.latestReview.body ?? '', + authorMasked: dto.latestReview.authorMasked ?? '', + createdAt: dto.latestReview.createdAt, + } + : null; + return { - nurseId: badge.nurseId, - nurseName: '', - avatarUrl: null, - bio: null, - yearsExperience: null, - averageRating: 0, - totalReviews: 0, - totalCompletedBookings: 0, - isVerified: badge.isVerified, - inoMembership: badge.credentialTypes.includes(INO_MEMBERSHIP_CODE), - attributeChips: badge.credentialTypes, - services: [], - latestReview: null, + nurseId: dto.nurseId, + nurseName: dto.nurseName, + avatarUrl: dto.avatarUrl, + bio: dto.bio || null, + yearsExperience: dto.yearsExperience, + averageRating: dto.averageRating, + totalReviews: dto.totalReviews, + totalCompletedBookings: dto.totalCompletedBookings, + isVerified: dto.isVerified, + inoMembership: dto.inoMembership, + attributeChips: dto.attributeChips, + services, + latestReview, + // Not carried by the public profile DTO; the same-gender intent for booking comes from the C1 + // filter carried through the query string, not this field. Unused by the C3 page. nurseGender: 'female', }; }, diff --git a/client/src/services/search/constants.ts b/client/src/services/search/constants.ts index b550316..656a5ea 100644 --- a/client/src/services/search/constants.ts +++ b/client/src/services/search/constants.ts @@ -6,7 +6,7 @@ * mock supplies real-shaped fixtures so C1/C2/C3 demo end-to-end. Flip to false once the backend fills * the gap — no hook/component changes (see `dev/shared-working-context/reports/frontend-phase-6-report.md`). */ -export const USE_SEARCH_MOCK = true; +export const USE_SEARCH_MOCK = false; /** * Results are read-heavy and change slowly, so a revisit (or a filter **revert**) serves from cache diff --git a/client/src/services/serviceAreas/constants.ts b/client/src/services/serviceAreas/constants.ts index e82b7a0..e9fc1c9 100644 --- a/client/src/services/serviceAreas/constants.ts +++ b/client/src/services/serviceAreas/constants.ts @@ -5,7 +5,7 @@ * reachable. Flip to false to use the live `api/v1/nurse_service_areas/*` routes — no * hook/component changes (see dev/shared-working-context/reports/mocks-registry.md). */ -export const USE_SERVICE_AREAS_MOCK = true; +export const USE_SERVICE_AREAS_MOCK = false; /** Service areas change only on mutation; keep them warm across screen visits. */ export const SERVICE_AREAS_STALE_TIME = 60_000; diff --git a/client/src/services/tickets/apis/clientApi.ts b/client/src/services/tickets/apis/clientApi.ts index 182d407..9723d59 100644 --- a/client/src/services/tickets/apis/clientApi.ts +++ b/client/src/services/tickets/apis/clientApi.ts @@ -21,7 +21,7 @@ import type { const API = '/api/v1'; -/** Wire `TicketSummaryDto` (camelCase, per api-conventions). */ +/** Wire `TicketSummaryDto` (camelCase). REQ-028 (delivered) added `lastMessageAt`/`unreadCount`. */ interface TicketSummaryWire { id: number; referenceCode: string; @@ -31,6 +31,8 @@ interface TicketSummaryWire { bookingId: number | null; refundId: number | null; createdAt: string; + lastMessageAt: string | null; + unreadCount: number; } /** Wire `TicketMessageDto`. `isInternal` is present on the DTO but is `false` in the user view (server-stripped). */ @@ -67,6 +69,9 @@ function mapSummary(w: TicketSummaryWire): TicketSummary { bookingId: w.bookingId, refundId: w.refundId, createdAt: w.createdAt, + // REQ-028 (delivered): the inbox unread badge + last-activity sort now come off the wire. + lastMessageAt: w.lastMessageAt, + unreadCount: w.unreadCount, }; } @@ -162,9 +167,11 @@ function mapAdminThread(w: TicketThreadWire, viewerUserId?: number): AdminTicket * - `openTicket` → `POST /tickets`. * - `postMessage` → `POST /tickets/{id}/messages` (a non-staff caller never sets `isInternal`). * - * NOT the primary implementation this phase (`USE_TICKETS_MOCK = true`) — see `constants.ts`. The wire - * summary has no `unreadCount`/`lastMessageAt` (REQ-028), so those stay undefined here (the inbox degrades). - * `clientMessageId` is client-only (optimistic reconcile) — not sent (the server has no field for it yet). + * PRIMARY once `USE_TICKETS_MOCK = false` (refinement-phase-4; REQ-028 delivered): the summary now carries + * `unreadCount`/`lastMessageAt` (inbox badge + last-activity sort) and the message post sends the optimistic + * `clientMessageId` (server dedupes + echoes it back). The user list still filters only by `Status`; the + * "jump to the existing coordination ticket" by-booking lookup is a minor follow-up (REQ-028 #3 — + * `GET /tickets?BookingId=` is served, but no client method targets it yet). */ export const ticketsClientApi: TicketsApi = { listMyTickets: async (params: TicketListParams): Promise> => { @@ -202,7 +209,9 @@ export const ticketsClientApi: TicketsApi = { unwrap( await clientFetch>(`${API}/tickets/${ticketId}/messages`, { method: 'POST', - body: JSON.stringify({ body: body.body }), + // REQ-028 (delivered): send the optimistic `clientMessageId` so the server dedupes a retried send + // and echoes it back on `PostMessageResult` for reconciliation. + body: JSON.stringify({ body: body.body, clientMessageId: body.clientMessageId }), }), ), @@ -233,12 +242,17 @@ export const ticketsClientApi: TicketsApi = { return mapAdminThread(wire, viewerUserId); }, - // Staff post — may set `isInternal` (the one caller allowed to). `clientMessageId` stays client-only. + // Staff post — may set `isInternal` (the one caller allowed to). REQ-028: send `clientMessageId` too. postAdminMessage: async (ticketId: number, body: PostAdminMessageRequest): Promise => unwrap( await clientFetch>(`${API}/tickets/${ticketId}/messages`, { method: 'POST', - body: JSON.stringify({ body: body.body, isInternal: body.isInternal }), + body: JSON.stringify({ + body: body.body, + isInternal: body.isInternal, + clientMessageId: body.clientMessageId, + }), }), ), + }; diff --git a/client/src/services/tickets/constants.ts b/client/src/services/tickets/constants.ts index ad72556..ff42d95 100644 --- a/client/src/services/tickets/constants.ts +++ b/client/src/services/tickets/constants.ts @@ -12,7 +12,7 @@ * ticket reachable. Flip to `false` once the upstreams are real — no hook/component change (only the seam * selection in `apis/index.ts`). */ -export const USE_TICKETS_MOCK = true; +export const USE_TICKETS_MOCK = false; /** Inbox page size (api-conventions `pageSize`, default 50 / max 100). */ export const TICKETS_PAGE_SIZE = 20; diff --git a/dev/shared-working-context/reports/mocks-registry.md b/dev/shared-working-context/reports/mocks-registry.md index 37f85dd..6c6d1c3 100644 --- a/dev/shared-working-context/reports/mocks-registry.md +++ b/dev/shared-working-context/reports/mocks-registry.md @@ -57,31 +57,51 @@ Status legend: 🔴 not built · 🟡 mocked (seam + fake impl in place) · 🟢 These are in-browser mocks behind a `services/{domain}` interface, selected by a config flag. They exist so the frontend can build before the backend phase merges, and swap to the real HTTP client in one line. +> **Refinement-phase-4 de-mock (2026-07-13) — 14 domains flipped to REAL** (`USE_*_MOCK = false`), verified +> against the regenerated swagger + `npm run check`/`test:ci` green: **geography, patients, profiles, +> nurse (bank), addresses, serviceAreas, catalog, search, bookingRequests, bookings, payment, reviews, +> tickets, notifications** (auth was already real). The flip was **not** a pure flag flip for most: Phase-3 +> delivered fields the `clientApi.ts` mappers were written to null-override, so each mapper was updated to +> consume/send them (search name/avatar/distance + `nurses/{id}/profile`; patient relation/conditions; +> address `provinceId`; booking-request `variantPrice`/`bookingId`; ticket `unreadCount`/`lastMessageAt` + +> `clientMessageId`; review `my_review`; profile `avatarUrl`/`preferredLanguage` + a real **multipart avatar +> upload** — `clientFetch` now passes `FormData` through; the customer profile sources name from `/me`). The +> **payment mock-gateway harness page was deleted**; `EVV_GPS_MODE` auto-selects `off` (real geolocation). +> +> **7 domains stay 🟡 mocked — precondition REQ deferred/unsafe (documented, not forgotten):** +> `verification` (REQ-034 admin queue/doc-URL/approve — nurse flow ready, admin half blocks the shared flag), +> `refunds` (REQ-035 admin preview/approve — customer cancel/policy ready), `payouts` (REQ-036 admin +> preview/holidayShifted/transfer-ref — nurse earnings ready), `admin` (REQ-031 RBAC roles — config/audit/ +> holidays/alerts ready), `bnpl` (REQ-022 options/schedule + REQ-024 wallet_installments deferred), +> `partnerCenter` (REQ-032/033 portal split reads + REQ-038 `/me` signal deferred), `patientRecords` (REQ-027 +> endpoints exist but the client family-record `id` model is `string` vs the wire's `int` → the customer-edit +> PUT is write-unsafe until the id types are reconciled; the nurse visit-note history half is contract-real). + | Seam (interface) | File | What it fakes | Config flag | Make it real → | Status | | --- | --- | --- | --- | --- | --- | -| `PatientsApi` | `client/src/services/patients/apis/mockApi.ts` | In-memory patient CRUD (list/get/create/update/soft-archive), **seeded empty** so onboarding + the empty state both demo; persists the client-augmented `relation`/`conditions` the wire `PatientDto` lacks (REQ-005) | `USE_PATIENTS_MOCK` (`services/patients/constants.ts`, default `true`) | Deliver REQ-005 (relation/conditions on `PatientDto` + create/update), then set flag `false` — `patientsClientApi` is already wired to the b3 `patients/*` routes | 🟡 | -| `ProfilesApi` | `client/src/services/profiles/apis/mockApi.ts` | Customer + nurse profile get/upsert and **avatar upload** (echoes an object-URL). Keeps guarded read-only fields (`isVerified=false`, zero aggregates). Augments customer name/language (REQ-007) + nurse `avatarUrl` (REQ-006) the wire DTOs lack | `USE_PROFILES_MOCK` (`services/profiles/constants.ts`, default `true`) | b3 `customer_profiles/*` + `nurse_profiles/*` are live; deliver REQ-006 (avatar route/field) + REQ-007 (customer name/language) then set flag `false` — `profilesClientApi` is wired (its `uploadAvatar` throws `501` until REQ-006) | 🟡 | -| `NurseBankAccountsApi` | `client/src/services/nurse/apis/mockApi.ts` | Bank-account list/add/set-primary/verify-ownership. Drives the استعلام شبا **pending→verified/mismatch** transition over 2 list reads (so the poll shows it), single-primary enforcement, masked-IBAN (last-4); the configured mismatch IBAN (`IR000000000000000000000000`, matches backend default) resolves to `matchedNationalId=false` | `USE_NURSE_BANK_MOCK` (`services/nurse/constants.ts`, default `true`) | b3 `nurse_bank_accounts/*` are live (the real `add` resolves the inquiry synchronously — no client poll needed); set flag `false` — `nurseBankClientApi` is wired | 🟡 | +| `PatientsApi` | `client/src/services/patients/apis/mockApi.ts` | In-memory patient CRUD (list/get/create/update/soft-archive), **seeded empty** so onboarding + the empty state both demo; persists the client-augmented `relation`/`conditions` the wire `PatientDto` lacks (REQ-005) | `USE_PATIENTS_MOCK` (`services/patients/constants.ts`, default `true`) | Deliver REQ-005 (relation/conditions on `PatientDto` + create/update), then set flag `false` — `patientsClientApi` is already wired to the b3 `patients/*` routes | 🟢 (real, refinement-phase-4) | +| `ProfilesApi` | `client/src/services/profiles/apis/mockApi.ts` | Customer + nurse profile get/upsert and **avatar upload** (echoes an object-URL). Keeps guarded read-only fields (`isVerified=false`, zero aggregates). Augments customer name/language (REQ-007) + nurse `avatarUrl` (REQ-006) the wire DTOs lack | `USE_PROFILES_MOCK` (`services/profiles/constants.ts`, default `true`) | b3 `customer_profiles/*` + `nurse_profiles/*` are live; deliver REQ-006 (avatar route/field) + REQ-007 (customer name/language) then set flag `false` — `profilesClientApi` is wired (its `uploadAvatar` throws `501` until REQ-006) | 🟢 (real, refinement-phase-4) | +| `NurseBankAccountsApi` | `client/src/services/nurse/apis/mockApi.ts` | Bank-account list/add/set-primary/verify-ownership. Drives the استعلام شبا **pending→verified/mismatch** transition over 2 list reads (so the poll shows it), single-primary enforcement, masked-IBAN (last-4); the configured mismatch IBAN (`IR000000000000000000000000`, matches backend default) resolves to `matchedNationalId=false` | `USE_NURSE_BANK_MOCK` (`services/nurse/constants.ts`, default `true`) | b3 `nurse_bank_accounts/*` are live (the real `add` resolves the inquiry synchronously — no client poll needed); set flag `false` — `nurseBankClientApi` is wired | 🟢 (real, refinement-phase-4) | | `AuthApi` | `client/src/services/auth/apis/mockApi.ts` (`authMockApi`) | Phone-OTP login offline: `requestOtp`→`{otpSent,resendAvailableInSeconds:120}`; `verifyOtp` accepts dev code **`123456`** and locks after 3 wrong tries (`otp_locked`); `getMe`/`selectRole`/`refresh` from a `MOCK_SCENARIO` toggle (`customer`/`nurse_unverified`/`no_role`) to exercise all router branches | `USE_AUTH_MOCK` (`services/auth/constants.ts`, default **false** — b2 is live) + `MOCK_SCENARIO` in `mockApi.ts` | The real `authClientApi` is already wired to the live b2 routes; set `USE_AUTH_MOCK = false` (already the default) — no hook/screen change | 🟢 real by default, 🟡 mock available | -| `GeographyApi` | `client/src/services/geography/apis/mockApi.ts` (+ `apis/seed.ts`) | The province→city→district reference hierarchy — a faithful subset of the b4 seed: 8 provinces, Tehran (city 101) with its 22 مناطق (1001…1022), and the white-space cities Mashhad/Isfahan/Shiraz/Tabriz/Ahvaz/Qom/Karaj as whole-city-only. Active-only, `sortOrder`-ordered. `seed.ts` also resolves a saved `cityId`/`districtId` back to names for the addresses & serviceAreas mocks | `USE_GEOGRAPHY_MOCK` (`services/geography/constants.ts`, default `true`) | b4 `geo/{provinces,cities,districts}` are live; set flag `false` — `geographyClientApi` is wired to the snake_case-param lookups. No hook/component change | 🟡 | -| `AddressesApi` | `client/src/services/addresses/apis/mockApi.ts` | Customer address CRUD (list primary-first / create / update / set-primary / soft-delete) with the **exactly-one-primary** invariant enforced in-memory (first address auto-primary; promoting clears the prior; deleting the primary promotes the next). Persists the client-augmented `provinceId` (REQ-009) and the picked `latitude`/`longitude` (REQ-008) the wire DTO/create-body lack | `USE_ADDRESSES_MOCK` (`services/addresses/constants.ts`, default `true`) | b4 `customer_addresses/*` are live; deliver REQ-008 (accept the pin) + REQ-009 (`provinceId` on the DTO), then set flag `false` — `addressesClientApi` is wired (sends the pin + `pageSize`, echoes `provinceId` locally) | 🟡 | -| `ServiceAreasApi` | `client/src/services/serviceAreas/apis/mockApi.ts` | Nurse coverage areas (list whole-city-first / add / remove). Enforces `UNIQUE(cityId, districtId)` exactly as the server — a duplicate (incl. a second whole-city row) throws the same **`409`** (`area_duplicate`) so the coverage editor's inline dup handling is demonstrable | `USE_SERVICE_AREAS_MOCK` (`services/serviceAreas/constants.ts`, default `true`) | b4 `nurse_service_areas/*` are live; set flag `false` — `serviceAreasClientApi` is wired (maps the server 409 to the same inline message). No hook/component change | 🟡 | +| `GeographyApi` | `client/src/services/geography/apis/mockApi.ts` (+ `apis/seed.ts`) | The province→city→district reference hierarchy — a faithful subset of the b4 seed: 8 provinces, Tehran (city 101) with its 22 مناطق (1001…1022), and the white-space cities Mashhad/Isfahan/Shiraz/Tabriz/Ahvaz/Qom/Karaj as whole-city-only. Active-only, `sortOrder`-ordered. `seed.ts` also resolves a saved `cityId`/`districtId` back to names for the addresses & serviceAreas mocks | `USE_GEOGRAPHY_MOCK` (`services/geography/constants.ts`, default `true`) | b4 `geo/{provinces,cities,districts}` are live; set flag `false` — `geographyClientApi` is wired to the snake_case-param lookups. No hook/component change | 🟢 (real, refinement-phase-4) | +| `AddressesApi` | `client/src/services/addresses/apis/mockApi.ts` | Customer address CRUD (list primary-first / create / update / set-primary / soft-delete) with the **exactly-one-primary** invariant enforced in-memory (first address auto-primary; promoting clears the prior; deleting the primary promotes the next). Persists the client-augmented `provinceId` (REQ-009) and the picked `latitude`/`longitude` (REQ-008) the wire DTO/create-body lack | `USE_ADDRESSES_MOCK` (`services/addresses/constants.ts`, default `true`) | b4 `customer_addresses/*` are live; deliver REQ-008 (accept the pin) + REQ-009 (`provinceId` on the DTO), then set flag `false` — `addressesClientApi` is wired (sends the pin + `pageSize`, echoes `provinceId` locally) | 🟢 (real, refinement-phase-4) | +| `ServiceAreasApi` | `client/src/services/serviceAreas/apis/mockApi.ts` | Nurse coverage areas (list whole-city-first / add / remove). Enforces `UNIQUE(cityId, districtId)` exactly as the server — a duplicate (incl. a second whole-city row) throws the same **`409`** (`area_duplicate`) so the coverage editor's inline dup handling is demonstrable | `USE_SERVICE_AREAS_MOCK` (`services/serviceAreas/constants.ts`, default `true`) | b4 `nurse_service_areas/*` are live; set flag `false` — `serviceAreasClientApi` is wired (maps the server 409 to the same inline message). No hook/component change | 🟢 (real, refinement-phase-4) | | `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 | 🟡 | +| `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 | 🟢 (real, refinement-phase-4) | | `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 | 🟡 | -| `PaymentApi` | `client/src/services/payment/apis/mockApi.ts` | **The f9 checkout money path** — plays the PSP + webhook roles the client can't reach: `getCheckoutSummary` serves the unserved C6 breakdown (REQ-016; commission-net/VAT/service split via **integer parts-per-10000 BigInt math**, 12% fee / 10% VAT, reconciles to the rial); `initiatePayment` enforces b10 idempotency (same `Idempotency-Key` → same attempt; repeat after capture / lapsed window → **`409`**) and returns a `redirectUrl` into the local mock-gateway harness; `confirmGatewayReturn` on success is the **webhook-confirm stand-in and the missing f7↔f8 bridge** — flips the request `converted` (+ client-augmented `bookingId`, via `mockMarkBookingRequestConverted` in the f7 mock), inserts a **confirmed** booking into the f8 store (`mockInsertConvertedBooking`), and auto-issues the b11-shaped invoice (`moadianStatus: pending`, `pdfUrl: null` so the print path exercises); replayed returns converge idempotently; `getInvoice` 404s until issued | `USE_PAYMENT_MOCK` (`services/payment/constants.ts`, default `true`) | b10 initiate + b11 invoice are live and `paymentClientApi` maps them 1:1 (`Idempotency-Key` header, `GET invoices/{bookingId}`); deliver **REQ-016** (checkout summary — the real client already targets the proposed `booking_requests/checkout_summary/{id}` slug) + **REQ-017** (transaction status / `bookingId`; until then the real outcome poll maps `booking_requests/get` statuses and can't distinguish declined from slow) + **REQ-018** (invoice reachable post-capture), make the upstream `bookingRequests` flow real, then set flag `false`. No hook/component change | 🟡 | -| Mock-gateway page (test harness) | `client/src/app/[locale]/(private-routes)/(customer)/bookings/checkout/gateway/page.tsx` | **Not a product feature** — a dev stand-in for the PSP's hosted payment page so the initiate → redirect → return round-trip is exercisable without a gateway: the mock `redirectUrl` points here, and its success/failure buttons drive both branches of the return surface (`?outcome=success\|failure`). Clearly labelled «درگاه پرداخت آزمایشی», dashed border | _none — only reachable via the mock's `redirectUrl`_ | On the real path b10's `redirectUrl` is the PSP's **absolute** URL (the checkout does a full `window.location.assign` for `http(s)` URLs), so this page is simply never linked; delete it when `USE_PAYMENT_MOCK` retires. The PSP's return deep-link into `/bookings/checkout/return` is backend/PSP config | 🟡 | +| `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 | 🟢 (real, refinement-phase-4) | +| `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 | 🟢 (real, refinement-phase-4) | +| `PaymentApi` | `client/src/services/payment/apis/mockApi.ts` | **The f9 checkout money path** — plays the PSP + webhook roles the client can't reach: `getCheckoutSummary` serves the unserved C6 breakdown (REQ-016; commission-net/VAT/service split via **integer parts-per-10000 BigInt math**, 12% fee / 10% VAT, reconciles to the rial); `initiatePayment` enforces b10 idempotency (same `Idempotency-Key` → same attempt; repeat after capture / lapsed window → **`409`**) and returns a `redirectUrl` into the local mock-gateway harness; `confirmGatewayReturn` on success is the **webhook-confirm stand-in and the missing f7↔f8 bridge** — flips the request `converted` (+ client-augmented `bookingId`, via `mockMarkBookingRequestConverted` in the f7 mock), inserts a **confirmed** booking into the f8 store (`mockInsertConvertedBooking`), and auto-issues the b11-shaped invoice (`moadianStatus: pending`, `pdfUrl: null` so the print path exercises); replayed returns converge idempotently; `getInvoice` 404s until issued | `USE_PAYMENT_MOCK` (`services/payment/constants.ts`, default `true`) | b10 initiate + b11 invoice are live and `paymentClientApi` maps them 1:1 (`Idempotency-Key` header, `GET invoices/{bookingId}`); deliver **REQ-016** (checkout summary — the real client already targets the proposed `booking_requests/checkout_summary/{id}` slug) + **REQ-017** (transaction status / `bookingId`; until then the real outcome poll maps `booking_requests/get` statuses and can't distinguish declined from slow) + **REQ-018** (invoice reachable post-capture), make the upstream `bookingRequests` flow real, then set flag `false`. No hook/component change | 🟢 (real, refinement-phase-4) | +| Mock-gateway page (test harness) | `client/src/app/[locale]/(private-routes)/(customer)/bookings/checkout/gateway/page.tsx` | **Not a product feature** — a dev stand-in for the PSP's hosted payment page so the initiate → redirect → return round-trip is exercisable without a gateway: the mock `redirectUrl` points here, and its success/failure buttons drive both branches of the return surface (`?outcome=success\|failure`). Clearly labelled «درگاه پرداخت آزمایشی», dashed border | _none — only reachable via the mock's `redirectUrl`_ | On the real path b10's `redirectUrl` is the PSP's **absolute** URL (the checkout does a full `window.location.assign` for `http(s)` URLs), so this page is simply never linked; delete it when `USE_PAYMENT_MOCK` retires. The PSP's return deep-link into `/bookings/checkout/return` is backend/PSP config | 🗑 removed in refinement-phase-4 (payment flipped real) | | `RefundsApi` | `client/src/services/refunds/apis/mockApi.ts` | **The f10 customer cancel + refund surface** b11 doesn't serve (refunds are admin-only; no customer cancel command, no policy preview, no refund-by-booking, no fee-leg decomposition on the customer status → REQ-019/020/021). Reads the shared **f8 bookings store** (`mockGetBookingForRefund`) to resolve the tier by lead time (`free_24h` >24h / `partial_under_24h` <24h / `customer_no_show` started — client-invented codes → i18n keys) and the per-session refundable(un-started)/locked(completed-and-verified) breakdown, decomposing the refund across the two fee legs via **integer parts-per-10000 BigInt math** (`refundAmount + fee = refundableGross` to the rial). `cancelBooking` flips the booking → `cancelled` (`mockMarkBookingCancelled` stamps the b9 snapshot + cancels only un-started sessions) and creates a refund: **card → `succeeded`** immediately (no ETA); **BNPL → `approved`→`processing`→`succeeded`** over status polls with a `expected_customer_refund_eta` ~10 business days out (Fridays skipped) so the ~7–10-day banner renders. Enforces the outside-policy **`409`** (already-cancelled / nothing-refundable / non-refundable session). Seeds a **`failed`** refund on the cancelled booking 5004 so the contact-support state demos; booking 5002 is pinned to the BNPL channel; booking 5003 (new, mid-engagement) demos the mixed refundable/locked breakdown. Also adds bookings-store seeds 5003/5004 + the two non-seam exports | `USE_REFUNDS_MOCK` (`services/refunds/constants.ts`, default `true`) | Deliver **REQ-019** (customer cancel command — the real `refundsClientApi.cancelBooking` already targets `POST bookings/{id}/cancel`) + **REQ-020** (cancellation-policy preview → `GET bookings/{id}/cancellation_policy`, incl. the canonical `cancellation_policy_code` set) + **REQ-021** (`GET refunds/by_booking/{id}` + the decomposition fields on the customer `refunds/{id}/status`), then set flag `false` — the real client maps the published `refunds/{id}/status` 1:1 and targets the proposed slugs for the rest. No hook/component change | 🟡 | | `BnplApi` | `client/src/services/bnpl/apis/mockApi.ts` | **The f11 BNPL installment checkout (D1–D5)** b12 doesn't serve client-side (b12 is order-centric — eligibility/initiate/status/webhook — and **explicitly does not model the repayment schedule**; no provider/plan options, no wallet installment status → REQ-022/023/024). Reads the frozen request gross from the shared **f7 store** and plays the provider: `getBnplOptions` builds the provider set as **data** (دیجی‌پی 3/6/12 · اسنپ‌پی ۴ · اقساط بالین‌یار; per-plan monthly/down-payment/total via **integer parts-per-10000 BigInt math**, never a hardcoded fee in the UI); `checkEligibility` returns `eligible` unless the national-id last digit is `0` (→`not_eligible`) or the order exceeds `MOCK_CREDIT_CEILING_IRR` (→`ceiling_exceeded`) so both declined paths demo; `getBnplSchedule` serves the down-payment + N-installment rows (last absorbs the remainder → rows sum to total); `issueBnplToken` enforces b12 idempotency (same key → same token; repeat after settle / lapsed window → **`409`**) + a `redirectUrl` into the local provider-handoff harness; `acceptBnplSchedule` on success is the **settle stand-in and reuses the f9 conversion bridge** — flips the request `converted` (`mockMarkBookingRequestConverted`), inserts a **confirmed** booking (`mockInsertConvertedBooking`; a settled BNPL order = a card payment net-of-fee, payout invariant to method), and **seeds a provider-reported Wallet plan**; `getWalletInstallments` serves D5 (seeded active دیجی‌پی ۶-ماهه with paid/due-soon/upcoming rows + each settled checkout's plan). Money = served IRR digit-strings end-to-end (components only format) | `USE_BNPL_MOCK` (`services/bnpl/constants.ts`, default `true`) | Deliver **REQ-022** (options + schedule — real `bnplClientApi` targets `checkout_bnpl/options/{id}` + `checkout_bnpl/schedule/{id}`), **REQ-023** (eligibility accepts the D3 national-id/mobile/consent), **REQ-024** (`checkout_bnpl/wallet_installments` provider-reported status + a customer `bookingId` on the settled order), and make the upstream `bookingRequests` flow real, then set flag `false` — `checkEligibility`/`issueBnplToken`(`Idempotency-Key`)/`getBnplOrder` already map the live b12 routes 1:1; the settle-on-return reads the order (the real settle is the provider webhook). No hook/component change | 🟡 | | BNPL provider-handoff harness (test harness) | `client/src/app/[locale]/(private-routes)/(customer)/bookings/checkout/bnpl/gateway/page.tsx` | **Not a product feature** — a dev stand-in for the provider's hosted BNPL page so the initiate → redirect → return round-trip is exercisable without a provider: the mock `redirectUrl` points here, and its pay/cancel buttons drive both branches of the return surface (`?outcome=success\|failure`). Clearly labelled «در حال انتقال به ارائه‌دهنده», dashed border | _none — only reachable via the mock's `redirectUrl`_ | On the real path b12's `redirectUrl` is the provider's **absolute** URL (the wizard does a full `window.location.assign` for `http(s)`), so this page is never linked; delete it when `USE_BNPL_MOCK` retires. The provider's return deep-link into `/bookings/checkout/bnpl/return` is backend/provider config | 🟡 | | `PayoutsApi` | `client/src/services/payouts/apis/mockApi.ts` | **The f12 nurse earnings surface** b13 doesn't serve read-side for a nurse (b13's only nurse route is `GET nurse_payouts/history`; the four-bucket **earnings summary**, the per-booking **earnings list + money-state**, and a **nurse-readable payout detail** with batch context + booking links are gaps → **REQ-025**). Self-contained, money-correct fixtures exercising **every** UI state: all four earnings states (`pending`/`eligible`/`paid`/`clawback_applied`; booking ids 5001–5004 align with the f8 bookings-store seeds so "view booking" deep-links land), all four `PayoutStatus` values in history (`pending`/`submitted`/`paid`/`failed`, incl. a `failed` payout with `failureReason: 'invalid_sheba'` for the read-only failure banner), payout **details that reconcile** (`gross − clawback = net = amount`, Σ booking-link amounts = `grossEarnings`), and a **signed net balance** computed with BigInt via a `MOCK_SCENARIO` toggle (`standard` = positive; **`clawback_heavy` = negative "owed back"** for phase §7 step 3). Timestamps are relative to `now` so the pending dispute-window countdown always ticks; money stays IRR digit-strings end-to-end (components only format). `getNurseEarnings` filters by `state` + paginates | `USE_PAYOUTS_MOCK` (`services/payouts/constants.ts`, default `true`) + `MOCK_SCENARIO` in `constants.ts` | Deliver **REQ-025** (earnings_balance + earnings list + nurse `nurse_payouts/{id}` detail + `failureReason` on the history DTO), then set flag `false` — `payoutsClientApi` already maps the live `GET nurse_payouts/history` 1:1 and targets the proposed slugs for the other three. No hook/component change | 🟡 | -| `ReviewsApi` | `client/src/services/reviews/apis/mockApi.ts` | **The f13 moderated-review trust loop.** b14 serves the review **submit** (`POST bookings/{id}/review`), the public **nurse reviews** page (`GET nurses/{id}/reviews`), and the tag rollup — those are mapped 1:1 in `reviewsClientApi`. But there is **no review-eligibility read** and **no my-review-for-booking read** (**REQ-026**), and the whole moderation transition (`pending_moderation → published`) is **admin-only (f15)**. The mock reads a booking from the shared **f8 bookings store** (`mockGetBookingForReview`) to gate eligibility on a **completed/closed** booking (aligns with the new completed seed 5005 / nurse 1 / patient 905), tracks the customer's submission as `pending_moderation` so eligibility flips `already_reviewed` + `getMyReviewForBooking` returns the persistent "under review" state, and seeds a **published list per nurse** (nurse 1 has 7 → the profile tab paginates; nurses 5/6 empty → empty state). The aggregate is **recomputed from the published list** (never a stored sum). A submitted review **never** enters any public list. Dev-only `__mockPublishSubmittedReview(bookingId)` stands in for the deferred (f15) admin queue so a human can watch a review appear on the profile. Money-free | `USE_REVIEWS_MOCK` (`services/reviews/constants.ts`, default `true`) | Deliver **REQ-026** (`review_eligibility` + `my_review` reads; confirm masked-author omission), then set flag `false` — `reviewsClientApi.getNurseReviews`/`createReview` already map the live b14 routes 1:1 and target the two proposed slugs for the gaps. Moderation UI itself is **f15** (admin). No hook/component change | 🟡 | +| `ReviewsApi` | `client/src/services/reviews/apis/mockApi.ts` | **The f13 moderated-review trust loop.** b14 serves the review **submit** (`POST bookings/{id}/review`), the public **nurse reviews** page (`GET nurses/{id}/reviews`), and the tag rollup — those are mapped 1:1 in `reviewsClientApi`. But there is **no review-eligibility read** and **no my-review-for-booking read** (**REQ-026**), and the whole moderation transition (`pending_moderation → published`) is **admin-only (f15)**. The mock reads a booking from the shared **f8 bookings store** (`mockGetBookingForReview`) to gate eligibility on a **completed/closed** booking (aligns with the new completed seed 5005 / nurse 1 / patient 905), tracks the customer's submission as `pending_moderation` so eligibility flips `already_reviewed` + `getMyReviewForBooking` returns the persistent "under review" state, and seeds a **published list per nurse** (nurse 1 has 7 → the profile tab paginates; nurses 5/6 empty → empty state). The aggregate is **recomputed from the published list** (never a stored sum). A submitted review **never** enters any public list. Dev-only `__mockPublishSubmittedReview(bookingId)` stands in for the deferred (f15) admin queue so a human can watch a review appear on the profile. Money-free | `USE_REVIEWS_MOCK` (`services/reviews/constants.ts`, default `true`) | Deliver **REQ-026** (`review_eligibility` + `my_review` reads; confirm masked-author omission), then set flag `false` — `reviewsClientApi.getNurseReviews`/`createReview` already map the live b14 routes 1:1 and target the two proposed slugs for the gaps. Moderation UI itself is **f15** (admin). No hook/component change | 🟢 (real, refinement-phase-4) | | `PatientRecordsApi` | `client/src/services/patientRecords/apis/mockApi.ts` | **The f13 continuity-of-care surface.** Two very different things: (1) the **nurse-authored visit-note history** (`getPatientHistory`/`createVisitNote`) is **REAL b14** (`GET`/`POST patients/{id}/care_records`), mapped 1:1 in `patientRecordsClientApi` (the append composes the ticked task checklist into the note `body` since the wire has no structured task field); (2) the **family-owned editable record** (medications/routine/tasks — the داروها/روتین/وظایف tabs) and the **access check** have **NO backend at all** (neither the b14 contract nor `data-model/10-reviews-and-records.md` model them → **REQ-027**). The mock is **patient-scoped** and lazily seeds a coherent default per patient: a default family record (customer edits it), a **multi-nurse continuity history** (two prior notes from *different* nurses, proving the history persists across nurse changes; a nurse append prepends to the same patient's history), and a **foreign-patient access-denied** path (`MOCK_FOREIGN_PATIENT_ID = 8888` → `canView:false` + a `403` on every read) so the non-leaking access-denied card is demoable. Clinical text is fixture data (never logged) | `USE_PATIENT_RECORDS_MOCK` (`services/patientRecords/constants.ts`, default `true`) | Deliver **REQ-027** (family-owned `care_record` GET/PUT + `record_access` + structured `taskResults`), then set flag `false` — the history/append methods already map the real b14 routes; only the family-record/access methods flip. Confirm whether the family-owned record is a real MVP entity | 🟡 | -| f8 bookings mock — completed-booking seed 5005 + f13 cross-mock reads | `client/src/services/bookings/apis/mockApi.ts` | **Non-seam additions (mirrors the f10 refunds precedent).** The f8 seeds had **no `completed` booking** (only `confirmed`/`in_progress`/`cancelled`), so f13's review flow needs one: added **booking 5005** (`status: 'completed'`, nurse 1, patient 905, one completed EVV session) so the customer can open a completed booking and leave a review. Also added a **cross-mock read helper** — `mockGetBookingForReview(id)` (single booking, clone) — imported by the reviews mock to gate eligibility and read the patient/nurse snapshot for a submission (the `listBookings` seam row omits `patientId`/`nurseId`). One-way edge INTO bookings (the bookings mock never imports f13), so no cycle | — (part of `USE_BOOKINGS_MOCK`) | When the bookings flow goes real (b9/b10 conversion live), 5005 stops being a static seed and the cross-mock helpers retire with the reviews/records mocks | 🟡 | -| `TicketsApi` | `client/src/services/tickets/apis/mockApi.ts` | **The f14 ticket channel (b15).** b15 serves open/list/thread/message and `ticketsClientApi` maps them 1:1 — but the linked bookings are themselves mock-primary and the wire summary lacks `unreadCount`/`lastMessageAt` (**REQ-028**), so the mock is primary. It seeds 3 tickets (a booking-5001 **coordination** ticket with a **stored internal admin note the user view NEVER returns** — the no-leak demo — plus a support + a closed refund ticket), returns them newest-activity first with a per-ticket unread count that **clears on open**; `openTicket` is **idempotent for `coordination + bookingId`** (so "Get support" from a booking jumps to the existing thread) and prepends a new ticket to the inbox; `postMessage` appends as the current viewer (tracked from the last `getTicket` so an optimistic message reconciles as **mine** in whichever app is open), throws `403` on a **closed** ticket, and throws `500` on the dev sentinel body `'/fail'` (the optimistic failure→retry path). `MOCK_VIEWER_USER_ID` (per-role "me") drives `isMine`; **`isInternal` is never modelled in the user-app types** | `USE_TICKETS_MOCK` (`services/tickets/constants.ts`, default `true`) | Deliver **REQ-028** (`unreadCount`/`lastMessageAt` on the summary + a by-booking user lookup + optional author name + optional `clientMessageId` idempotency) and make the upstream bookings flow real, then set flag `false` — `ticketsClientApi` already maps the live b15 routes 1:1 (drops any leaked internal message defensively). No hook/component change | 🟡 | -| `NotificationsApi` | `client/src/services/notifications/apis/mockApi.ts` | **The f14 notification center + polled bell (b1).** The b1 endpoints are live and `notificationsClientApi` maps them 1:1, but a notification only exists once some other backend domain **dispatches** one (`INotificationDispatcher`) — none run client-side while the upstream flows are mock-primary — so there'd be nothing to show. The mock seeds a realistic **unread-first** feed spanning **every deep-link class** (ticket_message/booking_confirmed/refund_processed/payment_captured/payout_paid/review_published + one unknown-type/no-payload row that degrades to no deep-link), each with a snake_case `dataJson` string the list maps through the **real** `parseNotificationData`; `getUnreadCount`/`markRead`/`markAllRead` mutate the in-memory feed. **Dev-only `__mockPushNotification(type,title,dataJson?,body?)`** prepends a fresh **unread** row so a human can watch the bell badge increment within the poll interval (phase §7 step 4). Ids align with the f8 bookings + tickets mocks so a deep-link lands on a real screen | `USE_NOTIFICATIONS_MOCK` (`services/notifications/constants.ts`, default `true`) | When the upstream domains dispatch real notifications, set flag `false` — `notificationsClientApi` already maps the live b1 `notifications/*` routes 1:1 (`page`/`pageSize`, `{count}`, `{notificationId}`). No hook/component change | 🟡 | +| f8 bookings mock — completed-booking seed 5005 + f13 cross-mock reads | `client/src/services/bookings/apis/mockApi.ts` | **Non-seam additions (mirrors the f10 refunds precedent).** The f8 seeds had **no `completed` booking** (only `confirmed`/`in_progress`/`cancelled`), so f13's review flow needs one: added **booking 5005** (`status: 'completed'`, nurse 1, patient 905, one completed EVV session) so the customer can open a completed booking and leave a review. Also added a **cross-mock read helper** — `mockGetBookingForReview(id)` (single booking, clone) — imported by the reviews mock to gate eligibility and read the patient/nurse snapshot for a submission (the `listBookings` seam row omits `patientId`/`nurseId`). One-way edge INTO bookings (the bookings mock never imports f13), so no cycle | — (part of `USE_BOOKINGS_MOCK`) | When the bookings flow goes real (b9/b10 conversion live), 5005 stops being a static seed and the cross-mock helpers retire with the reviews/records mocks | 🟢 (real, refinement-phase-4) | +| `TicketsApi` | `client/src/services/tickets/apis/mockApi.ts` | **The f14 ticket channel (b15).** b15 serves open/list/thread/message and `ticketsClientApi` maps them 1:1 — but the linked bookings are themselves mock-primary and the wire summary lacks `unreadCount`/`lastMessageAt` (**REQ-028**), so the mock is primary. It seeds 3 tickets (a booking-5001 **coordination** ticket with a **stored internal admin note the user view NEVER returns** — the no-leak demo — plus a support + a closed refund ticket), returns them newest-activity first with a per-ticket unread count that **clears on open**; `openTicket` is **idempotent for `coordination + bookingId`** (so "Get support" from a booking jumps to the existing thread) and prepends a new ticket to the inbox; `postMessage` appends as the current viewer (tracked from the last `getTicket` so an optimistic message reconciles as **mine** in whichever app is open), throws `403` on a **closed** ticket, and throws `500` on the dev sentinel body `'/fail'` (the optimistic failure→retry path). `MOCK_VIEWER_USER_ID` (per-role "me") drives `isMine`; **`isInternal` is never modelled in the user-app types** | `USE_TICKETS_MOCK` (`services/tickets/constants.ts`, default `true`) | Deliver **REQ-028** (`unreadCount`/`lastMessageAt` on the summary + a by-booking user lookup + optional author name + optional `clientMessageId` idempotency) and make the upstream bookings flow real, then set flag `false` — `ticketsClientApi` already maps the live b15 routes 1:1 (drops any leaked internal message defensively). No hook/component change | 🟢 (real, refinement-phase-4) | +| `NotificationsApi` | `client/src/services/notifications/apis/mockApi.ts` | **The f14 notification center + polled bell (b1).** The b1 endpoints are live and `notificationsClientApi` maps them 1:1, but a notification only exists once some other backend domain **dispatches** one (`INotificationDispatcher`) — none run client-side while the upstream flows are mock-primary — so there'd be nothing to show. The mock seeds a realistic **unread-first** feed spanning **every deep-link class** (ticket_message/booking_confirmed/refund_processed/payment_captured/payout_paid/review_published + one unknown-type/no-payload row that degrades to no deep-link), each with a snake_case `dataJson` string the list maps through the **real** `parseNotificationData`; `getUnreadCount`/`markRead`/`markAllRead` mutate the in-memory feed. **Dev-only `__mockPushNotification(type,title,dataJson?,body?)`** prepends a fresh **unread** row so a human can watch the bell badge increment within the poll interval (phase §7 step 4). Ids align with the f8 bookings + tickets mocks so a deep-link lands on a real screen | `USE_NOTIFICATIONS_MOCK` (`services/notifications/constants.ts`, default `true`) | When the upstream domains dispatch real notifications, set flag `false` — `notificationsClientApi` already maps the live b1 `notifications/*` routes 1:1 (`page`/`pageSize`, `{count}`, `{notificationId}`). No hook/component change | 🟢 (real, refinement-phase-4) | | `AdminApi` | `client/src/services/admin/apis/mockApi.ts` | **The f15 backoffice-owned data (b1 + b15).** Fixtures engineered to exercise every console state: **one config per `data_type`** (decimal/int/bool/json/string — so the typed inputs + the 0–1 rate validation are all reachable) with a **change-history** trail; **holidays** with bank-closed days; a **paged audit log** with `changedFields` diffs (one row `` for a PII field); a **support-alert** list spanning **every** `type` (`low_rating`/`evv_no_show`/`evv_location_mismatch`/`verification_expired`/`shared_sim`/`payment_anomaly`/`fraud_signal`/`nurse_clawback`/`emergency`) and all three statuses so the worklist filters are testable; and **RBAC** grants. Mutations mutate the in-memory arrays (a config save writes a history row; assign/resolve advance an alert; grant/revoke flip a role). Timestamps relative to `now` | `USE_ADMIN_MOCK` (`services/admin/constants.ts`, default `true`) | b1 config/holiday/audit/support-alert routes are live and `adminClientApi` maps them 1:1 — deliver **REQ-029** (config `updatedAt`/`updatedBy`) + **REQ-030** (audit actor/action/date filters) + **REQ-031** (the RBAC `admin_roles/*` endpoints, which don't exist yet), then set flag `false`. No hook/component change | 🟡 | | `PartnerCenterApi` | `client/src/services/partnerCenter/apis/mockApi.ts` | **The f15 partner centers (b15) — admin management + the center-scoped portal.** Returns **center #1 = merchant-of-record** (the settlement/invoice view renders) **and** #2 = non-MoR (the "settlement runs through Balinyaar" state) **and** a **draft** #3 (unverified banner); sponsored nurses (verified + unverified), sponsored bookings, and commission invoices whose **platform commission + BNPL commission + VAT = total** (VAT on the commission line only) with a fake 22-digit `moadianReferenceNumber` + a stub PDF url. `settlementIbanMasked` is **last-4 only** (write-then-masked: create/edit submit a full IBAN, only last-4 ever returns). Admin CRUD/verify/set-active/assign-nurse + the portal "my center" reads all mutate/read the in-memory world; "my center" resolves to `MOCK_MY_CENTER_ID` (=1, MoR) | `USE_PARTNER_MOCK` (`services/partnerCenter/constants.ts`, default `true`) + `MOCK_MY_CENTER_ID` | b15 admin partner-center CRUD/verify/sponsor are live; deliver **REQ-032** (portal split reads `centers/me[/nurses|/bookings|/settlement]` + the activate/suspend toggle + confirm the write-then-masked IBAN) + **REQ-033** (center-scoped invoice list + invoice `totalIrr`), then set flag `false` — `partnerCenterClientApi` maps the live admin routes and targets the proposed portal slugs. No hook/component change | 🟡 | -| Admin-endpoint additions to existing domain mocks (`verification`/`refunds`/`payouts`/`reviews`/`tickets`) | the same `apis/mockApi.ts` files (+ their `clientApi.ts`) | **The f15 staff lens over prior domains** — new admin methods added behind the existing seams (no new seam, no hook/component change on swap). **verification:** a nurse-level review queue (`pending`/`in_review`, one with an expiring credential) + a per-nurse case whose manual credential steps carry a document, and `getDocumentSignedUrl` that returns a **fresh short-lived URL each call** (sentinel `documentId 9999` throws → viewer error/re-request path); `decideStep`/`approve`/`reject` re-aggregate. **refunds:** a `getRefundPreview` with the fee/payout split reconciling to the rial per booking (a normal card, a BNPL w/ ETA, a post-payout w/ clawback notice, and a provider-decline **sentinel that fails then retries succeeds**). **payouts:** batches spanning `completed`/`partially_failed`/`processing` (one holiday-shifted), a preview w/ eligible + skipped(no-IBAN) + clawback line + holiday-shifted date, an **idempotency-keyed** run/retry (same key → same result, never double-pays), a `failed` payout to retry, and record-transfer-reference. **reviews:** a moderation queue incl. a low-rating flagged review; `moderateReview` returns a plausible recomputed aggregate. **tickets:** a global admin queue + a thread that **includes** the seeded internal note (the no-leak *inverse* demo) + `postAdminMessage` w/ `isInternal`; a refund-linked ticket (bookingId+refundId) so the RefundPanel opens from it | the owning domain's flag (`USE_VERIFICATION_MOCK` / `USE_REFUNDS_MOCK` / `USE_PAYOUTS_MOCK` / `USE_REVIEWS_MOCK` / `USE_TICKETS_MOCK`, all default `true`) | Deliver the per-domain admin gaps — **REQ-034** (verification nurse-queue + on-demand doc URL + whole-verification approve/reject), **REQ-035** (refund preview + explicit approve/reject), **REQ-036** (payout single-preview + `holidayShifted` + record-transfer-reference), **REQ-037** (moderation `tagCodes`) — then flip the owning domain's flag. The real `clientApi` methods already map the live admin routes 1:1 and target the proposed slugs for the gaps | 🟡 | +| Admin-endpoint additions to existing domain mocks (`verification`/`refunds`/`payouts`/`reviews`/`tickets`) | the same `apis/mockApi.ts` files (+ their `clientApi.ts`) | **The f15 staff lens over prior domains** — new admin methods added behind the existing seams (no new seam, no hook/component change on swap). **verification:** a nurse-level review queue (`pending`/`in_review`, one with an expiring credential) + a per-nurse case whose manual credential steps carry a document, and `getDocumentSignedUrl` that returns a **fresh short-lived URL each call** (sentinel `documentId 9999` throws → viewer error/re-request path); `decideStep`/`approve`/`reject` re-aggregate. **refunds:** a `getRefundPreview` with the fee/payout split reconciling to the rial per booking (a normal card, a BNPL w/ ETA, a post-payout w/ clawback notice, and a provider-decline **sentinel that fails then retries succeeds**). **payouts:** batches spanning `completed`/`partially_failed`/`processing` (one holiday-shifted), a preview w/ eligible + skipped(no-IBAN) + clawback line + holiday-shifted date, an **idempotency-keyed** run/retry (same key → same result, never double-pays), a `failed` payout to retry, and record-transfer-reference. **reviews:** a moderation queue incl. a low-rating flagged review; `moderateReview` returns a plausible recomputed aggregate. **tickets:** a global admin queue + a thread that **includes** the seeded internal note (the no-leak *inverse* demo) + `postAdminMessage` w/ `isInternal`; a refund-linked ticket (bookingId+refundId) so the RefundPanel opens from it | the owning domain's flag (`USE_VERIFICATION_MOCK` / `USE_REFUNDS_MOCK` / `USE_PAYOUTS_MOCK` / `USE_REVIEWS_MOCK` / `USE_TICKETS_MOCK`, all default `true`) | Deliver the per-domain admin gaps — **REQ-034** (verification nurse-queue + on-demand doc URL + whole-verification approve/reject), **REQ-035** (refund preview + explicit approve/reject), **REQ-036** (payout single-preview + `holidayShifted` + record-transfer-reference), **REQ-037** (moderation `tagCodes`) — then flip the owning domain's flag. The real `clientApi` methods already map the live admin routes 1:1 and target the proposed slugs for the gaps | 🟢 (real, refinement-phase-4) |