Skip to Content

← All archived runs

Run: expert-earnings-data

run.md

Run: expert-earnings-data

  • branch: claude/pipeline-expert-earnings-summary-uritf2
  • pr: #534

01_define/output/spec.md

Spec: Expert earnings summary — real data

  • slug: expert-earnings-data
  • personas: Expert
  • touches: packages/services/src/db/services/expert-workbench (new earnings.ts derivation + extend ExpertWorkbenchEarnings and wire getWorkbench), apps/web/components/dashboard/expert/performance-section.tsx (bring up to the enriched earnings design, fed by real data)
  • complexity: complex

Problem

On the expert Go live workbench (apps/web/expert/dashboard), the Earnings half of the Performance section is still placeholder: getWorkbench returns a hard-coded EMPTY_EARNINGS slot and the web component renders only three em-dash tiles. The apps/dashboards design source of truth has since moved on — the merged expert-earnings-summary run (#509) enriched that card into a fuller earnings summary (earned-this-month with a month-on-month trend, a paid / awaiting / in-delivery / at-risk breakdown bar, and an average payout cycle time) — but it did so with mock data in the dashboards app only. The real per-expert figures exist in the database (paid invoices with amount/paidAt, accepted proposals/quotes for contract value, milestones for delivery completion, and the sla-stage-targets timing engine that flags at-risk leads), but nothing aggregates them for the web workbench. This is the real-data sibling of the expert-dashboard-data epic (intake stub expert-earnings-summary, feature 7 of 8), advancing Build the Bridge / Q2 2026 Objective 3 — Validate Technical Infrastructure & Payout Flow: experts still can't see real money moving through the platform, which is exactly what that objective needs to validate.

Proposed change

Build a per-expert earnings aggregation in @sustentus/services/server (a new earnings.ts in the expert-workbench service) for the signed-in expert, and bring the apps/web earnings card up to a real-data pixel copy of the enriched apps/dashboards design (#509). Replace EMPTY_EARNINGS and extend ExpertWorkbenchEarnings to the enriched shape; the component reads it from the existing getWorkbench envelope (server-fetched on load). All money is formatted in the expert's currency (expert.currency, default EUR) via the existing formatCurrency util, reusing the workbench's shared lead-value helper (resolveValueLabelsForLeads / formatLeadValue, accepted-proposal price) and the sla-stage-targets timing layer so figures agree with the other workbench sections.

The enriched earnings slot the service produces and the web card renders:

  • thisMonth ("Earned this month") — sum of the expert's paid invoices with paidAt in the current calendar month, using each invoice's work value amount (falling back to total when amount is unset) — i.e. the expert's earnings excluding the platform serviceFee.
  • lastMonth + changePct + trend — the same earned figure for the previous calendar month, plus the month-on-month delta as a percentage and an up/down direction. When last month is zero, render an honest placeholder rather than a divide-by-zero (e.g. / "new").
  • paid — the expert's realised earnings: sum of all paid invoice amount.
  • awaitingPayment — sum of amount over the expert's approved-but-unpaid invoices (isApproved && !paid): billed money still outstanding.
  • inProgress ("In delivery") — accepted-proposal contract value of the expert's active-project leads (expert = me, isActiveProject) not yet fully paid and not flagged at-risk, resolved through the shared workbench value helper so it matches the Work-in-progress table's value column.
  • atRisk ("Revenue at risk from delays") — contract value of those active-project leads whose sla-stage-targets timing band is at-risk/breached (the same red/at-risk band the active-projects derivation uses). Carved out of inProgress so the breakdown bar segments partition the book without double-counting.
  • breakdown — the numeric { paid, awaiting, inDelivery, atRisk } the proportional bar widths are computed from (same values as the tiles above, unformatted).
  • avgCompletionTime ("Average payout cycle time") — mean elapsed time from each completed lead's startDate to its last delivery milestone marked done, across the expert's completed leads, rendered as a duration label (e.g. 12d). A lead counts as completed once its delivery milestones are all done.

Acceptance criteria

  • The apps/web expert workbench Earnings card matches the enriched apps/dashboards design (#509): earned-this-month headline with a month-on-month trend, paid / awaiting payment / in delivery / revenue-at-risk tiles, a proportional breakdown bar + legend, and average payout cycle time — all fed by real per-expert data, with EMPTY_EARNINGS removed from the populated path.
  • Every figure is computed from real invoices / accepted proposals / milestones for the signed-in expert, scoped to their tenant — no hard-coded amounts.
  • thisMonth sums only paid invoices whose paidAt is in the current calendar month, using invoice amount (work value, ex serviceFee); lastMonth is the same for the prior month; the trend changePct/direction derives from the two, with a safe zero-last-month case.
  • paid is the sum of all paid invoice amount; awaitingPayment is the sum of amount over approved-but-unpaid invoices (isApproved && !paid).
  • inProgress is the accepted-proposal contract value of the expert's not-fully-paid, not-at-risk active-project leads (via the shared workbench value helper); atRisk reconciles with the sla-stage-targets at-risk band and is carved out of inProgress so the breakdown segments do not double-count.
  • avgCompletionTime derives from real startDate → last-milestone-done durations across the expert's completed leads, rendered as a duration label.
  • All money figures are formatted in the expert's currency (expert.currency, default EUR).
  • A zero-data expert (no invoices / leads) renders zeros or (and a sensible empty breakdown bar), not mock values, with no error.

Out of scope

  • The dashboards design (#509) — it is the look-and-feel source of truth and is unchanged; this run brings apps/web to real-data parity with it.
  • The Quality half of the Performance section (feature 8, expert-quality-metrics) — CSAT, vendor rating, repeat-work and rework rates are wired separately and untouched.
  • Defining SLA targets — the at-risk band is consumed from sla-stage-targets, not redefined.
  • Payout / invoice generation changes — this is a read-only aggregation; no writes to invoices, quotes, or milestones.
  • Cross-currency conversion / FX — figures aggregate in the expert's single currency (expert.currency); the codebase has no FX rate source, so amounts on leads/invoices in other quote currencies are summed at face value rather than converted. Multi-currency handling is a later concern.
  • Realtime updates — the section is server-fetched on page load like the rest of the workbench.

Open questions

  • none — the build-affecting decisions (earnings field = work value ex-fee; single expert currency, no FX; in-delivery = active-lead contract value; completion = last milestone done; and the run-identity choice to ship under the new slug expert-earnings-data mirroring the enriched #509 design) were all resolved during Define.

02_build/output/notes.md

Build notes: expert-earnings-data

  • commits: feat: expert-earnings-data — real per-expert earnings aggregation + enriched web card

What changed

  • packages/services/src/db/services/expert-workbench/earnings.ts (new): deriveEarnings(tenantId, expertUserId, currency) — the per-expert earnings derivation.
    • Invoice-derived (one query, expert = me): thisMonth / lastMonth sum paid invoices by paidAt calendar month using work value amount (fallback total); paid sums all paid amount; awaitingPayment sums isApproved && !paid amount. changePct + trend derive from this/last month with a safe zero-last-month case ("New" / "—").
    • Active-project value: active-project leads (expert = me, isActiveProject) resolve their accepted-proposal price via proposalService.findAcceptedByLead and an SLA band via the shared deriveLeadSlaTimingForLeads. At-risk leads (red / at-risk band — same predicate as active-projects.ts) sum into atRisk; the rest into inProgress (in-delivery), so the two never double-count.
    • avgCompletionTime: mean startDate → last-milestone-done across completed leads (isActiveProject: false with an accepted proposal whose delivery milestones are all done); completion timestamp is the max milestone updatedAt. Rendered as Nd, or when nothing to average.
    • All money formatted in the expert's currency (default EUR) via the shared formatCurrency.
  • packages/services/src/db/services/expert-workbench/index.ts: extended ExpertWorkbenchEarnings to the enriched shape (added lastMonth, changePct, trend, paid, awaitingPayment, numeric breakdown) + new ExpertWorkbenchEarningsBreakdown type; removed the EMPTY_EARNINGS placeholder and wired deriveEarnings into getWorkbench (Promise.all, passing expert.currency).
  • packages/services/src/db/services/index.ts: re-export ExpertWorkbenchEarningsBreakdown.
  • apps/web/components/dashboard/expert/performance-section.tsx: reworked the Earnings card into the enriched layout (earned-this-month headline + month-on-month trend icon/delta, paid / awaiting payment / in delivery / revenue-at-risk tiles, proportional breakdown bar + legend, average payout cycle time) fed by the real workbench.earnings. Uses web theme tokens and a total > 0 guard so a zero-data expert renders an empty bar (no NaN% width). Quality card unchanged.

Acceptance criteria status

  • apps/web Earnings card matches the enriched apps/dashboards design (#509), fed by real per-expert data; EMPTY_EARNINGS removed from the populated path.
  • Every figure computed from real invoices / accepted proposals / milestones for the signed-in expert, tenant-scoped — no hard-coded amounts.
  • thisMonth sums paid invoices with paidAt in the current month using amount (ex serviceFee); lastMonth the prior month; trend derives from the two with a safe zero-last-month case.
  • paid = sum of all paid invoice amount; awaitingPayment = sum of amount over approved-but-unpaid invoices (isApproved && !paid).
  • inProgress = accepted-proposal contract value of not-at-risk active-project leads (shared value resolution); atRisk reconciles with the sla-stage-targets band and is carved out of inProgress.
  • avgCompletionTime derives from real startDate → last-milestone-done durations across completed leads.
  • All money figures formatted in the expert's currency (expert.currency, default EUR).
  • Zero-data expert renders zeros / and an empty breakdown bar, not mock values, with no error.

Verify result

  • mechanical checks (format · lint · typecheck · build) run in CI + the Vercel preview, not here. No check is expected to fail. (Note: the Quality-card JSX block was left at its prior indentation after the Earnings rework — Husky/Prettier normalises it on commit.)

Notes for review

  • "Not fully paid" qualifier on inProgress: active-project leads are treated as the in-delivery set (an active project is in-flight by definition) rather than running a separate per-lead fully-paid scan. atRisk is carved out by SLA band. If a stricter fully-paid exclusion is wanted, that's a follow-up.
  • Breakdown bar is illustrative: segments (paid / awaiting / inDelivery / atRisk) come from different sources (realised invoices vs active-lead contract value), matching #509's design intent — it's a proportional snapshot of earnings state, not a reconciled ledger.
  • Cost: avgCompletionTime does one findAcceptedByLead per completed-candidate lead (consistent with active-projects.ts / value.ts patterns) then one batched milestone query. Fine at current volumes; could batch the proposal lookup later if an expert accumulates many historical leads.
  • Currency: single-currency per the spec — no FX. Amounts on leads/invoices in other quote currencies are summed at face value into the expert's currency.

03_release/output/changelog.md

Changelog: See your real earnings on the expert workbench

You can now see your real earnings on the go live workbench. The earnings card shows what you have earned this month and how that compares with last month, what has been paid, what is awaiting payment, the value still in delivery, and any revenue at risk from delays — plus your average payout cycle time. Every figure is drawn from your own invoices and projects, shown in your currency.

03_release/output/investor-update.md

Experts now see their real earnings

Who it's for: Experts What shipped: The expert workbench earnings card now shows each expert's real money — earned this month with a trend, paid, awaiting payment, in-delivery and at-risk value, and average payout cycle time — instead of placeholders. Why it matters: Another step in Build the Bridge → Validate Technical Infrastructure & Payout Flow: experts can see real money moving through the platform.

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