add build development phases
This commit is contained in:
@@ -0,0 +1,661 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user