vendor-revenue-flowrun.md00_intake/stub.mdThe 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.
arrMovementStrip mock remains.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.mdgetRevenueFlow 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.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.
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:
quotation_process — Bid Process Started (scoping / bidding)awaiting_confirmation — Quote Reviewwork_in_progress — Work In Progress (active delivery)delivered — Delivered — terminal 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]):
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.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.
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.status-history), and
real conversion-to-next (share of stage's leads that reached the next stage), over the selected
window.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.{ from, to } window as the other sections) and stays tenant-scoped to the signed-in vendor.vendor-funnel-customers) — the count + post-BRD revenue funnel.
It reads the same lifecycle but is a distinct section; not touched here.serviceFee / commercial-config take is not applied this round.getPeriodEnvelope) — this feature
consumes them; it does not redefine the window or the period set.revenue-flow-section.tsx component — it already renders the strip; this feature only
feeds it real data.apps/dashboards source-of-truth copy, the
Export control, or realtime updates — all unchanged.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.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.mdpackages/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:status is that stage (open, not-yet-paid service revenue);status-history timeline (entering the stage →
next status change, or now if still there), averaged over leads that entered it;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.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.
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.status-history) and conversion-to-next (share reaching the next stage).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.{ from, to } window as the other sections (re-queries with the
date-range selector) and stays tenant-scoped (getTenantOrNull).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).serviceFee/margin, per the spec's
out-of-scope note on vendor-take.03_release/output/changelog.mdThe 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.mdWho 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.mdVendorService method; no new app/route/env/architecture — consistent with the sibling features)feature-role-matrix/vendors does not enumerate per-datapoint metrics)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.
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.arrMovementStrip field/type alignment (page → view → section), barrel re-exports,
and getQuoteModel import — all verified clean by the cross-file tracer.type over interface, braces-on-every-if, named imports, sentence-case copy)
and reuse (percent, DAY_MS, statusService, getStatusHistoryModel) — no violations.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.status-history (now loop-aware); conversion-to-next = share reaching the next
stage — verified.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.{ from, to } window and stays tenant-scoped — verified.