# Track D Status

**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.

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 `8e2d713`** — 4 files conflicted
  (providers.php, README.md, api_engagement.php, track_d.md), resolved by
  union; full suite **484/484 green** (2631 assertions). 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).
  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: **in progress** (see table) — 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 | 🚀 pushed | d/T-D-05-forums-study-groups | Forums / study groups / peer Q&A / moderation (engagement connection; `BaseEngagementModel`-derived). **Pre-post mode** — new thread/post starts `pending`, invisible to everyone but its author until approved. **Merge** — a thread becomes a canonical redirect (`merged_into_id`); reading it surfaces the redirect target, content preserved (merge ≠ delete). **Self-vote rejected** (`409 self_vote`); duplicate upvote idempotent. **Group at max size rejects join** (`409 group_full`); pending group not joinable (`409 group_pending`); **rejection notifies the creator with the reason** (never silent). **Moderation log is IMMUTABLE** — entries are append-only (no `updated_at`); **overturn creates a NEW `overturn` entry whose `references_entry_id` points at the original** (original intact); the reporter is told the outcome. **Forum tier gating** — `requires_entitled_tier` forums gate posting via the DEPS⚡ `ForumTierGate` contract (deny-by-default Phase-1 stub; A rebinds when T-A-12 lands). Reputation accrues for valid (approved) contributions and decays on inactivity; level-gated. Own-post edit/delete allowed, foreign denied (`409 not_own_post`). Moderation + search-config endpoints deny-by-default (`rbac.can:forum.moderate`). 10 new models, 3 services, 1 controller, 1 migration (11 tables), 15 tests. DEPS T-D-01 + T-D-03 (both merged). Full suite 427/427 (2267 assertions). |
| T-D-06 | 🚀 pushed (rebased on latest main) | d/T-D-06-customer-support | Support tickets (priority-based SLA, agent-load-aware assignment, close requires resolution note, reopen window, owner→super-admin escalation), chat (chatbot→human handoff, transcript preserved), knowledge base suggestions, account requests (SLA + sweep, recorded outcome required), metrics tile. DEPS T-D-01 (merged) + DEPS⚡ T-A-09 behind the D-owned `SupportAccountGate` (Phase-1 stub reads identity `User.status`; A rebinds when T-A-09 lands). 17 tests. Rebased onto latest `origin/main` (post T-D-04 merge); full suite 484/484 (2631 assertions). |
| 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 | 🚀 pushed (this branch) | d/T-D-07-affiliate-portal | Affiliate portal runtime: enrollment gate (approved-only, re-checked per operation — unapproved → 403 on all referral tools), referral links (unique token; **QR rotates on regeneration — stale QR 410**), tracked links (labeled, deletable, reported separately), attribution engine (click→signup→purchase per channel/campaign), commission accrual **frozen at accrual-time tier/rate (never retroactive)** + `source_ref` idempotency (duplicate events = no-op), **payouts: min threshold 409 + verified payment method + balance sufficiency; pending commissions settled with payout linkage; one statement per payout**, commission entries **immutable — any write endpoint 409 `commission_immutable`; corrections via disputes only** (open flags entry, second dispute 409), tier changes (Bronze→Platinum, history, **new rate on new accruals only — verified**), win-back sweep (**fires only for leads inactive >14 days; active leads skipped; step never fires twice** — AC fixture test), scheduled promotions (future-dated only; edit/cancel conflict once due; auto-publish sweep), affiliate notifications via the real dispatcher (commission earned, payout state, tier upgrade). 15 new tests; Engagement 32 passing; full suite 243/243 green. |
| T-D-10 | 🚀 pushed (this branch, rebased on latest main) | d/T-D-10-corporate-tenant-portal | **Corporate tenant portal** (engagement DB, `BaseEngagementModel`-derived). Behind A's tenant gate (verified+active only) + **CORP role checks** (Corporate Admin / Department Manager / HR Manager / Employee / Billing Admin; role changes take effect on the next session — the gate re-reads the current role row, no cache). **AC1 bulk import** — per-row all-or-nothing (a bad email fails THAT row, never guessed; duplicates skipped) with a 10k-row cap. **AC2 seat assignment** — enrollment REQUIRES a seat; without one → `409 seatless_enrollment` and the plan is **never auto-expanded**; one-to-one. **AC3 mandatory training** — deadline required; a dept assignment covers current employees only unless `ongoing`; **escalation ladder** employee → dept manager (14d) → corporate admin (30d), **halts on completion + resolution notice**. **AC4 HRIS/LMS/SIS** — while an active HRIS connector is the source of truth (a manual dept edit is **reverted by the next sync**); inbound completions **deduped by activity id**; failed syncs use **exponential backoff ≤24h**. **AC5 compliance** = obligations met, only when **every module is done AND the assessment ≥ the per-course min passing score (default 70%)**. **AC6 budgets** — mid-year edits recorded; **80%/100% alerts fire ONCE per period**; clean cost-center statement. DEPS⚡ **T-A-12** (TenantGate) + **T-A-21** (SeatService) not on main → D-owned `CorporateTenantGate`/`CorporateSeatService` contracts bound to Phase-1 deny-by-default stubs (A rebinds on one-line provider change). 11 models, 5 services, 2 DEPS⚡ contracts + 2 stubs, 1 D-owned provider, 1 migration (11 tables), 1 controller, 12 routes, 25 tests. DEPS T-D-01 (merged) + T-D-02 (merged). Rebased onto latest `origin/main` (post T-D-09 merge); full suite **505/505 (2719 assertions)**. |
| T-D-12 | 🚀 pushed (this branch) | 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. 12 new tests; full suite green. 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 | 🚀 pushed (this branch) | d/T-D-17-finance-revenue-dashboards | Finance & revenue dashboards — the `analytics` finance read-model. **Pure read** (`GET /api/finance/summary`, `as_of` always; RBAC deny-by-default `rbac.can:finance.view`). Transactional figures (total revenue, refunds processed, commissions settled, payouts issued per affiliate, period trends) come from the T-D-02 event stream (`PaymentSucceeded/RefundProcessed/CommissionSettled/PayoutIssued`, as_of-bounded, PHP-side aggregation — identical across SQLite/MySQL/PostgreSQL); domain-DB figures (active-subscription count/revenue, pending + failed payments **with reasons**, subscription health, renewal stats, plan prices, license seats) come via **DEPS⚡ feeds** — `SubscriptionHealthFeed` (T-A-20) + `LicenseFeed` (T-A-21), Phase-1 stubs, **never a cross-DB query**. **ACs**: (1) **failed payment without reason rejected at ingestion** — T-D-17's own strict `FinanceEventIngestor` (additive: T-D-02's write-once `event_ingestion` is untouched and still carries all 21 events; the finance side rejects/quarantines an unreasoned `PaymentFailed` and never presents a bare failure); (2) **renewal rate = renewed/total** (zero-guarded); (3) **license expiry alerts at the configured 30/60-day thresholds** (lapsed/critical/warning, most-urgent-first); (4) **upgrade/downgrade tracked from→to with amount delta** via the plan-price catalogue. No new queue jobs (read-side only → no exact-queue-count impact). 2 feed contracts + stubs, 1 ingestor, 1 service, 1 controller, 1 provider, 1 config; 9 tests (51 assertions). DEPS T-D-02 (merged); DEPS⚡ T-A-20, T-A-21 (stubs). Full suite **497/497** (2712 assertions) on latest main (re-based `8c06d7f`). A-owned `bootstrap/providers.php` NOT edited by this branch — DEPS⚡ feed stubs + service bound in D-owned test concern; production provider line handed to orchestrator (see note). |
| T-D-10, T-D-11, T-D-13, T-D-19 | ⏸ not started | — | see developer_D.md. T-D-10+ DEPS T-D-02 (merged) + T-D-12 (merged). T-D-19 DEPS T-D-15/16/17/18 (T-D-15/16/18 merged; T-D-17 is this branch). (T-D-14 is pushed.) |

⚠️ Shared-file note (T-D-03 + T-D-07 + T-D-10): `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.

🔧 **Orchestrator note — `bootstrap/providers.php` (shared, A-owned) wiring
required by Track D branches (2026-10):** `bootstrap/providers.php` is A-owned
and **outside Track D's editable dirs**, so a D branch must not register its own
ServiceProvider there. T-D-17 (finance & revenue dashboards) needs one A-owned
append that only the orchestrator can safely land on main:
- **T-D-17**: `use App\Providers\FinanceDashboardServiceProvider;`
  + `FinanceDashboardServiceProvider::class,` (binds `SubscriptionHealthFeed`
  → `SubscriptionHealthFeedStub`, `LicenseFeed` → `LicenseFeedStub`,
  `FinanceDashboardService` singleton, `FinanceEventIngestor` singleton).
  T-D-17's branch itself **does not edit** the shared file — the bindings are
  mirrored in the D-owned test concern (`BootsAnalytics::setUpAnalytics`), so
  the branch is green (497/497) without touching it. The production provider
  line above is what you apply at merge time. (T-D-06's branch carries the same
  treatment for its `SupportServiceProvider` line.) Flagging per §2 / the
  track-ownership rule.

⚠️ 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 D-owned feed contracts bound to Phase-1 stubs in 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**. A rebinds each singleton to its real implementation when T-A-20 /
T-A-21 land (one line each, no call-site churn).

## 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`).
- **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 at ingestion**: 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 adds its OWN strict `FinanceEventIngestor::validate()`: a `PaymentFailed`
  whose `reason` is missing/blank is **rejected** (flagged `failed_payment_requires_
  reason`), never presented as a bare failure. This is additive — the raw sink still
  receives every contract event exactly as before.
- **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.
- **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 → stubs +
  the `FinanceDashboardService` + `FinanceEventIngestor` singletons), appended to
  `bootstrap/providers.php` (append-only). `config/finance.php` holds the alert
  thresholds + the revenue target.

## Phase Gates (Track D)
- Gate 1 (end Phase 0): pending — T-D-01, T-D-02, T-D-03, T-D-07, T-D-12 merged; T-D-04/05/06/09 pushed.

- 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 (DEPS⚡ T-A-12)**: `requires_entitled_tier` forums gate
  posting through the `ForumTierGate` contract; the Phase-1
  `ForumTierGateStub` is deny-by-default and bound in D-owned
  `EngagementServiceProvider`. A rebinds to its real entitlement service when
  T-A-12 merges (one provider line, no D call-site churn).
- **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).

## Blockers
- (none new from T-D-07)
- T-D-05 (DEPS⚡ T-A-12): forum tier gating compiles against D-owned
  `ForumTierGateStub` (deny-by-default); A rebinds `ForumTierGate` to its real
  entitlement service when T-A-12 (TenantGate) merges — one provider line in
  `EngagementServiceProvider`, no D call-site change.
- Watch: T-A-12 (TenantGate), T-A-21 (seats), T-A-23 (SubscriptionService —
  DEPS⚡ double in use), T-A-15 (discounts), T-A-16 (affiliate program config —
  D-owned stub in use until A rebinds), T-B CatalogService, T-C ProgressService —
  verified for real in T-D-19.
- (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).
