# 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<T>`, 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/<Area>/{Commands|Queries}/<Name>/` 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/<Area>/{Commands|Queries}/<Name>/`. Suggested areas:
> **`Configuration`**, **`Audit`**, **`Analytics`**, **`Holidays`**, **`Notifications`**,
> **`SupportAlerts`**. Each entity gets exactly one `IEntityTypeConfiguration<T>` under
> `Persistence/Configuration/<Area>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<T> GetConfig<T>(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<T>` 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<bool> IsHoliday(DateOnly date, …)`, `Task<bool> IsBankClosed(DateOnly date, …)`, and
  `Task<DateOnly> 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 <age> > 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. `"<redacted>"`), 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<T>` 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<decimal>("vat_rate")` → `0.10m`; `GetConfig<int>("dispute_window_hours")`
   → `72`; `GetConfig<int>("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<decimal>("platform_fee_rate")` returns the new value (cache evicted).
   Confirm no path can update/delete the `audit_logs` row.
4. **Holiday calendar.** `IsBankClosed(<a seeded bank-closed date>)` → `true`; `IsHoliday(<non-holiday>)`
   → `false`; `NextBusinessDay(<a seeded holiday>)` → 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.
