service-fee-schema-and-data-removalrun.md03_define/output/spec.mdservice-fee-write-path-removal (#744) stopped the platform computing, storing and displaying the
10% service fee, but the concept is still load-bearing. serviceFee remains a persisted field on
the quote and invoice schemas, six read paths still reconstruct money as
total ?? (amount + serviceFee), and every document written before #744 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. Finishing the removal is the last step
of the fee deprecation under Refine the bridge / Q2-2026 Objective 3 — Validate Technical
Infrastructure & Payout Flow: the money the platform records must be the work value the expert
quoted, on every record, old and new.
Delete the concept, then reconcile the data — schema change and migration in the same PR.
serviceFee from the quote and invoice Mongoose schemas and from the IQuote / IInvoice
interfaces.totalAmount virtual — it exists only to express amount + serviceFee
and has zero callers monorepo-wide.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 the VendorEnvelopeInvoice type and its projection — it
is carried through the envelope and never read.$unset: { serviceFee: "" } from quoteService.updateFinancialsForProposal.
It was #744's belt-and-braces cleanup; once the field leaves the schema Mongoose strict mode
strips it from the update anyway, and the migration owns that cleanup instead.quotes and invoices collections, $unsets serviceFee
and resets total = amount on documents where the field is present, so total means the same
thing on every record.Historic-data decision (owner-confirmed, the gate breakdown.md reserved): the migration
resets total = amount as well as unsetting the fee. This rewrites a money field on already-issued
quotes and invoices, and historic vendor revenue, SDM market signals, CSM revenue-at-risk and
customer contract values will drop by the former fee percentage for pre-#744 records. That drop is
the correct figure — the fee was never a real commercial term — and Ship must state it plainly in
the release notes.
The migration is not symmetric and cannot be: once serviceFee is unset and total is reset,
the original split is unrecoverable from the document. down is written as the best available
inverse — recompute serviceFee = amount × 10 / 100 and total = amount + serviceFee — with a
file header saying it reconstructs an assumed 10% rather than restoring the recorded value.
serviceFee and the totalAmount virtual appear nowhere under packages/, apps/web/, or
apps/demo/ (apps/docs/archive/** and the shipped apps/help/app/changelog/** entry are
excluded — they are historical records).IQuote, IInvoice and both Mongoose schemas no longer declare serviceFee; the quote
model no longer declares the totalAmount virtual.total ?? amount, and no query projection
selects serviceFee.VendorEnvelopeInvoice no longer carries a serviceFee field.packages/services/src/db/migrations/ whose up runs a single
aggregation-pipeline updateMany per collection against raw quotes and invoices, scoped
to documents where serviceFee exists, setting total = amount and unsetting serviceFee.serviceFee field, and every quote
and invoice satisfies total === amount wherever amount is set.down is written, and its header states explicitly that it reconstructs an
assumed 10% fee rather than restoring the recorded value, and that a backup of the quotes
and invoices collections should be taken before running it in production.pnpm format:check, pnpm lint and the per-app builds pass.total field itself, provably redundant once this lands — a separate leanness chore.amount where it is unset; those documents fall back to total, leave them.apps/docs/archive/pipeline-runs/** or the shipped
apps/help/app/changelog/2026-08-03-service-fee-write-path-removal/ entry.total vs leave history) is settled above; Build implements
the reset. Build should confirm the raw collection names are quotes and invoices before
writing the migration — the models register as quote / invoice and Mongoose pluralizes, but
the migration operates on raw collections and a wrong name would silently no-op.04_build/output/notes.mdclaude/service-fee-schema-data-removal-ixwspp)packages/services/src/db/models/quote.ts: dropped serviceFee from IQuote and the schema, and
deleted the totalAmount virtual (amount + serviceFee) — it had zero callers monorepo-wide.packages/services/src/db/models/invoice.ts: dropped serviceFee from IInvoice and the schema.packages/services/src/db/services/vendor/index.ts: simplified both money reads (revenue flow and
open revenue) to total ?? amount, dropped serviceFee from their query projections and inline
row types, and removed the field from VendorEnvelopeInvoice, its projection and its mapping —
it was carried through the envelope and never read.packages/services/src/db/services/sdm-market-signals/index.ts: quoteTotal is now
total ?? amount; serviceFee gone from QuoteDoc and the .select(...).packages/services/src/db/services/csm-blocker-queue/index.ts: revenue-at-risk read simplified to
total ?? amount.packages/services/src/db/services/customer-project/index.ts: totalContract simplified to
total ?? amount; corrected the stale doc comment that described it as amount + serviceFee.packages/services/src/db/services/quote/index.ts: removed the now-dead
$unset: { serviceFee: "" } from updateFinancialsForProposal. It was #744's belt-and-braces
cleanup; with the field gone from the schema Mongoose strict mode would strip it from the update
anyway, and the migration owns that cleanup.packages/services/src/db/migrations/1784400000000-remove-service-fee.ts: new — one
aggregation-pipeline updateMany per raw collection (quotes, invoices), scoped to documents
where serviceFee exists, resetting total = amount and unsetting serviceFee.serviceFee / totalAmount gone from packages/, apps/web/, apps/demo/ — post-edit sweep
over packages/** (excluding dist/) returns no matches; apps/** was already clean after
#744 (the only remaining hits are apps/docs/archive/** and the shipped
apps/help/app/changelog/** entry, both excluded as historical records).IQuote, IInvoice and both schemas no longer declare serviceFee; the totalAmount virtual
is gone from the quote model.total ?? amount; no projection selects
serviceFee.VendorEnvelopeInvoice no longer carries serviceFee.updateMany per collection against raw
quotes / invoices, scoped to { serviceFee: { $exists: true } }.serviceFee, and total === amount wherever amount is set — cannot be
confirmed here: the agent sandbox has no route to the database. The migration is written to
produce exactly this; CI applies it (.github/workflows/db-migrate.yaml runs it against the
preview DB on this PR and against production on merge). Verify should confirm from the
migration check run, not from a local run.down is written, and the file header states in full that it reconstructs an assumed 10% fee
rather than restoring the recorded value, and that a backup of both collections should be taken
before running it in production.pnpm format:check, pnpm lint, per-app builds — the factory's to report, not mine (per the
Build contract these are not run locally). Read them off the PR's check runs.total on already-issued quotes and
invoices. That was the owner decision recorded in the spec (and in the scope breakdown.md), not a
build choice — but it means historic vendor revenue, SDM market signals, CSM revenue-at-risk and
customer contract values will drop by the former fee percentage for pre-#744 records. Ship must say
so plainly in the release notes.total = amount wherever the two diverge. A blunt $set: { total: "$amount" } would remove
total on documents whose amount is unset (an aggregation $set from a missing field drops the
field), destroying their only surviving value and contradicting the spec's own "Out of scope:
backfilling amount where it is unset — those documents fall back to total; leave them." The
up therefore guards with $cond on $ne: ["$amount", null], so those documents keep their
total and only lose serviceFee. Confirm that reading of $set-from-missing if you want the
belt-and-braces check.down filter is { serviceFee: { $exists: false }, amount: { $ne: null } } so a rollback
can't double-apply a fee to a document that already has one.quotes, invoices) — the models register as quote /
invoice via getModel, which calls mongoose.model(name, schema) and pluralizes. Worth a
sanity check against the preview DB, since a wrong name would silently no-op rather than fail.05_verify/output/verify.mddown
over-application, below). One operational carry-over from #744 restated for Ship.complexity: standard), performed inline — the CI Claude
review (Review diff against CONVENTIONS.md) reported skipped, so ENABLE_CLAUDE_REVIEW is
off, and the /code-review skill is user-invocation-only so the agent could not launch it. Two
findings: one fixed, one recorded. No CI review comments to triage (get_review_comments → 0).Quality Project (format + lint + typecheck) ✅ · Migrate preview database ✅ ·
Audit database ✅ · Spec structure (advisory) ✅ · Project run labels ✅ ·
Vercel Preview Comments ✅ · Migrate production database skipped (correct — merge-only) ·
Review diff against CONVENTIONS.md skipped (feature flag off).
Vercel previews: web and marketing READY; demo, docs, help-centre, storybook Ignored —
correct, the diff touches none of their paths.
serviceFee / totalAmount appear nowhere under packages/, apps/web/, apps/demo/ —
demonstrated: full-tree sweep post-change returns no matches outside apps/docs/archive/**
and the shipped apps/help/app/changelog/** entry, both excluded by the spec (agent)IQuote, IInvoice, both schemas and the totalAmount virtual — demonstrated: read in the
branch diff; field and virtual both deleted (agent)total ?? amount, no projection selects serviceFee —
demonstrated: all six sites traced in the diff (vendor revenue flow + open revenue,
sdm-market-signals, csm-blocker-queue, customer-project) plus the two .select(...)
projections and the envelope mapping (agent)VendorEnvelopeInvoice no longer carries serviceFee — demonstrated: type, projection and
row mapping all dropped in the diff (agent)updateMany per raw collection,
filtered on { serviceFee: { $exists: true } } (agent)Migrate preview database CI job log, quoted under "Migration evidence" below)down written, irreversibility + backup stated in the header — demonstrated: header now also
states the two specific ways it is lossy (agent)serviceFee, and total === amount wherever amount is
set — the migration ran green against the preview DB, which is strong evidence it did what
it says, but neither the agent nor CI queries the collections afterwards to confirm the
resulting document state. Operator check, still outstanding.packages/services/src/notifications shows no serviceFee
reference (agent)From the Migrate preview database job on the head commit — the migration really ran against a
database, it was not merely present in the tree:
Adding migration .../src/db/migrations/1784400000000-remove-service-fee into database
from file system. State is down
up: 1784400000000-remove-service-fee
All migrations finished successfully
The workflow also guards the target: it refuses to run if MONGODB_URI / MONGODB_DATABASE_NAME
resolve to sustentus-prod, so this was a preview database.
down invents a fee on records that never had one — fixed on branch. The rollback filter
(serviceFee absent + amount set) cannot distinguish a document whose fee up removed from one
written after #744 that legitimately never had a fee. Running down would apply a 10% fee to the
latter and inflate their totals. The behaviour is inherent to a lossy inverse and the spec
accepted a best-effort down, so the fix is honesty rather than logic: the file header now states
this failure mode explicitly and recommends restoring a backup over running down. Also recorded
there that the sweep is deliberately cross-tenant (a platform-wide concept retirement), so a
future reader doesn't flag the missing tenantId filter as an oversight.totalAmount removal is technically an API-shape change. schemaPlugin sets
toJSON/toObject { virtuals: true }, so the virtual was serialized into every quote object
(quoteService returns doc.toObject()). Removing it drops totalAmount from that payload.
Accepted: a monorepo-wide sweep finds no consumer — the only totalAmount hits are unrelated mock
strings in an apps/storybook Table story — and the apps/api named in the old comments does not
exist in this repo. Flagging it because it is invisible in the diff.$unset: { serviceFee: "" } from
updateFinancialsForProposal is safe in either order: a quote re-priced between the code deploy
and the migration keeps a stale serviceFee that nothing reads, still gets total = amount from
the $set, and is cleaned up when the migration passes over it. Nothing writes the field any more,
so no new ones appear.up preserves total where amount is unset. $ne: ["$amount", null]
is false for a missing field, so those documents fall through to "$total" and keep it, matching
the spec's "leave them" instruction. up is also idempotent: the $exists filter makes a re-run
a no-op.SERVICE_FEE_PERCENTAGE still needs
unsetting in the Vercel dashboard. Nothing reads it (it is absent from turbo.json, docs/ENV.md
and all code), so it is inert rather than dangerous, but it should not outlive this scope.06_ship/output/changelog.mdRun copy of the published entry: apps/help/app/changelog/2026-08-03-service-fee-schema-and-data-removal/page.mdx
Title: Older quotes and invoices now show the work value too Personas: customer, expert, vendor, csm, sdm, admin
Covers: the migration that cleared serviceFee from stored quotes and invoices and reset each
total to its amount, and the resulting downward shift in historic revenue figures. Written as
the follow-on to #744's entry, which told customers older records would keep their issued totals —
this entry is what changes that.
06_ship/output/investor-update.mdWho it's for: Vendors, SDMs, CSMs, customers and admins What shipped: The retired 10% service fee has been cleared from stored quotes and invoices, and each total reset to the amount the expert quoted. Why it matters: Refine the Bridge — Q2-2026 Objective 3: Validate Technical Infrastructure & Payout Flow. Money means one thing on every record.
Historic revenue, market signals and contract values drop by the former fee's share: the fee was folded into those totals and no longer inflates them.
Dig deeper: https://github.com/sustentus/sustentus/pull/746 · https://help.sustentus.com/changelog/2026-08-03-service-fee-schema-and-data-removal
06_ship/output/release.md0940e76aQuality Project, Migrate preview database, Audit database, Spec structure,
Project run labels, Vercel Preview Comments all ✅; Migrate production database skipped
(merge-only, runs on this merge); Review diff against CONVENTIONS.md skipped (flag off)apps/docs/app/technical/** describes the
serviceFee field or the quote/invoice money modelbusiness/**; a sweep of apps/docs/app outside archive/ returns no matchesserviceFee / totalAmount nowhere under packages/, apps/web/, apps/demo/ — full-tree
sweep clean outside apps/docs/archive/** and the shipped changelog entries (Verify, agent)IQuote, IInvoice, both schemas and the totalAmount virtual dropped — read in the difftotal ?? amount; no projection selects serviceFee — all six
sites traced in the diff (vendor revenue flow + open revenue, sdm-market-signals,
csm-blocker-queue, customer-project)VendorEnvelopeInvoice no longer carries serviceFee — type, projection and mapping droppedupdateMany per raw collection, scoped to
{ serviceFee: { $exists: true } }up: 1784400000000-remove-service-fee → All migrations finished successfully), proving it
is also idempotent on re-rundown written; header states the assumed-10% reconstruction, that it cannot distinguish a
record whose fee up removed from one that never had a fee, and the backup requirementpnpm format:check, pnpm lint, per-app builds — Quality Project green on the head commitserviceFee and total === amount wherever amount is set — the
production migration runs on this merge. The preview runs are strong evidence, but the
post-migration production document state was never queried. Still an operator check.SERVICE_FEE_PERCENTAGE in the Vercel dashboard. Inherited from #744, still outstanding.
Nothing reads it (absent from turbo.json, docs/ENV.md and all code), so it is inert rather than
dangerous — but it should not outlive this scope.Migrate production database was skipped on the
PR by design and fires on the merge to main. It rewrites total on issued quotes and invoices,
so it is worth confirming it completed rather than assuming.Shipped on the user's instruction with those operator checks outstanding — recorded here rather than quietly ticked, so the gap is visible in the durable record.