Skip to Content

← All archived runs

Run: vendor-revenue-foundation

run.md

Run: vendor-revenue-foundation

  • branch: claude/vendor-revenue-foundation-pipeline-y9x5ya
  • pr: #528

00_intake/stub.md

Stub: Vendor dashboard — period-scoped read foundation

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

Problem

The web vendor dashboard (apps/web/app/(app)/vendor/dashboard/page.tsx) is already a server component scoped to the signed-in vendor's tenant (getTenantOrNull), and a vendorService already serves two sections for real (getLeadFunnel, getTopCustomers). But the page still spreads ...vendorData from apps/web/lib/mock/vendor.ts as the base and falls back to it section-by-section, so five of seven sections are dummy. Two things block the rest from being real: (1) there is no time window — the date-range selector (last 30 days / quarter / FY) and the time/region view toggle are cosmetic client state, so no section can aggregate "over the period"; and (2) there is no shared read envelope — each future section needs the same period-scoped set of the vendor's customers, paid invoices, leads, projects and CSAT to aggregate over.

Proposed change

  • Make the date-range selector a real query window: the selected range (last 30 days / this quarter / last quarter / YTD / FY) resolves to a { from, to } the page fetches against (server-driven, e.g. via searchParams), not client-only state. The time vs region view toggle stays a presentation switch over the same fetched set.
  • Stand up the period-scoped read envelope in @sustentus/services/server (extending vendorService): one tenant + window query resolves the vendor's in-scope customers, paid invoices, leads, projects and CSAT that every section (features 2–8) reads from — no section re-queries the raw collections.
  • Remove the vendorData mock fallback from the page: the view renders only from the service envelope. (Sections not yet wired by later features may temporarily render empty/zero states, but the page no longer imports lib/mock/vendor.)

Acceptance criteria (rough)

  • Selecting a date range re-queries the dashboard over that window (server-driven); the data changes with the range.
  • A single period-scoped read-service returns the vendor tenant's in-scope customers, paid invoices, leads, projects and CSAT for the other features to aggregate over.
  • The dashboard page no longer imports or spreads lib/mock/vendor; the time/region toggle switches presentation over the real set.
  • The page stays tenant-scoped to the signed-in vendor and renders without the mock present.

Out of scope (this feature)

  • The seven section computations (features 2–8) — this only stands up the window + envelope they read.
  • The Export button (stays cosmetic) and any CSV/PDF generation.
  • A territory/team scope toggle — the dashboard is whole-tenant (per the epic decision); there is no "my area vs all".

Notes for Define

  • Decide where the window lives: searchParams on /vendor/dashboard is the lightest server-driven option and keeps the selector shareable; confirm with the existing page conventions.
  • The envelope should be the one place "paid invoice in window" / "lead created in window" is defined, so features 2–8 share one definition of the period set (avoid each section bucketing time differently).
  • touches: packages/services/src/server (extend vendorService → period-scoped envelope), apps/web/app/(app)/vendor/dashboard/{page.tsx,vendor-dashboard-view.tsx} (window plumbing, drop mock), reads packages/services/src/db/models/{invoice,lead,csat,quote}.ts.

01_define/output/spec.md

Spec: Vendor dashboard — period-scoped read foundation

  • slug: vendor-revenue-foundation
  • personas: Vendor
  • touches: packages/services/src/db/services/vendor (extend VendorService → period-scoped envelope), apps/web/app/(app)/vendor/dashboard/{page.tsx,vendor-dashboard-view.tsx}; reads packages/services/src/db/models/{invoice,lead,csat,project}.ts
  • complexity: standard

Problem

The web vendor dashboard (apps/web/app/(app)/vendor/dashboard/page.tsx) is already a server component scoped to the signed-in vendor's tenant (getTenantOrNull), and vendorService already serves two of its seven sections for real (getLeadFunnel, getTopCustomers). But the page still spreads ...vendorData from apps/web/lib/mock/vendor.ts as its base and falls back to it section-by-section, so five of seven sections are dummy. Two things block the rest from becoming real: (1) there is no time window — the date-range selector and the time/region view toggle are cosmetic client state, so no section can aggregate "over the period"; and (2) there is no shared read envelope — each future section (features 2–8 of the vendor-dashboard-data epic) needs the same period-scoped set of the vendor's customers, paid invoices, leads, projects and CSAT to aggregate over.

This is feature 1 (the spine) 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 it can only be driven by real, per-tenant data once a real query window and a shared read envelope exist for every section to build on.

Proposed change

  • Make the date-range selector a real, server-driven query window. The selected range resolves to a { from, to } window the page fetches against, driven by a searchParams value on /vendor/dashboard (matching the existing server-driven searchParams convention on the CSM dashboard) — not client-only state. The selector reflects the active range from the URL and changing it re-queries the dashboard. The six ranges resolve as:
    • Last 30 days (default when no param) — [now − 30 days, now]
    • This quarter[start of current calendar quarter, now]
    • Last quarter[start of previous calendar quarter, start of current quarter]
    • Year to date[Jan 1 of current year, now]
    • This fiscal year[Jan 1, Dec 31] of the current year (fiscal year = calendar year, per the Define decision; no fiscal-year config entity this round)
    • Last fiscal year[Jan 1, Dec 31] of the previous calendar year
  • The time vs region view toggle stays a presentation switch over the same fetched set (it does not change the window or re-query).
  • Stand up the period-scoped read envelope in @sustentus/services/server by extending VendorService: a single tenant + { from, to } query resolves the vendor tenant's in-scope customers, paid invoices, leads, projects and CSAT for the window — the one place each window predicate is defined, so features 2–8 share one definition of the period set and no section re-queries the raw collections or buckets time differently. Canonical window predicates:
    • Paid invoicespaid === true and paidAt within [from, to]
    • LeadscreatedAt within [from, to]
    • Projects — the tenant's projects active/created within [from, to]
    • CSATcreatedAt within [from, to]
    • Customers — the in-scope customer set the above are grouped by (the vendor tenant's customers appearing in the windowed leads/invoices)
  • Remove the vendorData mock fallback from the page: page.tsx renders only from the service envelope and no longer imports or spreads lib/mock/vendor. Sections not yet wired by later features may render empty/zero states this round (that wiring is features 2–8), but the mock import is gone.

Acceptance criteria

  • Selecting a date range re-queries the dashboard over that window (server-driven via searchParams); the data returned changes with the range, and the selector reflects the range from the URL.
  • The six ranges resolve to the windows specified above, with Last 30 days as the default when no range param is present, and fiscal-year ranges treated as calendar years.
  • A single period-scoped read service on VendorService accepts the vendor tenant + { from, to } and returns the in-scope customers, paid invoices, leads, projects and CSAT for that window, defining each window predicate in one place for features 2–8 to consume.
  • The dashboard page.tsx no longer imports or spreads lib/mock/vendor; the page renders solely from the service envelope, and the time/region toggle switches presentation over the real fetched set.
  • The page stays tenant-scoped to the signed-in vendor (getTenantOrNull) and renders without the mock present (sections not yet wired show empty/zero states rather than dummy values).

Out of scope

  • The seven section computations (epic features 2–8: revenue retention, time-to-value, retention cohorts, revenue flow, revenue risk, customer health, funnel/customers) — this feature only stands up the window and the envelope they read from.
  • The Export button (stays cosmetic) and any CSV/PDF generation.
  • A territory/team/region scope toggle — the dashboard is whole-tenant per the epic decision; there is no "my area vs all". (The time-vs-region view toggle is presentation only and stays as-is.)
  • A real fiscal-year configuration entity (non-calendar fiscal years) — fiscal year = calendar year this round.
  • Replicating any change back into apps/dashboards — that app stays the dummy source of truth.
  • Realtime/websocket updates — sections are server-fetched on load over the selected window.

Open questions

  • none — fiscal-year resolution (calendar year) was decided during Define; all other scope is settled by the epic breakdown.

02_build/output/notes.md

Build notes: vendor-revenue-foundation

  • commits:
    • feat: vendor-revenue-foundation — period-scoped read envelope on VendorService (service + page + view + types, mock deleted — landed together)

What changed

  • packages/services/src/db/services/vendor/index.ts — the foundation:
    • resolveVendorDateRange(range, now?) — maps a selector value (or searchParams string) to a { from, to } window. Six ranges; last-30-days is the default for unknown/absent values; fiscal-year ranges are calendar years; boundaries computed in UTC. Exported VENDOR_DATE_RANGES is the ordered key list.
    • getPeriodEnvelope(tenantId, window) — the single period-scoped read every section (features 2–8) will consume: in-scope paid invoices (paid && paidAt ∈ window), leads (createdAt ∈ window), projects (active-project leads in window), CSAT (createdAt ∈ window) and the distinct customers those touch. The window predicates are defined once, here.
    • getLeadFunnel and getTopCustomers gained an optional window param so the two already-real sections scope to the selected range (so the visible data changes with the range). Existing callers (no window) are unaffected.
    • New exported types: VendorDateRangeKey, VendorPeriodWindow, VendorPeriodEnvelope, VendorEnvelopeInvoice, VendorEnvelopeLead, VendorEnvelopeCsat (re-exported via @sustentus/services/server).
  • apps/web/app/(app)/vendor/dashboard/page.tsx — reads range from searchParams, resolves the window, and fetches the envelope + windowed funnel + windowed top customers in parallel. Renders from the service only; the lib/mock/vendor import/spread is gone. Sections not yet wired build from the zero-valued base; passes the active range + a periodSummary (leads / paid invoices / active projects from the envelope) to the view.
  • apps/web/app/(app)/vendor/dashboard/vendor-dashboard-view.tsx — the date-range Select is now server-driven: value={range} reflects the URL and onValueChange navigates to ?range=…. The header subtitle shows the live range label + envelope counts (replacing the static "Last 30 days · Data updated 2 hours ago"). The time/region toggle stays client-only presentation state.
  • apps/web/components/dashboard/vendor/types.ts (new) — the VendorData view-model types, moved out of the deleted mock; the view and customer-health-section import from here.
  • apps/web/app/(app)/vendor/dashboard/empty-data.ts (new) — emptyVendorData, the zero-valued base the page assembles from until features 2–8 wire each section.
  • apps/web/lib/mock/vendor.ts — deleted (the dummy fallback the foundation removes).

Acceptance criteria status

  • Selecting a date range re-queries the dashboard over that window (server-driven via searchParams) — the Select navigates to ?range=…, the server page re-resolves the window and re-queries; funnel, top customers and the period summary change with the range, and the selector reflects the URL value.
  • The six ranges resolve to the specified windows, last-30-days is the default when no param is present, and fiscal-year ranges are treated as calendar years — in resolveVendorDateRange.
  • A single period-scoped read service (VendorService.getPeriodEnvelope) accepts tenant + { from, to } and returns in-scope customers, paid invoices, leads, projects and CSAT, defining each window predicate in one place for features 2–8.
  • page.tsx no longer imports or spreads lib/mock/vendor (file deleted); the page renders solely from the service envelope/reads, and the time/region toggle is presentation only.
  • The page stays tenant-scoped to the signed-in vendor (getTenantOrNull) and renders without the mock present — unwired sections show empty/zero states from emptyVendorData.

Verify result

  • Mechanical checks (format · lint · typecheck · build) run in CI + the Vercel preview, not here. Release reads them back from the PR's check runs. No check is expected to fail.

Notes for review

  • Envelope shape is the contract for features 2–8. It returns lean projected sets, not full docs; projects is the active-project subset of the in-scope leads (one definition, no second query). If a later feature needs a field not projected here, widen the .select(...) rather than re-querying the raw collection.
  • getLeadFunnel/getTopCustomers are now window-aware but otherwise unchanged; feature 8 extends their columns/metrics. Top-customer names still come from the existing $lookup.
  • The header subtitle is the foundation's visible proof the window is real; section bodies stay zero-state until their feature lands.

03_release/output/investor-update.md

Vendor dashboards now run on real data, not demo numbers

Who it's for: Vendor partners What shipped: The vendor dashboard now reads each vendor's own leads, invoices and CSAT over a real, selectable time window — the placeholder demo data is gone. Why it matters: It lays the period-scoped foundation for the vendor revenue-retention cockpit, advancing Build the Bridge — Q2 objective 1, product-market fit with vendor partners.

It's the spine the next seven revenue, retention and risk panels build on.

Dig deeper: https://github.com/sustentus/sustentus/pull/528

03_release/output/release.md

Release: vendor-revenue-foundation

  • pr: #528 · merged: yes — squash 924321fe on 2026-06-24
  • CI: green — Quality Project ✓, Audit database ✓, Migrate preview database ✓, Migrate production database (skipped, expected on PR), Vercel Preview Comments ✓
  • technical docs: no technical docs impact — the change extends the existing vendorService internally and removes a mock; no new app, package, route, env var, or build step, so no apps/docs/app/technical/** page changed
  • business docs: no business docs impact — the vendor feature-role-matrix rows (lead funnel, activation, delivery trend, CSAT) are access capabilities that are unchanged; the foundation swaps dummy data for real period-scoped data without adding/removing a documented capability or journey step
  • release notes: investor-only — the user-visible result is a real-data dashboard whose later panels stay zero-state until features 2–8, so a customer changelog would read as "now empty"; recorded as a foundation / delivery-velocity change. No end-user changelog entry this run.
  • deploy: web (web) READY in production for merge 924321fe. help (help-centre) CANCELED via Vercel ignored-build-step — benign: the squash touched no apps/help files (investor-only release, no changelog entry), so Vercel skipped the help deploy; not a build failure. The touched app (web) is live.
  • sent: investor update sent to 2 recipients on 2026-06-24, after confirming web production deploy READY (deploy gate's ERROR verdict was the benign help skip; sending confirmed with the user)

Review summary

  • /code-review (medium, two focused finder/verifier passes — service logic + page/view wiring): no correctness bugs found. Verified: the six date-range windows (incl. Q1 last-quarter rollover, fiscal-year = calendar year, default last-30-days); getPeriodEnvelope tenant scoping + window predicates + lean projections; optional window params don't break callers; the page's awaited searchParams, null-guarded Promise.all, and zero-state assembly; the client-component type-only import (erased, no server code in bundle); deleted mock has no dangling references (apps/dashboards mock is a separate app). No fixes required.

Acceptance check (vs spec)

  • Date range re-queries server-side via searchParams; selector reflects the URL — resolveVendorDateRange + the controlled Select navigating to ?range=….
  • Six ranges resolve as specified; last-30-days default; fiscal-year = calendar year — resolveVendorDateRange.
  • Single period-scoped read service (getPeriodEnvelope) returns in-scope customers, paid invoices, leads, projects, CSAT — predicates defined once.
  • page.tsx no longer imports/spreads lib/mock/vendor (file deleted); renders from the service; time/region toggle is presentation only.
  • Page stays tenant-scoped (getTenantOrNull); unwired sections show zero/empty states from emptyVendorData.