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