Files
baya-monorepo/dev/phases/frontend/frontend-phase-15-b15.md
T
2026-06-28 21:59:59 +03:30

662 lines
56 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 A1E3 — 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 **01**), `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 01 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 b9b13):** 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 **01**, 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 01), 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 **01** (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.