Files
2026-06-28 21:59:59 +03:30

32 KiB

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 (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. 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:

  • 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 stubthis 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

Product / domain truth (the business rules are decisions, read them — don't infer from code)

  • 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 — 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.mdwhy 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.mdwhy 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

Prior handoff

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 (openassignedresolved).
  • 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 (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, 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); CreateNotificationListMyNotifications (unread-first, tenant-scoped) + GetUnreadCount work; MarkNotificationRead/MarkAllRead flip is_read; PurgeOldReadNotifications deletes only read>90d.
  • RaiseSupportAlertListSupportAlerts (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_alertassigned with owner; POST resolve_support_alertresolved 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 — 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 (don't invent rules — record decisions) and regenerate the HTML view per product/CLAUDE.md.
  • Contract: write ../../contracts/domains/config-reference.md (use _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. 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.