# Frontend Phase 15 — Admin backoffice & partner-center consoles > **Mission:** ship the **operational cockpit** that runs Balinyaar — the internal, role-gated admin > backoffice in the **desktop sidebar shell** from f0, plus the **partner-center portal** (a separate > authz scope for the licensed sponsoring centers). The backoffice consolidates the worklists ops needs: > the **verification review queue** (pass/reject nurse steps with a signed-URL document viewer + structured > credential entry), **refund admin** (ticket-linked, fee/payout-decomposed, channel-aware, BNPL ETA), > the **payout dashboard** (batch preview → processing → completed/partially-failed, retry), **review > moderation** (publish/hide/reject), the **config editor** (typed inputs by `data_type`, audited save + > change-history), the **holiday calendar manager**, the **audit-log viewer** (filtered, paginated), and > the **support-alert worklist** (assign/resolve). The partner portal shows a center its onboarding/ > verification state, its sponsored nurses, its sponsored bookings, and — when it is merchant-of-record — > its settlement/invoice view. There is **no wireframe** for any of these screens — you design them with > the **frontend-designer** skill. Internal-only data (`support_alerts`, internal ticket notes) must > **never** leak to a non-admin. When this lands, **MVP is complete.** > > **Track:** frontend · **Depends on:** [`frontend-phase-14-b15`](./frontend-phase-14-b15.md) (the > `services/tickets` + `services/notifications` domains this phase layers the admin lens over) + the > **b15** contract ([`messaging-notifications-admin`](../../contracts/domains/messaging-notifications-admin.md)) > **and** the admin endpoints across **b1** (config/holidays/audit/support-alerts), **b6** (verification > queue), **b11** (refunds/invoices), **b13** (payout batches), **b14** (review moderation) · > **Unlocks:** **MVP complete** — this is the final frontend phase. > **Before you start, read [`../_shared/agent-operating-rules.md`](../_shared/agent-operating-rules.md).** It is not optional. --- ## 1. Context — where this sits This is the **last frontend phase** and the only one whose primary audience is **internal staff** (and the licensed partner centers), not families or nurses. Every customer- and nurse-facing surface already exists; what is missing is the back office that makes the marketplace *run*: an admin must be able to **verify a nurse**, **process a refund**, **preview and run a payout batch**, **moderate a review**, **edit a config value**, and **resolve a support alert** — and a partner-center admin must be able to see the nurses and bookings their license covers. All of this rides the **desktop sidebar admin shell** established in [`frontend-phase-0`](./frontend-phase-0.md) (the third actor shell) and the existing `services/{domain}` + TanStack Query patterns. No new app-shell architecture; this phase fills the admin shell with real worklists and adds a **separately-scoped** partner portal. The backoffice is a **read-and-act** surface over data other domains own. It does **not** re-implement verification logic, refund math, payout scheduling, moderation recompute, or config typing — those are **server authority** (b1/b6/b11/b13/b14). The client renders the contract's values and issues the sanctioned admin commands; it **never** computes eligibility, money decomposition, holiday shifts, or the `is_verified` flip on the client (§5). **What already exists (do not rebuild) — link the prior phases:** - **f0 foundations** ([`frontend-phase-0`](./frontend-phase-0.md)): the three actor app shells + route groups, the **admin/backoffice desktop sidebar shell** (the shell this phase lives in), role-aware nav from `AuthContext`, the `services/{domain}` + TanStack Query caching pattern (`keys.ts` factory, `apis/clientApi.ts`, one-hook-per-file, hooks-only `index.ts`), the contracts→`types.ts` step, the **money/format util** in `src/utils/` (`formatIrrToToman`, integer-safe IRR-string parse, Shamsi date display), the shared composites (**status chip**, **stepper/progress header**, **price-breakdown**, cards), and the i18n namespace conventions (the `admin` namespace was reserved in f0 — fill it). **Reuse the money util, the status chip, and the price-breakdown — do not re-implement them.** - **f1-b2 auth** ([`frontend-phase-1-b2`](./frontend-phase-1-b2.md)): phone-OTP login, the role router, **roles in `AuthContext`**. Admins arrive authenticated with an admin role (`super_admin` / `admin` / `support` / `finance` / `moderator`); partner-center admins arrive with the **partner-center scope**. This phase **role-gates** every admin route and command off these roles — there is no separate admin login to build. - **f5-b6 nurse verification flow** ([`frontend-phase-5-b6`](./frontend-phase-5-b6.md)): the nurse-facing verification checklist, the per-step status enums, the `services/verification` types, the document uploader, and the trust badge. **The admin verification queue is the staff lens over the same `services/verification` domain** — extend it with the admin-review endpoints; reuse the step/status enums and the badge, do not fork them. - **f10-b11 refund & cancellation** ([`frontend-phase-10-b11`](./frontend-phase-10-b11.md)): the customer-side cancellation/refund-status UI, the policy-fee disclosure, the BNPL ETA display, and the refund money-display conventions. **The admin refund tool is the staff lens** — reuse the same fee/payout decomposition rendering and the money util; the admin **initiates/approves** refunds the customer can only watch. - **f12-b13 nurse earnings & payouts** ([`frontend-phase-12-b13`](./frontend-phase-12-b13.md)): the read-only nurse earnings/payout-history view, the four earnings states, the payout-status enum (`pending`/`processing`/`paid`/`failed`), the batch-detail shape with booking links. **The admin payout dashboard is the action surface** explicitly deferred from f12 — build the **batch preview → run → retry** flow here; reuse the payout shapes and money rendering. - **f13-b14 reviews & patient records** ([`frontend-phase-13-b14`](./frontend-phase-13-b14.md)): the `services/reviews` domain, the review shapes, the star/tag rendering. **The admin moderation queue is the staff lens** — add the `publish`/`hide`/`reject` moderation actions over the same domain. - **f14-b15 messaging & notifications** ([`frontend-phase-14-b15`](./frontend-phase-14-b15.md)): the `services/tickets` and `services/notifications` domains, the ticket thread, the `reference_code` rendering. **This phase layers the admin ticket lens** (the global queue, the **internal-note composer**, the refund-from-ticket entry) on top of `services/tickets`, and reuses the notification bell in the admin shell. The user-app types deliberately omit `isInternal`; the **admin** types include it (admins *see* internal notes) — keep the user/admin type surfaces distinct (§5). - **f9-b10 / f11-b12** money UI conventions (checkout, BNPL) — the Toman-display + IRR-string handling the admin money screens must match. **Reuse the single money util; do not fork a formatter.** - `clientFetch`/`serverFetch` + `ApiError`, the toast bridge (already toasts 401/403/5xx — do **not** re-toast those in hooks), the cookie manager, `APP_THEME_LTR/RTL`, `tokens.css`. > **Backend readiness note.** The primary contract you consume, > [`messaging-notifications-admin.md`](../../contracts/domains/messaging-notifications-admin.md), is > produced by **backend-phase-b15** (admin ticket queue, support-alert worklist, partner centers, > RBAC role grants) and is the consolidation point for the admin endpoints. The other admin endpoints > live in their own domain contracts — verification (**b6**, [`verification.md`](../../contracts/domains/verification.md)), > refunds/invoices (**b11**, [`refunds.md`](../../contracts/domains/refunds.md)), payouts (**b13**, > [`payouts.md`](../../contracts/domains/payouts.md)), reviews (**b14**, [`reviews.md`](../../contracts/domains/reviews.md)), > config/holidays/audit (**b1**, [`config-reference.md`](../../contracts/domains/config-reference.md)). > If a shape you need is absent or wrong when you start, **do not guess and do not block** — append a > `REQ-NNN` request to > [`../../shared-working-context/frontend/requests/for-backend.md`](../../shared-working-context/frontend/requests/for-backend.md) > and mock behind the `services/admin` / `services/partnerCenter` seam meanwhile (operating-rules §6). > Record every mock in your report so it swaps cleanly once the endpoint lands. ## 2. Required reading (do this first) **Operating rules & checklists** - [`../_shared/agent-operating-rules.md`](../_shared/agent-operating-rules.md) and [`../_shared/frontend-conventions-checklist.md`](../_shared/frontend-conventions-checklist.md) — how you work, the gate, the contract/handoff lanes, the mock-then-swap rule (§6). - [`../_shared/definition-of-done.md`](../_shared/definition-of-done.md) — the bar this phase adds to (§6). **Product / business truth (read before designing any screen)** - [`../../../product/business/14-notifications-and-admin.md`](../../../product/business/14-notifications-and-admin.md) — **the admin operational spine**: the five worklists (verification / refund / payout / support-alert / RBAC), the append-only audit trail, config-change auditing, in-app notifications, and that back-office must reason over the Shamsi calendar + `iranian_holidays`. This is *why* each console exists. - [`../../../product/business/13-tax-invoicing-and-legal.md`](../../../product/business/13-tax-invoicing-and-legal.md) — the **partner-center / merchant-of-record** model: the licensed center (پروانه تأسیس + مسئول فنی + نماد اعتماد الکترونیکی) sponsors nurses and **may be the merchant-of-record / invoice issuer**; the commission invoice (gross / platform commission / BNPL commission / VAT on commission) and the config-driven VAT rate. This drives the partner portal's settlement/invoice view and what it may show. - [`../../../product/data-model/12-audit-config-and-reference.md`](../../../product/data-model/12-audit-config-and-reference.md) — `audit_logs` (immutable, append-only, `changed_fields_json`), `platform_configs` (typed by `data_type`; the seeded keys — `platform_fee_rate`, `vat_rate`, `dispute_window_hours`, `nurse_payout_interval_days`, `evv_location_tolerance_meters`, `min_rating_for_support_alert`, cancellation-tier defaults, BNPL keys), and `iranian_holidays` (`holiday_date`, `name_fa`, `type`, `is_bank_closed`). These shape the config editor, the holiday manager, and the audit viewer. - [`../../../product/data-model/13-partner-centers-and-future.md`](../../../product/data-model/13-partner-centers-and-future.md) — `partner_centers` fields (`name`, `legal_entity_type`, `moh_establishment_permit_no`, `technical_director_nurse_user_id` + `technical_director_license_no`, `enamad_code`, `settlement_iban` (enc), `is_merchant_of_record`, `commission_rate`, `admin_user_id`, `is_active`, `verified_at`); the 1:N relations to `nurse_profiles` / `bookings` / `invoices`; and the **deferred** tables (`organizations`, `organization_nurses`, `fraud_flags`, `recurring_booking_schedules`) that have **no UI** this phase. > **No wireframe exists for the admin or partner screens** (the wireframe is the mobile customer/nurse > flow A1–E3 — see [`product/wireframes/index.html`](../../../product/wireframes/index.html) for the > brand/RTL baseline only). The GTM notes explicitly flag the backoffice/ticket/partner surfaces as a > design gap. **You design these screens from scratch with the `frontend-designer` skill** against the > brand system — desktop, sidebar-driven, dense worklist layout, RTL-first. **Contracts & types (the source of truth for shapes — do not guess)** - [`../../contracts/domains/messaging-notifications-admin.md`](../../contracts/domains/messaging-notifications-admin.md) — **the primary contract** (b15): admin ticket queue + internal-note composer, the **support-alert worklist** (list/filter, assign, resolve), **partner-center** CRUD/verify/sponsor + roster, and **RBAC** role grant/revoke. The user-vs-admin `is_internal` filtering note is here. - The per-domain admin contracts: [`verification.md`](../../contracts/domains/verification.md) (b6 — queue, record-step, upsert-credential, approve/reject, signed-URL document fetch), [`refunds.md`](../../contracts/domains/refunds.md) (b11 — initiate/approve/reject, decomposition, channel, BNPL ETA, invoices), [`payouts.md`](../../contracts/domains/payouts.md) (b13 — batch preview/list/detail, initiate, retry, transfer-reference reconcile), [`reviews.md`](../../contracts/domains/reviews.md) (b14 — moderation queue, set-status), [`config-reference.md`](../../contracts/domains/config-reference.md) (b1 — list/update config, config change history, holidays CRUD, audit-log list, support-alert raise/list). - [`../../contracts/conventions/api-conventions.md`](../../contracts/conventions/api-conventions.md) and [`money-and-types.md`](../../contracts/conventions/money-and-types.md) — the envelope (`OperationResult` → already unwrapped by `clientFetch`), `snake_case` routes/properties, pagination (`page`/`page_size`, default/max), **enums as stable string codes** (mirror as string-literal unions; labels are i18n keys), **IRR as integer string on the wire** (parse integer-safe, Toman display-only), UTC timestamps → **Shamsi** on the client. **Code to mirror (existing patterns — copy, don't invent)** - `client/src/services/auth/*` (`types.ts` / `keys.ts` / `apis/clientApi.ts` / `hooks/use*.ts` / `index.ts`) — the exact shape every new domain service copies. - The prior domains this phase extends: `services/verification` (f5), `services/refunds` (f10), `services/payouts` (f12), `services/reviews` (f13), `services/tickets` + `services/notifications` (f14). **Extend these with the admin endpoints; do not re-create them.** Only the genuinely new admin-owned data (config, holidays, audit, support-alerts, RBAC) and partner centers get **new** domains (`services/admin`, `services/partnerCenter` — §3). - The f5 document uploader / signed-URL handling (the verification document viewer reuses the signed-URL pattern), the f0 money util + status chip + price-breakdown, the f10 refund decomposition rendering, the f12 payout shapes, the f14 ticket thread. - [`client/CLAUDE.md`](../../../client/CLAUDE.md) — RSC/client boundary, the admin route group, layouts, i18n, theme, fetch services, anti-patterns. **Design** - **Invoke the `frontend-designer` skill** before building any screen. Every admin worklist, the document viewer, the refund/payout action panels, the config editor, the holiday manager, the audit table, the support-alert board, and the entire partner portal go through it — brand palette, tokens, typography, the `App*` library, the desktop sidebar density, table/virtualization treatment, empty/loading/error states, RTL mirroring. **Do not hand-roll colours, spacing, or table styling.** Because there is no wireframe, the designer skill owns the visual language for the whole back office. ## 3. Scope — build this Two **new** domain services (`services/admin`, `services/partnerCenter`) plus **admin endpoint additions** to the existing `verification` / `refunds` / `payouts` / `reviews` / `tickets` domains, and the **admin-shell screens** + the **partner-center portal** on top of them. Everything is **internal-only**, behind **role-gated routes** in the f0 admin shell (the partner portal is its own scope). Build desktop- first, RTL, both locales, query-cached, virtualized lists, minimal re-renders. > **Routing & RBAC.** All admin screens mount under the f0 **admin route group** (e.g. > `(private-routes)/admin/…`) gated by an admin role from `AuthContext`; the partner portal mounts under a > **distinct** partner-scope segment (e.g. `(private-routes)/partner/…`) gated by the partner-center > scope. A `support` admin must not see the payout-run control, a `moderator` must not see the refund > tool, etc. — the **server enforces** role scopes on every command (don't rely on UI hiding for > security), but the UI **also** hides/disables actions the current role can't perform so a user never > sees a control that will 403. Drive this from a small `useAdminCapabilities()` selector off `AuthContext` > roles. Update the **Project Structure** tree in `client/CLAUDE.md` for the new route segments + services. ### 3.1 `services/admin` (new domain — config, holidays, audit, support-alerts, RBAC) Copy the `auth` service shape into `client/src/services/admin/`. Types come from the b1 + b15 contracts — do not invent. (Sub-namespace by area if it keeps files small: `config`, `holidays`, `audit`, `supportAlerts`, `rbac`.) - **`types.ts`** — string-literal unions + DTOs from the contracts: - **Config:** `PlatformConfig` (`key`, `value` (string on the wire), `data_type` enum `string|int|decimal|bool|json`, `description`, `updated_at`, `updated_by`), and a `ConfigChange` (audit) row (`changed_at`, `actor`, `old_value`, `new_value`). - **Holidays:** `Holiday` (`id`, `holiday_date`, `name_fa`, `type` enum `official|religious|national`, `is_bank_closed`). - **Audit:** `AuditLogEntry` (`id`, `entity_type`, `entity_id`, `action`, `changed_fields_json` (parsed to a typed record), `actor`, `created_at`) + paged list envelope; filter params (`entity_type`, `entity_id`, `actor_id`, `from`/`to`, `action`). - **Support alerts:** `SupportAlert` (`id`, `type` enum e.g. `low_rating|no_show|evv_mismatch|verification_expiry|fraud_signal`, `status` enum `open|assigned|resolved`, `entity_type`, `entity_id`, nullable `booking_id`/`review_id`, `assigned_to`, `resolved_at`, `resolution_note`, `created_at`). - **RBAC:** `AdminRole` enum (`super_admin|admin|support|finance|moderator`), `RoleGrant` (`user_id`, `role`, `granted_by`, `granted_at`, `revoked_at`). - **`keys.ts`** — `adminKeys.config()`, `adminKeys.configHistory(key)`, `adminKeys.holidays(yearOrRange)`, `adminKeys.audit(filters, page)`, `adminKeys.supportAlerts(filters, page)`, `adminKeys.roles(userId?)`. Filters + page are part of the key so each filter caches separately. - **`apis/clientApi.ts`** wrapping `clientFetch` (exact `snake_case` routes from the contracts): config `list_platform_configs` / `update_platform_config` / `get_config_change_history`; holidays `list_holidays` / `create_holiday` / `update_holiday`; audit `list_audit_logs`; support-alerts `list_support_alerts` / `assign_support_alert` / `resolve_support_alert`; RBAC `list_roles` / `grant_role` / `revoke_role`. - **`hooks/` (one per file):** `usePlatformConfigs`, `useUpdatePlatformConfig`, `useConfigChangeHistory`, `useHolidays`, `useUpsertHoliday`, `useAuditLogs`, `useSupportAlerts`, `useAssignSupportAlert`, `useResolveSupportAlert`, `useAdminRoles`, `useGrantRole`, `useRevokeRole`. Mutations **invalidate** the relevant `adminKeys` (and the config-history key after a config save) on settle so cached data isn't refetched needlessly. - **`index.ts`** barrel (hooks only). ### 3.2 `services/partnerCenter` (new domain — partner portal + admin-side center management) Copy the same skeleton into `client/src/services/partnerCenter/`. Types from the b15 contract. - **`types.ts`** — `PartnerCenter` (`id`, `name`, `legal_entity_type`, `moh_establishment_permit_no` (پروانه تأسیس), `technical_director_nurse_user_id`, `technical_director_license_no`, `enamad_code`, **`settlement_iban` masked last-4 only** (never the full IBAN), `is_merchant_of_record`, `commission_rate`, `admin_user_id`, `is_active`, `verified_at`); `SponsoredNurse` (nurse summary + verification badge); `SponsoredBooking` (booking summary the center covers); `CenterSettlementRow` / `CenterInvoice` (only meaningful when `is_merchant_of_record` — gross / platform commission / BNPL commission / VAT / total, `moadian_reference_number`, `pdf` signed-URL link). A **center verification/onboarding state** enum (`draft|pending_verification|verified|suspended`). - **`keys.ts`** — `centerKeys.list(filters)`, `centerKeys.detail(id)`, `centerKeys.sponsoredNurses(id)`, `centerKeys.sponsoredBookings(id, filters)`, `centerKeys.settlement(id, filters)`, `centerKeys.myCenter()` (the partner-scope "my center" view). - **`apis/clientApi.ts`** — admin-side: `list_partner_centers`, `get_partner_center`, `create_partner_center`, `update_partner_center`, `verify_partner_center`, `set_partner_center_active`, `assign_nurse_to_partner_center`; partner-scope: `get_my_partner_center`, `list_my_sponsored_nurses`, `list_my_sponsored_bookings`, `list_my_settlement`. - **`hooks/`:** admin — `usePartnerCenters`, `usePartnerCenter`, `useCreatePartnerCenter`, `useUpdatePartnerCenter`, `useVerifyPartnerCenter`, `useSetPartnerCenterActive`, `useAssignNurseToPartnerCenter`; partner-scope — `useMyPartnerCenter`, `useMySponsoredNurses`, `useMySponsoredBookings`, `useMySettlement`. Mutations invalidate `centerKeys`. - **`index.ts`** barrel (hooks only). ### 3.3 Admin: verification review queue The staff lens over `services/verification` (f5) — add the admin endpoints to that domain (queue list, record-step, upsert-credential, approve/reject, signed-URL document fetch); reuse the f5 step/status enums and the trust badge. - **Queue list** (`/admin/verification`) — a paginated, **status-filtered** worklist of nurses with pending verification (filter by aggregate status `pending|in_review`; sort by oldest-first). Each row: nurse name/photo, the **step progress** (e.g. "۳ از ۵"), the next pending step, submitted-at (Shamsi), and any expiring credential warning. Virtualize/paginate; empty state "صف خالی است / Queue clear". - **Per-nurse review screen** (`/admin/verification/[nurseId]`) — the ordered steps with status chips, a **document viewer** that fetches each `verification_documents` item via a **signed URL** (never a public URL — handle **loading / expired-link → re-request / load-error** states; the URL is short-lived, so fetch on demand, don't cache the URL in a long-lived query). For each step an admin can **pass** or **reject (with a required reason)** → `record_step`; for credential steps a **structured credential entry** form (`credential_number`, issuing authority, issue/expiry dates) → `upsert_credential`. The screen's **Approve / Reject** action (`approve_verification` / `reject_verification`) is enabled **only when all required steps are `passed`** (the **server** flips `is_verified` transactionally — the client only enables the button and shows a confirmation; it **never** writes `is_verified` itself, §5). Approve/reject require a confirmation dialog and, on reject, a reason. On success, **invalidate** the queue + the nurse detail so the row leaves the queue. ### 3.4 Admin: refund tooling (inside the ticket lens) The staff lens over `services/refunds` (f10) + `services/tickets` (f14). Refunds are **admin-only and ticket-linked** — the entry point is **from a ticket**, not a standalone form. - **Refund panel in the admin ticket view** — opened from a ticket (the admin global ticket queue, §3.9): shows the linked booking, computes a **preview** of the tiered `refund_percentage_applied` from the cancellation policy and the **fee/payout decomposition** (`platform_fee_refunded_irr` + `nurse_payout_refunded_irr`) via the f0 **price-breakdown** primitive (the **server** computes these — the client renders the preview the contract returns, never recomputes the percentage), a **channel selector** (`psp_card` / `bnpl_revert` / `manual_bank`), and — for BNPL — an **ETA banner** (`expected_customer_refund_eta`). Actions: **Initiate refund** (`initiate_refund`, carries the `ticket_id`), **Approve** (`approve_refund`), **Reject** (`reject_refund`). If the nurse was already paid, surface that a **clawback** will be created (read-only notice — the server creates it). States: preview / confirm / **provider-revert-failure → retry** (BNPL/PSP), success. Invalidate the refund + ticket + (if shown) the customer refund-status query on settle. ### 3.5 Admin: payout dashboard The action surface over `services/payouts` (b13), explicitly deferred from f12. Reuse the f12 payout shapes, the payout-status enum, and the money util. - **Batch dashboard** (`/admin/payouts`) — a list of `nurse_payout_batches` with status (`pending|processing|paid|partially_failed|failed`), period (holiday-shifted `period_start`/ `period_end`, Shamsi), `payout_count`, `total_amount`, and a **holiday-shift indicator** when the processing date moved off a bank-closed day. Empty/loading/error states. - **Batch preview → run** — a **"preview next batch"** action (`preview_payout_batch`) that shows the **eligibility breakdown**: which completed/unpaid bookings qualify (EVV confirmed **and** `dispute_window_ends_at` passed), the per-nurse roll-up, the **clawback-netting line** (`clawback_applied_irr`), and the holiday-shifted processing date — all **server-computed**; the client only renders the preview (never computes eligibility or the holiday shift, §5). A **"run batch"** action (`initiate_payout_batch`) behind a **confirmation dialog** that requires an **idempotency key** (the contract's mechanism) so a double-click or retry **cannot pay a booking twice** (one-payout-per-booking, §5). After running, the batch moves to **processing** → poll/refetch to **completed** or **partially-failed**. - **Batch detail + per-nurse drill-down** (`/admin/payouts/[batchId]`) — the per-nurse `nurse_payouts` rows with status, the net decomposition (`gross_earnings_irr − clawback_applied_irr = net_amount_irr`), the masked IBAN (last-4), and the `transfer_reference`. A **failed** payout shows its `failure_reason` and a **retry** action (`retry_payout`) — also idempotency-keyed. The **`RecordPayoutTransferReference`** reconciliation action (`record_transfer_reference`) lets finance attach the real bank transfer reference to a payout. Invalidate the batch/detail on each action. ### 3.6 Admin: review moderation queue The staff lens over `services/reviews` (f13). - **Moderation queue** (`/admin/reviews`) — a paginated list of reviews in `pending_moderation` (filterable by status), each showing the rating, body, tags, the booking/nurse context, and a **low-rating flag** (when `rating < min_rating_for_support_alert`). Actions per review: **publish / hide / reject** (`moderate_review` with the target status; reject carries a reason). Each transition triggers a **server-side aggregate recompute** of the nurse's rating — the client just invalidates the review + the nurse's reviews query; it **never** computes the aggregate (§5). States: empty ("صف بررسی خالی است / Nothing to moderate"), loading, error, optimistic-vs-confirmed on the action. Never render `pending_moderation` content as if public. ### 3.7 Admin: config editor + change history The config surface over `services/admin` (b1). Config edits are **money-correctness sensitive** and **audited** (§5). - **Config list** (`/admin/config`) — all `platform_configs` rows grouped sensibly (fees/VAT, deadlines, EVV, BNPL, cancellation tiers), each rendered with a **typed input by `data_type`**: `bool` → switch, `int`/`decimal` → numeric field with **range validation** (e.g. a rate field validates **0–1**), `json` → a validated JSON editor, `string` → text. Show the `description` and `updated_at`/`updated_by`. - **Audited save** — `update_platform_config` behind a **confirmation dialog** that states "this change is audited and takes effect immediately; it does **not** retroactively change already-computed bookings/ledger" (copy, both locales). On success show the **optimistic-vs-confirmed** save state and invalidate the config + the change-history key. - **Change-history drawer** — per config key, a drawer (`get_config_change_history`) listing each change (old → new value, actor, Shamsi timestamp) so finance can prove the rate in effect at any past moment. ### 3.8 Admin: holiday calendar manager Over `services/admin` holidays (b1). - **Holiday manager** (`/admin/holidays`) — a calendar/list of `iranian_holidays` (by year/range), each row `holiday_date` (Shamsi), `name_fa`, `type` chip, and an **`is_bank_closed` toggle** (this is what shifts payout scheduling — surface that consequence in the UI copy). Add/edit a holiday (`create_holiday`/`update_holiday`). The client **does not** compute next-business-day shifts — it only maintains the calendar the **server** uses for scheduling (§5). States: empty, loading, error, save-confirmation. ### 3.9 Admin: support-alert worklist + audit viewer + global ticket queue - **Support-alert worklist** (`/admin/alerts`) — the **internal-only** triage board over `services/admin` support-alerts: filter by `type` / `status` (`open|assigned|resolved`) / `assigned_to`; each card shows the alert type (low-rating / no-show / EVV-mismatch / verification-expiry / fraud-signal), the linked entity (deep-link to the booking/review/nurse), and severity styling (admin-only). Actions: **assign** (`assign_support_alert` — to self or another admin) and **resolve** (`resolve_support_alert` with a resolution note). States: empty ("هیچ هشدار بازی نیست / No open alerts"), loading, error. **`support_alerts` content NEVER appears in any non-admin surface** (§5). - **Audit-log viewer** (`/admin/audit`) — a **read-only** table over `list_audit_logs` with **filters** (entity type/id, actor, action, date range) and **pagination/virtualization** for large result sets; each row shows the entity, action, actor, Shamsi timestamp, and an expandable `changed_fields_json` diff. Append-only — there are **no edit/delete affordances** (§5). Empty/loading/error states. - **Global ticket queue + internal-note composer** (`/admin/tickets`, `/admin/tickets/[id]`) — the **admin lens** over `services/tickets` (f14): a queue across **all** tickets (filter by status / linked booking / `reference_code`), and the admin thread view that — unlike the user thread — **renders internal (`is_internal`) messages distinctly** and provides an **internal-note composer** (post a message with `is_internal=true`). The refund panel (§3.4) opens from here. The admin types include `isInternal`; the **user-app types from f14 do not** — keep them separate so an internal note can never bleed into the user view (§5). - **(Optional) RBAC admin** (`/admin/roles`) — a user↔role grid over `services/admin` RBAC with grant/revoke (confirmation + records `granted_by`/`granted_at`). If the b15 contract doesn't expose the role endpoints when you run, **defer this screen** (file a `REQ` and build it when the endpoints land); it is not part of the testable acceptance path. Tag it **(DEFERRED-IF-MISSING)** in your report. ### 3.10 Partner-center portal (separate authz scope) The partner portal mounts under the **distinct partner segment** gated by the partner-center scope (a center admin is **not** a Balinyaar admin and must see **only their own center's** data — server-enforced tenancy; never fetch a center by raw id the user doesn't own, §5). - **Center home / onboarding state** (`/partner`) — the center's onboarding/verification state (`draft|pending_verification|verified|suspended`) with an **unverified banner** when not yet active, its license fields read-mostly (پروانه تأسیس / مسئول فنی / نماد اعتماد الکترونیکی), and `is_merchant_of_record` clearly indicated. - **Sponsored-nurse list** (`/partner/nurses`) — the nurses this center sponsors (`list_my_sponsored_nurses`), each with their verification badge; empty ("هنوز پرستاری اسپانسر نشده / No nurses sponsored yet"). - **Sponsored-bookings list** (`/partner/bookings`) — the bookings the center legally covers (`list_my_sponsored_bookings`), filterable by status/date; read-only summaries (no PII beyond what the contract exposes to a center). - **Settlement / invoice view** (`/partner/settlement`) — **rendered only when `is_merchant_of_record === true`** (otherwise show a "not merchant-of-record / settlement runs through Balinyaar" state, no settlement table): the per-booking **commission invoices** (gross / platform commission / BNPL commission / **VAT on the commission line** / total, via the price-breakdown), the `moadian_reference_number` when issued, and the **invoice PDF** via a signed-URL download. Money via the f0 util (Toman display, IRR-string integer-safe). States: empty, loading, error on PDF fetch → retry. - **Admin-side partner management** (`/admin/partners`, `/admin/partners/[id]`) — the Balinyaar-admin surface for centers: list/create/edit a center (`create_partner_center`/`update_partner_center` — the `settlement_iban` field is **write-then-masked**: submit a full IBAN, but the list/detail only ever shows last-4), **verify** (`verify_partner_center`) and **activate/suspend** (`set_partner_center_active`) toggles, and the **sponsored-nurse roster** with **assign-nurse** (`assign_nurse_to_partner_center`). ### 3.11 i18n + types housekeeping - Fill the **`admin`** namespace (reserved in f0) and add a **`partner`** namespace to **both** `messages/en.json` and `messages/fa.json`, in sync, RTL-first (`fa` default). Every user-visible string is a key, including the **Persian legal terms** (پروانه تأسیس, مسئول فنی, نماد اعتماد الکترونیکی, سامانه مودیان) and the admin worklist labels. - Types come from the published contracts; any gap → append a `REQ-NNN` to [`for-backend.md`](../../shared-working-context/frontend/requests/for-backend.md) and mock behind the `services/admin` / `services/partnerCenter` seam (or the extended-domain seam) meanwhile. **(DEFERRED)** — `organizations` / `organization_nurses` employer model, `fraud_flags` ML console, `recurring_booking_schedules` recurrence UI ([`data-model/13`](../../../product/data-model/13-partner-centers-and-future.md) — modeled-but-inactive, **no UI**); full سامانه مودیان e-invoice automation / digital-signature pipeline (the portal only **views** the issued invoice/ref + PDF — it does not submit); push/SMS notification channels; an analytics-warehouse dashboard over `system_events`; on-demand/instant payout; per-nurse payout-frequency settings. Build none of these — flag them in the report if a contract field hints at them. ## 4. Mocks & seams in this phase This is a **frontend** phase — its only "seams" are the domain services behind which a mock `clientApi` lives until each backend endpoint is merged (operating-rules §6, frontend-checklist last bullet). **Do not** introduce backend seams — `IObjectStorage` (signed URLs), `IBankTransferProvider` (payouts), `IBnplProvider`/`IPaymentGateway` (refund reverts), `IMoadianClient` (invoices), `ILicenseVerificationService` (partner-center verify), the audit interceptor, the notification dispatcher — those are **server-side** (b1/b6/b11/b13/b14/b15) and the frontend never touches them. **Reuse** the f5 signed-URL document pattern, the f10 refund decomposition, the f12 payout shapes, and the f14 ticket thread — do not re-implement them. - **`services/admin` seam** — if b1/b15 admin endpoints aren't merged, ship a mock `clientApi` (same method signatures) returning realistic **typed configs** (one per `data_type` so the typed inputs + the 0–1 range validation are exercisable), a **change-history** trail, **holidays** (some bank-closed), a **paged audit log** with `changed_fields_json` diffs, and a **support-alert** list spanning all statuses/types — including at least one of **each** alert type so the worklist filters are testable. - **`services/partnerCenter` seam** — a mock returning a **merchant-of-record** center (so the settlement view renders) **and** a non-MoR center (so the "settlement runs through Balinyaar" state renders), sponsored nurses (verified + unverified), sponsored bookings, and a couple of commission invoices with a fake 22-digit `moadian_reference_number` and a stub PDF URL. The `settlement_iban` mock returns **last-4 only**. - **Extended-domain seams** (`verification`/`refunds`/`payouts`/`reviews`/`tickets`) — for the admin endpoints added to existing domains, mock the **new** admin methods behind the same domain `clientApi` (e.g. a verification queue with documents needing a signed URL, a refund preview with a fee/payout split and a BNPL ETA, a batch **preview** with an eligibility breakdown + a **partially-failed** batch + a **failed** payout to retry, a moderation queue with a low-rating review, an admin ticket thread **with an internal note**). Record **every** mock in your **frontend report** and the [`mocks-registry.md`](../../shared-working-context/reports/mocks-registry.md) so the real-endpoint swap is a one-file change per domain (the hooks/screens stay unchanged — only `apis/clientApi.ts` flips). Append the corresponding shape requests to [`for-backend.md`](../../shared-working-context/frontend/requests/for-backend.md). ## 5. Critical rules you must not get wrong - **Internal-only data never reaches a non-admin.** `support_alerts` and internal ticket notes (`is_internal=true`) are **staff-only** — they appear **only** in admin routes, are fetched **only** by admin-scoped queries, and must never be joined into or rendered in any customer/nurse/partner surface. The **admin** ticket types include `isInternal` (admins see internal notes, styled distinctly); the **user-app types from f14 deliberately omit it** — keep the two type surfaces separate so an internal note cannot bleed into a user view. Treat any internal content in a non-admin payload as a backend defect — file it via `for-backend.md`, don't render it. - **Internal-only & role-gated routes.** Every admin screen is behind the role-gated admin shell; the partner portal is a **separate authz scope**. The **server enforces** role scopes on every command (a `support` admin can't run a payout, a `moderator` can't refund, a center admin sees only their own center) — **never rely on UI hiding for security** — but the UI must **also** hide/disable controls the current role can't use so a user never sees a button that 403s. - **The server is the only authority; the client never computes the load-bearing values.** Never compute on the client: the `is_verified` flip (the server flips it transactionally when all required steps pass — the UI only enables Approve and confirms), the refund **percentage / fee-vs-payout decomposition** (render the server's preview), payout **eligibility** or the **holiday-shifted** processing date (render the server's breakdown), the review **aggregate recompute** (just invalidate), and config parsing beyond rendering by `data_type`. The client renders contract values and issues commands. - **Money correctness (verbatim — the sacred invariants across b9–b13):** money is **IRR `BIGINT`, no floats** — parse the wire integer string with the f0 integer-safe util, never `Number()`/float math; **Toman is display-only**. The three booking amounts always satisfy **gross = commission + payout** (`gross_price_irr = balinyaar_commission_irr + nurse_payout_amount`); render every breakdown so it sums. Escrow is an **append-only, balanced double-entry ledger** — refund/payout/clawback figures are ledger-derived; a clawback **nets**, it does not auto-reverse, and a nurse's payable balance **may go negative** (don't clamp). Payout gating is **dispute-window gating**: an amount is eligible only after EVV completion **AND** `dispute_window_ends_at < now()` — never show "eligible"/run a payout for an amount still in its dispute window, and **never compute eligibility on the client**. **One payout per booking** (`nurse_payout_booking_links.booking_id` is UNIQUE) — the "run batch"/"retry payout" commands are **idempotency-keyed** so a double-click or retry can never pay a booking twice; render the status the contract returns (`pending`/`processing`/`paid`/`failed`/`partially_failed`), never an optimistic "done". **Webhook idempotency** is a server concern, but its client consequence is real: settlement/transfer is **never instant** — poll/refetch the status, don't assume completion. - **Commission invoice / VAT (partner portal):** the platform issues **only its commission invoice** (never the nurse's service invoice); **VAT applies to the commission line, not the gross service fee**; the VAT rate is **config-driven** (read from `platform_configs.vat_rate`, snapshotted on the invoice) — never hardcode 10%. **Merchant-of-record drives the settlement/invoice view**: render the settlement table **only** when `is_merchant_of_record === true`; the issuer/settlement target follows `partner_centers`, not a hardcoded platform. - **Refunds are admin-only and ticket-linked.** No customer self-service initiation — the refund panel opens **from a ticket** and every initiate carries the `ticket_id`. The decomposition (fee leg vs payout leg) and channel (`psp_card`/`bnpl_revert`/`manual_bank`) come from the server; a post-payout refund creates a **clawback** (server-created — show it read-only). - **Append-only audit is read-only.** The audit viewer has **no edit/delete affordance**; config edits are **audited** and a config change does **not** retroactively alter already-computed bookings/ledger — say so in the save confirmation. Finance must be able to prove the commission/VAT rate at any past moment via the change-history drawer. - **Config typing + validation.** Render each config by its `data_type` and **validate** at the boundary (a rate field is **0–1**, an int is integer, a bool is a switch, json must parse) before allowing the audited save. - **Signed URLs are short-lived.** Verification documents and invoice PDFs load via **signed URLs** (never public) — fetch the URL **on demand**, don't cache it in a long-lived query; handle **loading / expired → re-request / load-error → retry**. - **PII / masking.** `settlement_iban` (center) and the payout `iban_snapshot` are encrypted/masked — show **last-4 only**, never a full IBAN; `transfer_reference`/`moadian_reference_number` are opaque strings shown for reconciliation. Don't log full sensitive values. Verification documents are PII — signed-URL only, never embedded as a public asset. - **Tenancy.** A partner-center admin sees **only their own center**; an admin sees only what their role scopes. Never fetch by a raw id the current principal doesn't own (server-enforced — don't bypass it). - **Frontend conventions (non-negotiable):** fetch only through `clientFetch` in `services/{domain}`; TanStack Query caching with deliberate keys (filters + page in the key) + invalidation/`setQueryData` (no needless refetch — switching a worklist filter or paging must not refetch loaded data); **large worklists are paginated/virtualized** with **empty/loading/error** states each; minimise re-renders (`select` to subscribe to slices, stable refs, colocate filter state low — a fast-changing config-input value must not re-render the whole config table); MUI primitives stay MUI, shared composites (the worklist row/table, the document viewer, the refund panel, the config row, the audit row, the support-alert card, the partner settlement row) live at `src/components/…` with co-located tests; colours from `tokens.css` (status chips off the `--bal-{success,warning,info,error}` semantic tokens); **both locales in sync**, RTL-correct (the desktop sidebar mirrors; Persian legal terms render correctly); no layout above `[locale]`; respect the RSC/client boundary. ## 6. Definition of Done The shared [definition-of-done.md](../_shared/definition-of-done.md), plus this phase's specifics: - [ ] `services/admin` and `services/partnerCenter` exist in the `auth`-service shape (types from the b1/b15 contracts, `keys.ts` with filters/page in the key, `apis/clientApi.ts`, one hook per file, hooks-only `index.ts`), and the **admin endpoints are added to the existing** `verification`/`refunds`/`payouts`/`reviews`/`tickets` domains (not re-created); mutations invalidate the right keys. - [ ] **Verification queue** lists pending nurses, the **per-nurse review** loads documents via **signed URLs** (loading/expired/error handled), supports **pass/reject + reason** per step and **structured credential entry**, and **Approve** is enabled only when all required steps pass — the client never writes `is_verified`. - [ ] **Refund tooling** opens **from a ticket**, renders the **server-computed** fee/payout decomposition + channel + BNPL ETA, supports initiate/approve/reject with provider-revert-failure → retry, and shows the read-only clawback notice when applicable. - [ ] **Payout dashboard** shows batches with status + holiday-shift indicator, a **batch preview** with the **server-computed eligibility breakdown** + clawback netting, a **run-batch** action that is **idempotency-keyed** (no double-pay), and a **batch detail** with per-nurse rows, masked IBAN, `transfer_reference`, a **failed-payout retry**, and transfer-reference reconciliation. - [ ] **Review moderation** queue supports **publish/hide/reject** (reject reason); the client invalidates and never computes the aggregate; `pending_moderation` content is never shown as public. - [ ] **Config editor** renders each value by `data_type`, **validates** (rate 0–1), saves with an **audited-save confirmation** ("audited; effective immediately; not retroactive"), and exposes a **change-history drawer**. - [ ] **Holiday manager** lists/adds/edits holidays with the **`is_bank_closed` toggle** (consequence surfaced); the client never computes the next-business-day shift. - [ ] **Audit-log viewer** is **read-only** (no edit/delete), filtered (entity/actor/action/date) and **paginated/virtualized** with a `changed_fields_json` diff. - [ ] **Support-alert worklist** filters by type/status/owner and supports **assign/resolve** with a note; **no `support_alerts` content appears in any non-admin surface**. - [ ] **Global ticket queue** + **internal-note composer** exist (admin sees `is_internal` notes, styled distinctly); the refund panel opens from here. - [ ] **Partner-center portal** (separate scope) shows the center's onboarding/verification state, sponsored nurses, sponsored bookings, and — **only when `is_merchant_of_record`** — the settlement/invoice view (commission/VAT decomposition, signed-URL PDF, masked IBAN); the **admin-side partner management** supports create/edit/verify/activate + assign-nurse, IBAN write-then-masked. - [ ] Every admin route is **role-gated** (controls hidden/disabled per the current role) and the partner portal is a **separate scope**; UI hiding never substitutes for the server's enforcement. - [ ] `admin` + `partner` i18n namespaces in `en.json` **and** `fa.json` in sync (incl. پروانه تأسیس / مسئول فنی / نماد اعتماد الکترونیکی / سامانه مودیان); RTL verified on the desktop shell. - [ ] Shared composites (worklist row/table, document viewer, refund panel, config row, audit row, support-alert card, partner settlement row) live at the shared level each with a co-located `*.test.tsx`; **all money via the f0 util** (no float math); `npm run check` green; `npm run test:ci` green. - [ ] Any contract gap is a `REQ-NNN` in [`for-backend.md`](../../shared-working-context/frontend/requests/for-backend.md) and the corresponding client-side mock is behind the seam and recorded in the report + mock registry. - [ ] `client/CLAUDE.md` *Project Structure* updated for `services/admin`, `services/partnerCenter`, the admin endpoint additions to existing domains, the `/admin/*` route segments, the `/partner/*` scope, and the new shared admin components; the **frontend-designer skill was invoked** for the visual work. ## 7. How to test (what a human can verify after this phase) Run `npm run dev` signed in as an **admin** (with the b1/b6/b11/b13/b14/b15 admin endpoints live, or the seam mocks if not yet merged): 1. **Verify a nurse.** Open **/admin/verification** → pick a pending nurse → the per-nurse screen loads each document via a **signed URL** (let one expire → it offers re-request). **Pass** the remaining steps (reject one with a reason to see the reason captured), enter a **structured credential**, then **Approve** → confirm dialog → the nurse leaves the queue and their trust badge flips. *Expected:* Approve is disabled until all required steps are `passed`; the client never wrote `is_verified` itself. 2. **Process a refund.** From the **/admin/tickets** queue, open a ticket linked to a booking → open the **refund panel** → see the **server-computed** tiered percentage + **fee/payout decomposition** + the **channel selector**; for a BNPL booking see the **ETA banner**. **Initiate → Approve** → success; the decomposition sums (`fee_refunded + payout_refunded` reconciles). If the nurse was already paid, a **clawback notice** shows (read-only). *Expected:* the refund carries the `ticket_id`; a forced provider-revert failure shows **retry**. 3. **Preview + run a payout batch.** Open **/admin/payouts** → **preview next batch** → the **eligibility breakdown** lists only EVV-confirmed, dispute-window-closed bookings, the **clawback-netting** line, and the **holiday-shifted** processing date. **Run batch** (confirmation requires an **idempotency key**) → it goes **processing** → **completed** (or **partially-failed**). Open the batch detail → a **failed** payout offers **retry**; reconcile a `transfer_reference`. *Expected:* clicking "run" twice does **not** pay any booking twice; an amount still in its dispute window never appears as eligible. 4. **Moderate a review.** Open **/admin/reviews** → a `pending_moderation` review (with a low-rating flag) → **publish** it → it leaves the queue and the nurse's public reviews update (server-recomputed aggregate); **hide**/**reject** behave likewise (reject captures a reason). *Expected:* pending content is never shown publicly; the client didn't compute the aggregate. 5. **Edit a config value (audited + history).** Open **/admin/config** → edit `vat_rate` → the input validates **0–1** (try `1.5` → blocked) → **save** → the confirmation states it's **audited, immediate, non-retroactive** → open the **change-history drawer** → the old→new change, actor, and Shamsi timestamp appear. *Expected:* the change is recorded; already-computed bookings are unaffected. 6. **Resolve a support alert.** Open **/admin/alerts** → filter to **open** → **assign** an alert to yourself (status → assigned) → **resolve** it with a note (status → resolved). *Expected:* the alert is internal-only — it appears in **no** customer/nurse/partner view anywhere. 7. **Holiday manager.** Open **/admin/holidays** → add a holiday with **`is_bank_closed` on** → it's listed; (cross-check in §3 step 3 that the next payout preview's processing date shifts off it, server-side). *Expected:* the client only maintains the calendar; it doesn't compute the shift. 8. **Audit viewer.** Open **/admin/audit** → filter by entity/actor/date → results paginate/virtualize; expand a row to see the `changed_fields_json` diff. *Expected:* **no** edit/delete control exists. 9. **Partner portal (separate scope).** Sign in as a **partner-center admin** → **/partner** shows the center's onboarding/verification state; **/partner/nurses** and **/partner/bookings** show **only that center's** sponsored nurses/bookings; **/partner/settlement** renders the commission/VAT invoice view **only when the center is merchant-of-record** (a non-MoR center shows the "settlement via Balinyaar" state), with a signed-URL PDF and a **masked IBAN (last-4)**. Sign back in as a Balinyaar admin → **/admin/partners** → create/verify/activate a center and **assign a nurse**. *Expected:* a center admin cannot see another center's data; the IBAN is never shown in full. 10. **RBAC + i18n + RTL + caching.** A `support`-role admin **cannot** see the payout-run control; a `moderator` **cannot** see the refund tool. Switch `fa`↔`en` → every label (incl. the Persian legal terms) translates and the desktop sidebar mirrors. Switch worklist filters / page the lists → React Query Devtools shows **separate cache entries per filter/page** and **no refetch** of loaded data. 11. **Gate:** `npm run check` and `npm run test:ci` pass. ## 8. Hand off & document (close the phase) - **Docs to update:** `client/CLAUDE.md` *Project Structure* — add `services/admin`, `services/partnerCenter`, the admin endpoint additions to the existing `verification`/`refunds`/`payouts`/`reviews`/`tickets` domains, the `/admin/*` route segments + the `/partner/*` scope, the new shared admin/partner components, and a one-line note on the `useAdminCapabilities()` role-gating selector and the signed-URL on-demand-fetch pattern so they're reused. If you discover/decide a business rule the `product/` docs don't capture (e.g. an admin-only payout-preview field, a partner-center onboarding sub-state, a config validation bound), record it in the relevant `product/**.md` — **don't invent rules**; record decisions and flag uncertain ones in your report. - **Contracts to consume:** the primary [`../../contracts/domains/messaging-notifications-admin.md`](../../contracts/domains/messaging-notifications-admin.md) (b15 — admin tickets, support-alert worklist, partner centers, RBAC) **plus** the per-domain admin endpoints in [`verification.md`](../../contracts/domains/verification.md) (b6), [`refunds.md`](../../contracts/domains/refunds.md) (b11), [`payouts.md`](../../contracts/domains/payouts.md) (b13), [`reviews.md`](../../contracts/domains/reviews.md) (b14), and [`config-reference.md`](../../contracts/domains/config-reference.md) (b1). Derive **all** types from these — **never** guess shapes. Any missing field/filter/endpoint (e.g. the payout **preview** eligibility breakdown, the refund decomposition preview, the config `data_type`, the partner settlement rows, the admin `is_internal` message flag, the RBAC role endpoints) → append a `REQ-NNN` to [`../../shared-working-context/frontend/requests/for-backend.md`](../../shared-working-context/frontend/requests/for-backend.md) and mock behind the seam meanwhile. - **Handoff & report:** append your phase summary to [`../../shared-working-context/frontend/STATUS.md`](../../shared-working-context/frontend/STATUS.md); write [`../../shared-working-context/reports/frontend-phase-15-report.md`](../../shared-working-context/reports/frontend-phase-15-report.md) (operating-rules §7) — what was built, **what is now testable and exactly how** (the §7 steps), which client-side mocks sit behind the `services/admin` / `services/partnerCenter` / extended-domain seams and how each swaps to the real endpoint, the contracts consumed, the `REQ` gaps filed, and — since this is the **final frontend phase** — a short **"MVP complete" closeout** noting any deferred-if-missing screen (e.g. RBAC admin) and the modeled-but-inactive tables with no UI. Update the mock registry [`../../shared-working-context/reports/mocks-registry.md`](../../shared-working-context/reports/mocks-registry.md) for every client-side mock. - **Memory:** save a `project` memory note (with a `MEMORY.md` pointer) for the non-obvious decisions this phase locks in — the admin-shell route-gating + `useAdminCapabilities()` role selector, the separate partner-center authz scope and its tenancy, the admin-vs-user ticket type split (`isInternal` admin-only), the merchant-of-record-gated settlement view + commission-only/VAT-on-commission invoice rule, the signed-URL on-demand-fetch pattern for documents/PDFs, the idempotency-keyed payout-run/retry, and the "server is the only authority" boundary (no client-side `is_verified` flip / eligibility / decomposition / aggregate / holiday-shift). Don't record what the code/docs already make obvious.