# Backend Phase 1 — Config, reference & platform signals > **Mission:** lay the platform backbone every later phase reads from or writes to. This phase creates > the **first marketplace EF migration baseline** (b0 deleted the old starter migrations) and stands up > the cross-cutting tables nothing in the marketplace can run without: typed runtime config > (`platform_configs`), the immutable audit trail (`audit_logs`) wired to the SaveChanges interceptor, > the analytics event log (`system_events`), the shared `iranian_holidays` calendar that shifts payout > dates, in-app `notifications`, and the internal `support_alerts` staff worklist. It ships the typed > cached config accessor, the holiday calendar, the audit/event/notification/alert capabilities, seeds > the canonical config keys (fee rate, VAT, dispute window, deadlines, payout interval, EVV tolerance, > BNPL flags, cancellation tiers) and a sample holiday set — so b2…b15 read real values instead of > hardcoding money-critical constants. > > **Track:** backend · **Depends on:** [backend-phase-0](backend-phase-0.md) (the cross-cutting seams, `ICurrentUser`, the audit-field SaveChanges interceptor, `IFieldEncryptor`, `ICacheService`) · **Unlocks:** every phase that reads config/holidays or raises notifications/alerts — i.e. all of b2…b15 > **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 **second** backend phase and the foundation for the rest of the chain. b0 cleaned the starter skeleton, stood up the REST surface, wired rate limiting + request logging + current-user/ audit-field stamping, and **defined the cross-cutting seams as DI-registered mocks**. It deliberately shipped **no domain tables** — that starts here. Everything b2…b15 does (auth, profiles, catalog, booking, payments, payouts, reviews, admin) reads config rows, shifts dates off holidays, writes audit rows, raises notifications, or raises support alerts. Those four mechanisms are built **once, here**, so no later phase reinvents them. This is also where the **first marketplace migration baseline** is born: b0 removed the three pre-2024 starter migrations and their snapshot. The migration you generate in this phase is the new `Persistence` baseline that every subsequent phase adds incrementally onto. **What already exists (do not rebuild) — built by [backend-phase-0](backend-phase-0.md):** - The cross-cutting **seams** with in-memory mocks, DI-registered via `ServiceConfiguration/` extensions and selected by config: **`ICacheService`** (in-memory `IMemoryCache`; your config accessor caches through it), **`IFieldEncryptor`** (`Encrypt`/`Decrypt`/`Hash`; reuse for any encrypted column), **`IDateTimeProvider`** (`DateTimeOffset UtcNow`; never call `DateTime.Now`), **`IObjectStorage`**, and **`INotificationDispatcher`** (b0 shipped a **no-op/log stub** — **this phase supersedes it** with a real in-app `notifications` write; see §3.6 and §4). - **`ICurrentUser`** (Application contract, Scoped, null-object outside HTTP) and the **SaveChanges interceptor** that stamps `CreatedAt/ModifiedAt/CreatedById/ModifiedById`. b0 left a **clean extension point on that interceptor for b1 to add audit-log-row writing** — you extend it here, you do not write a parallel interceptor. - The full working spine: ASP.NET Core Identity + JWE/JWT + phone-OTP, dynamic-permission RBAC, the CQRS pipeline (`ValidateCommandBehavior`, `MetricsBehaviour`, and the now-registered `LoggingBehavior`), `OperationResult`, Mapster, FluentValidation, the `BaseController` + MVC/versioning/Swagger stack, the REST controller surface under `Baya.Web.Api/Controllers/V1/`, rate-limiting policies, and `Baya.Tests.Setup` (in-memory SQLite). Identity tables live in the **`usr`** schema. - The 12-project Clean-Arch solution `Baya.sln`. The `swagger.json` envelope snapshot is published so the frontend's f0 type pipeline already knows the `OperationResult`/`ApiResult` shape. **What is NOT yet built (you build it here):** any marketplace table, any migration, the typed config accessor, the holiday calendar, audit-row writing, system-event emission, notifications, support alerts. ## 2. Required reading (do this first) **Operating rules & conventions** - [`../_shared/agent-operating-rules.md`](../_shared/agent-operating-rules.md) and [`../_shared/backend-conventions-checklist.md`](../_shared/backend-conventions-checklist.md). - [`server/CLAUDE.md`](../../../server/CLAUDE.md) — *Project map*, *Persistence*, *Startup wiring*, *Features* (the `Features//{Commands|Queries}//` layout); and [`server/CONVENTIONS.md`](../../../server/CONVENTIONS.md) — §6 persistence (configs per entity, audit fields, soft-delete filters, EF projection + pagination), §9 logging (no PII), §12 service registration. **Product / domain truth (the business rules are decisions, read them — don't infer from code)** - [`product/data-model/12-audit-config-and-reference.md`](../../../product/data-model/12-audit-config-and-reference.md) — the canonical field list and roles of `audit_logs`, `system_events`, `platform_configs`, and the **new** `iranian_holidays` table (exact columns) + the full config-key list to seed. - [`product/data-model/11-notifications.md`](../../../product/data-model/11-notifications.md) — the roles of `notifications` (N:1 → `users`, `data_json` typed payload, 90-day read-retention) and `support_alerts` (internal-only, polymorphic `entity_type`/`entity_id` + nullable typed FKs). - [`product/business/14-notifications-and-admin.md`](../../../product/business/14-notifications-and-admin.md) — *why* in-app-only/polled, the 90-day retention rule, the admin/backoffice spine these rows feed, the append-only audit requirement, and the Shamsi/holiday reasoning back-office depends on. - [`product/business/13-tax-invoicing-and-legal.md`](../../../product/business/13-tax-invoicing-and-legal.md) — *why* `vat_rate` (10%, configurable, applies to the **commission line** only) and the BNPL merchant-of-record config flags exist; finance must be able to prove the exact rate at any past moment (this is the reason every config change is audited). **Contract conventions you must honour** - [`../../contracts/conventions/api-conventions.md`](../../contracts/conventions/api-conventions.md) — the `OperationResult` envelope, snake_case routes, mandatory list pagination (`page`/`page_size`), localisation (`name_fa` returned for reference data), status codes. - [`../../contracts/conventions/money-and-types.md`](../../contracts/conventions/money-and-types.md) — IRR `BIGINT`/`long`, no floats. `platform_fee_rate`/`vat_rate` are **rates** (`DECIMAL`), not money; the amounts they later multiply are IRR `long`. **Prior handoff** - [`../../shared-working-context/backend/handoff/after-backend-phase-0.md`](../../shared-working-context/backend/handoff/after-backend-phase-0.md) and [`../../shared-working-context/reports/backend-phase-0-report.md`](../../shared-working-context/reports/backend-phase-0-report.md) — exactly what b0 left you (seam names/files, the interceptor extension point, the REST pattern). **Code to mirror** - The seam interfaces + their mock implementations and `ServiceConfiguration/` registration from b0 (mirror that style for `IHolidayCalendar`, `IAnalyticsSink`, and the real `INotificationDispatcher`). - The SaveChanges interceptor b0 extended — you add audit-row writing to its diff-collection path. - The `BaseController` REST pattern from b0's controller — mirror it for the new admin controllers. ## 3. Scope — build this > Layout follows `Features//{Commands|Queries}//`. Suggested areas: > **`Configuration`**, **`Audit`**, **`Analytics`**, **`Holidays`**, **`Notifications`**, > **`SupportAlerts`**. Each entity gets exactly one `IEntityTypeConfiguration` under > `Persistence/Configuration/Config/`. Reads use `AsNoTracking()` + `.Select(...)` projection + > pagination; commands return `OperationResult`. Put these tables in a dedicated **`ref`/`ops` schema** > (mirror how Identity uses `usr`) — keep them off the `usr` schema. ### 3.1 Entities & the first marketplace migration baseline Create these EF entities + configurations, then generate **one** migration that becomes the new `Persistence` baseline. (b0 already deleted the old migrations/snapshot.) - **`platform_configs`** — typed key-value runtime parameters. Columns: `id` (BIGINT PK), `key` (NVARCHAR, **UNIQUE**), `value` (NVARCHAR — the raw string), `data_type` (NVARCHAR — `decimal` / `int` / `bool` / `string` / `json`; **tells the app how to parse `value`**), `description` (NVARCHAR, nullable), plus the standard audit fields. **Every change to a row here is audited** (§3.3). Soft-delete is **not** appropriate — configs are updated in place, the audit trail is the history. - **`audit_logs`** — immutable, append-only. Columns: `id` (BIGINT PK), `entity_type` (NVARCHAR), `entity_id` (NVARCHAR — string so it's polymorphic across PK types), `action` (NVARCHAR — `created`/`updated`/`deleted`), `changed_fields_json` (NVARCHAR(MAX) — `{ field: { old, new } }` for fast filtering), `actor_user_id` (FK→`users`, nullable for system actions), `occurred_at` (DATETIMEOFFSET, from `IDateTimeProvider`). **No `ModifiedAt`/`IsDeleted` — append-only, never updated or deleted.** Index `(entity_type, entity_id)` and `occurred_at`. (Month-partitioning + cold-storage archival is **(DEFERRED)** — note the index design now; the archive store seam lands later.) - **`system_events`** — high-volume analytics, **NOT compliance**. Columns: `id` (BIGINT PK), `name` (NVARCHAR — event name), `props_json` (NVARCHAR(MAX) — arbitrary properties), `user_id` (FK→`users`, nullable), `occurred_at` (DATETIMEOFFSET). Append-only; no audit fields needed beyond `occurred_at`. - **`iranian_holidays`** — exact columns from the data-model doc: `id` (BIGINT PK), `holiday_date` (DATE, index it — lookups are by date), `name_fa` (NVARCHAR(200)), `type` (NVARCHAR(20) — `official`/`religious`/`national`), `is_bank_closed` (BIT). Unique on `holiday_date`. - **`notifications`** — in-app, per user. Columns: `id` (BIGINT PK), `user_id` (FK→`users`, indexed), `type` (NVARCHAR — drives front-end rendering/deep-link), `title` (NVARCHAR), `body` (NVARCHAR, nullable), `data_json` (NVARCHAR(MAX) — **typed deep-link payload**, a versioned contract, not an arbitrary blob), `is_read` (BIT, default 0), `read_at` (DATETIMEOFFSET, nullable), `created_at`. Index `(user_id, is_read, created_at)` to serve unread-first paging and the unread-count query cheaply. - **`support_alerts`** — internal staff worklist, **never user-facing**. Columns: `id` (BIGINT PK), `type` (NVARCHAR — `low_rating`/`evv_no_show`/`evv_location_mismatch`/`verification_expired`/ `payment_anomaly`/`fraud_signal`), `severity` (NVARCHAR — `low`/`medium`/`high`), `status` (NVARCHAR — `open`/`assigned`/`resolved`), `entity_type` (NVARCHAR) + `entity_id` (NVARCHAR) — **polymorphic, validated at the application layer, no DB FK** — plus **nullable typed FKs** `booking_id` (FK→`bookings`, nullable) and `review_id` (FK→`reviews`, nullable) for the common cases (declare the FK columns now; the `bookings`/`reviews` tables arrive in b9/b14 — gate the relationship config so the migration is additive-safe, or add the FK constraints in the phase that creates the target table and note it in your handoff), `owner_user_id` (FK→`users`, nullable — the assigned admin), `resolution_note` (NVARCHAR, nullable), `resolved_at` (DATETIMEOFFSET, nullable), plus standard audit fields. > **Migration discipline:** generate the migration with a descriptive name (e.g. > `InitialMarketplaceBaseline`) into `Baya.Infrastructure.Persistence/Migrations`. It must apply cleanly > against a fresh DB and be the snapshot every later phase builds on. Run `dotnet ef database update` > (or the project's migration entry point) to confirm. ### 3.2 Config — `IPlatformConfig` (Application contract) + cached accessor - **`IPlatformConfig`** in `Application/Contracts/` with: `Task GetConfig(string key, …)` (parses by the row's `data_type`, **cached** through `ICacheService` with a sensible TTL and key scheme), and `Task SetConfig(string key, string value, …)` (admin path — writes the row **and** an audit entry; see §3.3) and `Task<…> GetConfigChangeHistory(string key, …)` (reads the audit trail filtered to that key's rows). Implement in Infrastructure. - **Cache invalidation:** `SetConfig` must evict the cache key it changes so the next `GetConfig` reads the new value — but **see the money-correctness rule in §5**: changing a rate must never retroactively alter already-computed amounts. - **Commands/queries:** `UpdatePlatformConfig` (command, admin-only, validated, audited), `ListPlatformConfigs` (query, paged), `GetConfigChangeHistory` (query, paged). ### 3.3 Audit — interceptor-driven write + trail query - **Extend the b0 SaveChanges interceptor** (do not write a new one) so that on `SavingChanges` it inspects tracked entries for entities marked **auditable** (a marker interface, e.g. `IAuditable`, or an explicit allow-list — `platform_configs` is auditable; `system_events`/`notifications` are **not**) and appends an `audit_logs` row per change with `entity_type`, `entity_id`, `action`, a computed `changed_fields_json` (old/new per modified property; redact encrypted PII — never write plaintext into the diff), and `actor_user_id` from `ICurrentUser`. The audit write happens in the **same transaction** as the change. - **`WriteAuditLog`** is the interceptor-driven mechanism above; also expose an explicit `IAuditLogger.WriteAsync(...)` Application contract for cases a handler needs to record a non-entity state change (e.g. an action with no row diff). - **Query `GetAuditTrail(entity_type, entity_id)`** — paged, `AsNoTracking()` + projection, admin-only. ### 3.4 Analytics — `IAnalyticsSink` + `EmitSystemEvent` - **`IAnalyticsSink`** (Application contract, **seam introduced here**) with `Task EmitAsync(string name, object props, …)`. Mock implementation = **insert a `system_events` row**. Calls are **fire-and-forget** from the caller's perspective (do not block or fail the user's operation if the sink errors — log and continue). `EmitSystemEvent` is the thin command/helper that calls the sink. **Never** route compliance-relevant facts here — those go to `audit_logs`. ### 3.5 Holidays — `IHolidayCalendar` + seed - **`IHolidayCalendar`** (Application contract, **seam introduced here**) with `Task IsHoliday(DateOnly date, …)`, `Task IsBankClosed(DateOnly date, …)`, and `Task NextBusinessDay(DateOnly date, …)`. Mock implementation = the **seeded `iranian_holidays` table** (cache the lookups through `ICacheService`). `NextBusinessDay` skips `is_bank_closed` days (and weekend rules per the Iranian banking week) — this is what payout scheduling (b13) calls. - **Admin CRUD** for the calendar: `ListHolidays` (query, paged/by-range), `UpsertHoliday`, `DeleteHoliday` (commands, admin-only, audited if you make the table auditable — at minimum log via the interceptor allow-list if finance cares; otherwise standard audit fields suffice). ### 3.6 Notifications — real in-app write (supersedes the b0 stub) - **Replace** the b0 no-op `INotificationDispatcher` mock with a **real in-app implementation** that writes a `notifications` row (channel concept retained: in-app **now**; SMS/push are **(DEFERRED)** to later channels behind the same dispatcher — do **not** build them). Keep the seam interface stable so other domains (booking, payments, reviews, verification) call `DispatchAsync(...)` unchanged. - **`CreateNotification(user_id, type, data_json)`** — the internal command other domains call (via the dispatcher) to mint a typed in-app record with a deep-link payload. - **Queries:** `ListMyNotifications` (paged, **unread-first** ordering, tenant-scoped to the caller), `GetUnreadCount` (cheap count for the polling bell — index-backed). - **Commands:** `MarkNotificationRead` (single, sets `is_read`+`read_at`), `MarkAllRead` (bulk for the caller). - **Retention job `PurgeOldReadNotifications`** — hard-deletes notifications where `is_read = 1` **AND** `created_at` (or `read_at`) older than 90 days; **never deletes unread**. There is no scheduler in the codebase yet — introduce the job behind a small **`IJobScheduler`/hosted-service seam** (mock = a `BackgroundService` running on an interval, or an endpoint/CLI trigger), register it, and record it in the mock registry. Real Hangfire/Quartz is **(DEFERRED)**. ### 3.7 Support alerts — raise / assign / resolve / list - **`RaiseSupportAlert(type, entity_type, entity_id, severity, booking_id?, review_id?)`** — the internal command review/EVV/verification/payment flows call later. Validate the polymorphic `(entity_type, entity_id)` at the application layer; prefer setting the typed FK when the entity is a booking/review. - **`AssignSupportAlert(alert_id, owner_user_id)`** and **`ResolveSupportAlert(alert_id, note)`** — owner + resolution trail; forward-only status (`open`→`assigned`→`resolved`). - **`ListSupportAlerts`** — admin-only query, paged, filter by `type`/`status`/`owner`. **These are on admin-only routes and never joined into any user-facing endpoint.** ### 3.8 Admin REST surface Add the controllers (sealed, `BaseController`, inject `ISender`, `base.OperationResult(...)`, `[controller]`/`[action]` tokens, narrowest fitting admin policy, lists paginated). Suggested actions (final route casing is snake_case per conventions): - **Config:** `GET get_platform_configs`, `POST update_platform_config`, `GET get_config_change_history`. - **Holidays:** `GET get_holidays`, `POST upsert_holiday`, `POST delete_holiday`. - **Audit:** `GET get_audit_trail`. - **Support alerts (admin):** `GET get_support_alerts`, `POST assign_support_alert`, `POST resolve_support_alert`. - **Notifications (current user):** `GET get_notifications`, `GET get_unread_count`, `POST mark_notification_read`, `POST mark_all_read`. > **Not exposed via REST:** `CreateNotification`, `RaiseSupportAlert`, `EmitSystemEvent`, > `WriteAuditLog` — these are **internal application commands** other domains invoke through their > contracts/the dispatcher, never user-callable HTTP endpoints. ### 3.9 Seed data - **`platform_configs` keys (seed all, with `data_type`):** `platform_fee_rate` (decimal — platform commission %), `vat_rate` (decimal, **0.10**), `dispute_window_hours` (int, **72**), `booking_payment_deadline_minutes` (int, **30**), `nurse_response_deadline_hours` (int), `nurse_payout_interval_days` (int), `evv_location_tolerance_meters` (int), `min_rating_for_support_alert` (decimal/int), `bnpl_merchant_of_record` (string), `bnpl_provider_commission_rate` (decimal), `bnpl_settlement_timing` (string), and the **cancellation-tier defaults** (the tiered cancellation policy thresholds/percentages — store as `json` or as discrete tier keys; b9/b11 consume them). Seed via the migration or an idempotent seeder. - **`iranian_holidays`:** seed a representative sample set (Nowruz block, a few official/religious/national days) with correct `is_bank_closed` flags so `IsBankClosed`/`NextBusinessDay` are testable. The full, maintained, partly-lunar-Hijri calendar feed is **(DEFERRED)** behind the `IHolidayCalendar` "make it real" path. ### 3.10 Out of scope (DEFERRED — do not build here) - `roles`/`user_roles` admin RBAC tables and the role-scope authorization grid → **(DEFERRED to b15)**; use the existing dynamic-permission RBAC + the narrowest fitting policy for now. - The verification queue, refund tooling, payout tooling, invoices, partner centers, tickets → their own phases (b6, b11, b13, b15). This phase builds only the **shared signals** they all use. - `audit_logs` month-partitioning + cold-storage archival (`IArchiveStore`); `system_events` warehouse/exporter; SMS/push notification channels; real Hangfire/Quartz scheduler; the external Iranian-holiday sync feed → all **(DEFERRED)** behind their seams. ## 4. Mocks & seams in this phase This phase **introduces** three seams and **supersedes** one b0 stub. Reuse the b0 seams (`ICacheService`, `IFieldEncryptor`, `IDateTimeProvider`, `IObjectStorage`) as-is. | Seam | New / changed | Mock behaviour | Registry | | --- | --- | --- | --- | | `IHolidayCalendar` | **introduced (b1)** | reads the seeded `iranian_holidays` table; cached via `ICacheService` | update row (was listed for b1) | | `IAnalyticsSink` | **introduced (b1)** | inserts a `system_events` row; fire-and-forget | **add new row** | | `INotificationDispatcher` | **superseded** (b0 stub → real) | writes a real `notifications` row (in-app channel); SMS/push deferred | update row (b0/15 → in-app real here) | | `IJobScheduler` (retention) | **introduced (b1)** | in-proc `BackgroundService`/interval runner for `PurgeOldReadNotifications`; real Hangfire/Quartz deferred | **add new row** | | `ICacheService` | reuse (b0) | in-memory `IMemoryCache` — config + holiday lookups cache through it | n/a (already 🟡) | | `IFieldEncryptor` | reuse (b0) | used to redact PII in `changed_fields_json` | n/a (already 🟡) | Record each introduced/changed seam in [`mocks-registry.md`](../../shared-working-context/reports/mocks-registry.md) (seam, file, what's faked, config keys, how to make it real, status 🟡). Selection stays **by registration/config**, never by an `if (mock)` branch in a handler. ## 5. Critical rules you must not get wrong - **`audit_logs` is append-only / immutable.** There is **NO update or delete of `audit_logs` rows in app code** — ever. No `Update`/`Remove` on the set, no soft-delete column, no admin edit endpoint. It is the system of record for compliance; enforce append-only at the repository/configuration level. - **EVERY `platform_configs` change is audited.** Finance must be able to **prove the exact commission/VAT rate in effect at any moment**. `SetConfig`/`UpdatePlatformConfig` must write the row **and** an `audit_logs` entry in the **same transaction** — never mutate a config without the audit entry. - **Config-as-rows = money correctness.** Commission %, VAT rate, dispute window, payout interval, EVV tolerance, cancellation tiers all flow from `platform_configs`. **Read them at compute time (cached), never hardcode**, and respect `data_type` when parsing. **Changing a rate must NOT retroactively alter already-computed bookings/ledger** — later phases snapshot the rate they used onto the booking/invoice at compute time; this phase must not encourage live re-reading of a rate for an already-priced row. - **`iranian_holidays` drives payout date shifting.** If PAYA/SATNA banks are closed (`is_bank_closed = 1`), a weekly payout shifts to the next business day via `NextBusinessDay`. Get the calendar and the "next open bank day" logic right or payouts (b13) mis-schedule. Holidays are partly movable/lunar-Hijri — the table is maintained, the calendar is a seam. - **`system_events` is analytics, NOT compliance.** Never rely on it for audit/dispute evidence — it can be sampled, dropped, or exported. Compliance facts go to `audit_logs`. Emission is fire-and-forget and must never fail or slow the user's operation. - **Notifications retention purges only READ notifications older than 90 days — NEVER unread.** The purge predicate is `is_read = 1 AND > 90d`. Unread notifications are never auto-deleted. - **`support_alerts` are internal-only.** They must **never** appear in any user-facing endpoint, query, or join. Keep them on admin-only routes with admin authorization. Never leak alert content into a user-facing `notification`. - **`data_json` is a typed contract.** Front-end navigation/deep-linking depends on its shape — version it and validate it; do not dump arbitrary blobs. Same discipline for `support_alerts` polymorphic `(entity_type, entity_id)`: validate at the application layer (no DB FK), prefer the typed FK (`booking_id`/`review_id`) when available. - **Tenancy.** A user sees only their **own** notifications. `ListMyNotifications`/`GetUnreadCount`/ `MarkAllRead` are always scoped to `ICurrentUser` — never a `user_id` from the request body. - **Encrypted PII never enters `changed_fields_json`.** When the interceptor diffs an auditable entity, redact any encrypted/PII property — write a marker (e.g. `""`), never the plaintext. - **Money is IRR `BIGINT`/`long`, no floats.** Rates (`platform_fee_rate`, `vat_rate`, `bnpl_provider_commission_rate`) are `DECIMAL` rate values; the IRR amounts they later multiply are `long`. No money column or float appears in this phase's tables, but keep the rule visible for the phases that read these rates. ## 6. Definition of Done The shared [definition-of-done.md](../_shared/definition-of-done.md), plus: - [ ] The **first marketplace migration baseline** is generated and applies cleanly to a fresh DB; all six tables (`platform_configs`, `audit_logs`, `system_events`, `iranian_holidays`, `notifications`, `support_alerts`) exist with the columns/indexes/uniques above. - [ ] `IPlatformConfig.GetConfig` returns a correctly **typed** value parsed by `data_type` and is cached; `SetConfig`/`UpdatePlatformConfig` writes the row **and** an `audit_logs` row in one transaction; `GetConfigChangeHistory` returns that audit entry. - [ ] The SaveChanges interceptor (extended from b0) writes an `audit_logs` row on an auditable-entity change with correct `changed_fields_json` and `actor_user_id`; no path updates/deletes `audit_logs`. - [ ] `IHolidayCalendar.IsBankClosed`/`IsHoliday`/`NextBusinessDay` answer correctly over the seeded calendar; the seed set is present. - [ ] `IAnalyticsSink.EmitAsync` inserts a `system_events` row and is fire-and-forget. - [ ] The **real** `INotificationDispatcher` writes a `notifications` row (b0 stub removed/replaced); `CreateNotification` → `ListMyNotifications` (unread-first, tenant-scoped) + `GetUnreadCount` work; `MarkNotificationRead`/`MarkAllRead` flip `is_read`; `PurgeOldReadNotifications` deletes only read>90d. - [ ] `RaiseSupportAlert` → `ListSupportAlerts` (admin) works; `AssignSupportAlert`/`ResolveSupportAlert` record owner + resolution; support alerts appear on no user-facing route. - [ ] All seeded config keys (§3.9) and the sample holidays are present after migration/seed. - [ ] Admin + notification controllers reachable via Swagger, returning the `OperationResult` envelope; lists paginated; admin routes authorized. - [ ] `dotnet build Baya.sln` zero new warnings; `dotnet test Baya.sln` green (the handler + integration tests this phase adds included). The **Project map** in `server/CLAUDE.md` is updated (new areas, new schema, the three new seams + where they're registered). ## 7. How to test (what a human can verify after this phase) Run `dotnet ef database update` (fresh DB) then `dotnet run` the API and use Swagger; back the unit/ integration assertions with `Baya.Tests.Setup` (SQLite). 1. **Migration creates all tables.** Apply the migration to a clean DB → all six tables + indexes/uniques exist; the seeded config keys and sample holidays are present (query them). 2. **Typed config read.** `GetConfig("vat_rate")` → `0.10m`; `GetConfig("dispute_window_hours")` → `72`; `GetConfig("booking_payment_deadline_minutes")` → `30`. A second read hits the cache. 3. **Config change is audited.** `POST update_platform_config { key:"platform_fee_rate", value:"0.18" }` → succeeds; `GET get_config_change_history?key=platform_fee_rate` shows the change (old→new, actor, timestamp); a follow-up `GetConfig("platform_fee_rate")` returns the new value (cache evicted). Confirm no path can update/delete the `audit_logs` row. 4. **Holiday calendar.** `IsBankClosed()` → `true`; `IsHoliday()` → `false`; `NextBusinessDay()` → the next open bank day. 5. **System event.** `EmitSystemEvent("nurse_search_performed", {...})` → a `system_events` row exists; failure of the sink does not surface to the caller. 6. **Notifications round-trip.** `CreateNotification(user, "booking_confirmed", {...})` then `GET get_notifications` (as that user) shows it unread-first; `GET get_unread_count` returns `1`; `POST mark_notification_read` → unread count `0`. As a **different** user, the notification is not visible (tenancy). 7. **Retention.** Insert a read notification dated >90 days, run `PurgeOldReadNotifications` → it is gone; an unread >90-day notification and a read <90-day notification both **remain**. 8. **Support alerts.** `RaiseSupportAlert("low_rating", "review", "42", review_id:42)` then `GET get_support_alerts` (admin) shows it `open`; `POST assign_support_alert` → `assigned` with owner; `POST resolve_support_alert` → `resolved` with note. Confirm it appears on **no** user-facing endpoint. ## 8. Hand off & document (close the phase) - **Docs:** update the **Project map** in [`server/CLAUDE.md`](../../../server/CLAUDE.md) — the new feature areas (`Configuration`/`Audit`/`Analytics`/`Holidays`/`Notifications`/`SupportAlerts`), the new `ref`/`ops` schema, and the three new seams + where they register (`ServiceConfiguration/`). Add a short note in `server/CONVENTIONS.md` if you established a reusable pattern (the typed config accessor, the auditable-entity marker, the retention-job seam). If you discovered/decided any config-key default the `product/` docs don't capture, record it in [`product/data-model/12-audit-config-and-reference.md`](../../../product/data-model/12-audit-config-and-reference.md) (don't invent rules — record decisions) and regenerate the HTML view per `product/CLAUDE.md`. - **Contract:** write [`../../contracts/domains/config-reference.md`](../../contracts/domains/config-reference.md) (use [`_TEMPLATE.md`](../../contracts/domains/_TEMPLATE.md)) covering the admin config/holiday/audit/ support-alert endpoints and the current-user notification endpoints — request/response shapes, the `data_json`/`changed_fields_json` payload contracts, enums (`data_type`, holiday `type`, alert `type`/`status`/`severity`, notification `type`), pagination, status codes. Re-publish the `swagger.json` snapshot per [`../../contracts/openapi/README.md`](../../contracts/openapi/README.md). Mark it **live as of backend-phase-1**, frontend consumer **f14** (notification center) / **f15** (admin config/holidays/ audit/alerts). - **Handoff & report:** write `shared-working-context/backend/handoff/after-backend-phase-1.md` (the baseline migration is live; config is read via `IPlatformConfig`; holidays via `IHolidayCalendar`; audit rows write automatically on auditable entities; `INotificationDispatcher` now writes real in-app notifications; `RaiseSupportAlert` exists for later domains to call — list the internal contracts b2…b15 should depend on, not re-create), append to `shared-working-context/backend/STATUS.md`, write `shared-working-context/reports/backend-phase-1-report.md` (what was built, what is now testable and exactly how — §7, what is mocked + how to make it real, contracts produced, follow-ups: the FK constraints for `support_alerts.booking_id`/`review_id` to be added in b9/b14), and update `shared-working-context/reports/mocks-registry.md` (`IHolidayCalendar`, `IAnalyticsSink`, the retention `IJobScheduler` → 🟡; flip `INotificationDispatcher` to in-app-real 🟡). - **Memory:** save a `project` memory note for the non-obvious decisions this phase fixes (config-as-rows read-at-compute-time + rate-snapshot rule; the auditable-entity marker + append-only enforcement; the notification retention predicate; the support-alert internal-only boundary; the new `ref`/`ops` schema) with a one-line `MEMORY.md` pointer.