vendor-revenue-foundationrun.md00_intake/stub.mdThe 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.
{ 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.@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.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.)lib/mock/vendor; the time/region toggle switches
presentation over the real set.searchParams on /vendor/dashboard is the lightest server-driven
option and keeps the selector shareable; confirm with the existing page conventions.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.mdVendorService → 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}.tsThe 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.
{ 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:[now − 30 days, now][start of current calendar quarter, now][start of previous calendar quarter, start of current quarter][Jan 1 of current year, now][Jan 1, Dec 31] of the current year (fiscal year = calendar year, per the
Define decision; no fiscal-year config entity this round)[Jan 1, Dec 31] of the previous calendar year@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 === true and paidAt within [from, to]createdAt within [from, to][from, to]createdAt within [from, to]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.searchParams);
the data returned changes with the range, and the selector reflects the range from the URL.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.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.getTenantOrNull) and renders without the mock
present (sections not yet wired show empty/zero states rather than dummy values).apps/dashboards — that app stays the dummy source of truth.02_build/output/notes.mdfeat: vendor-revenue-foundation — period-scoped read envelope on VendorService (service + page +
view + types, mock deleted — landed together)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.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).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.resolveVendorDateRange.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.getTenantOrNull) and renders without the
mock present — unwired sections show empty/zero states from emptyVendorData.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.03_release/output/investor-update.mdWho 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.md924321fe on 2026-06-24vendorService internally and removes a mock; no new app, package, route, env var, or build step, so no apps/docs/app/technical/** page changed924321fe. 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./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.searchParams; selector reflects the URL — resolveVendorDateRange + the controlled Select navigating to ?range=….resolveVendorDateRange.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.getTenantOrNull); unwired sections show zero/empty states from emptyVendorData.