Skip to Content

← All archived runs

Run: customer-finance-summary

run.md

Run: customer-finance-summary

  • branch: claude/modest-rubin-5u7j0l
  • pr: #502

00_intake/stub.md

Stub: Customer dashboard — finance & value summary

  • feature-slug: customer-finance-summary
  • epic: customer-dashboard-data
  • personas: Customer
  • initiative: Build the Bridge / objective: Q2 2026 Objective 3 — Validate Technical Infrastructure & Payout Flow
  • depends-on: customer-project-foundation, milestone-delivery-tracking
  • sequence: 5 of 9

Problem

The Value section's finance block (paid, valueDelivered, totalContract, invoiceDue {amount, ref, dueDate}) is dummy. The underlying data mostly exists — quote/proposal carry the contract total/price, and invoice carries amount/total/paid/paidAt per milestone — but nothing aggregates them into the customer-facing figures, and valueDelivered needs the milestone delivery status from feature 2.

Proposed change

  • Build a finance aggregation in the read service for the customer's active project:
    • totalContract — from the accepted quote / proposal price
    • paid — sum of paid invoices
    • valueDelivered — sum of the amounts of delivered milestones (milestone amount virtual × completion from milestone-delivery-tracking)
    • invoiceDue — the next unpaid invoice: amount, ref (invoice id/number), dueDate
  • Wire the ValueSection finance column (paid, value-delivered bar vs. total, next-payment-due card) to these aggregates.

Acceptance criteria (rough)

  • paid / valueDelivered / totalContract reflect real invoices, quote and milestone completion.
  • invoiceDue shows the genuine next unpaid invoice (or a clean "nothing due" state).
  • Figures are tenant- and customer-scoped to the active project only.
  • Currency is honoured from the quote (EUR/USD/GBP), not hard-coded.

Out of scope (this feature)

  • Taking payment / Stripe — read and aggregate only.
  • The activity feed (the other half of the Value section) — customer-activity-feed.

Notes for Define

  • Confirm the ref shown on the due card (invoice number vs. _id) and the currency formatting.
  • valueDelivered depends on milestone completion — ensure the delivered-amount definition matches the milestone feature's status.
  • touches: packages/services/src/db/models/{invoice,quote,proposal,milestone}.ts (read), packages/services/src/server (aggregation), apps/web/components/dashboard/customer/value-section.tsx.

01_define/output/spec.md

Spec: Customer dashboard — finance & value summary

  • slug: customer-finance-summary
  • personas: Customer
  • touches: packages/services/src/db/models/invoice.ts, packages/services/src/db/models/{quote,proposal,milestone}.ts (read), packages/services/src/db/services/customer-project/index.ts, packages/services/src/db/services/invoice, apps/web/app/(app)/customer/dashboard/page.tsx, apps/web/components/dashboard/customer/value-section.tsx, apps/web/lib/dashboard-utils.ts, apps/web/lib/mock/customer.ts
  • complexity: standard

Problem

The customer dashboard's Value section finance block — "Paid so far", "Work delivered" vs. total contract, and the "Next payment due" card — renders entirely from apps/web/lib/mock/customer.ts (ValueSection takes a hard-coded finance prop: paid, valueDelivered, totalContract, invoiceDue {ref, dueDate, amount}). The underlying data already exists — the accepted quote carries the contract total and currency, and each invoice carries amount/total/paid per milestone — but nothing aggregates them into the customer-facing figures, and valueDelivered needs the milestone completion signal now exposed by milestone-delivery-tracking. This is sequence 5 of 9 in the customer-dashboard-data epic and advances Build the Bridge / Q2 2026 Objective 3 — Validate Technical Infrastructure & Payout Flow: the finance summary proves the delivery → invoicing money flow renders as real, tenant-scoped figures to the customer.

Proposed change

  • Extend the shared customer project envelope (customerProjectService, established by customer-project-foundation and already extended by milestone-delivery-tracking) with a typed finance block, resolved once from the signed-in customer's active project lead → accepted proposal → its quote + per-milestone invoices. No new top-level fetch entry; the other sections keep reading the same envelope.

    • totalContract — the accepted quote's total (falling back to the totalAmount virtual, amount + serviceFee) for the active project's accepted proposal.
    • paid — the sum of total (falling back to amount) across the project's paid invoices (invoice.paid === true), scoped to the accepted proposal's milestones.
    • valueDeliveredpercentage-weighted: totalContract × (progress ÷ 100), reusing the done-milestone progress already computed on the envelope. The value-delivered bar therefore tracks the progress bar exactly; no per-milestone money split is introduced.
    • invoiceDue — the next unpaid invoice (paid === false) for the project, ordered by due date ascending: { ref, dueDate, amount }, or null when nothing is outstanding.
      • ref — the name of the milestone the invoice belongs to (e.g. "Discovery & setup").
      • dueDate — read from a new invoice.dueDate field (see schema change), falling back to the linked milestone's existing dueDate so the card shows a real date today before any write path populates the invoice field. Returned as an ISO string or null.
      • amount — the invoice total (falling back to amount).
    • currency — the accepted quote's currency (EUR | USD | GBP, default EUR), carried on the finance block so the UI formats money in the project's real currency rather than a hard-coded symbol.
  • Add an optional dueDate?: Date field to the invoice model. It is read by the aggregation (authoritative when present, milestone-dueDate fallback otherwise). No write path, editor, or backfill is built this run — populating it is left to seeding / a future invoicing write flow.

  • Add the invoice read helpers needed for the aggregation in a tenant-scoped invoice read service (following the existing index.ts class + instance.ts singleton pattern, consumed by customerProjectService — invoices are not re-queried elsewhere).

  • Teach fmt to honour a currency. apps/web/lib/dashboard-utils.ts fmt currently hard-codes £; extend it to accept the envelope's currency (EUR/USD/GBP symbol) so the finance figures render in the right currency. Other call sites keep working (default unchanged or threaded as needed).

  • Wire the ValueSection finance column to the envelope. The customer dashboard page passes the envelope's finance block (and currency) to ValueSection; the "Paid so far" / "Work delivered" stat boxes, the value-delivered-vs-total progress bar, and the "Next payment due" card all read real aggregates. The invoiceDue card renders a clean "nothing due" state (no card / empty state) when invoiceDue is null, and guards the progress bar against a zero totalContract.

  • Retire the finance half of the mock. Remove the finance block from apps/web/lib/mock/customer.ts; the activity half of the Value section stays on the mock until customer-activity-feed (sequence 6) lands.

Dependency / sequencing

Sequence 5 of the customer-dashboard-data epic. Extends the envelope from customer-project-foundation (sequence 1) and consumes the milestone completion signal / progress from milestone-delivery-tracking (sequence 2) — so Build is gated on both being merged first. This is a build-ordering note, not an unresolved requirement.

Acceptance criteria

  • paid, valueDelivered, and totalContract on the customer dashboard reflect real data — the accepted quote total, the sum of paid invoices, and totalContract × done-milestone progress respectively — not values from lib/mock/customer.ts.
  • The "Next payment due" card shows the genuine next unpaid invoice — milestone name as ref, its due date, and amount — or a clean "nothing due" state when no invoice is outstanding.
  • All figures are tenant- and customer-scoped to the signed-in customer's active project only (via the shared envelope); a customer cannot see another customer's or tenant's finance data.
  • Money is formatted in the project's currency from the accepted quote (EUR/USD/GBP), not a hard-coded £.
  • The invoice model carries an optional dueDate field; existing invoice documents (no dueDate) remain valid and the card falls back to the linked milestone's due date.
  • The finance block is returned on the customerProjectService envelope (not a separate fetch), and the finance data is removed from apps/web/lib/mock/customer.ts while the Value section's activity half still renders unchanged.

Out of scope

  • Taking payment / Stripe / any invoice write or status mutation — read and aggregate only.
  • A write path, editor UI, or backfill/migration for the new invoice.dueDate field.
  • The activity feed (the other half of the Value section) — customer-activity-feed (sequence 6).
  • Per-milestone money breakdown of valueDelivered (percentage-weighting is the chosen basis).
  • A full invoices list / /customer/invoices page — the "View invoices" link target is unchanged.

Open questions

  • none

02_build/output/notes.md

Build notes: customer-finance-summary

  • commits: feat: customer-finance-summary — finance aggregation on the customer project envelope

What changed

  • packages/services/src/db/models/invoice.ts: added an optional dueDate?: Date field (read-only this run; no write path) so the next-payment-due date can be authoritative when set.
  • packages/services/src/db/services/invoice/index.ts: added findProjectInvoicesByQuote + ProjectFinanceInvoiceRow — tenant-scoped read of an accepted quote's invoices, resolving each to { paid, amount (total ?? amount), dueDate (invoice.dueDate ?? milestone.dueDate), milestoneName }.
  • packages/services/src/db/services/customer-project/index.ts: extended the shared project envelope with a typed finance block (CustomerProjectFinance). financeFrom aggregates the accepted proposal's quote + invoices: totalContract (quote total / amount+serviceFee), paid (sum of paid invoices), valueDelivered (percentage-weighted: totalContract × progress ÷ 100, reusing the milestone progress), currency (from the quote), and invoiceDue (the next unpaid invoice by due date, or null). Resolved from the same active-project chain — no extra fetch entry.
  • packages/services/src/db/services/index.ts: exported CustomerProjectFinance and ProjectFinanceInvoiceRow.
  • apps/web/lib/dashboard-utils.ts: fmt now takes a currency (EUR/USD/GBP symbol), defaulting to GBP so existing call sites are unchanged.
  • apps/web/components/dashboard/customer/value-section.tsx: finance column reads currency-aware figures, renders a clean "nothing due" state when no invoice is outstanding, formats the ISO due date, and guards the progress bar against a zero totalContract.
  • apps/web/app/(app)/customer/dashboard/page.tsx: passes project.finance (envelope) to ValueSection instead of the mock.
  • apps/web/lib/mock/customer.ts: removed the finance block (and the now-unused formatMonthDay helper); the activity half of the Value section still renders from the mock.

Acceptance criteria status

  • paid / valueDelivered / totalContract reflect real data — accepted quote total, sum of paid invoices, and totalContract × done-milestone progress — via the envelope, not the mock.
  • The next-payment-due card shows the genuine next unpaid invoice (milestone name as ref, due date, amount), or a clean "nothing due" state when none is outstanding.
  • All figures are tenant- and customer-scoped to the active project: lead → proposal → quote → invoices each filtered by tenantId and chained from the customer's active project.
  • Money is formatted in the quote's currency (EUR/USD/GBP), not a hard-coded £.
  • invoice carries an optional dueDate; existing docs (no dueDate) stay valid and the card falls back to the linked milestone's due date.
  • finance is returned on the customerProjectService envelope (no separate fetch); the mock's finance block is removed while the Value section's activity half renders unchanged.

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.

Notes for review

  • valueDelivered is intentionally percentage-weighted (a Define decision), so the value-delivered bar tracks the progress bar exactly rather than summing per-invoice money.
  • findProjectInvoicesByQuote filters { tenantId, quote } (no dedicated index — consistent with the existing archiveByQuoteIds query); invoice volume per quote is small.
  • The new invoice.dueDate has no writer this run; populating it (seeding / future invoicing flow) is out of scope, hence the milestone-dueDate fallback to keep the card meaningful today.

03_release/output/changelog.md

Changelog: customer-finance-summary

Published as apps/help/app/changelog/2026-06-22-customer-finance-summary/page.mdx.


See your real finances on your project (customer)

You can now see where your project stands financially, right on your dashboard. It shows how much you have paid so far, the value delivered against your total contract, and your next payment due — which milestone it covers, when it is due, and how much — all in your project's own currency. When nothing is outstanding, you will see a clear "nothing due" note instead of placeholder figures.

03_release/output/investor-update.md

Customers now see their real project finances, not placeholders

Who it's for: Customers What shipped: The customer dashboard now shows real paid, value-delivered, total-contract and next-payment-due figures, in the project's currency. Why it matters: Proves the delivery-to-invoicing money flow renders as real, tenant-scoped figures — advancing Build the Bridge, Objective 3: Validate Technical Infrastructure & Payout Flow.

All figures are scoped to the signed-in customer's active project — no cross-tenant leakage.

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