Skip to Content

← All archived runs

Run: vendor-funnel-customers

run.md

Run: vendor-funnel-customers

  • branch: claude/wizardly-galileo-einv4x
  • pr: #545

00_intake/stub.md

Stub: Vendor dashboard — revenue funnel + top customers

  • feature-slug: vendor-funnel-customers
  • epic: vendor-dashboard-data
  • personas: Vendor
  • initiative: Build the Bridge / objective: Q2 2026 Objective 1 — Establish Product-Market Fit with Vendor Partners
  • depends-on: vendor-revenue-retention
  • sequence: 8 of 8

Problem

The funnel section (funnel-section.tsx) shows the revenue funnel (lead → activated → BRD → in-delivery → delivered → CSAT, with counts + post-BRD revenue + conversion % + avg days), revenue-lost-by-stage, a lead-to-BRD conversion card, and the top-customers table. The existing vendorService.getLeadFunnel + getTopCustomers already supply the funnel counts and a partial top-customers list (the page adapts them, defaulting revenueAwaitingCSAT: 0, status: "stable", and revWeighted CSAT to the raw CSAT). The revenue overlay, revenue-lost, lead-to-BRD and the missing customer columns are still mock.

Proposed change

  • Extend getLeadFunnel to overlay post-BRD revenue, conversion % and avg days per stage on the existing counts (reusing feature 2's revenue), and add revenue-lost-by-stage (count + revenue
    • pct lost between stages) and the lead-to-BRD conversion card (rate, change, revenue created at BRD, avg days lead→scoped).
  • Complete getTopCustomers: real revenue-weighted CSAT, revenue awaiting CSAT, and an at-risk vs stable status (from feature 6's risk flags) — removing the page's hard-coded adapter defaults.
  • Wire the funnel section so all four blocks render from the extended service; drop the remaining vendorData.funnel / topCustomers fallbacks on the page.

Acceptance criteria (rough)

  • The funnel shows real counts and post-BRD revenue, conversion % and avg days per stage over the window.
  • Revenue-lost-by-stage and the lead-to-BRD card are computed from real data.
  • The top-customers table shows real revWeighted CSAT, revenue awaiting CSAT and at-risk status; the page no longer applies hard-coded adapter defaults or vendorData fallbacks for this section.

Out of scope (this feature)

  • The revenue-flow strip (feature 5) — that is the in-stage revenue + timing view; this funnel is the count + post-BRD-revenue conversion view.
  • A drill-through customer-detail page — the table is the dashboard summary (the vendor's customer CRM pages are out of epic scope).

Notes for Define

  • The funnel counts service already exists — extend getLeadFunnel / getTopCustomers, don't fork. Confirm the canonical funnel stages against the status graph (the dashboards mock and the existing service may differ — reconcile to one).
  • At-risk status on a customer row should reuse feature 6's renewal-risk flags so the funnel and the risk section agree.
  • touches: packages/services/src/server (extend vendorService funnel + top-customers), reads packages/services/src/db/models/{lead,invoice,csat,status-history}.ts, apps/web/app/(app)/vendor/dashboard/page.tsx (drop adapter defaults + fallbacks), apps/web/components/dashboard/vendor/{funnel-section,funnel-chart-card,top-customers-table}.tsx.

01_define/output/spec.md

Spec: Vendor dashboard — revenue funnel + top customers

  • slug: vendor-funnel-customers
  • personas: Vendor
  • touches: packages/services/src/db/services/vendor (extend getLeadFunnel + getTopCustomers, add revenue-lost + lead-to-BRD); apps/web/app/(app)/vendor/dashboard/{page.tsx,empty-data.ts}; apps/web/components/dashboard/vendor/types.ts; reads packages/services/src/db/models/{lead,invoice,csat,status-history,quote}.ts
  • complexity: standard

Problem

The vendor dashboard's "Work entering service" section — the revenue funnel chart, the lead-to-BRD conversion card, the revenue-lost-by-stage table and the top-customers table — is the last section of the vendor-dashboard-data epic still running on placeholder data. Today vendorService.getLeadFunnel supplies only stage counts and getTopCustomers supplies a partial list; the page's adaptTopCustomer hard-codes revWeightedCSAT to the raw CSAT, revenueAwaitingCSAT: 0 and status: "stable", and the post-BRD revenue overlay, the revenue-lost-by-stage table and the lead-to-BRD conversion card fall back to the zeroed emptyVendorData. That leaves the vendor looking at a funnel that shows volume but not the money moving through it, and a top-customers table whose risk and CSAT-by-revenue columns are fabricated.

Completing this section finishes the epic's mandate — every vendor-dashboard section driven by real, per-tenant data over the selected window — which advances the Build the Bridge initiative's Q2 2026 Objective 1 (Establish Product-Market Fit with Vendor Partners): vendors, the paying subscriber org, can only judge product fit if the conversion-and-revenue view they care about ("conversion rate at every stage of the funnel", time-in-stage, CSAT) reflects their real book of business.

Proposed change

Extend the existing tenant- and period-scoped vendorService (do not fork it) so the funnel section renders entirely from real data, then drop the page's adapter defaults and emptyVendorData fallbacks for this section.

  • Revenue funnel (extend getLeadFunnel). Keep the real per-stage counts, and overlay, per stage from the BRD/scoped stage onward: post-BRD revenue, conversion % (share of the prior stage that reached this stage) and avg days in stage. Post-BRD revenue reuses feature 2's per-customer revenue (getRevenueRetentionperCustomerRevenue) / accepted-quote totals — it is not a new revenue definition; avg days-in-stage reuses the lead status-history dwell approach the revenue-flow engine (getRevenueFlow) already uses. Pre-BRD stages carry count only (matching the chart's "volume pre-BRD / revenue post-BRD" legend).
  • Revenue-lost-by-stage. For each pre-go-live stage, compute count lost, revenue lost and % of stage revenue lost between consecutive stages (leads that dropped / were rejected before progressing), so the "Revenue lost before go-live" table is real.
  • Lead-to-BRD conversion card. Compute the conversion rate (lead → BRD/scoped) with its change vs the prior equal-length period, the revenue created at BRD, and the avg days lead → scoped with its change.
  • Top customers (complete getTopCustomers). Add real revenue-weighted CSAT, revenue awaiting CSAT (windowed paid revenue from customers with no CSAT in the window) and an at-risk vs stable status per customer row, removing the page's hard-coded adapter defaults.
  • Wire the page. apps/web/app/(app)/vendor/dashboard/page.tsx passes the extended service output straight through to FunnelSection; drop adaptTopCustomer's hard-coded revWeightedCSAT / revenueAwaitingCSAT / status and the ?? emptyVendorData.{funnel, revenueLostByStage, leadToBRD, topCustomers} fallbacks for this section (the empty-data zero-state stays only as the failed-read fallback, consistent with the sibling sections). Widen VendorData.funnel in components/dashboard/vendor/types.ts to carry the optional per-stage revenue the chart already reads.

Decisions resolved here

  • Canonical funnel stages. The funnel is built from the canonical lead status graph (statusService.getByWorkflow("lead"), as getLeadFunnel already does) — not the old dashboards mock labels. Where the mock and the status graph disagree, the status graph wins; "BRD" is resolved to the canonical scoped/BRD status and is the boundary at which the revenue overlay begins.
  • At-risk status definition (feature 6 not yet built). The renewal-risk classifier (vendor-revenue-risk, feature 6) has not landed, so a customer row's at-risk flag reuses the same heuristic risk-signal set the already-shipped customer-health section useslowCSAT (revenue-weighted/normalised CSAT below the shared LOW_CSAT_CUTOFF of 4.0) or an ageing open project (> 30 days). This keeps the funnel's status column and the customer-health section consistent today; when feature 6 lands it owns the flags and both converge on it (no parallel risk definition is introduced here).

Acceptance criteria

  • The revenue funnel shows real per-stage counts and, from the BRD stage onward, real post-BRD revenue, conversion % and avg days in stage over the selected window.
  • The revenue-lost-by-stage table shows real count-lost, revenue-lost and % stage-revenue-lost per pre-go-live stage (no emptyVendorData fallback for this block on a successful read).
  • The lead-to-BRD conversion card shows a real conversion rate (+ change vs prior period), revenue created at BRD, and avg days lead → scoped (+ change), all from real data.
  • The top-customers table shows real revenue-weighted CSAT, revenue awaiting CSAT and an at-risk / stable status per row; page.tsx no longer hard-codes revWeightedCSAT, revenueAwaitingCSAT or status in adaptTopCustomer.
  • The funnel section's blocks no longer read emptyVendorData.{funnel, revenueLostByStage, leadToBRD, topCustomers} except as the explicit failed-read zero-state (matching the sibling sections' error handling).
  • The funnel reads/extensions stay tenant-scoped and honour the dashboard's selected date-range window; the at-risk flag agrees with the customer-health section for the same customer.

Out of scope

  • The revenue-flow strip (feature 5) — the in-stage open-revenue + timing view is already shipped; this funnel is the count + post-BRD-revenue conversion view, a separate section.
  • Building the renewal-risk classifier (feature 6, vendor-revenue-risk) — this feature only consumes a heuristic at-risk signal; it does not build the revenue-risk section or its flags.
  • A drill-through customer-detail / CRM page — the table is the dashboard summary; the "View all customers" / "Analyse drop-offs" links point at existing routes and are not built here.
  • The Export button / CSV-PDF generation — the header control stays cosmetic (epic-level scope).
  • New funnel UI componentsfunnel-section.tsx, funnel-chart-card.tsx and top-customers-table.tsx already render the richer shapes; this run wires data to them, not redesign.
  • Replicating any change back into apps/dashboards — that app stays the dummy source-of-truth.

Open questions

  • none. (When feature 6 vendor-revenue-risk lands, the at-risk flag should switch from the shared heuristic to feature 6's renewal-risk flags — that is the documented follow-up, not a blocker for this run, since both are defined to agree.)

02_build/output/notes.md

Build notes: vendor-funnel-customers

  • commits: feat: vendor-funnel-customers — revenue funnel + top customers from real data

What changed

  • packages/services/src/db/services/vendor/index.ts — extended the funnel service (no fork):
    • getLeadFunnel now returns VendorFunnel ({ funnel, revenueLostByStage, leadToBRD }) instead of { funnel }. Funnel rows keep the snapshot per-stage counts (non-rejected, by current status) and, from the BRD stage (quotation_process) onward, overlay open post-BRD revenue (accepted-quote total → latest quote → 0, the same definition the revenue-flow strip uses), conversion % (reached-this ÷ reached-prior over the canonical forward path) and avg days-in-stage (status-history dwell, re-entries summed). Revenue-lost-by-stage: leads that stalled before delivery (rejected / qualified-out) attributed to their furthest stage, with revenue lost as a share of revenue that reached the stage. Lead-to-BRD card: conversion rate, revenue created at BRD and avg lead→scoped days, each with a change vs the equal-length prior window.
    • New private helpers loadFunnelWindow (windowed leads + open revenue + status timeline), funnelProgress (furthest forward-path stage + first BRD transition) and leadToBrdStats (the three lead→BRD scalars, reused for the prior window).
    • getTopCustomers now returns real revWeightedCSAT, revenueAwaitingCSAT (paid revenue from customers with no CSAT response) and an at-risk / stable status (low-CSAT < shared 4.0 cutoff or ageing open project > 30d — the same heuristic, and the same ageing query shape, the customer-health section uses, so the two sections' risk view agrees).
    • New constants (LEAD_FUNNEL_PATH, BRD_STAGE, QUALIFIED_OUT_STATUS, AGEING_DAYS) + new exported types (VendorRevenueLostRow, VendorLeadToBRD, VendorFunnel); VendorFunnelRow / VendorTopCustomer gained the overlay / risk fields.
  • packages/services/src/db/services/index.ts — re-export the three new funnel types.
  • apps/web/app/(app)/vendor/dashboard/page.tsxadaptTopCustomer no longer hard-codes revWeightedCSAT / revenueAwaitingCSAT / status (reads them from the service); the funnel section's revenueLostByStage and leadToBRD are now wired from the extended getLeadFunnel result, with the emptyVendorData zero-state kept only as the failed-read fallback.
  • apps/web/components/dashboard/vendor/types.ts — widened VendorData.funnel to carry the optional revenue / conversionPct / avgDays overlay.
  • apps/web/components/dashboard/vendor/funnel-chart-card.tsxFunnelRow carries the overlay fields and the chart tooltip surfaces post-BRD revenue, conversion % and avg days when present.

Acceptance criteria status

  • Funnel shows real counts + post-BRD revenue, conversion % and avg days per stage over the window — counts as before; overlay from quotation_process onward, surfaced in the chart tooltip.
  • Revenue-lost-by-stage computed from real data (stalled leads attributed to their furthest pre-delivery stage; % of revenue that reached the stage).
  • Lead-to-BRD card computed from real data (rate + change, revenue created at BRD, avg lead→scoped + change vs prior window).
  • Top-customers table shows real revWeighted CSAT, revenue awaiting CSAT and at-risk status; page.tsx no longer hard-codes those three fields.
  • Funnel blocks no longer read emptyVendorData.{funnel, revenueLostByStage, leadToBRD, topCustomers} except as the explicit failed-read zero-state.
  • Funnel reads stay tenant-scoped + window-aware; the at-risk flag uses the same definition as the customer-health section.

Verify result

  • pnpm --filter @sustentus/services typecheck — clean.
  • pnpm --filter web typecheck — clean (after building @sustentus/services so the web app resolves the updated package types).
  • Format · lint · full build run in CI + the Vercel preview; nothing here is expected to fail.

Notes for review

  • Canonical stages — the funnel forward path is resolved from the lead status graph (pending → backlog → quotation_process → awaiting_confirmation → work_in_progress → delivered → survey_sent → completed); quotation_process ("Bid Process Started") is treated as the BRD/scoping boundary, matching the revenue-flow strip's first revenue-bearing stage. qualified_out is off-path (a drop-off), so it never carries an overlay.
  • Revenue per funnel stage is the open quote revenue of leads currently at that stage (snapshot), to stay consistent with the count bar; the revenue-flow strip (feature 5) is a separate section and was not touched.
  • Row-level revWeightedCSAT equals the customer's own normalised CSAT — at a single account there is nothing to weight across; the portfolio-level revenue-weighted CSAT remains in the customer-health section. csat/spend lookups in getTopCustomers stay lifetime (as shipped in feature 1); the window still selects which customers appear. The at-risk flag shares the customer-health definition (low-CSAT-or-ageing).
  • Follow-up (not this run): when feature 6 vendor-revenue-risk lands, the at-risk flag should switch from this shared heuristic to its renewal-risk flags.

03_release/output/changelog.md

Changelog: vendor-funnel-customers

Entry: apps/help/app/changelog/2026-06-25-vendor-funnel-customers/page.mdx


title: See your revenue funnel and top customers with real numbers personas: [vendor]

The "work entering service" section of your vendor dashboard now runs on real data instead of placeholders. For the date range you choose, the revenue funnel shows how many leads sit at each stage and — from scoping onward — the revenue, conversion rate and average time in each stage. Alongside it, a lead-to-scoping card and a revenue-lost-before-go-live table show where value is created and where it leaks. The top-customers table now shows each account's revenue-weighted satisfaction, revenue still awaiting a satisfaction score, and whether the account is at risk or stable.

03_release/output/investor-update.md

Vendors can now see their full revenue funnel and top customers on real data

Who it's for: Vendor partners — the paying tenant What shipped: The vendor dashboard's revenue funnel, lead-to-scoping card, revenue-lost table and top-customers table now run on real per-tenant data over any date range. Why it matters: It completes the vendor revenue cockpit — every section now real — advancing Build the Bridge, Q2 2026 Objective 1: establish product-market fit with vendor partners.

Dig deeper: <merged-PR URL> · <changelog entry URL>