# admin — platform config, holidays, audit, support alerts

> Client seam `client/src/services/admin/` · `USE_ADMIN_MOCK = true` (**mock is primary**) · 14 server ops
> Last verified: 2026-07-30 against commit `d3ec723` and swagger.v1.json (2026-07-29).

The ops console's cross-cutting reads and writes. Domain-specific admin surfaces live with their domain —
verification admin in [verification.md](verification.md), refunds in [refunds.md](refunds.md), payouts in
[payouts.md](payouts.md), catalog in [catalog.md](catalog.md), geo in [geography.md](geography.md),
partner centers in [partner-center.md](partner-center.md), the ticket queue in [tickets.md](tickets.md).

Every endpoint here is `[Authorize(ConstantPolicies.DynamicPermission)]` + `sensitive` rate limit
(20/min), **except** the three `platform_config`/`holidays`/`audit`/`support_alerts` controllers, which
carry the dynamic-permission policy without the sensitive limit — they fall to the 100/min global limiter.

## Endpoints

| Method | Path | Verdict |
| --- | --- | --- |
| GET | `/api/v1/platform_config/get_platform_configs` | wired · paginated |
| POST | `/api/v1/platform_config/update_platform_config` | wired |
| GET | `/api/v1/platform_config/get_config_change_history` | wired · paginated |
| GET | `/api/v1/holidays/get_holidays` | wired · paginated |
| POST | `/api/v1/holidays/upsert_holiday` | wired |
| POST | `/api/v1/holidays/delete_holiday` | **unwired** — no real client caller; the console offers no delete |
| GET | `/api/v1/audit/get_audit_trail` | wired · paginated |
| GET | `/api/v1/support_alerts/get_support_alerts` | wired · paginated |
| POST | `/api/v1/support_alerts/assign_support_alert` | wired |
| POST | `/api/v1/support_alerts/resolve_support_alert` | wired |
| GET | `/api/v1/admin_cancellation_policies/list` | **unwired** — the tier table is read through [refunds.md](refunds.md)'s policy preview instead |
| POST | `/api/v1/admin_cancellation_policies/upsert` | **unwired** — no console screen edits tiers |
| POST | `/api/v1/admin_search/rebuild_index` | **unwired** — an ops one-shot, no UI |
| POST | `/api/v1/admin_booking_requests/expire` | **unwired** — an ops one-shot; the scheduler does this unattended |

### Phantom — 5

The client's real `clientApi.ts` calls five routes the server does not expose. Both groups are
**deliberately written real-shaped** so flipping the seam is one line once they ship.

| Client call | REQ | Note |
| --- | --- | --- |
| `GET /api/v1/admin_roles/list_roles` | REQ-031 | **Deferred** in refinement-phase-3. The admin sub-role vocabulary and phone-OTP admins are seeded, not managed |
| `POST /api/v1/admin_roles/grant_role` | REQ-031 | |
| `POST /api/v1/admin_roles/revoke_role` | REQ-031 | |
| `GET /api/v1/admin_users/search` | **REQ-061 — never filed** | Backs `UserPicker`/`NursePicker` |
| `POST /api/v1/admin_users/lookup` | **REQ-061 — never filed** | Batch id→label resolve for `AuditLogRow` |

> **REQ-061 does not exist in the ledger.** `ui-phase-11-report.md` records "REQ-061…064 appended", but
> only 062/063/064 were. Ten live client files cite REQ-061 for the admin user directory. Phase 4 must
> file it rather than assume it is tracked.

## Two live drifts

**1. The client sends `page_size`; these endpoints declare `PageSize`.** `admin/apis/clientApi.ts`'s
`pageQuery()` builds `page` + `page_size` "per b1 api-conventions". Model binding is case-*insensitive*,
not separator-insensitive, so `page_size` does **not** bind to `PageSize` — every admin list would
silently fall back to the server's default page size. Invisible today because the mock is primary; it
becomes a real defect the moment `USE_ADMIN_MOCK` flips. See
[../api-contract.md](../api-contract.md#pagination).

**2. `updatedAt`/`updatedBy` and the audit filters *are* on the wire.** REQ-029 (config audit fields) and
REQ-030 (`actorId`/`action`/`from`/`to` filters on `audit/get_audit_trail`) were both **delivered** in
refinement-phase-3. `admin/constants.ts` still gives them as reasons the mock is primary. The only
remaining reason is REQ-031 + REQ-061.

## Shape rules the JSON does not express

- **Platform config is rows read at compute time**, never hardcoded, and **a rate change is never
  retroactive** — the effective rate is snapshotted onto the row when the amount is computed.
- `platform_fee_rate` and `vat_rate` are **rates in `[0, 1)`** — the console validates the closed-open
  interval before writing (`RATE_CONFIG_KEYS` in `admin/constants.ts`). The canonical values are
  `0.15` fee / `0.10` VAT (refinement-phase-3).
- The audit trail's `changedFieldsJson` is a **string containing JSON**, not an object:
  `{"Field": {"old": …, "new": …}}`. The client parses it defensively and yields `null` on malformed input.
- `POST update_platform_config` and the holiday/alert writes go through self-committing facades that call
  `SaveChanges` on the shared scoped context — they run **after** the handler's own `CommitAsync`.
- Holidays drive **payout date shifting**: the server resolves a bank-closure-safe payout date from this
  calendar and the client never computes one.

## Enums

| Vocabulary | Values |
| --- | --- |
| `ConfigDataType` | `string` `int` `decimal` `bool` `json` |
| `AuditAction` | `created` `updated` `deleted` |
| `HolidayType` | `official` `religious` `national` |
| `SupportAlertType` | `low_rating` `evv_no_show` `evv_location_mismatch` `verification_expired` `shared_sim` `payment_anomaly` `fraud_signal` `nurse_clawback` `emergency` |
| `SupportAlertSeverity` | `low` `medium` `high` |
| `SupportAlertStatus` | `open` `assigned` `resolved` |
| `AdminRole` *(phantom surface)* | `super_admin` `admin` `support` `finance` `moderation` |
| `DirectoryUserRole` *(phantom surface)* | `customer` `nurse` `admin` `partner` |

The last two describe the REQ-031/REQ-061 shapes and are **not on the wire**.

## Open REQs

| REQ | Status | Effect |
| --- | --- | --- |
| REQ-031 | deferred | No RBAC console. Admin roles are seeded |
| **REQ-061** | **never filed** | No admin user directory. `AuditLogRow` shows `#id` instead of a name |
