Skip to Content

← All archived runs

Run: vendor-revenue-flow

run.md

Run: vendor-revenue-flow

  • branch: claude/vendor-revenue-pipeline-pe14h2
  • pr: #541

00_intake/stub.md

Stub: Vendor dashboard — ARR movement / revenue flow strip

  • feature-slug: vendor-revenue-flow
  • 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-foundation
  • sequence: 5 of 8

Problem

The revenue-flow section (revenue-flow-section.tsx) renders an ARR-movement strip: one tile per lifecycle stage (in-design / in-delivery / awaiting sign-off / delivered) showing revenue-in-stage, avg days-in-stage, and conversion-to-next. It is entirely from the mock arrMovementStrip. There is no engine that places the tenant's leads/projects into lifecycle stages and rolls revenue + timing per stage.

Proposed change

  • Map the tenant's in-window leads/projects onto the delivery lifecycle stages (from the status graph / service-journey), and per stage compute revenue-in-stage (open revenue currently at that stage), avg days-in-stage, and conversion-to-next (share that progressed to the following stage).
  • Wire the revenue-flow section strip to this engine over the real set.

Acceptance criteria (rough)

  • Each lifecycle-stage tile shows real revenue-in-stage, avg days-in-stage and conversion-to-next over the window.
  • The terminal stage (delivered) has no avg-days/conversion (matching the strip's null handling).
  • The section renders entirely from the engine; no arrMovementStrip mock remains.

Out of scope (this feature)

  • The revenue funnel (feature 8) — that is the count + post-BRD revenue funnel; this strip is the revenue-in-stage + timing view. They read the same lifecycle but are distinct sections.
  • Vendor-take / margin per stage — revenue here is service revenue in stage, not margin.

Notes for Define

  • Resolve the stage vocabulary against the real status graph — the dashboards mock uses in-design / in-delivery / awaiting-sign-off / delivered; confirm the canonical stages so the strip matches how delivery is actually modelled (avoid hard-coding the mock's labels).
  • Reuse the foundation's window + revenue definitions; "revenue-in-stage" is open (not-yet-paid) revenue positioned at the stage, distinct from feature 2's paid-revenue retention.
  • touches: packages/services/src/server (stage rollup on the envelope), reads packages/services/src/db/models/{lead,status,status-history,quote}.ts, apps/web/components/dashboard/vendor/revenue-flow-section.tsx.

01_define/output/spec.md

Spec: Vendor dashboard — ARR-movement / revenue-flow strip

  • slug: vendor-revenue-flow
  • personas: Vendor
  • touches: packages/services/src/db/services/vendor (add getRevenueFlow on VendorService + exported type), apps/web/app/(app)/vendor/dashboard/{page.tsx,vendor-dashboard-view.tsx} (wire the section to the live read); reads packages/services/src/db/models/{lead,status-history,quote}.ts and the lead workflow (packages/services/src/db/workflows/workflows.json). The revenue-flow-section.tsx component already renders the strip and does not change.
  • complexity: standard

Problem

The vendor dashboard's revenue-flow section (revenue-flow-section.tsx) renders an ARR-movement strip — one tile per delivery lifecycle stage showing revenue-in-stage, avg days-in-stage and conversion-to-next. The vendor-revenue-foundation feature (epic feature 1) removed the vendorData mock, so the strip now reads from emptyVendorData and renders an empty arrMovementStrip: [] — the section is blank. There is no engine that places the tenant's leads onto the real lifecycle stages and rolls revenue + timing per stage.

This is feature 5 of the vendor-dashboard-data epic, which advances the Build the Bridge initiative / Q2 2026 Objective 1 — Establish Product-Market Fit with Vendor Partners: the vendor dashboard is the paying subscriber's revenue-retention & service-health cockpit, and the revenue-flow strip is how a vendor sees where open service revenue is sitting in delivery and how fast it moves through. It can only be real once an engine maps the tenant's windowed leads onto the canonical lifecycle and rolls revenue/timing per stage.

Proposed change

Add a getRevenueFlow(tenantId, window) read on VendorService (exported via @sustentus/services/server) that, over the foundation's period-scoped envelope, returns one tile per canonical lead lifecycle stage, and wire the revenue-flow section to it in page.tsx (replacing the empty arrMovementStrip).

Stage vocabulary (resolved against the real status graph, not the mock labels). The lead workflow (workflows.json) is pending → backlog → quotation_process → awaiting_confirmation → work_in_progress → delivered → survey_sent → completed (+ qualified_out). The strip renders the revenue-bearing delivery path — the four stages, in workflow order, using each status' canonical displayName:

  1. quotation_processBid Process Started (scoping / bidding)
  2. awaiting_confirmationQuote Review
  3. work_in_progressWork In Progress (active delivery)
  4. deliveredDeliveredterminal tile for the strip (no avg-days, no conversion-to-next)

Pre-revenue stages (pending, backlog) and post-delivery stages (survey_sent, completed, qualified_out) are not tiles: revenue is not yet positioned before a quote exists, and delivered is the strip's "delivered value" terminal. (The stage list is derived from the workflow, not hard-coded labels — if the workflow's revenue-bearing stages change, the strip follows.)

Per-tile computation, over the foundation envelope's windowed leads (the vendor tenant's leads created in [from, to]):

  • revenue-in-stage — the sum of the accepted quote amount of leads currently at that stage (open, contracted-but-not-yet-paid service revenue). Where a lead at the stage has no accepted quote, fall back to its latest quote amount; a lead with no quote contributes 0.
  • avg days-in-stage — the average time leads spent in that stage, from status-history (entityType lead): for each lead that passed through the stage, the elapsed time between entering it and its next status change; averaged over the windowed leads that entered the stage. Rounded to whole days.
  • conversion-to-next — of the windowed leads that reached this stage, the share that progressed to the next stage in the path (reached the next stage or beyond), as a whole-number percent.
  • The terminal stage (delivered) reports avgDays: null and conversionToNext: null — matching the section's null handling (it shows the "Active service revenue" label instead).

The returned shape matches the view's existing arrMovementStrip type ({ stage, revenue, avgDays, conversionToNext }[]), so page.tsx feeds it straight through VendorDashboardView to RevenueFlowSection with no component change. On a failed read the section falls back to the empty strip (the established per-section zero-state pattern), never to a mock.

Acceptance criteria

  • VendorService exposes getRevenueFlow(tenantId, window) (exported from @sustentus/services/server) returning one tile per revenue-bearing lifecycle stage (quotation_process, awaiting_confirmation, work_in_progress, delivered) in workflow order, using canonical displayNames, computed over the foundation's period envelope.
  • Each tile shows real revenue-in-stage (sum of accepted quote amount of leads currently at the stage, fallback to latest quote, 0 where none), real avg days-in-stage (from status-history), and real conversion-to-next (share of stage's leads that reached the next stage), over the selected window.
  • The terminal Delivered tile reports avgDays: null and conversionToNext: null (the section renders its "Active service revenue" label), matching the strip's null handling.
  • page.tsx wires the revenue-flow section to getRevenueFlow over the active window; the section renders entirely from the engine and no longer reads the empty arrMovementStrip. No mock is reintroduced; a failed read shows the empty strip.
  • The strip re-queries with the dashboard's date-range selector (it consumes the same { from, to } window as the other sections) and stays tenant-scoped to the signed-in vendor.

Out of scope

  • The revenue funnel (epic feature 8, vendor-funnel-customers) — the count + post-BRD revenue funnel. It reads the same lifecycle but is a distinct section; not touched here.
  • Vendor-take / margin per stage — revenue-in-stage is open service revenue (quote value), not margin; the serviceFee / commercial-config take is not applied this round.
  • Changing the foundation's window predicates or envelope shape (getPeriodEnvelope) — this feature consumes them; it does not redefine the window or the period set.
  • Any change to the revenue-flow-section.tsx component — it already renders the strip; this feature only feeds it real data.
  • Editing the section's subtitle / labels / layout, the apps/dashboards source-of-truth copy, the Export control, or realtime updates — all unchanged.

Open questions

  • none — stage vocabulary (revenue-bearing delivery path, canonical displayNames, delivered terminal) and the revenue figure (accepted quote amount, fallback to latest) were decided during Define; the window and envelope are inherited from vendor-revenue-foundation.

Dependency note

Depends on vendor-revenue-foundation (epic feature 1), which provides the period-scoped envelope (getPeriodEnvelope) and the date-range window this strip aggregates over. Build should consume the foundation's envelope + window helpers rather than re-querying raw collections or re-deriving the window.

02_build/output/notes.md

Build notes: vendor-revenue-flow

  • commits: feat(vendor-revenue-flow): revenue-flow ARR-movement engine + wire section

What changed

  • packages/services/src/db/services/vendor/index.ts: added getRevenueFlow(tenantId, window) on VendorService plus the VendorArrMovementTile / VendorRevenueFlow types and the REVENUE_FLOW_STAGES / ACCEPTED_QUOTE_STATUS constants. The engine maps the period set (the tenant's leads created in the window — same predicate as the foundation envelope) onto the revenue-bearing delivery path and rolls, per stage:
    • revenue-in-stage — accepted-quote total (fallback latest quote, 0 where none) of the leads whose current status is that stage (open, not-yet-paid service revenue);
    • avg days-in-stage — dwell from each lead's status-history timeline (entering the stage → next status change, or now if still there), averaged over leads that entered it;
    • conversion-to-next — of the leads that reached the stage (max path-index over their history + current status), the share that reached the next stage. The terminal delivered tile reports avgDays: null / conversionToNext: null.
  • packages/services/src/db/services/index.ts: re-export VendorArrMovementTile and VendorRevenueFlow from @sustentus/services/server.
  • apps/web/app/(app)/vendor/dashboard/page.tsx: call getRevenueFlow over the active window in the existing Promise.all, and feed its arrMovementStrip into data (fallback to the empty strip on a failed read — no mock). The section component is unchanged.

Stage vocabulary (resolved against the real status graph)

Revenue-bearing delivery path, canonical displayNames, in workflow order: quotation_process (Bid Process Started) → awaiting_confirmation (Quote Review) → work_in_progress (Work In Progress) → delivered (Delivered, terminal). Pre-quote (pending, backlog) and post-delivery (survey_sent, completed) stages are excluded.

Acceptance criteria status

  • VendorService.getRevenueFlow(tenantId, window) exported from @sustentus/services/server, one tile per revenue-bearing stage in workflow order using canonical displayNames, over the period set.
  • Each tile shows real revenue-in-stage (accepted-quote total, fallback latest, 0 where none), avg days-in-stage (from status-history) and conversion-to-next (share reaching the next stage).
  • Terminal Delivered tile reports avgDays: null and conversionToNext: null (section renders the "Active service revenue" label).
  • page.tsx wires the section to getRevenueFlow over the active window; the strip renders from the engine, no mock reintroduced, failed read → empty strip.
  • The strip consumes the same { from, to } window as the other sections (re-queries with the date-range selector) and stays tenant-scoped (getTenantOrNull).

Verify result

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

Notes for review

  • avg days-in-stage counts a lead still sitting in a stage as now − enteredAt (dwell-so-far), so the figure reflects live ageing rather than only completed spans — see the dwell loop. If reviewers prefer completed-spans-only, that's a one-line change (skip leads with no next entry).
  • conversion-to-next uses "reached stage index ≥ i" (furthest path stage over history + current status), so a lead that skipped a recorded intermediate status still counts as having passed through it — the funnel "got at least this far" semantic.
  • Revenue uses the quote total (full service revenue), not serviceFee/margin, per the spec's out-of-scope note on vendor-take.

03_release/output/changelog.md

See where revenue sits across your delivery stages on your vendor dashboard

The revenue-in-service strip on your vendor dashboard now shows real numbers instead of placeholders. For the date range you choose, each delivery stage — bid process, quote review, work in progress and delivered — shows how much open revenue is sitting there, the average days it spends in that stage, and how much of it moves on to the next.

03_release/output/investor-update.md

Vendors can now see where open revenue sits across their delivery pipeline

Who it's for: Vendor partners — the paying tenant What shipped: The vendor dashboard's revenue-flow strip now shows real open revenue, average days-in-stage and stage-to-stage conversion per delivery stage, over any date range. Why it matters: Vendors can see where revenue is sitting and slowing in delivery — advancing Build the Bridge, Q2 2026 Objective 1: establish product-market fit with vendor partners.

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

03_release/output/release.md

Release: vendor-revenue-flow

  • pr: #541 · merged: no — pending CI green on the review-fix commit + the Ready to merge tick
  • CI: green on the Build commit (Quality Project, preview DB migrate, audit, Vercel preview); re-running after the review-fix push
  • technical docs: no technical docs impact (adds a VendorService method; no new app/route/env/architecture — consistent with the sibling features)
  • business docs: no business docs impact (the vendor dashboard and its revenue-flow section are already the documented capability; this wires one section to real data — feature-role-matrix/vendors does not enumerate per-datapoint metrics)
  • release notes: both — changelog entry (apps/help/app/changelog/2026-06-24-vendor-revenue-flow) + investor draft in this PR
  • deploy: pending (poll after merge)
  • sent: pending green deploy

Review summary

Ran a focused review at medium effort (standard complexity) — 3 finder angles (correctness, cross-file/caller, conventions/cleanup), each adversarially scoped. Cross-file and conventions came back clean; one correctness finding, fixed on branch.

  • Loop transitions undercounted avg days-in-stage — the dwell used findIndex (first entry only), but the lead workflow has loop transitions (e.g. awaiting_confirmation → quotation_process on quote rejection), so a lead can re-enter a stage. Resolved on branch (commit f085019): every visit to the stage is now summed, averaged over leads that entered it.
  • Promise.all tuple order, arrMovementStrip field/type alignment (page → view → section), barrel re-exports, and getQuoteModel import — all verified clean by the cross-file tracer.
  • Conventions (arrow functions, type over interface, braces-on-every-if, named imports, sentence-case copy) and reuse (percent, DAY_MS, statusService, getStatusHistoryModel) — no violations.

Acceptance check (vs spec)

  • getRevenueFlow(tenantId, window) exported from @sustentus/services/server; one tile per revenue-bearing stage (quotation_process, awaiting_confirmation, work_in_progress, delivered) in workflow order via canonical displayNames — verified in getRevenueFlow.
  • revenue-in-stage = accepted-quote total (fallback latest, 0 where none) of leads currently at the stage; avg days-in-stage from status-history (now loop-aware); conversion-to-next = share reaching the next stage — verified.
  • Terminal Delivered tile reports avgDays: null / conversionToNext: null — verified.
  • page.tsx wires the section to getRevenueFlow over the active window; renders from the engine, no mock, failed read → empty strip — verified.
  • Re-queries with the date-range { from, to } window and stays tenant-scoped — verified.