Skip to Content

← All archived runs

Run: sdm-demand-aggregation

run.md

Run: sdm-demand-aggregation

  • branch: claude/zealous-galileo-htm6jm
  • pr: #521

00_intake/stub.md

Stub: SDM dashboard — demand aggregation (go-live demand table)

  • feature-slug: sdm-demand-aggregation
  • epic: sdm-dashboard-data
  • personas: SDM
  • initiative: Build the Bridge / objective: Q2 2026 Objective 3 — Validate Technical Infrastructure & Payout Flow
  • depends-on: sdm-area-foundation
  • sequence: 3 of 8

Problem

The go-live demand table (goLiveDemand in apps/web/lib/mock/sdm.ts, rendered by demand-table.tsx) shows demand the SDM must cover, one row per (region, country, language, platform, module) with a demand count, quotesWaiting, a bidReadyExperts list, a timeBand (Immediate / Next / Upcoming) and daysToStart. None of this is aggregated today — it is a static array. The real lead pipeline has the raw signal (active leads in BRD/quote/bid with a product, location, language and start date) but nothing rolls it into per-cell demand counts or buckets it by time-to-start.

Proposed change

  • Build a demand aggregation over the area's active leads in the BRD → quote → bid stages, grouped into cells by (platform, module, region, language) using sdm-supply-taxonomy, producing the demand count and quotesWaiting per cell.
  • Derive the time-band (Immediate / Next / Upcoming) from each cell's earliest days-to-start (requested start / go-live date on the lead) and surface daysToStart.
  • Populate bidReadyExperts per cell from the existing bid-pool / lead-expert-match data (the experts already matched/bid-ready for those leads).
  • Wire the demand table to this aggregation. The coverage columns (status, readiness, coverage, coverageGap) render in a pending/neutral state until sdm-supply-coverage lands, then they fill in — this feature owns the demand half of the row.

Acceptance criteria (rough)

  • Active leads in BRD/quote/bid roll into demand cells keyed by platform+module+region+language, scoped to the SDM's area.
  • Each cell shows a real demand count and quotesWaiting; bidReadyExperts come from the bid pool.
  • Each cell is bucketed into a time-band from real days-to-start; daysToStart is the cell's soonest start.
  • The demand table renders from the aggregation — no longer from lib/mock/sdm.

Out of scope (this feature)

  • The supply/coverage classification and the status/readiness/coverage columns (feature 4) — those are computed against supply and only enriched here.
  • The risk + action-strip derivation (feature 5).

Notes for Define

  • Confirm which lead statuses count as "demand" for each time-band, and the source of days-to-start (requested start vs derived go-live date).
  • Reuse the bid-pool query / lead-expert-match rather than re-deriving who is bid-ready — see expert-dashboard-data/bid-pool-workbench.
  • touches: packages/services/src/server (demand aggregation read-service over leads/quotes), apps/web/components/dashboard/sdm/demand-table.tsx (consume the real shape), apps/web/app/(app)/sdm/dashboard/page.tsx.

01_define/output/spec.md

Spec: SDM dashboard — demand aggregation (go-live demand table)

  • slug: sdm-demand-aggregation
  • personas: SDM
  • touches: packages/services/src/db/services/sdm-demand/, packages/services/src/db/services/index.ts, apps/web/app/(app)/sdm/dashboard/page.tsx, apps/web/components/dashboard/sdm/demand-table.tsx, apps/web/lib/mock/sdm.ts
  • complexity: standard

Problem

The SDM "Go live readiness" dashboard's go-live demand table (goLiveDemand in apps/web/lib/mock/sdm.ts, rendered by apps/web/components/dashboard/sdm/demand-table.tsx) shows the demand an SDM must cover — one row per supply/location cell with a demand count, quotesWaiting, a bidReadyExperts list, a timeBand (Immediate / Next / Upcoming) and daysToStart. None of it is real: it is a static array. sdm-area-foundation (sequence 2) already resolves the signed-in SDM's area and returns a typed envelope with the in-scope leads[] (the demand side) and experts[] (the supply side), and sdm-supply-taxonomy (sequence 1) now provides the platform→module key; but nothing rolls those leads into per-cell demand counts or buckets them by time-to-start, so the table the SDM lands on still can't be trusted. This is sequence 3 of 8 in the sdm-dashboard-data epic. It advances Build the Bridge / Q2 2026 Objective 3 — Validate Technical Infrastructure & Payout Flow by proving the dashboard can turn the real, area-scoped lead pipeline into the demand picture an SDM plans coverage against.

Proposed change

Aggregate the SDM's area-scoped, active leads into demand cells, and wire the demand table to that aggregation. This feature owns the demand half of each row; the supply/coverage half stays neutral until sdm-supply-coverage (sequence 4) lands.

  • Supply key — platform → module (from sdm-supply-taxonomy). A lead's product is a module that references its platform (vendor) parent. Cells key on (platform, module, region, language): platform fills the demand table's Vendor column and module (the product name) fills the Product column. Area scoping reuses the rule sdm-area-foundation defines — area holds platforms (platform ids), resolved to their module (product) ids so a lead is in-area when its product sits under one of the SDM's owned platforms.

  • Demand window — which leads count. A lead counts as demand when it is an active, pre-delivery lead in the BRD → quote → bid window: its workflow status is one of pending / backlog / quotation_process / awaiting_confirmation (everything before work_in_progress, excluding qualified_out), it is not a delivery project (isActiveProject: false), and it is not deleted. Tenant- and area-scoped, driven by the live mine/tenant scope.

  • Cell key + measures. Group the in-scope leads into cells keyed by (platform, module, region, language):

    • platform = the lead's product's platform parent name; module = the lead's product name; region = lead.location.region; language = the lead's primary language (languages[0]), so each lead lands in exactly one cell (mirroring the one-language-per-row mock). A missing platform, module, region or language is grouped under an explicit "unspecified" bucket rather than silently dropped.
    • demand = count of in-scope leads in the cell.
    • quotesWaiting = the subset of the cell's leads in the quote stage (quotation_process / awaiting_confirmation).
    • bidReadyExperts = distinct experts who already have a non-excluded lead_expert_match against the cell's leads — reused from the existing bid-pool / lead-expert-match data (as bid-pool-workbench does), not re-derived by a fresh matching pass.
  • Time-band + days-to-start. daysToStart for a cell is the soonest days-to-start across its leads, computed from lead.startDate (the requested start — the only start signal on the lead; there is no separate go-live date field). The cell's timeBand is bucketed from that soonest value: Immediate = starts in under 7 days, Next = 7–14 days, Upcoming = more than 14 days. Leads with no startDate fall into Upcoming with a "—" days-to-start. The thresholds live as named constants so they can be tuned without a schema change.

  • Aggregation read-service. Add a lean sdmDemandService (new packages/services/src/db/services/sdm-demand/, following the established index.ts class + instance.ts singleton pattern, exported through db/services/index.ts, mirroring sdm-area/). It resolves the SDM's area dimensions (reusing the area-membership rule, not re-implementing the dashboard's scope resolution), queries the in-scope active leads with the fields demand needs (resolving product → platform for the cell key), reads the bid-ready matches, and returns the typed demand-cell array the table renders.

  • Wire the table + retire the mock. apps/web/app/(app)/sdm/dashboard/page.tsx fetches the aggregated cells alongside the area envelope (same tenant + SDM + scope) and passes them to DemandTable, which renders from them — no longer from lib/mock/sdm. The Vendor and Product columns now show the real platform + module; the coverage columns (status / coverage-status badge, coverageGap badge) render in a neutral "pending" state for every row until sdm-supply-coverage lands. Remove goLiveDemand from apps/web/lib/mock/sdm.ts. The table keeps its Immediate / Next / Upcoming tab filter and its per-tab empty state.

Acceptance criteria

  • Active leads in the BRD/quote/bid window (pending / backlog / quotation_process / awaiting_confirmation, excluding qualified-out and delivery-stage leads) roll into demand cells keyed by platform + module + region + language, scoped to the SDM's area and tenant via the area-membership rule.
  • Each cell shows a real demand count and quotesWaiting (the quote-stage subset); bidReadyExperts are the distinct non-excluded matches reused from the bid-pool / lead-expert-match data, not a fresh matching pass.
  • Each cell is bucketed into a time-band from real days-to-start derived from lead.startDate (Immediate < 7 days, Next 7–14, Upcoming > 14); daysToStart is the cell's soonest start, and leads with no start date fall into Upcoming.
  • The demand table renders from the aggregation via sdmDemandService — no longer from lib/mock/sdm; the Vendor + Product columns show the real platform + module, the mine/tenant scope toggle changes the aggregated cells, and goLiveDemand is removed from apps/web/lib/mock/sdm.ts.
  • The coverage columns (status, coverageGap) render in a neutral pending state for every row this run; they are not computed here.

Out of scope

  • The supply/coverage classification — the real status, readiness, coverage, and coverageGap values are computed against supply by sdm-supply-coverage (sequence 4); here those columns are neutral placeholders only.
  • The platform→module taxonomy itself — defined and seeded by sdm-supply-taxonomy (sequence 1, now merged); this feature consumes the resolved key, it does not build it.
  • The risk + action-strip derivation (goLiveRisks / actionStrip, sequence 5) — untouched.
  • Re-implementing area scoping or the mine/tenant toggle — owned by sdm-area-foundation (sequence 2); this feature reuses its area-membership rule.
  • Changing the match/scoring enginelead_expert_match rows are read as-is for bid-ready experts.
  • The standalone apps/dashboards SDM page — it stays mock by convention (the dashboards app is self-contained).

Open questions

  • none

02_build/output/notes.md

Build notes: sdm-demand-aggregation

  • commits:
    • feat: sdm-demand-aggregation — demand read-service + wire go-live demand table
    • merge main (sdm-supply-taxonomy) + rework demand aggregation onto platform→module

What changed

  • packages/services/src/db/services/sdm-demand/ (new): sdmDemandService.getDemandForSdm(tenantId, sdmUserId, scope) aggregates the SDM's area-scoped, active leads into demand cells keyed by (platform, module, region, language). It reuses the same area-membership rule as sdmAreaServicearea.platforms (platform ids) resolved to their module (product) ids — filters to the BRD→quote→bid demand window (pending / backlog / quotation_process / awaiting_confirmation, isActiveProject: false), resolves product → platform for the cell key, and returns per-cell demand, quotesWaiting, bidReadyExperts, timeBand and daysToStart.
  • …/services/index.ts: export sdmDemandService + the SdmDemandService / SdmDemandCell / DemandTimeBand types.
  • apps/web/app/(app)/sdm/dashboard/page.tsx: resolveArearesolveDashboard, which fetches the area envelope and the demand aggregation in parallel (same tenant + SDM + scope) and passes the cells to DemandTable.
  • apps/web/components/dashboard/sdm/demand-table.tsx: consumes the aggregated cells; the Vendor column shows the real platform, Product shows the real module, and only the coverage columns render a neutral "Pending" badge until sdm-supply-coverage.
  • apps/web/lib/mock/sdm.ts: removed goLiveDemand.

Dependency note

  • The spec's cell key is platform + module, supplied by sdm-supply-taxonomy (sequence 1). That feature was unbuilt when this run started, so the first build keyed on product alone as an interim (the convention sdm-area-foundation had set) with the vendor column pending. Sequence 1 then merged to main (PR #524: platform model, product.platform ref, sdm.area.platforms re-pointed to platform). This branch merged main and was reworked onto the real platform→module key — the Vendor column is now real; only the coverage columns stay pending (their feature, sequence 4, is still unbuilt).

Acceptance criteria status

  • Active BRD/quote/bid leads roll into demand cells keyed by platform + module + region + language, scoped to the SDM's area and tenant — sdmDemandService lead filter + product→platform resolution + cell aggregation.
  • Each cell shows real demand + quotesWaiting (quote-stage subset); bidReadyExperts are the distinct non-excluded lead_expert_match rows, not a fresh matching pass.
  • Each cell is bucketed by a time-band from lead.startDate (Immediate < 7, Next 7–14, Upcoming > 14); daysToStart is the cell's soonest; no-start leads fall into Upcoming.
  • The table renders from sdmDemandService with real Vendor + Product, the mine/tenant toggle changes the cells, and goLiveDemand is removed from the mock.
  • The coverage columns render a neutral pending state this run.

Verify result

  • Mechanical checks (format · lint · typecheck · build) run in CI + the Vercel preview, not here.

Notes for review

  • The demand service resolves the SDM's area directly and rebuilds the same scoped lead filter as sdmAreaService (platforms → product ids), because the envelope's projected leads[] don't carry startDate / languages / the product+platform needed to aggregate. The area-membership rule is intentionally kept identical to the area service — worth a glance to confirm they stay in step.
  • bidReadyExperts and daysToStart are produced but not yet rendered as table columns (the table didn't surface them before either); they're in the cell shape for the coverage/risk features downstream.
  • Lead status is matched against lowercase workflow names, consistent with leadService.

03_release/output/changelog.md

Changelog entry — sdm-demand-aggregation

Live at apps/help/app/changelog/2026-06-23-sdm-demand-aggregation/page.mdx.


title: "See your real go-live demand on the SDM dashboard" personas: [sdm]

See your real go-live demand on the SDM dashboard

Your go live readiness dashboard's demand table now reflects the real work heading your way:

  • Demand is rolled up from your live leads — counts and quotes waiting for each vendor, product, region and language in your area.
  • Each row shows the experts already bid-ready for that work, drawn from real matches.
  • Rows are grouped by how soon delivery starts — immediate, next, and upcoming.
  • The "My area" / "All supply" toggle reshapes the demand view to match.

03_release/output/investor-update.md

SDMs now see their real go-live demand, not mock rows

Who it's for: Service delivery managers (SDM) What shipped: The go live readiness dashboard's demand table now builds from each SDM's real, area-scoped lead pipeline — demand counts, quotes waiting and bid-ready experts per platform and module, bucketed by time to start. Why it matters: SDMs can plan coverage against real demand — Build the Bridge, Q2 2026 Objective 3 (validate technical infrastructure & payout flow).

Dig deeper: https://github.com/sustentus/sustentus/pull/521 · https://help.sustentus.com/changelog/2026-06-23-sdm-demand-aggregation

03_release/output/release.md

Release: sdm-demand-aggregation

  • pr: #521 · merged: pending squash on main (Ready to merge ticked, CI green) — finalised in this session's release summary
  • CI: green — Typecheck, Lint, Format, Audit database, Migrate preview database all passed; Migrate production skipped (pre-merge, expected).
  • technical docs: no technical docs impact — sdmDemandService is an internal read-service not described at service granularity in apps/docs/app/technical/**; no new app, route, env var, or architecture change (mirrors the sdm-area-foundation sibling).
  • business docs: no business docs impact — the SDM "go live readiness" dashboard and its go-live demand are already described in feature-role-matrix/service-journey; this wires real, area-scoped data behind the existing demand table without changing documented persona capabilities.
  • release notes: both — changelog entry (apps/help/app/changelog/2026-06-23-sdm-demand-aggregation/) + investor draft in this PR.
  • deploy: pending — production web + help green-deploy gate verified post-merge via the Vercel MCP.
  • sent: pending — investor update sent after the green deploy.

Review summary

  • Demand failure could blank the whole dashboard (fixed on branch). resolveDashboard ran the area and demand resolves in one Promise.all inside a single try; a getDemandForSdm failure rejected the lot → the page fell back to SdmDashboardEmpty, hiding the header, area panel and every other section even when the area resolved. Demand now degrades independently (.catch → empty cells → empty demand table); area stays the gate.
  • Cell-key collision on space-joined fields (hardened on branch). The (platform, module, region, language) key was space-joined, so e.g. "A B"/"C" and "A"/"B C" could collapse into one cell. Switched to a NUL (\u0000) join — a separator that can't appear in a name.
  • Accepted: duplicated fullName helper (matches the per-service pattern across sdm-area/csm-portfolio); the 5-resolve/Promise.all that registers the product + platform models for the nested populate(); bidReadyExperts/daysToStart produced but not yet rendered as columns (carried for the coverage/risk features downstream, as the mock did).

Acceptance check (vs spec)

  • Active BRD/quote/bid leads roll into demand cells keyed by platform + module + region + language, scoped to the SDM's area and tenant — lead filter (DEMAND_STATUSES, isActiveProject: false, region + resolved-product scoping) + cell aggregation.
  • Each cell shows real demand + quotesWaiting (quote-stage subset); bidReadyExperts are the distinct non-excluded lead_expert_match rows, not a fresh matching pass.
  • Each cell is time-banded from lead.startDate (Immediate < 7, Next 7–14, Upcoming > 14); daysToStart is the cell's soonest; no-start leads fall into Upcoming.
  • The table renders from sdmDemandService with real Vendor + Product, the mine/tenant toggle changes the cells, and goLiveDemand is removed from apps/web/lib/mock/sdm.ts.
  • The coverage columns render a neutral pending state this run.