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 sharediranian_holidayscalendar that shifts payout dates, in-appnotifications, and the internalsupport_alertsstaff 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-memoryIMemoryCache; your config accessor caches through it),IFieldEncryptor(Encrypt/Decrypt/Hash; reuse for any encrypted column),IDateTimeProvider(DateTimeOffset UtcNow; never callDateTime.Now),IObjectStorage, andINotificationDispatcher(b0 shipped a no-op/log stub — this phase supersedes it with a real in-appnotificationswrite; see §3.6 and §4). ICurrentUser(Application contract, Scoped, null-object outside HTTP) and the SaveChanges interceptor that stampsCreatedAt/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-registeredLoggingBehavior),OperationResult<T>, Mapster, FluentValidation, theBaseController+ MVC/versioning/Swagger stack, the REST controller surface underBaya.Web.Api/Controllers/V1/, rate-limiting policies, andBaya.Tests.Setup(in-memory SQLite). Identity tables live in theusrschema. - The 12-project Clean-Arch solution
Baya.sln. Theswagger.jsonenvelope snapshot is published so the frontend's f0 type pipeline already knows theOperationResult/ApiResultshape.
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.mdand../_shared/backend-conventions-checklist.md.server/CLAUDE.md— Project map, Persistence, Startup wiring, Features (theFeatures/<Area>/{Commands|Queries}/<Name>/layout); andserver/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— the canonical field list and roles ofaudit_logs,system_events,platform_configs, and the newiranian_holidaystable (exact columns) + the full config-key list to seed.product/data-model/11-notifications.md— the roles ofnotifications(N:1 →users,data_jsontyped payload, 90-day read-retention) andsupport_alerts(internal-only, polymorphicentity_type/entity_id+ nullable typed FKs).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— whyvat_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— theOperationResultenvelope, snake_case routes, mandatory list pagination (page/page_size), localisation (name_fareturned for reference data), status codes.../../contracts/conventions/money-and-types.md— IRRBIGINT/long, no floats.platform_fee_rate/vat_rateare rates (DECIMAL), not money; the amounts they later multiply are IRRlong.
Prior handoff
../../shared-working-context/backend/handoff/after-backend-phase-0.mdand../../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 forIHolidayCalendar,IAnalyticsSink, and the realINotificationDispatcher). - The SaveChanges interceptor b0 extended — you add audit-row writing to its diff-collection path.
- The
BaseControllerREST 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 oneIEntityTypeConfiguration<T>underPersistence/Configuration/<Area>Config/. Reads useAsNoTracking()+.Select(...)projection + pagination; commands returnOperationResult. Put these tables in a dedicatedref/opsschema (mirror how Identity usesusr) — keep them off theusrschema.
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 parsevalue),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, fromIDateTimeProvider). NoModifiedAt/IsDeleted— append-only, never updated or deleted. Index(entity_type, entity_id)andoccurred_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 beyondoccurred_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 onholiday_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 FKsbooking_id(FK→bookings, nullable) andreview_id(FK→reviews, nullable) for the common cases (declare the FK columns now; thebookings/reviewstables 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) intoBaya.Infrastructure.Persistence/Migrations. It must apply cleanly against a fresh DB and be the snapshot every later phase builds on. Rundotnet ef database update(or the project's migration entry point) to confirm.
3.2 Config — IPlatformConfig (Application contract) + cached accessor
IPlatformConfiginApplication/Contracts/with:Task<T> GetConfig<T>(string key, …)(parses by the row'sdata_type, cached throughICacheServicewith a sensible TTL and key scheme), andTask SetConfig(string key, string value, …)(admin path — writes the row and an audit entry; see §3.3) andTask<…> GetConfigChangeHistory(string key, …)(reads the audit trail filtered to that key's rows). Implement in Infrastructure.- Cache invalidation:
SetConfigmust evict the cache key it changes so the nextGetConfig<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
SavingChangesit inspects tracked entries for entities marked auditable (a marker interface, e.g.IAuditable, or an explicit allow-list —platform_configsis auditable;system_events/notificationsare not) and appends anaudit_logsrow per change withentity_type,entity_id,action, a computedchanged_fields_json(old/new per modified property; redact encrypted PII — never write plaintext into the diff), andactor_user_idfromICurrentUser. The audit write happens in the same transaction as the change. WriteAuditLogis the interceptor-driven mechanism above; also expose an explicitIAuditLogger.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) withTask EmitAsync(string name, object props, …). Mock implementation = insert asystem_eventsrow. 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).EmitSystemEventis the thin command/helper that calls the sink. Never route compliance-relevant facts here — those go toaudit_logs.
3.5 Holidays — IHolidayCalendar + seed
IHolidayCalendar(Application contract, seam introduced here) withTask<bool> IsHoliday(DateOnly date, …),Task<bool> IsBankClosed(DateOnly date, …), andTask<DateOnly> NextBusinessDay(DateOnly date, …). Mock implementation = the seedediranian_holidaystable (cache the lookups throughICacheService).NextBusinessDayskipsis_bank_closeddays (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
INotificationDispatchermock with a real in-app implementation that writes anotificationsrow (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) callDispatchAsync(...)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, setsis_read+read_at),MarkAllRead(bulk for the caller). - Retention job
PurgeOldReadNotifications— hard-deletes notifications whereis_read = 1ANDcreated_at(orread_at) older than 90 days; never deletes unread. There is no scheduler in the codebase yet — introduce the job behind a smallIJobScheduler/hosted-service seam (mock = aBackgroundServicerunning 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)andResolveSupportAlert(alert_id, note)— owner + resolution trail; forward-only status (open→assigned→resolved).ListSupportAlerts— admin-only query, paged, filter bytype/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_configskeys (seed all, withdata_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 asjsonor 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 correctis_bank_closedflags soIsBankClosed/NextBusinessDayare testable. The full, maintained, partly-lunar-Hijri calendar feed is (DEFERRED) behind theIHolidayCalendar"make it real" path.
3.10 Out of scope (DEFERRED — do not build here)
roles/user_rolesadmin 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_logsmonth-partitioning + cold-storage archival (IArchiveStore);system_eventswarehouse/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_logsis append-only / immutable. There is NO update or delete ofaudit_logsrows in app code — ever. NoUpdate/Removeon 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_configschange is audited. Finance must be able to prove the exact commission/VAT rate in effect at any moment.SetConfig/UpdatePlatformConfigmust write the row and anaudit_logsentry 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 respectdata_typewhen 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_holidaysdrives payout date shifting. If PAYA/SATNA banks are closed (is_bank_closed = 1), a weekly payout shifts to the next business day viaNextBusinessDay. 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_eventsis analytics, NOT compliance. Never rely on it for audit/dispute evidence — it can be sampled, dropped, or exported. Compliance facts go toaudit_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_alertsare 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-facingnotification.data_jsonis a typed contract. Front-end navigation/deep-linking depends on its shape — version it and validate it; do not dump arbitrary blobs. Same discipline forsupport_alertspolymorphic(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/MarkAllReadare always scoped toICurrentUser— never auser_idfrom 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) areDECIMALrate values; the IRR amounts they later multiply arelong. 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 bydata_typeand is cached;SetConfig/UpdatePlatformConfigwrites the row and anaudit_logsrow in one transaction;GetConfigChangeHistoryreturns that audit entry.- The SaveChanges interceptor (extended from b0) writes an
audit_logsrow on an auditable-entity change with correctchanged_fields_jsonandactor_user_id; no path updates/deletesaudit_logs. IHolidayCalendar.IsBankClosed/IsHoliday/NextBusinessDayanswer correctly over the seeded calendar; the seed set is present.IAnalyticsSink.EmitAsyncinserts asystem_eventsrow and is fire-and-forget.- The real
INotificationDispatcherwrites anotificationsrow (b0 stub removed/replaced);CreateNotification→ListMyNotifications(unread-first, tenant-scoped) +GetUnreadCountwork;MarkNotificationRead/MarkAllReadflipis_read;PurgeOldReadNotificationsdeletes only read>90d. RaiseSupportAlert→ListSupportAlerts(admin) works;AssignSupportAlert/ResolveSupportAlertrecord 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
OperationResultenvelope; lists paginated; admin routes authorized. dotnet build Baya.slnzero new warnings;dotnet test Baya.slngreen (the handler + integration tests this phase adds included). The Project map inserver/CLAUDE.mdis 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).
- 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).
- 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. - Config change is audited.
POST update_platform_config { key:"platform_fee_rate", value:"0.18" }→ succeeds;GET get_config_change_history?key=platform_fee_rateshows the change (old→new, actor, timestamp); a follow-upGetConfig<decimal>("platform_fee_rate")returns the new value (cache evicted). Confirm no path can update/delete theaudit_logsrow. - Holiday calendar.
IsBankClosed(<a seeded bank-closed date>)→true;IsHoliday(<non-holiday>)→false;NextBusinessDay(<a seeded holiday>)→ the next open bank day. - System event.
EmitSystemEvent("nurse_search_performed", {...})→ asystem_eventsrow exists; failure of the sink does not surface to the caller. - Notifications round-trip.
CreateNotification(user, "booking_confirmed", {...})thenGET get_notifications(as that user) shows it unread-first;GET get_unread_countreturns1;POST mark_notification_read→ unread count0. As a different user, the notification is not visible (tenancy). - 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. - Support alerts.
RaiseSupportAlert("low_rating", "review", "42", review_id:42)thenGET get_support_alerts(admin) shows itopen;POST assign_support_alert→assignedwith owner;POST resolve_support_alert→resolvedwith 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 newref/opsschema, and the three new seams + where they register (ServiceConfiguration/). Add a short note inserver/CONVENTIONS.mdif 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 theproduct/docs don't capture, record it inproduct/data-model/12-audit-config-and-reference.md(don't invent rules — record decisions) and regenerate the HTML view perproduct/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, thedata_json/changed_fields_jsonpayload contracts, enums (data_type, holidaytype, alerttype/status/severity, notificationtype), pagination, status codes. Re-publish theswagger.jsonsnapshot 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 viaIPlatformConfig; holidays viaIHolidayCalendar; audit rows write automatically on auditable entities;INotificationDispatchernow writes real in-app notifications;RaiseSupportAlertexists for later domains to call — list the internal contracts b2…b15 should depend on, not re-create), append toshared-working-context/backend/STATUS.md, writeshared-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 forsupport_alerts.booking_id/review_idto be added in b9/b14), and updateshared-working-context/reports/mocks-registry.md(IHolidayCalendar,IAnalyticsSink, the retentionIJobScheduler→ 🟡; flipINotificationDispatcherto in-app-real 🟡). - Memory: save a
projectmemory 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 newref/opsschema) with a one-lineMEMORY.mdpointer.