service-fee-deprecationbreakdown.mdThe 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.
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.Everything below was found by sweeping serviceFee / SERVICE_FEE / "service fee" across the
monorepo (excluding node_modules and packages/*/dist build output).
| 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.
| 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 |
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.serviceFee — carried 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) |
| 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 |
| 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.
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.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.
Removing serviceFee from new records is unambiguous. Records already written are not:
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.
service-fee-write-path-removal — stop computing, storing and displaying the fee; new quotes and
invoices carry total = amount. — depends-on: noneservice-fee-schema-and-data-removal — drop the schema field and the read fallbacks; migrate
existing documents. — depends-on: service-fee-write-path-removalNone — 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.
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.apps/docs/archive/pipeline-runs/**._done/service-fee-schema-and-data-removal.mdAfter 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.
Delete the concept, then reconcile the data.
serviceFee from the quote and invoice schemas and their TypeScript interfaces.totalAmount virtual — it exists only to express amount + serviceFee.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.$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.serviceFee appears nowhere under packages/, apps/web/, or apps/demo/.serviceFee field.total === amount (where amount is set).down is written and documented, and its irreversibility is stated explicitly
(see Notes).pnpm format:check, pnpm lint, and the per-app builds pass.total field itself, now provably redundant — a separate leanness chore.amount where it is unset. Those documents fall back to total; leave them.apps/docs/archive/pipeline-runs/**.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.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.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.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.mdEvery 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.
New quotes and invoices record only the work value.
total becomes its amount. Nothing adds a fee at draft time or on re-price.total becomes its pro-rated amount (quote.amount × milestone percentage). The fee limb of that calculation goes.SERVICE_FEE_PERCENTAGE configuration point is retired — the constant that reads it, its
docs/ENV.md row, and the value in the Vercel dashboard.The serviceFee schema field stays in place this round, still readable, so documents already
written keep validating. Stub 2 removes it.
total === amount and no serviceFee written.total === amount, and clears any fee it carried.amount === quote.amount × percentage / 100 and
total === amount.getServiceFeePercentage no longer exists; getMaxMilestones is untouched and still works.SERVICE_FEE_PERCENTAGE appears nowhere in the repo outside apps/docs/archive/.total === amount.pnpm format:check, pnpm lint, and the per-app builds pass.serviceFee from the Mongoose schemas — stub 2.total ?? (amount + serviceFee) read fallbacks — stub 2 (they are harmless here: new
documents have no fee, so the fallback adds zero).total field itself.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.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-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.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/**.