expert-earnings-datarun.md01_define/output/spec.mdearnings.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)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.
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.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.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.expert.currency, default EUR).— (and a sensible empty breakdown
bar), not mock values, with no error.apps/web to real-data parity with it.expert-quality-metrics) — CSAT, vendor
rating, repeat-work and rework rates are wired separately and untouched.sla-stage-targets, not redefined.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.expert-earnings-data mirroring the enriched #509 design) were all
resolved during Define.02_build/output/notes.mdpackages/services/src/db/services/expert-workbench/earnings.ts (new): deriveEarnings(tenantId, expertUserId, currency) — the per-expert earnings derivation.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" / "—").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.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.apps/web Earnings card matches the enriched apps/dashboards design (#509), fed by real per-expert data; EMPTY_EARNINGS removed from the populated path.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.expert.currency, default EUR).— and an empty breakdown bar, not mock values, with no error.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.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.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.03_release/output/changelog.mdYou 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.mdWho 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>