# 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.
