Skip to Content

← All archived runs

Run: service-fee-write-path-removal

run.md

Run: service-fee-write-path-removal

  • branch: claude/service-fee-write-path-removal-njperx
  • pr: #744

03_define/output/spec.md

Spec: Stop computing and showing the service fee

  • slug: service-fee-write-path-removal
  • personas: Customer, Expert, Vendor, CSM
  • touches: packages/services/src/shared, packages/services/src/db/services/{quote,invoice,demo-data}, apps/web/app/(app)/finances, apps/docs/app/business, docs/ENV.md
  • complexity: standard
  • demo: none

Problem

Every quote adds a 10% platform service fee to the expert's price, and every milestone invoice pro-rates that fee onto itself: serviceFee = amount × SERVICE_FEE_PERCENTAGE, total = amount + serviceFee. No such fee exists as a system-side commercial term — anything Sustentus negotiates is agreed and 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. This advances Refine the bridge / Q2-2026 Objective 3 — Validate Technical Infrastructure & Payout Flow: the payout flow can only be validated against numbers that mean what they say, and today the recorded total does not.

Proposed change

New quotes and invoices record only the work value — the money the platform stores becomes the money the expert quoted.

  • A quote drafted from an accepted proposal records total = amount, with no fee written.
  • Re-pricing a proposal updates the quote to total = amount and clears any fee the document already carried.
  • A milestone invoice records amount = quote.amount × milestone percentage and total = amount. The fee limb of that calculation is deleted, not zeroed.
  • The "Service fee" row disappears from the quote and invoice detail screens, leaving Amount and Total in a two-column money grid — no empty cell where the fee used to sit.
  • 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. getMaxMilestones in the same file is unrelated and survives untouched.
  • Demo seed data stops seeding a fee, so demo money reconciles with real money.
  • The business docs that describe the fee are corrected in this PR. business/platform-overview describes it as a revenue stream — that section is rewritten so the page still explains how the platform earns, not deleted.

The serviceFee schema field stays in place this round, still readable, so documents already written keep validating and every existing read path keeps working. Stub 2 removes it.

Acceptance criteria

  • 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 serviceFee the document already carried.
  • An invoice raised against a milestone has amount === quote.amount × percentage / 100 and total === amount, with no serviceFee written.
  • The quote and invoice detail screens show Amount and Total only — no "Service fee" row, and the money grid is two columns, not three with a gap.
  • getServiceFeePercentage no longer exists, and no unused helper is left behind in proposal-constants.ts; 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.
  • The fee is removed from business/service-journey/{quote,invoicing,proposals,user-interaction-overview}, business/platform-overview (revenue section rewritten, not dropped), business/roles, business/initiatives/build-the-bridge, and docs/ENV.md.
  • The comment in expert-workbench/earnings.ts no longer explains earnings as excluding a platform service fee (the code there is already correct and does not change).
  • pnpm format:check, pnpm lint, and the per-app builds pass.

Out of scope

  • Removing serviceFee from the Mongoose schemas, and the totalAmount virtual on the quote model — stub 2. (The virtual coalesces with ?? 0 and has no callers, so it is inert here.)
  • The total ?? (amount + serviceFee) read fallbacks in vendor, sdm-market-signals, csm-blocker-queue and customer-project — stub 2. They are harmless now: new documents carry no fee, so the fallback adds zero.
  • Migrating documents already in the database, and the total = amount reset on historic records — stub 2, and gated on an owner decision recorded in the scope breakdown.
  • Removing the total field itself, now redundant — a follow-on leanness chore.
  • Any replacement fee, commission or take-rate model. There is none.
  • Rewriting apps/docs/archive/pipeline-runs/**.

Open questions

  • None. One operational note for Ship, not a build question: retiring SERVICE_FEE_PERCENTAGE also needs the value unset in the Vercel dashboard — env drift is a known failure surface here, so it belongs in the release notes.

04_build/output/notes.md

Build notes: service-fee-write-path-removal

  • commits: 99e5c3a feat: service-fee-write-path-removal — stop computing and showing the service fee
  • demo: none (Design was skipped — the change is a removal, nothing to prototype)

What changed

  • packages/services/src/shared/proposal-constants.ts: deleted getServiceFeePercentage and the parseEnvFloat helper it was the sole caller of — leaving the helper would have been an unused binding and a lint failure. parseEnvInt / getMaxMilestones untouched. The barrel is a wildcard export *, so no barrel line needed removing.
  • packages/services/src/db/services/quote/index.ts: createDraftForProposal writes total: params.amount and no serviceFee; updateFinancialsForProposal sets { amount, total: amount } and $unsets serviceFee, so re-pricing a quote written before this change clears the fee rather than leaving it stale. Dropped the now-unused import.
  • packages/services/src/db/services/invoice/index.ts: createFromMilestone pro-rates quote.amount only — the fee limb, the serviceFee projection field, and the stale doc comment are gone; the invoice is written with total: amount.
  • packages/services/src/db/services/demo-data/index.ts: seeded quotes and invoices carry total: amount with no fee.
  • packages/services/src/db/services/expert-workbench/earnings.ts: comment only — it explained amount as "ex platform serviceFee", which no longer describes anything. Code unchanged.
  • apps/web/app/(app)/finances/{quotes,invoices}/[id]/page.tsx: removed the "Service fee" cell and changed the money grid from sm:grid-cols-3 to sm:grid-cols-2, so there is no empty column.
  • docs/ENV.md: removed the SERVICE_FEE_PERCENTAGE row.
  • apps/docs/app/business/**: corrected service-journey/{quote,invoicing,proposals,user-interaction-overview}, roles, initiatives/build-the-bridge. On platform-overview the Service Fee section was rewritten as Transaction Terms (not deleted) — it now states the platform charges no system-side fee and that subscription tiers are the revenue the product models, so the page still explains how the platform earns.

Acceptance criteria status

  • Quote drafted from an accepted proposal has total === amount, no serviceFeecreateDraftForProposal writes total: params.amount and omits the field.
  • Re-pricing updates to total === amount and clears any carried fee — $set + $unset in one update.
  • Milestone invoice has amount === quote.amount × percentage / 100 and total === amount.
  • Quote and invoice screens show Amount and Total only, grid collapsed to two columns.
  • getServiceFeePercentage gone, no unused helper left; getMaxMilestones untouched (still consumed by validate-proposal-milestones.ts and apps/web/lib/proposals/proposal-form-schema.ts).
  • SERVICE_FEE_PERCENTAGE gone from code and docs — see the caveat below.
  • Seeded demo quotes and invoices satisfy total === amount.
  • Fee removed from the named business docs pages and docs/ENV.md.
  • earnings.ts comment no longer describes earnings as excluding a platform fee.
  • pnpm format:check, pnpm lint, per-app builds pass — owned by CI, not asserted locally (Build contract §Verify). Husky formatted the staged files on commit.

Notes for Verify

  • The SERVICE_FEE_PERCENTAGE criterion is satisfied for code, config and docs, but the string still appears under pipeline/ — in this run's own spec.md and the intake/ breakdown that specified the removal. Those are the durable record of the work and must not be rewritten; read the criterion as production surfaces only. apps/docs/archive/ is untouched as specified.
  • Deliberately still present, both deferred to service-fee-schema-and-data-removal: the serviceFee path on the quote and invoice schemas, the totalAmount virtual on the quote model, and the total ?? (amount + serviceFee) read fallbacks in vendor, sdm-market-signals, csm-blocker-queue and customer-project. They are inert for new documents — the fallback adds zero when no fee was written. A repo sweep for serviceFee will hit these; that is expected.
  • The totalAmount virtual ((amount ?? 0) + (serviceFee ?? 0)) has no callers anywhere in the monorepo, so it cannot produce a wrong number here; the ?? 0 coalescing also means no NaN.
  • Existing documents keep their old serviceFee and inflated total until stub 2's migration. That is intended for this run, and the historic-total reset is still the owner decision flagged in the scope breakdown.
  • Ship should carry the env note: SERVICE_FEE_PERCENTAGE must also be unset in the Vercel dashboard. Nothing reads it after this PR, so a stale value is inert rather than dangerous, but leaving it invites the next person to think the fee is still configurable.

05_verify/output/verify.md

Verify: service-fee-write-path-removal

  • production-readiness: run — ready, no ❌. One env action (unset SERVICE_FEE_PERCENTAGE in Vercel) and one accepted interim data inconsistency (below). Triggered by the DB write paths and the retired env var.
  • code-review: medium effort (spec complexity: standard), run inline — the /code-review command is user-invocation-only and cannot be model-invoked, so the review was performed directly against the branch diff. No blocking findings; 2 observations recorded below. The CI Claude review is not enabled (vars.ENABLE_CLAUDE_REVIEW unset — its check run reports skipped), so there were no posted comments to triage.
  • security-review: run — no HIGH or MEDIUM findings. Triggered because the diff changes customer-facing billing amounts.
  • playwright: TODO — manual DoD smoke performed instead.

CI on dca41ad: all green — Quality Project (format/lint/typecheck/build), Audit database, Migrate preview database, Spec structure, Project run labels, Vercel Preview Comments. Production migration and the CONVENTIONS review skipped. No failures at any point.

DoD smoke (on the preview — each line says who verified it)

Agent-verified (reachable without signing in):

  • Preview loads — web-git-claude-service-fee-write-path-removal-njperx-sustentus.vercel.app serves the app, no deployment-protection wall, no error page (agent)
  • getServiceFeePercentage no longer exists and no unused helper remains in proposal-constants.ts — repo sweep returns zero hits; CI typecheck+lint green, which is what would catch the orphaned parseEnvFloat. getMaxMilestones retained with both consumers intact (agent)
  • SERVICE_FEE_PERCENTAGE gone from code, config and docs — sweep clean outside apps/docs/archive/. Caveat: the string still appears under pipeline/ in this run's own spec.md and the intake breakdown that specified the removal; those are the durable record and must not be rewritten. Read the criterion as production surfaces only (agent)
  • Fee removed from the eight named docs surfaces; platform-overview's revenue section rewritten as "Transaction Terms" rather than deleted, so the page still explains how the platform earns — verified in the diff. Note the docs Vercel preview was marked Ignored, so there is no preview URL for these this round (agent)
  • expert-workbench/earnings.ts comment no longer describes earnings as excluding a platform fee; its code is unchanged (agent)
  • pnpm format:check, pnpm lint, per-app builds — Quality Project check run green (agent, CI)

Code paths traced in the diff but not demonstrated on the preview — these need a signed-in session and real records, so they are the operator's:

  • A quote drafted from an accepted proposal has total === amount and no serviceFee (operator — pending)
  • Re-pricing a proposal updates the quote to total === amount and clears any serviceFee the document already carried (operator — pending)
  • An invoice raised against a milestone has amount === quote.amount × percentage / 100 and total === amount, no serviceFee (operator — pending)
  • Quote and invoice detail screens show Amount and Total only, in a two-column grid with no empty cell (operator — pending)
  • Seeded demo quotes and invoices satisfy total === amount (operator — pending, needs a demo seed run)
  • auth: Customer / Expert / Vendor / CSM sign in and reach their dashboards (operator — pending)
  • payments: no payment integration touched — Stripe is not wired in this repo — but invoice amounts change, so one invoice raise should be exercised end to end (operator — pending)
  • notifications: no notification or email template references serviceFee (sweep clean) and none changed shape; confirm notifyInvoiceRaised still fires on an invoice raise, since notifications fail silently by design (operator — pending)

Findings & cleanup

  • Interim data inconsistency — accepted, not fixed here. Removing the UI row before migrating the data means every pre-existing quote/invoice enters a window where Amount and Total visibly disagree with nothing on screen explaining the gap (previously the fee row accounted for it). New invoices raised against an old quote correctly carry no fee, so their sum won't reconcile to that quote's inflated total; re-pricing an old quote silently repairs it, so the state clears unevenly. No code breaks: nothing sums invoices against a quote total or cross-validates the two — verified by sweep — so this is a display/reporting artifact only. This is the concrete shape of the "leave history alone vs reset total" decision the scope breakdown reserved for the owner and deferred to service-fee-schema-and-data-removal. Argues for running stub 2 close behind.
  • Env action for Ship: SERVICE_FEE_PERCENTAGE must be unset in the Vercel dashboard. Nothing reads it after this PR, so a stale value is inert rather than dangerous — the risk is a future reader assuming the fee is still configurable.
  • Code-review observation: float precision on (quote.amount ?? 0) * pct is unchanged by this diff and is now marginally bettertotal mirrors amount exactly instead of compounding two float limbs. No action.
  • Code-review observation: VendorEnvelopeInvoice.serviceFee is still populated but read nowhere, and the quote model's totalAmount virtual still has zero callers monorepo-wide. Both are deliberately deferred to stub 2; both are inert (the virtual coalesces with ?? 0, so no NaN).
  • Pre-existing, not introduced here: getMaxMilestones reads MAX_MILESTONES, which is absent from turbo.json globalEnv. Worth an intake item; out of scope for this run.
  • No fixes were needed on-branch — nothing found warranted a commit.

Gate

Agent-side passes are complete and clean. The operator-demonstrated lines above are outstanding; this record does not claim them. The Verify gate is Jamie's to confirm in conversation.

06_ship/output/changelog.md


title: Quotes and invoices now show only the work value date: 2026-08-03T12:50:00Z personas: [customer, expert, vendor, csm] slug: service-fee-write-path-removal pr: https://github.com/sustentus/sustentus/pull/744

Quotes and invoices now show only the work value

The platform no longer adds a 10% service fee on top of the expert's price. A new quote records the amount the expert quoted and nothing more, so its total is that same figure. Milestone invoices follow the quote — each one bills its share of the expert's price, with no fee added on top.

The "Service fee" line has gone from the quote and invoice detail screens, which now show the amount and the total side by side. Customers see one number to approve and pay; experts see the figure they quoted carried through to every invoice raised against it.

Quotes and invoices issued before this change keep the totals they were issued with, so a customer may still see an older quote whose total sits above its amount. Re-pricing a proposal brings its quote in line.

06_ship/output/investor-update.md

Quotes and invoices now record only the work value

Who it's for: Customers, experts, vendors and CSMs What shipped: The platform no longer adds a 10% service fee to quotes or milestone invoices. Why it matters: Refine the Bridge / Objective 3: Validate Technical Infrastructure & Payout Flow. Payouts can only be validated against figures that mean what they say.

Every new quote and invoice satisfies total equals amount.

Dig deeper: https://github.com/sustentus/sustentus/pull/744 · https://help.sustentus.com/changelog/2026-08-03-service-fee-write-path-removal

06_ship/output/release.md

Ship: service-fee-write-path-removal

  • pr: #744 · merged: yes — 2026-08-03, squash as 4d11774
  • CI: green on the ship commit 017fce1 — Quality Project, Audit database, Migrate preview database, Spec structure, Project run labels, Vercel Preview Comments all passed; production migration and the CONVENTIONS review skipped. No failures at any point in the run.
  • technical docs: no technical docs impact — swept apps/docs/app/technical/** for the fee and the env var, zero hits.
  • business docs: updated in this PR — service-journey/{quote,invoicing,proposals,user-interaction-overview}, platform-overview (Service Fee section rewritten as Transaction Terms, not deleted, so the page still explains how the platform earns), roles, initiatives/build-the-bridge, plus docs/ENV.md.
  • release notes: both — changelog entry + ship note.
  • sent: ship note sent to #changelog on 2026-08-03 (2 recipients, 0 failures).

Acceptance check (vs spec)

  • Quote drafted from an accepted proposal has total === amount, no serviceFee — verified in Verify (code trace)
  • Re-pricing updates to total === amount and clears any carried fee — verified in Verify (code trace)
  • Milestone invoice has amount === quote.amount × percentage / 100, total === amount — verified in Verify (code trace)
  • Quote and invoice screens show Amount and Total only, two-column grid — verified in Verify (source trace)
  • getServiceFeePercentage gone, no unused helper left; getMaxMilestones untouched — verified in Verify (sweep + CI lint/typecheck)
  • SERVICE_FEE_PERCENTAGE absent outside apps/docs/archive/ — verified in Verify (sweep; see caveat below)
  • Seeded demo quotes and invoices satisfy total === amount — verified in Verify (code trace)
  • Fee removed from the named docs surfaces — verified in Verify (diff)
  • earnings.ts comment corrected — verified in Verify (diff)
  • pnpm format:check, pnpm lint, per-app builds pass — Quality Project green

Outstanding after merge

  • Unset SERVICE_FEE_PERCENTAGE in the Vercel dashboard. Nothing reads it now, so a stale value is inert rather than dangerous — the risk is a future reader assuming the fee is still configurable. This is the one manual action this run leaves behind.
  • Interim data inconsistency until stub 2 lands. Quotes and invoices written before this merge keep their old serviceFee and inflated total, so their Amount and Total now disagree on screen with no line explaining the gap. New invoices raised against an old quote correctly carry no fee, so they won't reconcile to that quote's total; re-pricing an old quote repairs it. No code breaks — nothing sums invoices against a quote total or cross-validates them. Resolved by service-fee-schema-and-data-removal, which still needs the owner's decision on whether to reset total on historic records.
  • Verify's operator-demonstrated lines were accepted on the code trace, not demonstrated on the preview — the signed-in criteria, auth, an end-to-end invoice raise, and the notification check. Recorded honestly in 05_verify/output/verify.md.

Notes

  • The SERVICE_FEE_PERCENTAGE criterion is satisfied for production surfaces. The string still appears under pipeline/ in this run's own spec and the intake breakdown that specified the removal — those are the durable record and were deliberately not rewritten.
  • Deliberately deferred to service-fee-schema-and-data-removal: the serviceFee schema field on both models, the quote totalAmount virtual (zero callers monorepo-wide), the four total ?? (amount + serviceFee) read fallbacks, and the migration of existing documents.
  • Pre-existing, worth an intake item: getMaxMilestones reads MAX_MILESTONES, which is absent from turbo.json globalEnv.