# Track D Status

## Daily log — 2026-10-03
- **T-C-16 grade benchmark consumer:** D now ingests aggregate-only
  `GradeBenchmarkCohortSnapshot` learning events and binds C's `GradeBenchmark`
  port to the latest grade snapshot in the append-only analytics sink. The adapter
  returns only cohort size, mean, and dispersion; small-cohort snapshots are zeroed
  and absent grades return an empty aggregate. Focused tests cover payload minimization,
  redelivery, latest-snapshot
  selection, grade isolation, empty results, and provider resolution.
- **T-D-05 assignment visibility contract:** added the D-owned
  `CourseGroupMembership::activeGroupIdsForUser(int)` read interface and bound its
  engagement-only implementation in `EngagementServiceProvider`. It returns sorted,
  unique IDs only for the user's memberships in approved/active groups; focused provider
  tests cover joinable and excluded statuses, no membership, and deterministic uniqueness.
- **Analytics identity-data minimization:** new `UserCreated` ingestion stores only
  `userId` and `userType`; its frozen A event and identity event history remain
  unchanged. Regression coverage checks both the ingested payload and raw analytics
  read response. The raw read endpoint also redacts email from legacy `UserCreated`
  payloads, while the append-only rows remain unchanged at rest. Existing legacy email
  data is therefore still stored in analytics; free-text `reason` values in other
  event payloads remain unresolved pending a retention/redaction policy.
- **T-D-18 probe coverage:** health probes now accept and retain non-negative
  `db_query_ms` measurements, and the protected performance read returns the
  latest DB query time by component. Probe ingestion, read projection, and
  negative-value validation have focused regressions.
- **Analytics ingestion contract limits:** `MarketingCampaignEvent` now enters the
  analytics registry/listener path and exact queue redeliveries dedupe on the
  event payload. Its frozen payload has no occurrence identifier, so a later
  identical state transition (for example, rescheduling a campaign back to the
  same state) can still collapse as a duplicate. `MarketingService::sweepDrips`
  now supplies `drip-enrollment:{enrollmentId}:step:{stepIndex}` as the
  `NotificationRequested` delivery dedupe key. `StudyPlanService` now keys a
  check-in by plan ID and its persisted fire date, and a milestone by plan ID
  and milestone identity. Analytics stores only a hash of any supplied key, so
  notification content is not copied. Events from producers without a stable
  key still dedupe on user/category/critical.
- **Audit-hook follow-up:** replaced D-owned bulk model updates/deletes in
  `RosterService`, `ConnectorService`, `LiveSessionService`,
  `WebhookDispatchService`, and `ForumService` with model-level writes so the
  identity-billing audit hooks capture affected rows. Regression coverage
  verifies seat revocation, session no-show transitions, dead-letter cleanup,
  and forum upvote removal. The notification inbox's bulk read/clear endpoints
  now emit aggregate audit events when rows change. Focused D suites passed.
- **Affiliate conversion counter audit:** recording a conversion now locks and
  saves its affiliate link's purchase counter instead of issuing a static
  query-builder increment. `AffiliatePortalTest` asserts the before/after audit
  snapshot for that counter.
- **T-D-14 sponsor milestone delivery:** maintenance now publishes milestones for
  live placements before applying same-day expiry, so a completion milestone is
  delivered at the expiration boundary. Draft/pending/removed placements are
  excluded from milestone dispatch. Both the scheduled maintenance command and
  the tenant-scoped expiry endpoint dispatch before the expiry sweep. Regressions
  verify completion delivery through both paths and ensure a draft placement
  produces no milestone. `SponsorPortalTest`: 28 tests / 156 assertions passed.
- **T-D-14 report delivery spec audit:** MDA Data Ownership §7.2 calls for
  report artifacts to be delivered by secure link, not email attachment. The
  report archive/download route is authenticated and tenant-scoped; impact
  stories use expiring random-token links stored by hash. No report email
  attachment path exists. PDF rendering (including invoice PDFs) and real SMTP
  transport for sponsor notifications remain unresolved; object-storage-backed
  signed URLs depend on the still-open OD-39 provider/lifecycle decision.

**NotificationService LIVE** (2026-09-24, merged `81dd0a4`): the real dispatcher
(`App\Services\Engagement\NotificationDispatcher`, implementing the frozen
`NotificationService` interface) is **on main**. A/B/C: re-run your suites against
the real thing — inject `App\Services\Engagement\NotificationService` (constructor
injection; no-arg default resolves the concrete transport adapters, so production
wiring needs no container config). Event-side wiring is auto-discovered
(`NotificationRequested` → `HandleNotificationRequested`). Criticals bypass
mute/quiet/digest at the dispatcher level.

**NotificationService app-level binding** (2026-09-24, T-D-12): the frozen
`NotificationService` interface is now also bound to the real dispatcher at the
application level in `WebhookDispatchServiceProvider` (D-owned), so any
`app()->make(NotificationService::class)` outside tests resolves to the real
dispatcher. Track test suites override this singleton in their own `setUp` with
recording doubles, so tests are unaffected.

## Daily log — 2026-10-02
- **T-D-06 response-time analytics:** `first_agent_response_at` is set only by the first agent message, inside an engagement transaction holding the ticket row lock. The analytics event carries source-computed seconds from ticket creation to that first reply. T-D-15 reports `first_agent_response.count` and `average_seconds` (arithmetic mean over responded tickets; null mean when count is zero; unanswered tickets are excluded). A timely reply satisfies the response-window SLA and prevents a later breach; a late reply marks breach, and a breach already recorded never regresses. Repeated agent replies do not emit additional first-response events.
- **T-A-12 / tenant-gate integration:** integrated the existing A12 implementation and added A-owned adapters from identity_billing tenant/member records to D corporate, CSR, organization, and sponsor contracts. A21 corporate seats continue through `SeatLicenseService`. Focused validation: 14 tests / 70 assertions across tenant lifecycle, all four adapter contracts, and a real corporate portal HTTP request including suspended-tenant denial. `SupportAccountGateAdapter` is bound and its state mapping is tested, but SupportService does not yet consume that contract; acceptance does not define a lifecycle denial policy. T-D-19 uses production A12/A21 paths for tenant scenarios, public affiliate click and authenticated signup callbacks, and a persisted C learner completing catalog content through the learner HTTP route; its tenant dashboard reads now expose `as_of`, and scenario 6 verifies materialized analytics rollups within the 15-minute freshness window. The affiliate test still submits its conversion amount directly through D's API instead of deriving it from an A billing/payment transaction.
- **Learning event consumers:** D now routes `DoubtEscalatedToTeacher` to an idempotent high-priority support ticket for genuine teacher handoffs, sends consent-filtered generic notifications for `ParentalAlert`, and ingests `PerformanceAlert` into the analytics sink. The engagement dashboard counts performance alerts. Focused listener and dashboard tests pass.
- **T-D-14 scheduler:** registered `sponsors:maintain` every minute with overlap protection in `EngagementServiceProvider`; `schedule:list` shows it alongside scheduled notifications, billing, and backup processing. Maintenance tenant discovery now includes standalone visibility-event tenants, with a regression for null-placement events. Deployment still needs to invoke `schedule:run` each minute.
- **Remaining contract decisions:** forum enforcement is bound to the agreed `all` content-entitlement scope and covered separately by the production-adapter test. T-D-06's A-owned `SupportAccountGateAdapter` is bound, but `SupportService` does not currently consume `accountState()`. The support acceptance criteria do not specify which support operations should be denied for suspended/deactivated users, so no lifecycle rule is inferred. T-D-07's billing-authoritative conversion bridge is not implemented: A emits frozen `PaymentSucceeded` for initial subscription charges, renewals, and plan-upgrade proration, while its payload (`userId`, charged `amount`, `currency`, `invoiceId`) cannot distinguish eligible conversions or establish the commissionable amount/tax basis. D must not query A-owned invoice tables to infer these fields; the current cross-tenant E2E still supplies the amount through D's API. A must add a conversion-source resolver or event with validated user, eligible kind (purchase/renewal; excluding proration), commissionable minor-unit base and gross/net/tax semantics, currency, paid time, and stable source reference. The shared contract must also define refund/reversal behavior. `AffiliateProgramConfig` also exposes only `tierRates()` and `minimumPayoutThreshold()`; it does not expose the stored level thresholds, tier-2 rate, recruitment reward type/value/trigger, leadership rate/downline criteria, or payout frequency/schedule day. `CommissionLedgerService::createPending(..., kind: 'leadership')` currently selects the ordinary tier rate, so it must not be used for leadership accrual until a distinct rate and qualification contract is agreed. Also, D's `requestPayout()` settles D engagement commission rows and decrements its balance at request time, while the A identity-billing ledger entries remain pending: D has no A operation to reserve/settle/reverse a selected set of source references or a contract that defines whether A settlement happens on request, approval, or payment. Before implementing multi-level rewards or scheduled/authoritative payouts, the shared contract must specify the config fields and evaluation snapshots above, referral/downline attribution and qualifying event, stable cross-ledger entry identifiers, payout lifecycle transition timing, partial-batch selection, rejection/reversal behavior, and idempotency/event semantics.
- **T-A-25 engagement export source:** D registers `EngagementExportDataSource` under A's
  `BackupExportService::EXPORT_SOURCE_TAG` and appends engagement-owned records to the
  `user_data` portability export. The adapter reads only the engagement connection and
  directly owner-scoped rows; requester-authored support messages are included only
  under the requester's own ticket. Projection omits push tokens, notification payloads
  and deep links, affiliate tracking tokens/payment details, and related participant,
  agent, thread, session, conversion, and referral identifiers. Third-party support
  replies are excluded. A25 route integration verifies the D rows are included and these
  fields/other users' forum content stay out. No cross-database joins or A-owned edits.
- **T-A-23 notification settings consumer:** D's production dispatcher reads A-owned
  `notification_defaults` through `SystemSettingsService` and uses severity channel
  defaults/recipient-role routing while keeping category mutes, per-user channel toggles,
  verified-SMS/push-token checks, quiet hours, and digest behavior in D's dispatcher.
  Due `scheduled_notifications` are delivered by the registered
  `engagement:notifications:deliver-scheduled` command through `NotificationService`;
  deterministic schedule-content + recipient dedupe keys make repeated scheduler runs safe.
  Supported scheduled audiences are `user_ids`, `user_id`, `roles`, or `all`, resolved
  against identity_billing users/roles only; unsupported selectors such as the A test
  sample's `grade` are logged and skipped until an owned audience contract exists. No
  cross-database joins or A-owned edits. D's provider registers a minute schedule; the
  deployment must invoke Laravel `schedule:run` each minute for delivery. Numeric alert
  thresholds remain producer-level because the frozen notification payload has no
  universal metric/comparison semantics. Tests: A23 consumer 4/4 (22 assertions),
  dispatcher regression 10/10 (71 assertions), Engagement feature suite 266/266
  (1,643 assertions).
- T-D-17 production wiring and available finance fields: registered
  `FinanceDashboardServiceProvider` in `bootstrap/providers.php`; removed the
  test-only duplicate bindings so endpoint tests resolve the same provider used
  by the app. The read model now exposes settled commissions by affiliate and
  issued payouts by affiliate with the status that the event contracts actually
  establish. License rows expose renewal opportunities at lapsed/≤60-day expiry
  and upgrade opportunities only at full seat utilization. The historical
  trend-period fix is covered by an `as_of` regression test.
- Remaining contract limits: the frozen `PaymentFailed` event has no `reason`
  field, so T-D-17 leaves it unchanged; the production T-A-20 feed must supply
  and validate reason-bearing payment records. `CommissionSettled` and
  `PayoutIssued` establish only settled and issued states; pending/earned
  commissions and requested/approved/paid/rejected payout states require
  additional source fields/events. T-A-21 now supplies the real identity-billing
  `LicenseFeed` via `SeatLicenseService`; T-A-20 now supplies the production
  `SubscriptionHealthMetricsFeed` from identity-billing ledgers. The dashboard
  test asserts the empty-ledger production values (0 renewals / 0 total).

Pre-flight (2026-09-24): Track A Phase 0 verified on `origin/main` — T-A-01…T-A-06
+ T-A-24 all merged (merge commits c7a70f9, 91c1c62, f62aa76, 4f73694, 0d54b58,
4ad28ae, 5f33d23; T-A-24 concrete contract at 604496d). GATE 1 pre-condition
satisfied for Track D.

## Daily log — 2026-10-01
- **Merge-queue rebase onto new `origin/main` (`7e09d0b`, post T-C-08 merge).**
  The orchestrator merged T-C-08 (Track C, SRS runtime) to main — all its files
  (`app/**/Learning/**`, `routes/api_learning.php`, `track_c.md`) are disjoint from
  Track D's Engagement/Analytics surfaces, so all 6 pending D branches rebased
  onto `7e09d0b` with **zero conflicts** and each stays ownership-clean (0 lines
  touching A-owned `bootstrap/providers.php` — DEPS⚡ stubs bound in D-owned test
  setUp/concerns; production provider lines documented below for orchestrator).
  Force-pushed, all CURRENT on `7e09d0b`, full suite green on the new base:
  T-D-06 `c0d7252` 524/524 · T-D-17 `dd94526` 516/516 · T-D-10 `3092164` 532/532 ·
  T-D-11 `3378ecb` 520/520 · T-D-13 `cf5deee` 520/520 · T-D-14 `642cae6` 521/521
  (counts include T-C-08's SRS tests now on main). T-D-19 remains BLOCKED on
  T-D-17 not being merged to main (T-D-15/16/18 merged).
- **T-D-19 readiness / unblock-set analysis** (for orchestrator; no code change).
  T-D-19's *declared* `DEPS:` are only T-D-15/16/17/18 (`developer_D.md:445`) — the
  sole unmet one is **T-D-17**. But its 6 E2E scenarios exercise tenant-portal code
  from **five** branches that are pushed-but-unmerged, so for a *fully verifiable*
  T-D-19 the orchestrator should land all of: **T-D-17** (scenario 6 finance, +
  declared DEPS gate), **T-D-10** (scenario 1 CORP), **T-D-11** (scenario 2 CSR),
  **T-D-14** (scenario 3 SPON), **T-D-13** (scenario 4 ORG). Scenarios 5 (affiliate,
  T-D-07) and 6's analytics leg (T-D-15) are already on main. Cross-track fixture
  infra T-D-19 builds against (`track_a/b/c.md`, `database/seeders`,
  `UserFactory`/`LearnerProfileFactory`/`LiveSessionFactory`) is confirmed present
  on main. **Merge 10/11/13/14/17 (any order) → T-D-19 fully unblocked.**
- **Merge-queue rebase onto new `origin/main` (`9c309fc`, post T-A-09 merge).**
  The orchestrator merged T-A-09 (Track A, account lifecycle — relocated
  user-domain services to `app/Services/Auth/`, touched `Models/User`,
  `UserFactory`, `api_identity.php`) — all disjoint from Track D's
  Engagement/Analytics surfaces, so all 6 pending D branches rebased onto
  `9c309fc` with **zero conflicts**, each still ownership-clean (0 lines on
  `bootstrap/providers.php`). Force-pushed, all CURRENT on `9c309fc`, full suite
  green on the new base: T-D-17 `31e8ff8` 529/529 · T-D-06 `3decd7f` 537/537 ·
  T-D-10 `9676dd5` 545/545 · T-D-11 `9eaea86` 533/533 · T-D-13 `6d76edd` 533/533 ·
  T-D-14 `9c835a0` 534/534 (counts now include T-A-09's user-lifecycle tests).
  T-D-19 remains BLOCKED on T-D-17 not being merged to main (T-D-15/16/18 merged).

## Daily log — 2026-09-30
- T-D-17 Finance & revenue dashboards: **rebased onto latest `origin/main`**
  (`8c06d7f`, post T-D-16 merge) — 4 files conflicted (providers.php, README.md,
  api_engagement.php, track_d.md), resolved by union (kept both sides' additive
  content). Finance read-model over the T-D-02 event stream (revenue/refunds/
  commissions/payouts/trends) + DEPS⚡ domain feeds (subscription health, license
  seats — never a cross-DB query). Pure read (`/api/finance/summary`, RBAC
  deny-by-default `finance.view`, always `as_of`). Per the track-ownership rule,
  the earlier edit to A-owned `bootstrap/providers.php` was **reverted on this
  branch** — the DEPS⚡ feed stubs + `FinanceDashboardService`/`FinanceEventIngestor`
  are now bound in the D-owned test concern (`BootsAnalytics::setUpAnalytics`), so
  the branch is green **without** touching the shared file. Full suite
  **497/497** (2712 assertions). The production provider line is handed to the
  orchestrator (see note below). Re-pushed. DEPS T-D-02 (merged); DEPS⚡ T-A-20, T-A-21 (stubs).
- T-D-16 Custom reporting: **implemented from scratch** (DEPS T-D-15; no
  pre-existing branch) on a fresh `d/T-D-16-custom-reporting` off latest
  `origin/main` (`391f50f`). Saved reusable report templates
  (`report_configs`), on-demand runs as a **pure function of type+params+period**
  over the T-D-02 `analytics` sink (`ReportGenerator` — pure read, lives in the
  D-owned `Services/Engagement` path), `report_schedules` (daily/weekly/monthly;
  pause/resume/sweep) and `report_runs`, and `report_exports` (opaque unguessable
  token + hard `expires_at`; entitled-data-only). 5 report types (revenue, user,
  content, affiliate, institute); institute/tenant scope is enforced in PHP over
  the fetched sink rows (driver-portable — mirrors `DashboardTileService`). 4
  models (engagement; each declares `protected $connection = 'engagement'` per
  the ModelConnectionLint) + 1 migration (4 tables) + 3 services + 1 controller
  + routes (gated `rbac.can:analytics.view`). 8 tests (5 ACs + RBAC-deny +
  platform-wide-requires-super_admin + existence-safe token). Full suite
  **488/488 green** (2661 assertions). Additive `makeUserWithRole`/
  `grantPermission` helpers added to the D-owned `BootsAnalytics` concern.
- T-D-09 Marketing & communication: **rebased onto latest `origin/main`** (post
  T-D-04 merge) — 4 files conflicted (providers.php, README.md,
  api_engagement.php, track_d.md), resolved by union (kept both sides' additive
  content; dropped the stale T-D-04 branching note now that T-D-04 is merged).
  13 feature tests (5 frozen ACs: campaign state machine, stable A/B, popup
  frequency cap, template versioning, SMS OTP via A's TokenService); full
  suite **480/480 green** (2616 assertions).
- T-D-04 Live sessions: **merged to main (`8e2d713`)** — orchestrator picked up
  the rebased branch (2 conflicts: routes + track_d.md).
- T-D-06 Customer support: **rebased onto latest `origin/main`** (`8c06d7f`,
  post T-D-16 merge) — 4 files conflicted (providers.php, README.md,
  api_engagement.php, track_d.md), resolved by union (kept both sides' additive
  content; kept main's authoritative status rows, dropped T-D-06's stale duplicate
  row). Per the track-ownership rule, the earlier edit to A-owned
  `bootstrap/providers.php` was **reverted on this branch** — the DEPS⚡
  `SupportAccountGate` stub is now bound in the D-owned test concern
  (`BootsEngagement::setUpEngagement`), so the branch is green **without**
  touching the shared file. Full suite **505/505** (2749 assertions). The
  production provider line is handed to the orchestrator (see note below).
  Re-pushed.
- T-D-15 Super-admin + tenant dashboards: **pushed** — the analytics read-model
  layer ("source events → aggregate job → read endpoint"). Composite
  `/dashboard/summary` + 6 per-tile endpoints (users, revenue, content,
  system_health, engagement, support), each a PURE read over the T-D-02
  `event_ingestion` sink returning `metrics` + `as_of`; every view audit-logged
  (tile/period/viewer/as_of/timestamp). Rollup materialized by a queued
  idempotent recompute job (upsert, never duplicate); health/support tiles
  aggregate matching sink events (populating once T-D-18/T-D-06 emit them). 6
  new tests (per-tile as_of, read-never-writes lint, planted→aggregate sync
  dispatch + idempotent upsert, view audit, RBAC deny + unknown-tile); full
  suite **453/453 green** (2460 assertions). DEPS T-D-02 (merged) + DEPS⚡
  T-A-20/T-C-16 (satisfied by the analytics sink).
- T-D-18 System health, monitoring, incident management: **pushed** — public
  `/health/status` (worst-state of the latest probe per component; unknown when
  no probes) + ops dashboard tile (`rbac.can:analytics.view`, deny-by-default):
  performance metrics, **uptime math over daily/weekly/monthly windows** (mean
  of in-window probes; out-of-window + null-uptime probes excluded; empty
  window → `null`, never a fabricated 100%), **threshold alerts fire ONCE**
  (fingerprinted `(component, metric, condition)` — a still-open alert suppresses
  dupes; re-fires after resolve), **incident lifecycle** (open → in-progress →
  resolved; resolving REQUIRES a recorded resolution, else 409), and **cache
  clear resets state** (→ `cleared` + timestamp; refresh → `cached`). 9 new
  tests (4 ACs + public-probe + RBAC-deny); full suite **447/447 green**
  (2378 assertions). DEPS T-D-02 (merged); no DEPS⚡.
- T-D-11 CSR tenant portal (funding): **rebased onto latest `origin/main`** (post T-D-09 merge) — 4 files conflicted (providers.php, README.md, api_engagement.php, track_d.md), resolved by union; full suite **493/493 green** (2686 assertions). Programs (new / funds
  existing, `catalog_program_id` nullable) with a CSR-admin approval workflow
  before release; budget tranches (append-only reallocation, over-limit gated);
  disbursements strict lifecycle (`scheduled→pending→released/failed`, illegal
  → 409); milestone delay → program at-risk + alert; beneficiaries with
  **per-student identity masked in every CSR response** (name/email absent,
  aggregates OK); certificates (re-issue supersedes); funding invoices raised
  to the CSR + reconciliation variance (committed vs disbursed vs invoiced);
  tenant-scoped impact report with aggregate outcomes, funding totals, and `as_of`
  (no beneficiary identity or per-student rows in its response).
  9 models + 6 services + `CsrTenantGate`/stub (DEPS⚡ T-A-12) + D-owned
  `CsrServiceProvider` (append-only) + 1 controller + 11 routes + 13 tests
  (5 ACs + invariants, 70 assertions). DEPS T-D-01 + T-D-02 (merged).
- T-D-08 Gamification config + leaderboards: **pushed** — real
  `GamificationConfigServiceImpl` (replaces the T-D-03 stub behind the same
  binding) + leaderboard/computation + shared goals. 11 new tests (5 ACs + C
  contract-shape regression + RBAC gate); full suite **438/438 green** (2320
  assertions). DEPS T-D-03 (merged) + T-C-07 (soft, merged). (Now MERGED to main
  as `4a685ef`.)
- T-D-10 Corporate tenant portal: **pushed** — onboarding checklist, roster
  (bulk import per-row all-or-nothing; work-email must match the verified
  domain), departments (unique per account, delete blocked while members
  exist), seat assignment (enrollment REQUIRES a seat — `409
  seatless_enrollment`, plan never auto-expanded, one-to-one), mandatory
  training (deadline required; dept-only-unless-ongoing; escalation ladder
  employee→dept mgr 14d→corp admin 30d, halts on completion + resolution
  notice), HRIS/LMS/SIS (active HRIS = source of truth — manual dept edit
  reverted by the next sync; completions deduped by activity id; failed-sync
  exponential backoff ≤24h), budgets (80%/100% alerts fire ONCE per period),
  compliance score (all modules done AND ≥ per-course min passing score,
  default 70%). 6/6 ACs + tenant gate (verified+active) + CORP role checks
  (next-session role change). DEPS⚡ T-A-12 + T-A-21 → D-owned stubs. 11
  models, 5 services, 2 DEPS⚡ contracts + 2 stubs, 1 provider, 1 migration
  (11 tables), 1 controller, 12 routes, 25 tests. Full suite **463/463 green**
  (2423 assertions). DEPS T-D-01 (merged) + T-D-02 (merged).

## Daily log — 2026-09-24
- T-D-01 Notification platform: **merged to main** (81dd0a4) — 17 passing.
- T-D-02 Analytics ingestion: **pushed** — idempotent for the 15 README events.
- T-D-03 Engagement base model: **pushed**.
- T-D-04 Live sessions: **pushed**.
- T-D-06 Customer support: **pushed**.
- T-D-07 Affiliate portal: **pushed**.
- T-D-09 Marketing & communication: **pushed** — 13 passing.
- T-D-12 Webhook/API dispatch engine: **merged to main (`2150d93`)** — 12 new tests
  green; full Engagement suite 29 passing, 0 failing, 0 risky.

| Task | Status | Branch | Notes / blockers |
|---|---|---|---|
| T-D-01 | ✅ merged (`81dd0a4`) | track-d/T-D-01-notification-platform | NotificationService interface + real dispatcher + endpoints. ACs covered (critical-never-muted, quiet-hours, digest, unread counts, idempotency, event bus). 17 tests; full suite 102 passing. |
| T-D-02 | ✅ merged (`4d0dace`) | d/T-D-02-analytics-ingestion | analytics conventions + 15-event idempotent ingestion. Merged to main 2026-09-30. |
| T-D-03 | ✅ merged (`708f9ef`) | d/T-D-03-engagement-base-model | `BaseEngagementModel` ($connection='engagement', extends BaseDomainModel → audited) + engagement schema-marker migration; `GamificationConfigService` (interface, frozen for C's DEPS⚡) + stub; D-owned `EngagementServiceProvider` binds `NotificationService`→dispatcher + `GamificationConfigService`→stub + additive C-consumed methods. Merged to main 2026-09-30. |
| T-D-04 | ✅ merged (`8e2d713`) | d/T-D-04-live-sessions | Live sessions (class/doubt-clearing/webinar): scheduling (no past-dated 422, **no teacher double-booking → 409**), join (late-joiner flag, group-cap 409), cancel (**never silent** — every enrollee notified), doubt bookings (DEPS⚡ T-A-17 cap: free 1 / entitled 5, **exceed → 409 entitlement_exceeded**), recordings (retention sweep idempotent; two-step replace), feedback (**once per session → 409**; instructor view asserts no user_id/name/email). Rebased onto latest `origin/main` (post T-D-15 merge); 14 tests. DEPS T-D-01 (merged) + DEPS⚡ T-A-17/T-B-05 (stubs). Full suite 467/467 (2543 assertions). |
| T-D-05 | ✅ local integration | d/T-D-05-forums-study-groups | Forum tier checks now bind to A’s `SubscriptionService` through `ForumTierGateAdapter`: restricted forums require `all` content scope, so course/subject-only plans are refused. A’s existing entitlement semantics include eligible active/trial/grace subscriptions and full-access seats; this is entitlement, not a paid-only billing check. Forum feature tests explicitly bind their double. Added the D-owned `CourseGroupMembership` read contract and engagement-only provider binding for B’s assignment visibility consumer; focused provider tests cover its status filtering and stable unique IDs. |
| T-D-06 | ✅ local integration | codex/integration-2026-10-02 | Support tickets, SLA, assignment, chat, knowledge-base suggestions and account requests are implemented. A now binds `SupportAccountGate` to `SupportAccountGateAdapter`, which reads identity user lifecycle state. |
| T-D-09 | ✅ merged (`391f50f`) | d/T-D-09-marketing-communication | Marketing & communication (see note below). DEPS T-D-01 (merged) + DEPS⚡ T-A-15/T-B-05 (stubs). 13 tests. Merged to main post T-D-04 merge; suite 480/480 (2616 assertions). |
| T-D-08 | ✅ merged (`4a685ef`) | d/T-D-08-gamification-leaderboards | Real `GamificationConfigServiceImpl` (replaces the T-D-03 stub behind the same `EngagementServiceProvider` binding — C's call sites never change). **Non-retroactive point rules** — a change appends a new effective row + retires the old (version chain); already-earned ledger points are untouched. **Leaderboard** — anonymous by default; a member is named ONLY with stored `named_leaderboard_consent`; schedule-based computation materializes a per-user snapshot into the analytics read-model (`LeaderboardSnapshot`) read from C's immutable `PointsLedger` (no cross-DB join). **Badges** — all-or-nothing criteria (`{type, points}` the C contract shape); streak milestones EXACT (7/30/100); **revocation recorded with a reason** (never a silent delete) and drops out of the auto-award criteria. **Challenges** — draft→active→completed, hard `max_entries` cap (`409 challenge_max_entries`), documented tie-break, reward issuance recorded/unissued flagged. **Shared goals** — parent proposes, child MUST agree (a parent cannot force it — `409 not_the_child`); a proposed goal the child never agreed to is NOT tracked; agreed goals are adjustable (append-only record). **Tokens** — balance visible to parent + child. Admin config writes are deny-by-default (`rbac.can:gamification.manage`). 16 models (engagement) + 1 (analytics), 5 services, 1 controller, 2 migrations (17 tables), 11 tests. DEPS T-D-03 (merged) + T-C-07 (soft, merged). Merged to main 2026-09-30. |
| T-D-07 | ✅ local follow-up | d/T-D-07-affiliate-portal | Affiliate portal runtime: enrollment gate (approved + A account eligibility, re-checked at service boundary), referral links (unique token; **QR rotates on regeneration — stale QR 410**), tracked links, attribution, accrual-time commission snapshots, payout workflow, immutable commission disputes, tier history, win-back and scheduled promotions. Rejected D enrollments can be re-approved after A application reopen; append-only enrollment decision history records rejection/re-approval reason, actor, and application reference. Added D-owned `AffiliateAccountStatus::isEligibleForReferrals(userId)` gate; compatibility stub preserves D-only behavior, A binds its identity-backed adapter. 2 focused regression tests added. |
| T-D-10 | ✅ local integration | codex/integration-2026-10-02 | Corporate portal flows remain on engagement. Production corporate tenant membership, verified-domain/transactability, CSR/ORG/SPON tenant gates use A-owned adapters over identity_billing; the seat contract binds to A21 SeatLicenseService. TenantGateAdapterTest and TenantPortalGateIntegrationTest verify real container and HTTP behavior. |
| T-D-12 | ✅ merged (`2150d93`) | d/T-D-12-webhook-dispatch | Webhook/API dispatch engine: signed at-least-once dispatch, DLQ reprocess, API-key middleware, embed tokens, sync runs, consistency checker. 6/6 ACs + IP allowlist. `WebhookDispatchEngineTest`: 12/12 passed in current workspace. DEPS T-D-01 (merged) + T-A-24 (merged) — both real. |
| T-D-18 | ✅ merged | d/T-D-18-system-health-monitoring | System health, monitoring, incident management (engagement; `BaseEngagementModel`-derived). **Public `/health/status`** (orchestrator probe; overall = worst of the latest probe per component — any `down`→down, else `degraded`, else healthy; `unknown` when no probes) + ops dashboard tile gated `rbac.can:analytics.view` (deny-by-default; super_admin via `'*`). **Performance metrics** — page load, video quality, API response, per-component. **Uptime vs 99.9% SLA** over daily/weekly/monthly windows (mean of in-window probes; out-of-window + null-uptime excluded; empty window → `null` + `meets_sla:false`, never fabricated). **Threshold alerts** — fire ONCE per `(component, metric, condition)` fingerprint (a still-open alert suppresses dupes; re-fires after resolve); lifecycle sent → acknowledged → resolved. **Incidents** — open → in-progress → resolved; resolving REQUIRES a recorded resolution (else 409 `resolution_required`). **Cache/optimization** — cached/expired/cleared; clear resets state to `cleared` + timestamp, refresh back to `cached`. 1 migration (4 tables), 4 models, 4 services, 1 controller, 9 tests. DEPS T-D-02 (merged); no DEPS⚡. Full suite 447/447 (2378 assertions). |
| T-D-15 | ✅ merged | d/T-D-15-dashboards | Super-admin + tenant dashboards (analytics read-model layer). Composite `GET /dashboard/summary` + 6 per-tile endpoints (users, revenue, content, system_health, engagement, support), each a **PURE read** over the T-D-02 `event_ingestion` sink returning `metrics` + `as_of`; **every view audit-logged** (tile/period/viewer/as_of/timestamp) via the append-only `dashboard_views` log. Rollup materialized by a **queued, idempotent recompute job** (`dashboard_aggregates`, one row per tile+window, upsert never duplicate) — the "source events → aggregate job → read endpoint" pattern. Read endpoints NEVER write the aggregate read-model (lint-verified); health/support tiles aggregate matching sink events (populate once T-D-18/T-D-06 emit them; else zeroed structures — the endpoint still returns `metrics` + `as_of`). RBAC `rbac.can:analytics.view` (deny-by-default; super_admin via `'*`); unknown tile → 404. 1 migration (2 analytics tables), 2 models (analytics), 1 service (D-owned Engagement path), 1 job, 1 controller, 6 tests. DEPS T-D-02 (merged) + DEPS⚡ T-A-20/T-C-16 (satisfied by the analytics sink — no new contract). Full suite 453/453 (2460 assertions). |
| T-D-16 | ✅ merged | d/T-D-16-custom-reporting | Custom reporting: **saved reusable templates**, on-demand runs (a **pure function of type+params+period** over the T-D-02 `analytics` sink), **scheduled delivery** (daily/weekly/monthly; pause/resume/sweep), and **secure expiring exports** (opaque token; entitled-data-only). 4 models (engagement: report_configs/schedules/runs/exports), 1 migration (4 tables), `ReportGenerator` (pure read) + `ReportService` + `ReportController`. 8 tests. DEPS T-D-15. ACs: saved-config re-run reproduces output; schedule pause/resume; institute report scoped to its records; churn carries the reason dimension; export link expires (410). Full suite **488/488 green** (2661 assertions). |
| T-D-17 | ✅ main baseline; local follow-up | codex/T-D-17-finance | Finance read model with production provider registration, pure `as_of`-bounded summary, PHP-side event aggregation, reason-filtered failed-payment projection, renewal and plan-movement math, license expiry/utilization opportunities, and available per-affiliate settled/issued status breakdowns. Current tests exercise the actual provider registration and A20/A21 production feeds. |
| T-D-14 | ✅ local completion | codex/T-D-14-sponsor-tenant-portal | Full sponsor portal surfaces and tenant isolation complete locally; deployment integrations are documented in the 2026-10-02 log below. |
| T-D-19 | ✅ local integration | codex/T-D-19-cross-tenant | `CrossTenantE2ETest`: 7/7 passed in current workspace: all six acceptance scenarios plus the supplemental institute-scoped report fixture. Tests use real A12 identity tenant records and A21 seat pools for CORP/CSR/ORG/SPON; the corporate HTTP gate and seat enrollment use production adapters. Dashboard-style tenant reads in scenarios 1, 2, 3, and 4 expose `as_of`; scenario 2 verifies aggregate outcomes, funding totals, and beneficiary masking, and scenario 3 verifies scholarship-recipient masking. Scenario 5 records a public link click, registers the referred user through A's auth route, confirms referral attribution through the authenticated D callback, then exercises D's conversion/payout APIs and A's commission ledger; conversion amount is still supplied directly through D. The billing-authoritative purchase-to-commission bridge is a separate T-D-07 contract follow-up, not evidence that these T-D-19 acceptance scenarios failed. Scenario 6 completes persisted catalog content through C's production learner HTTP route, dispatches analytics aggregation, and verifies user/engagement rollups and finance `as_of` within the 15-minute freshness window. The institute fixture verifies institute scoping and report `as_of`. |

⚠️ Shared-file note (T-D-03 + T-D-07 + T-D-10 + T-D-17): `bootstrap/providers.php` gets one-line
append-only entries — `EngagementServiceProvider` (T-D-03),
`AffiliateServiceProvider` (T-D-07; binds the DEPS⚡ `AffiliateProgramConfig`
contract to its Phase-1 stub), and `CorporateTenantServiceProvider` (T-D-10; binds
the DEPS⚡ `CorporateTenantGate` + `CorporateSeatService` contracts to their
Phase-1 deny-by-default stubs). A owns that file in Phase 0 — flagged per §2;
all entries are purely additive (no existing lines touched) and revert cleanly
if you prefer a different registration mechanism. Each task deliberately uses its
own provider so its branch cannot conflict with the others' provider lines.

T-D-17's `FinanceDashboardServiceProvider` is an authorized additive registration
from 2026-10-02; it binds the finance read model and the two feed contracts.

🔧 **Shared-file note — `bootstrap/providers.php` (2026-10-02):** T-D-17's
`FinanceDashboardServiceProvider` is now registered there with the explicitly
authorized append. A20 binds `SubscriptionHealthFeed` to
`SubscriptionHealthMetricsFeed`, and A21 binds `LicenseFeed` to
`SeatLicenseService`. The finance feature tests no longer duplicate these
bindings in their setup helper.

⚠️ DEPS⚡ note (T-D-07): `App\Services\Billing\SubscriptionService` (A's T-A-23)
is consumed by the affiliate conversion flow only when A's billing side reports
conversions — Phase-1 tests exercise the D-owned path (Mockery doubles where a
DEPS⚡ contract is involved, T-B-05 pattern). Real verification lands in T-D-19
once T-A-23/T-A-15/T-A-16 merge. `AffiliateProgramConfig` (A's T-A-16 contract)
is a D-owned interface + stub for now; A rebinds the singleton when T-A-16 ships
(one-line change, no call-site churn).

**T-D-09 · Marketing & communication — pushed (rebased onto latest `origin/main`).** All 5 frozen ACs covered:
(1) campaign state machine draft→scheduled→active→completed, illegal transition = 409
`illegal_state_transition`; (2) A/B variant assignment stable per user (crc32 hash on
campaign_id|user_id, persisted); (3) popup frequency cap — one per user per day, enforced
in `MarketingService::recordPopupView` (never at call sites), 409 `popup_frequency_cap`,
resets next day, banners/push exempt; (4) template versioning — every save appends an
immutable `marketing_template_versions` snapshot, history endpoint; (5) SMS OTP delegates
to A's concrete `TokenService` (issue/verify round-trip, 422 on bad code / no phone).
Extra: discount-code grant gated behind DEPS⚡ T-A-15 `CampaignDefinitionProvider` (stub
bound in `MarketingServiceProvider`, single-line swap when A lands); performance
metrics recorded on a live read-mirror + additive `MarketingCampaignEvent` (not one of
the frozen 15) for T-D-02's async ingestion. 13 feature tests. DEPS T-D-01 (merged) +
DEPS⚡ T-A-15 (D-owned `CampaignDefinitionProvider` stub) + DEPS⚡ T-B-05.
⚠️ DEPS⚡ note (T-D-10): A's **T-A-12** (tenant gate + verified domain) and
**T-A-21** (seat service + the "never auto-expand the plan" rule) are not on
main, so T-D-10 defines D-owned contracts `CorporateTenantGate` +
`CorporateSeatService`, each bound to a Phase-1 **deny-by-default** stub
(`CorporateTenantGateStub`, `CorporateSeatServiceStub`) via the D-owned
`CorporateTenantServiceProvider`. When A's T-A-12/T-A-21 land, A rebinds those
two singletons to the real implementations — one-line provider change, no D
call-site churn. The stub seams (`setMembership`, `setPlanCapacity`, …) are
test-only and never called in production.

⚠️ DEPS⚡ note (T-D-17): the finance dashboard's domain-DB figures arrive via
two feed contracts registered by D's `FinanceDashboardServiceProvider`
(`bootstrap/providers.php`, append-only):
`SubscriptionHealthFeed` (A's T-A-20 — active-subscription count/revenue, pending +
failed payments WITH reasons, subscription health, renewal stats, plan prices) and
`LicenseFeed` (A's T-A-21 — licensed seats + expiry dates). The dashboard reads the
T-D-02 event stream for transactional figures (revenue/refunds/commissions/payouts/
trends) and delegates the billing-DB-only figures to these contracts — **never a
cross-DB query**. A20/A21 now provide the production feeds; remaining feed
limitations (including supported status fields) are documented below.

## T-D-17 — Finance & revenue dashboards (detail)
- **Read-model**: pure read + `as_of` over the `analytics` event stream (T-D-02) +
  the DEPS⚡ domain feeds. No materialized table and no new queue job (T-D-15 owns
  the scheduled, idempotent materialization for the general dashboard tiles); T-D-17
  computes finance math live so its ACs are unit-testable and the read side adds no
  exact-queue-count impact.
- **Transactional figures (from the stream, as_of-bounded, PHP-side aggregation —
  identical across SQLite/MySQL/PostgreSQL)**: total revenue (Σ PaymentSucceeded),
  refunds processed (Σ RefundProcessed), commissions settled (Σ CommissionSettled),
  payouts issued (Σ PayoutIssued, per affiliate), period revenue buckets (for
  trends vs the configured target in `config/finance.php`). Settled commissions
  are broken down by affiliate under status `settled`; issued payouts are broken
  down by affiliate under status `issued`. Those are the only statuses derivable
  from the frozen event fields.
- **Domain-DB figures (from the feeds, never a cross-DB query)**: active-subscription
  count + revenue, pending + failed payments (a failure ALWAYS carries a reason),
  subscription health (active/expiring/lapsed), renewal stats (renewed/total), the
  plan-price catalogue (for upgrade/downgrade delta), licensed seats + expiry dates.
- **AC (1) failed payment without reason rejected from the finance projection**:
  T-D-02's
  `IngestDomainEvent` is a pure write-once append with NO validation (its test locks
  `EventIngestion::count() === 21`, PaymentFailed included) and must stay that way —
  so T-D-17's `FinanceEventIngestor` validates reason-bearing billing-feed payloads
  and the finance read model rejects missing/blank reasons (`failed_payment_requires_
  reason`) rather than presenting a bare failure. The frozen `PaymentFailed` event
  has no reason property; its raw row remains untouched. Production A's T-A-20 feed
  must supply and invoke the validator on its reason-bearing projection.
- **AC (2) renewal rate**: `renewalRate()` = renewed/total, zero-guarded.
- **AC (3) license expiry alerts**: `licenseUtilization()` classifies each license
  lapsed/critical(≤30d)/warning(≤60d)/ok against `config/finance.license_expiry_
  alert_days`, most-urgent-first.
- **AC (4) upgrade/downgrade**: `planMovementDelta()` maps from→to via the
  `SubscriptionHealthFeed` plan-price catalogue → signed `amount_delta` +
  upgrade/downgrade/same direction.
- **Available license opportunities**: renewal opportunity is true for a lapsed
  license or one expiring within the configured warning threshold (default 60d);
  upgrade opportunity is true only when used seats meet/exceed licensed capacity.
- **Status breakdown limits**: no pending/earned commission, requested/approved/
  paid/rejected payout states are guessed. Additional event/feed fields are
  required before these states can be shown; current outputs report settled
  commissions and issued payouts only.
- **RBAC**: `auth:sanctum` + `rbac.can:finance.view` (deny-by-default; super_admin
  passes via `'*'`, a no-role user gets 403). Unknown verb → 405.
- **Wiring**: `FinanceDashboardServiceProvider` (binds the 2 feed contracts → A20/A21 production providers +
  the `FinanceDashboardService` + `FinanceEventIngestor` singletons), registered in
  `bootstrap/providers.php`. `config/finance.php` holds the alert
  thresholds + the revenue target.

## Phase Gates (Track D)
- Gate 1 (end Phase 0): pending — the authoritative criteria are the real `NotificationService` being exercised by A/B/C feature tests and idempotent analytics ingestion (see `developer_D.md`). The current integration contains the production dispatcher and ingestion implementation, but this status record does not establish that both cross-track criteria have been verified together. Do not infer gate passage from task branch/merge labels.

- Gate 2 (end Phase 1): pending.
- Gate 3 (end Phase 2): pending.
- Gate 4 (end Phase 3): pending.


## T-D-12 — Webhook/API dispatch engine (detail)
- **Dispatch engine** (`WebhookDispatchService` + `WebhookTransport` contract,
  real `HttpWebhookTransport`): fan-out one outbox row per (event, active
  endpoint); A's HMAC signature scheme (`X-Mi-Digital-Signature: t=<ts>,
  v1=HMAC(secret, ts.'.'.rawBody)`); at-least-once with exponential backoff
  (base·2^attempts, capped 24h); total-attempts ≥ `retry_max` → dead-letter;
  `reprocessDeadLetter()` re-queues and clears the DLQ row on success.
- **Sustained-unreachable alert**: exhaustion fires a high-severity
  `webhook.endpoint_down` alert through the T-D-01 `critical()` path (never
  muted/batched). The endpoint is kept ACTIVE so reprocess is the recovery
  path (a hard status flip would strand the DLQ).
- **IP allowlist**: enforced in `deliver()` on the outbound source IP
  (`endpoint->isIpAllowed`); a blocked IP dead-letters immediately (permanent
  config error, not a transient retry).
- **API-key enforcement middleware** (`ApiKeyEnforcement`): the enforcement
  point A's T-A-24 defers to. Order: key present/valid/active (401) → scope
  check (403 + logged) → A's `RateLimitService` hierarchy key>endpoint>global
  (429 + Retry-After + X-RateLimit-*). Bumps A's usage counters on pass.
- **Embed widgets** (`EmbedTokenService` + `embed_tokens`): domain allowlist
  (non-allowlisted → error), scoped token bound to (user, widget, domain),
  token shown once (sha256 hash stored), refresh interval clamped to
  [MIN,MAX], revocation stops loading.
- **Sync runs** (`SyncRunService` + `sync_runs`): append-only history, last
  sync/records/backlog, manual vs automated, slow/backlog thresholds alerted
  via the T-D-01 critical path; a run never deletes records.
- **Consistency checker** (`ConsistencyChecker`): report-only | auto-fix
  (source-of-truth applied → the model save's T-A-02 Auditable hook writes the
  audit row) | manual-review; orphans are KEPT, never deleted.
- Routes: `routes/api_engagement.php` (admin surface + the `apiKey`-gated
  `/integration/sample` sample resource). Wiring: `WebhookDispatchServiceProvider`
  (appends to `bootstrap/providers.php`, the T-D-07 pattern — D never edits A
  files).

## T-D-05 — Forums, study groups, peer Q&A, moderation (detail)
- **Scope**: engagement-connection domain models (`BaseEngagementModel` →
  audited); cross-domain refs (user_id, course_id) are plain indexed columns,
  never FKs/joins (§3). 11 tables: `forums`, `forum_threads`, `forum_posts`,
  `thread_follows`, `thread_upvotes`, `study_groups`, `study_group_members`,
  `study_group_resources`, `moderation_reports`, `moderation_logs`,
  `user_reputations`.
- **Pre-post mode**: a forum's `pre_post_approval` makes new threads/posts
  start `pending`; a pending row is invisible to everyone but its author
  (existence-safe 404 via `ForumService::visibleThread` / `listThreads`).
- **Merge**: `ForumService::merge` sets `merged_into_id` (same-forum only, no
  self-merge) — the source stays `approved` (content preserved) and reads
  surface the redirect target.
- **Self-vote**: `upvote` rejects the thread author (`409 self_vote`); a
  duplicate upvote is idempotent (one row per user).
- **Study groups**: `pending` group not joinable (`409 group_pending`); an
  `active` group at `max_members` rejects a join (`409 group_full`). Rejection
  is **never silent** — the creator is notified with the reason (real
  `NotificationService`). Resources: MIME allow-list + 25 MB cap; `group_only`
  vs `public` visibility. Chat moderation: `filter` / `mute` / `remove` a
  member (`study_group_members.chat_state`).
- **Moderation — IMMUTABLE log**: `moderation_logs` has no `updated_at`;
  `ModerationService::appendLog` is the only writer and only ever INSERTs.
  Taking action / dismissing writes an `action`/`dismiss` entry and notifies
  the reporter of the outcome. **Overturn** writes a NEW `overturn` entry whose
  `references_entry_id` points at the entry being overturned — the original row
  is never mutated; the content effect of an overturned `action` is undone.
- **Forum tier gating (T-A-12 integration)**: `requires_entitled_tier` forums
  gate posting through the `ForumTierGate` contract. A's
  `ForumTierGateAdapter` is bound in `AppServiceProvider` and checks full-catalog
  entitlement (`all`) through `SubscriptionService`; course- or subject-only
  plans do not qualify. `ForumTierEntitlementIntegrationTest` verifies the
  production binding and HTTP behavior. `ForumTest` binds the deny-by-default
  stub for isolated contract tests.
- **Reputation**: points accrue only for VALID (approved) contributions and
  decay on inactivity (`decayInactiveReputation`, 30-day threshold, floored at
  0); `level` is derived from points (level-gated).
- **RBAC**: moderation endpoints are `rbac.can:forum.moderate` (deny-by-default;
  super_admin passes via `'*`).
- **Note (test helpers)**: the `BootsEngagement` trait on the current `main`
  snapshot predates Track D's RBAC helpers (added on the T-D-04 branch, not
  yet merged), so `ForumTest` defines `seedRbacDefaults` / `grantPermission` /
  `makeUserWithRole` inline. These become redundant once T-D-04 merges.

## T-D-15 — Super-admin + tenant dashboards (detail)
- **Scope** (DEPS T-D-02; DEPS⚡ T-A-20/T-C-16 satisfied by the T-D-02
  analytics sink — no new cross-track contract): the analytics read-model
  layer. The pattern is `source events → aggregate job → read endpoint`;
  tenant variants (CORP/CSR/ORG/SPON, T-D-10..14) reuse the same read-model
  pattern later.
- **Read side** (`SuperAdminDashboardController`): `GET /dashboard/summary`
  (composite, all tiles + as_of) + `GET /dashboard/tiles/{tile}` for users,
  revenue, content, system_health, engagement, support. Each is a **PURE read**
  over `event_ingestion` (never a transactional DB) returning the tile's
  `metrics` + the `as_of` freshness frontier (the T-D-02 contract). The tile
  whitelist is a route constraint (unknown tile → 404; the controller's 422 is
  defense-in-depth). RBAC `rbac.can:analytics.view` (deny-by-default;
  super_admin via `'*`).
- **Aggregate job** (`RecomputeDashboardAggregates`, ShouldQueue; `DashboardTileService`
  in the D-owned `app/Services/Engagement/` path so it may reference the
  `analytics` connection per the T-D-02 lint allowlist): upserts one
  `dashboard_aggregates` rollup per (tile, period, window) — **idempotent**
  (re-runs / redelivery update, never duplicate), reflecting the newest
  `as_of`. Sync dispatch is the "planted event → aggregate within SLA" path.
- **Read vs write**: a read endpoint returns the materialized rollup when the
  job wrote it for the window, else computes live from the sink — either way it
  is pure. The **only** write is the append-only `dashboard_views` audit (the
  spec's "every view audit-logged with period/timestamp"), which is not an
  aggregate write (lint: reads create no rollup row and never touch the sink).
- **health/support tiles**: aggregate matching sink events
  (`ErrorLogged`/`IncidentOpened`; `SupportTicketOpened`/`Resolved`/`Breached`);
  they populate once T-D-18 / T-D-06 emit those events into the same sink, and
  return zeroed structures (still `metrics` + `as_of`) until then.
- **Footprint**: 1 migration (2 analytics tables), 2 analytics models
  (explicit `$connection` per A's 5-DB lint), 1 service, 1 job, 1 controller,
  6 tests (per-tile as_of, read-never-writes lint, planted→aggregate sync
  dispatch + idempotent upsert, view audit, RBAC deny + unknown-tile).

## T-D-18 — System health, monitoring, incident management (detail)
- **Scope** (DEPS T-D-02; no DEPS⚡): a SELF-OBSERVED ops surface — D records
  its own platform health (uptime, page load, video quality, API response) and
  exposes it to orchestrators + the ops dashboard tile. Values are plain data
  D accepts via probes; no cross-domain FK or join.
- **Public probe**: `GET /api/health/status` (no auth) returns the overall
  state = worst of the latest probe per component (any `down` → down, else
  `degraded`, else healthy; `unknown` when no probes recorded).
- **Ops tile RBAC**: all other endpoints are `rbac.can:analytics.view`
  (deny-by-default; super_admin via `'*`) — same convention as the T-D-02
  analytics + T-D-17 CSR tiles.
- **AC (1) — alert fires once**: an alert is fingerprinted by
  `(component, metric, condition)`; while an alert with the same fingerprint is
  still open (sent/acknowledged) a new crossing is a NO-OP (no dupe). It
  re-fires only after the alert is resolved. No crossing (below threshold) → no
  alert.
- **AC (2) — incident lifecycle**: open → in_progress → resolved; resolving
  REQUIRES a non-empty `resolution` record (else 409 `resolution_required`);
  re-resolving → 409.
- **AC (3) — uptime over a window**: `UptimeService::uptimeOverWindow` = the
  mean of in-window probes for a component (daily/weekly/monthly = 1/7/30
  days); out-of-window and null-uptime probes are excluded; an empty window
  yields `uptime_pct = null` + `meets_sla = false` (never a fabricated 100%).
  SLA target is 99.9%.
- **AC (4) — cache clear resets state**: `CacheService::clear` → state
  `cleared` + `cleared_at` stamped; `refresh` → back to `cached` (clears the
  stamp); `expire` → `expired`.
- **Footprint**: 1 migration (4 tables), 4 models (engagement, explicit
  `$connection` per A's 5-DB lint), 4 services + 1 conflict exception, 1
  controller (`App\Http\Controllers\Dashboard`), 9 tests (4 ACs + public probe
  + RBAC deny). No provider needed (all services are concrete, auto-resolved).
## T-D-14 — Sponsor tenant portal (detail)
- **Scope**: targeted sponsorships (course pages / content players /
  certificates / landing pages) with an **approval workflow + preview before
  go-live**, **auto-removal on expiry**, **renewal with proration**;
  scholarships (funded programs, eligibility, disbursement,
  **privacy-masked recipients**). The approval workflow reuses the same
  state-machine the other tenant portals use — it is state, not the entity;
  terminal states are immutable (a new row re-decides).
- **Tenant gate (T-A-12 integration)**: `SponsorTenantGate` (D-owned interface)
  is bound to A's `SponsorTenantGateAdapter` in `AppServiceProvider`. The adapter
  resolves an active sponsor membership and requires a verified, transactable
  sponsor tenant; missing membership or an ineligible tenant returns 403 at the
  portal boundary. `TenantGateAdapterTest` verifies the production binding and
  membership/state mapping. `SponsorPortalTest` intentionally binds the
  deny-by-default stub for isolated portal behavior tests; this is not the
  production binding.
- **Go-live gate (AC)**: `SponsorPlacementService::goLive` requires BOTH an
  APPROVED T-D-13 shared workflow (`org_approval_workflows`) AND `preview_done`. Missing approval → 409 `not_approved`;
  missing preview → 409 `preview_required`.
- **Expiry auto-removal (AC)**: `sweepExpiries($now, $tenantId)` is
  **tenant-scoped** (a sponsor's sweep never touches another tenant's rows)
  and idempotent — flips `live` placements past `expires_at` to `expired`
  (`removed_reason='expiry'`); non-expired stay `live`. Go-live derives
  `expires_at` from `start_date + duration_days` when duration is positive.
- **Recipient masking (AC)**: `SponsorScholarshipRecipient::maskedPayload()`
  is the only sponsor-facing shape — first-name token + `•••`, local part of
  the email redacted with the domain kept. Raw `name`/`email` are never
  returned.
- **Renewal proration (AC)**: `SponsorRenewalService::computeProration` =
  `round((to−from)·remaining/total)` (0-guarded when `total<=0`); downgrades
  yield negative credits. The route scopes the referenced placement to the
  caller's tenant and rejects negative prices or remaining days beyond cycle length.
- **RBAC**: `approve` requires shared `org.approve` (deny-by-default; super_admin
  passes via `'*`).
- **DEPS⚡ T-B-06**: sponsorship *content* (the sponsored course/content that a
  placement targets) is a soft dependency — `target_ref` is a plain indexed
  column (no FK, no join), so D builds + tests self-contained.
- **Footprint**: 2 migrations (12 tenant-scoped tables), 12 engagement models
  (explicit `$connection` per A's 5-DB lint), 7 services + gate/stub, 1
  controller, 1 registered D-owned provider binding, a registered
  `sponsors:maintain` artisan command, and 25 feature tests.
  Includes shared T-D-13 approval workflow integration, tenant-scoped
  visibility/impact/dashboard reads, CSV and SpreadsheetML `.xls` report
  archives, expiring impact-story links, invoice/payment-schedule/budget reads,
  invoice draft→issued→paid lifecycle with installment reconciliation, and
  notification preferences/subscriptions/delivery logs (including billing
  reminders and a scholarship-disbursement notification hook).
- **Integration gaps**: visibility and learning metrics need producers that
  emit sponsor placement attribution using `SponsorPortalInsightsService::recordVisibilityEvent`;
  scholarship learning outcomes likewise need a tenant-safe source feed.
  Billing lifecycle and payment reconciliation are implemented as D-owned
  service operations, but the external billing/payment system must call the
  reconciliation seam to supply authoritative settlements. The D provider
  registers `sponsors:maintain`, which sweeps expiry, milestones, renewal and
  installment reminders, and monthly digests. Laravel's scheduler is created
  by the A-owned `bootstrap/app.php`/console kernel path, so deployment must
  invoke this command from its scheduler/cron configuration. PDF output and
  invoice PDF downloads need an approved renderer; no PDF renderer is installed,
  so report archives currently offer CSV and SpreadsheetML `.xls`. Reports are
  delivered through the tenant-authenticated archive/download route; impact
  stories use expiring secure links. Sponsor notification email still needs a
  production SMTP transport. Object-storage-backed signed URLs await OD-39.

## Blockers
- T-D-14 integration gaps are listed in its detail above: sponsor-attributed
  analytics/learning producers, external billing settlement producer,
  deployment scheduler invocation, approved PDF renderer for report and invoice
  PDFs, and production SMTP transport for sponsor notifications. Report delivery
  is link-based; object-storage-backed signed URLs remain pending OD-39.
- (content dependency): sponsorship content uses a plain indexed `target_ref`
  column for the soft T-B-06 dependency, with no cross-database join. The T-A-12
  sponsor tenant-gate integration is present: A binds the identity-backed
  adapter in `AppServiceProvider`; the stub remains only for isolated D tests.
- T-D-07 follow-up (2026-10-02): `AffiliateAccountStatus::isEligibleForReferrals(int userId): bool` is consumed by the enrollment gate and affiliate attribution/accrual paths. D's `AffiliateAccountStatusStub` preserves D-only deployment behavior; A's identity-backed adapter is now bound in `AppServiceProvider` on the local integration branch and enforces `users.referral_enabled` / affiliate status on each operation. Enrollment decisions are recorded in D's append-only `affiliate_enrollment_history`; re-approval accepts a rejected enrollment and clears its prior rejection reason. No A-owned paths changed in this follow-up.
- T-D-05 forum-tier gate is now bound by A's `ForumTierGateAdapter` to the
  subscription service's full-catalog entitlement check. Course/subject-only
  plans do not grant access; active, trial, grace and qualifying full-access-seat
  entitlement follow A's existing policy. This is entitlement, not proof of payment.
- T-D-06 account lifecycle is now bound by A's `SupportAccountGateAdapter`,
  which checks identity user state through the owner connection. The support
  service contract exposes this state for callers that need the gate.
- Cross-track status: T-A-12 tenant gates and T-A-21 corporate seats are bound
  to production identity-billing adapters; T-A-23 supplies the production
  `SubscriptionService` used by the forum entitlement adapter; T-A-16's
  `AffiliateProgramConfig` is also bound to A's implementation. T-D-19 provides
  scenario-level evidence for several tenant paths, but its affiliate conversion
  still accepts an amount directly through D rather than deriving it from a real
  billing transaction, so it is not end-to-end proof of the billing-to-affiliate
  path.
- (T-D-02 A-side DomainEventsTest count-assertion blocker was resolved when T-D-02 merged 4d0dace — no longer carried.)
- T-D-12 built against real A contracts (T-A-24 merged) — no stubs; `bootstrap/providers.php` + `routes/api.php` are A-owned, D only APPENDS (documented pattern).

## T-D-07 follow-up — A16/A20 commission bridge (2026-10-02)

- Affiliate conversion accrual now calls A's `CommissionLedgerService::createPending` only after the D enrollment/A account-status gate and a matching clicked attribution are verified. D passes the referred user, deterministic `affiliate_conversion:{conversionId}` source reference, kind, uppercase currency, exact two-decimal minor-unit amount, and effective tier name.
- A's pending ledger is authoritative for the amount/rate snapshot mirrored into D's engagement commission entry. Duplicate conversion replay calls A idempotently and recovers a missing D mirror; A status checks remain an independent guard. D's own conversion and ledger records stay on the engagement connection.
- `AffiliatePortalTest` covers purchase, renewal, duplicate replay, blocked status, missing attribution, and minor-unit/currency/tier mapping (19/19 passed); the cross-tenant affiliate payout scenario also passed.
- **Follow-up blocker audit (2026-10-02):** A16's persisted configuration includes `levels`, `tier2_rate_pct`, `recruitment_reward`, `leadership_bonus`, and `payout.frequency/schedule_day`, but the frozen `AffiliateProgramConfig` only returns the four-tier rate ladder and minimum payout. A16's ledger accepts `leadership` as a kind but applies the regular tier rate. D's request flow independently settles its engagement rows before approval and does not transition the corresponding A ledger rows. No implementation is safe until A and D agree on config DTO fields and qualification snapshots, the tier-2/recruitment/leadership trigger and rate basis, payout authority and lifecycle timing, cross-ledger idempotent references, and rejection/reversal handling. The current purchase/renewal bridge remains valid within its narrower scope.

## T-D-05 follow-up — A22 report consumer (2026-10-02)

- Added a Laravel-discovered queued `ImportInappropriateContentReport` listener for A's `App\Events\Identity\InappropriateContentReported`. It maps only event scalars into the engagement database; it performs no identity-billing reads or joins.
- `moderation_reports` now stores the scalar `source_report_id` with a unique index and the report details. Imports remain pending and are idempotent under duplicate delivery. Imported catalog content uses its own `catalog_content` reportable type so moderation cannot act on a same-ID local forum post.
- `ForumTest` verifies listener discovery/queueing, imported fields and timestamp, and duplicate delivery. Focused suite: 16/16 passed; Pint and `git diff --check` passed.
