Skip to Content

← All archived intake epics

Intake: service-fee-deprecation

breakdown.md

Breakdown: Service fee deprecation

  • scope-slug: service-fee-deprecation · doc: — (owner request, no Scope Doc)
  • initiative: Refine the bridge / objective: Q2-2026 Objective 3 — Validate Technical Infrastructure & Payout Flow
  • personas: Customer, Expert, Vendor, CSM, SDM, Admin

What I understood

The platform currently bolts a platform service fee onto every quote and every milestone invoice: serviceFee = amount × SERVICE_FEE_PERCENTAGE (default 10%), and total = amount + serviceFee. That model is legacy and wrong. Any fee Sustentus negotiates is agreed and settled outside the platform, so the system should never compute, store, display or reason about it. The money the platform records is the work value the expert quoted — nothing more.

The removal is therefore not a re-modelling exercise: amount already is the correct number, and every read path already falls back to it. The work is deleting the fee limb, reconciling total back to amount, and clearing the field off documents already written.

Where it sits

  • service-journey/quote — where the fee is first computed and attached to the quote.
  • service-journey/invoicing — where the fee is pro-rated onto each milestone invoice.
  • service-journey/proposals — the hand-off sentence that promises the fee gets applied.
  • feature-role-matrix entities: quotes, invoices; downstream money reads on vendor revenue, SDM market signals, CSM blocker queue, customer project finance, expert earnings.

Current-state inventory

Everything below was found by sweeping serviceFee / SERVICE_FEE / "service fee" across the monorepo (excluding node_modules and packages/*/dist build output).

1. Write path — computes and persists the fee

File Lines What it does
packages/services/src/shared/proposal-constants.ts 21–23 getServiceFeePercentage() — reads env SERVICE_FEE_PERCENTAGE, default 10
packages/services/src/db/services/quote/index.ts 174–176, 195, 200 createDraftForProposal — computes fee, writes serviceFee + total
packages/services/src/db/services/quote/index.ts 215–217, 223 updateFinancialsForProposal — recomputes fee on re-price
packages/services/src/db/services/invoice/index.ts 292, 332, 410–411, 420 createFromMilestone — pro-rates quote.serviceFee by milestone %
packages/services/src/db/services/demo-data/index.ts 819, 829–830, 880, 891–892 Seeds a hard-coded 10% fee onto demo quotes and invoices

Only two call sites reach the quote write path — expert-bid-proposal-flow.ts:153 and :280.

2. Schema — persists the field

File Lines What it does
packages/services/src/db/models/quote.ts 20, 44 serviceFee?: number on IQuote + { type: Number, min: 0 }
packages/services/src/db/models/quote.ts 61–64 totalAmount virtual — amount + serviceFee
packages/services/src/db/models/invoice.ts 15, 38 serviceFee?: number on IInvoice + schema path

3. Read fallbacks — total ?? (amount + serviceFee)

File Lines
packages/services/src/db/services/vendor/index.ts 678, 685, 701–702 (revenue flow)
packages/services/src/db/services/vendor/index.ts 1896, 1903, 1945–1946 (open revenue)
packages/services/src/db/services/vendor/index.ts 213, 1215, 1237 (VendorEnvelopeInvoice.serviceFeecarried but never read)
packages/services/src/db/services/sdm-market-signals/index.ts 62, 84, 194
packages/services/src/db/services/csm-blocker-queue/index.ts 95
packages/services/src/db/services/customer-project/index.ts 68 (comment), 156
packages/services/src/db/services/expert-workbench/earnings.ts 15–16 (comment only — already uses amount, i.e. ex-fee)

4. UI

File Lines What it does
apps/web/app/(app)/finances/quotes/[id]/page.tsx 104–107 "Service fee" row in the money grid
apps/web/app/(app)/finances/invoices/[id]/page.tsx 107–110 "Service fee" row in the money grid

5. Config & docs

File Lines
docs/ENV.md 59
apps/docs/app/business/platform-overview/page.mdx 35, 46, 48, 56
apps/docs/app/business/service-journey/quote/page.mdx 3, 12–13, 34–35, 39, 83
apps/docs/app/business/service-journey/invoicing/page.mdx 30–31, 83
apps/docs/app/business/service-journey/proposals/page.mdx 83
apps/docs/app/business/service-journey/user-interaction-overview/page.mdx 55
apps/docs/app/business/initiatives/build-the-bridge/page.mdx 73, 91
apps/docs/app/business/roles/page.mdx 157

SERVICE_FEE_PERCENTAGE is declared only in docs/ENV.md and read only by proposal-constants.ts. It is not in turbo.json, any next.config, or any committed .env example — so removal is a docs edit plus an unset in the Vercel dashboard.

6. Deliberately untouched

  • apps/docs/archive/pipeline-runs/** (9 files) — dated records of past runs. Historical artefacts, never rewritten.
  • packages/services/dist/** — build output; regenerates.
  • getMaxMilestones() in proposal-constants.ts — unrelated, the file survives.

7. Confirmed clear

No references in: tests, email templates (packages/services/src/email/**), PDF generation, Zod schemas, server actions, apps/demo mock data, apps/marketing, packages/ui, or the tenant-settings registry. The apps/api app named in the proposal-constants.ts comments does not exist in this monorepo — that comment is itself stale.

The one decision this cut needs from a human

Removing serviceFee from new records is unambiguous. Records already written are not:

  • ~every existing quote and invoice has serviceFee set and total = amount + serviceFee.
  • $unset-ing serviceFee alone leaves the fee baked invisibly into total, which every read path prefers over amount. Historic vendor revenue, SDM market signals, CSM revenue-at-risk and customer contract values would silently stay 10% inflated with no field left to explain why.

Stub 2 therefore proposes the migration also resets total = amount on every existing quote and invoice, so total means one thing everywhere. That rewrites the recorded total on issued documents. Given the fee was never a real commercial term, that is the honest reconciliation — but it is a money-field rewrite and belongs to the owner, not to the build. Confirm before stub 2 is built. If the answer is "leave history alone", stub 2 drops the total reset and keeps the read fallbacks pointing at amount, accepting permanently mixed historic totals.

Build order

  1. service-fee-write-path-removal — stop computing, storing and displaying the fee; new quotes and invoices carry total = amount. — depends-on: none
  2. service-fee-schema-and-data-removal — drop the schema field and the read fallbacks; migrate existing documents. — depends-on: service-fee-write-path-removal

Parallelizable

None — a plain chain. Stub 2 drops the serviceFee schema path, so it must land after nothing writes to it. Splitting at that seam keeps the destructive half (a money-field data migration) in its own reviewable PR, and lets the fee stop accruing on day one even if the data decision above takes longer to settle. Collapse the two into a single run only if the owner has already answered the decision above.

Out of scope (whole scope)

  • Removing the total field itself. Once the fee is gone total is always equal to amount, so it is strictly redundant. Collapsing it touches every money read (apps/web/lib/queries/workspace.ts:309, sdm-risk, invoice, customer-project, vendor, csm-blocker-queue, sdm-market-signals) plus both finance UIs. Worth doing as a follow-on leanness chore; not worth entangling with the fee removal.
  • Any replacement fee/commission/take-rate model. There is none — fees are settled externally.
  • Rewriting apps/docs/archive/pipeline-runs/**.
  • Payout, Stripe or invoicing-provider work.

_done/service-fee-schema-and-data-removal.md

Stub: Remove the service fee from the schema and from stored data

  • feature-slug: service-fee-schema-and-data-removal
  • scope: service-fee-deprecation
  • personas: Vendor, SDM, CSM, Customer, Admin
  • initiative: Refine the bridge / objective: Q2-2026 Objective 3 — Validate Technical Infrastructure & Payout Flow
  • depends-on: service-fee-write-path-removal
  • sequence: 2 of 2

Problem

After stub 1 nothing writes a service fee, but the concept is still load-bearing. serviceFee is still a persisted field on quotes and invoices; six read paths still reconstruct money as total ?? (amount + serviceFee); and every document written before stub 1 still carries a fee — both in serviceFee and, invisibly, inside its total. Because every read prefers total, historic vendor revenue, SDM market signals, CSM revenue-at-risk and customer contract values stay 10% inflated with no field on the document left to explain why.

Proposed change

Delete the concept, then reconcile the data.

  • Drop serviceFee from the quote and invoice schemas and their TypeScript interfaces.
  • Drop the quote model's totalAmount virtual — it exists only to express amount + serviceFee.
  • Simplify every money read from total ?? (amount + serviceFee) to total ?? amount, across vendor revenue flow and open revenue, SDM market signals, the CSM blocker queue, and customer project finance. Drop serviceFee from VendorEnvelopeInvoice and its projection — it is carried through the envelope and never read.
  • Ship a migration, in the same PR as the schema change, that $unsets serviceFee from every quote and invoice and resets total = amount on documents where they diverge, so total means the same thing on every record.

Acceptance criteria (rough)

  • serviceFee appears nowhere under packages/, apps/web/, or apps/demo/.
  • No quote or invoice document in the database has a serviceFee field.
  • Every quote and invoice satisfies total === amount (where amount is set).
  • Vendor revenue, SDM market signals, CSM revenue-at-risk and customer project finance render the same figures before and after the migration for post-stub-1 records, and drop by the former fee percentage for pre-stub-1 records — an expected, explainable change.
  • The migration's down is written and documented, and its irreversibility is stated explicitly (see Notes).
  • pnpm format:check, pnpm lint, and the per-app builds pass.

Out of scope (this feature)

  • Removing the total field itself, now provably redundant — a separate leanness chore.
  • Backfilling amount where it is unset. Those documents fall back to total; leave them.
  • Any change to apps/docs/archive/pipeline-runs/**.

Notes for Define

  • Gate this run on the owner decision recorded in breakdown.md. Resetting total = amount rewrites a money field on already-issued quotes and invoices. If the owner would rather leave history untouched, build the $unset half only, keep total ?? amount as the read shape, and say plainly in the release notes that historic totals still include the retired fee.
  • The migration is not symmetric and cannot be: once serviceFee is unset and total is reset, the original split is unrecoverable from the document. Write down as the best available inverse — recompute serviceFee = amount × 10 / 100 and total = amount + serviceFee — and say in the file header that it reconstructs an assumed 10% rather than restoring the recorded value. Take a backup of the quotes and invoices collections before running it in production.
  • Schema change and migration must land in the same PR — schema/migration lockstep is a known failure surface in this repo.
  • Do the reset with a single aggregation-pipeline updateMany ($set: { total: "$amount" } plus $unset) against the raw collections rather than a document loop, and scope it to documents where the field is actually present.
  • touches: packages/services/src/db/models/{quote,invoice}.ts, packages/services/src/db/services/{vendor,sdm-market-signals,csm-blocker-queue,customer-project}/index.ts, packages/services/src/db/migrations/<timestamp>-remove-service-fee.ts.

_done/service-fee-write-path-removal.md

Stub: Stop computing and showing the service fee

  • feature-slug: service-fee-write-path-removal
  • scope: service-fee-deprecation
  • personas: Customer, Expert, Vendor, CSM
  • initiative: Refine the bridge / objective: Q2-2026 Objective 3 — Validate Technical Infrastructure & Payout Flow
  • depends-on: none
  • sequence: 1 of 2

Problem

Every quote adds a 10% platform service fee to the expert's price, and every milestone invoice pro-rates that fee. No such fee exists as a system-side commercial term — anything negotiated is settled outside the platform. So the customer-facing total on every quote and invoice is inflated by a number the business does not charge, and two finance screens show a "Service fee" line for money nobody collects.

Proposed change

New quotes and invoices record only the work value.

  • A quote's total becomes its amount. Nothing adds a fee at draft time or on re-price.
  • A milestone invoice's total becomes its pro-rated amount (quote.amount × milestone percentage). The fee limb of that calculation goes.
  • The "Service fee" row disappears from the quote and invoice detail screens, leaving Amount and Total.
  • The SERVICE_FEE_PERCENTAGE configuration point is retired — the constant that reads it, its docs/ENV.md row, and the value in the Vercel dashboard.
  • Demo data stops seeding a fee, so demo money reconciles with real money.

The serviceFee schema field stays in place this round, still readable, so documents already written keep validating. Stub 2 removes it.

Acceptance criteria (rough)

  • A quote drafted from an accepted proposal has total === amount and no serviceFee written.
  • Re-pricing a proposal updates the quote to total === amount, and clears any fee it carried.
  • An invoice raised against a milestone has amount === quote.amount × percentage / 100 and total === amount.
  • The quote and invoice detail screens show Amount and Total only — no "Service fee" row, and no empty grid cell where it used to sit.
  • getServiceFeePercentage no longer exists; getMaxMilestones is untouched and still works.
  • SERVICE_FEE_PERCENTAGE appears nowhere in the repo outside apps/docs/archive/.
  • Seeded demo quotes and invoices satisfy total === amount.
  • pnpm format:check, pnpm lint, and the per-app builds pass.

Out of scope (this feature)

  • Removing serviceFee from the Mongoose schemas — stub 2.
  • The total ?? (amount + serviceFee) read fallbacks — stub 2 (they are harmless here: new documents have no fee, so the fallback adds zero).
  • Migrating documents already in the database — stub 2.
  • Removing the total field itself.

Notes for Define

  • Retiring SERVICE_FEE_PERCENTAGE needs the value unset in the Vercel dashboard as well as the code removal. Call it out in the run's release notes — env drift is a known failure surface here.
  • Prefer deleting the fee arithmetic outright over passing 0. A zeroed fee is a dead limb that reads as intentional and will be resurrected by the next person.
  • expert-workbench/earnings.ts needs no code change, but its comment explaining that expert earnings exclude the platform serviceFee is now misleading — fix the wording.
  • Docs to correct in this PR (the Ship stage's docs-sync covers these): business/service-journey/quote, .../invoicing, .../proposals, .../user-interaction-overview, business/platform-overview, business/roles, business/initiatives/build-the-bridge, and docs/ENV.md. platform-overview describes the fee as a revenue stream — that section needs rewriting, not deleting, so the page still explains how the platform earns.
  • touches: packages/services/src/shared/proposal-constants.ts, packages/services/src/db/services/{quote,invoice,demo-data}/index.ts, apps/web/app/(app)/finances/{quotes,invoices}/[id]/page.tsx, docs/ENV.md, apps/docs/app/business/**.