# Contract — Config, Reference & Platform Signals (backend phase b1) > Admin config/holiday/audit/support-alert endpoints + the current-user notification endpoints. Assumes > [`../conventions/api-conventions.md`](../conventions/api-conventions.md) + > [`../conventions/money-and-types.md`](../conventions/money-and-types.md). Machine schema (authoritative > for exact field/param casing): [`../openapi/swagger.v1.json`](../openapi/README.md). **Status:** live as of backend-phase-1 · **Frontend consumer:** frontend-phase-f14 (notification center) / frontend-phase-f15 (admin config/holidays/audit/alerts) All responses are the standard `OperationResult`→`ApiResult` envelope (camelCase body, snake_case URLs). Lists carry `{ items, total, page, pageSize }`. Pagination inputs are `page` (1-based) + `pageSize` (default 50, max 100) — bound from the query string; derive exact casing from `swagger.v1.json`. ## Enums used - **config `data_type`**: `decimal` | `int` | `bool` | `string` | `json` — how to parse a config `value`. - **holiday `type`**: `official` | `religious` | `national`. - **support-alert `type`**: `low_rating` | `evv_no_show` | `evv_location_mismatch` | `verification_expired` | `payment_anomaly` | `fraud_signal`. - **support-alert `severity`**: `low` | `medium` | `high`. - **support-alert `status`**: `open` | `assigned` | `resolved` (forward-only). - **notification `type`**: open string code the front-end renders/deep-links on (e.g. `booking_confirmed`); its shape is the `data_json` contract (below), versioned per type. --- ## Admin — Platform config (`platform_config` controller, `[Authorize(DynamicPermission)]`) ### `GET api/v1/platform_config/get_platform_configs` - **Purpose:** list config rows. **Auth:** admin (DynamicPermission). **Rate-limited:** no. - **Query:** `page`, `pageSize`. - **200 `data`:** `PagedResult` — `{ items:[{ key, value, dataType, description }], total, page, pageSize }`. ### `POST api/v1/platform_config/update_platform_config` - **Purpose:** update one existing config row; writes an `audit_logs` entry in the same transaction and evicts the cache. **Auth:** admin. - **Body:** `{ "key": "platform_fee_rate", "value": "0.18" }`. - **200 `data`:** `true` (empty-body success). - **Failures:** `400` validation (empty key); `404` key does not exist. **Notes:** value is the raw string parsed per the row's `data_type`; changing a rate never retroactively re-prices already-computed rows. ### `GET api/v1/platform_config/get_config_change_history` - **Purpose:** the audited change history for one key (from the append-only trail). **Auth:** admin. - **Query:** `key` (required), `page`, `pageSize`. - **200 `data`:** `PagedResult` — `{ items:[{ id, action, changedFieldsJson, actorUserId, occurredAt }], … }`, newest first. --- ## Admin — Holidays (`holidays` controller, `[Authorize(DynamicPermission)]`) ### `GET api/v1/holidays/get_holidays` - **Query:** `from` (date, optional), `to` (date, optional), `page`, `pageSize`. - **200 `data`:** `PagedResult` — `{ items:[{ id, holidayDate, nameFa, type, isBankClosed }], … }`, by date. ### `POST api/v1/holidays/upsert_holiday` - **Body:** `{ "holidayDate": "2026-03-21", "nameFa": "نوروز", "type": "national", "isBankClosed": true }`. - **200 `data`:** `true`. **Failures:** `400` (bad `type`, empty `nameFa`, default date). **Notes:** upsert keyed on `holidayDate`. ### `POST api/v1/holidays/delete_holiday` - **Body:** `{ "holidayDate": "2026-03-21" }`. **200:** `true`; **404** if no holiday on that date. --- ## Admin — Audit (`audit` controller, `[Authorize(DynamicPermission)]`) ### `GET api/v1/audit/get_audit_trail` - **Purpose:** the immutable trail for one entity. **Auth:** admin. - **Query:** `entity_type` (e.g. `PlatformConfig`), `entity_id` (string), `page`, `pageSize`. - **200 `data`:** `PagedResult` — `{ items:[{ id, entityType, entityId, action, changedFieldsJson, actorUserId, occurredAt }], … }`, newest first. **Notes:** read-only; there is no write/update/delete endpoint for audit rows. --- ## Admin — Support alerts (`support_alerts` controller, `[Authorize(DynamicPermission)]`, never user-facing) ### `GET api/v1/support_alerts/get_support_alerts` - **Query:** `type?`, `status?`, `owner_user_id?`, `page`, `pageSize`. - **200 `data`:** `PagedResult` — `{ items:[{ id, type, severity, status, entityType, entityId, bookingId, reviewId, ownerUserId, resolutionNote, resolvedAt, createdAt }], … }`. ### `POST api/v1/support_alerts/assign_support_alert` - **Body:** `{ "alertId": 42, "ownerUserId": 7 }`. **200:** `true` (open → assigned); **404** if missing or already resolved. ### `POST api/v1/support_alerts/resolve_support_alert` - **Body:** `{ "alertId": 42, "note": "handled" }`. **200:** `true` (→ resolved); **404** if missing or already resolved. --- ## Current user — Notifications (`notifications` controller, `[Authorize]`, tenant-scoped) Every endpoint is scoped to the signed-in caller (`ICurrentUser`) — never a body-supplied user id. ### `GET api/v1/notifications/get_notifications` - **Query:** `page`, `pageSize`. - **200 `data`:** `PagedResult` — `{ items:[{ id, type, title, body, dataJson, isRead, readAt, createdAt }], … }`, **unread-first** then newest-first. ### `GET api/v1/notifications/get_unread_count` - **200 `data`:** `{ count }` — cheap index-backed count for the polling bell. ### `POST api/v1/notifications/mark_notification_read` - **Body:** `{ "notificationId": 100 }`. **200:** `true`; **404** if it isn't the caller's or doesn't exist. ### `POST api/v1/notifications/mark_all_read` - **No body. 200:** `true`. > **Not exposed via REST** (internal contracts other backend domains call): `CreateNotification` (via > `INotificationDispatcher.DispatchAsync`), `RaiseSupportAlert` (`ISupportAlertService.RaiseAsync`), > `EmitSystemEvent` (`IAnalyticsSink.EmitAsync`), `WriteAuditLog` (`IAuditLogger.WriteAsync`). The > notification retention purge runs on a background hosted service, not an endpoint. ## Shared shapes - **`PlatformConfigDto`**: `key` (string), `value` (string, raw — parse per `dataType`), `dataType` (enum), `description` (string, nullable). - **`ConfigChangeDto`**: `id` (long), `action` (`created`/`updated`/`deleted`), `changedFieldsJson` (string, nullable — `{ "Field": { "old": …, "new": … } }`; encrypted/PII fields redacted as `""`), `actorUserId` (int, nullable), `occurredAt` (UTC ISO-8601). - **`HolidayDto`**: `id` (long), `holidayDate` (date), `nameFa` (string), `type` (enum), `isBankClosed` (bool). - **`AuditLogDto`**: `id` (long), `entityType` (string), `entityId` (string), `action`, `changedFieldsJson` (nullable), `actorUserId` (nullable), `occurredAt`. - **`NotificationDto`**: `id` (long), `type` (string code), `title` (string), `body` (string, nullable), `dataJson` (string, nullable — a **typed, versioned deep-link payload**; shape depends on `type`, e.g. `{"booking_id": 1}`), `isRead` (bool), `readAt` (UTC, nullable), `createdAt` (UTC). - **`SupportAlertDto`**: `id`, `type`, `severity`, `status`, `entityType` (string), `entityId` (string), `bookingId` (long, nullable), `reviewId` (long, nullable), `ownerUserId` (int, nullable), `resolutionNote` (string, nullable), `resolvedAt` (UTC, nullable), `createdAt` (UTC). ## Changelog - b1 — initial contract (config, holidays, audit, support alerts, notifications).