Pylon buyout discounts now finalize net TCV after pricing
Dealops 2 quote finalization now applies Pylon’s organization-configured buyout concession once, keeps the order form and pricing footer on the same subtotal, and leaves raw pricing-engine facts intact.
finalizeQuoteSummary: calculate raw engine output, apply amendment accounting, then attach summary.buyoutAdjustment.
1. Why this exists
Two customer-facing surfaces disagreed because only the order-form formula subtracted the buyout.
The payable headline now uses the same concession as the order form, without mutating engine TCV.
2. What changes
finalizeQuoteSummary.ts and service/router callers now share one output boundary.
computeBuyoutAdjustment.ts produces baseTcv, subtotal, discount, and net.
orderFormQuoteTotals.ts is used by both server finalization and CEL builtins.
| Area | Files | Reviewer focus |
|---|---|---|
| Types | packages/types/v2/buyoutAdjustment.ts, pricingQuoteOutput.ts, pricingSpecData.ts |
Adds opt-in config and optional output schema; no DB migration. |
| Server quote paths | PricingQuoteService.ts, pricingEngine/calculatePricing.ts, pricingQuote/get.ts, pricingQuote/update.ts |
Create, update, recompute, preview, and recalculating GET branches finalize through the same function. |
| Order form runtime | buildInput.ts, builtins.ts, pylonPriceSummary.json, pylonBuyoutValue.json |
CEL formulas prefer saved concessions and keep legacy behavior when the block is absent. |
| Spec conversion | pricingSpec/converter.ts, pricingSpec/v2Converter.ts, flowSpec/* |
Preserves buyoutAdjustment, footer fallbackPath, and tooltip text across V2/V3. |
| Client display | FinancialSummaryBottomBar.tsx, PricingTraceSheet.tsx |
Footer can read a primary path with fallback; trace sheet explains buyout is applied after pricing. |
| Enablement + docs | enablePylonBuyoutAdjustments.ts, knowledge/concepts/quote-level-adjustments.md |
Local-only Neon enablement and a knowledge entry documenting the finalization contract. |
3. How it works
- Compute raw
PricingEngineSummary. - Copy the summary; do not mutate raw output.
- Apply amendment override when that context exists.
- Compute optional
buyoutAdjustment.
- Requires
pricingSpecData.buyoutAdjustment. - Only grants on
NEW_BUSINESS. - Selection term must be one of configured
activeValues. - Bad historical amount/unit values read as zero buyout.
- Eligible, non-complimentary product list value.
- Minus line discounts.
- Minus booked promotion from the summary.
- Product types are normalized, so Pylon title-case tags match.
baseTcv = summary.tcv.all
subtotal = eligibleList - max(eligibleList - eligibleNet, 0) - promotionDiscounts.total
buyoutDiscount =
inactive ? 0 :
unit === '$' ? amount :
baseTcv * amount / 100 // blank unit also means percent
netTcv = subtotal - buyoutDiscount
The percent base and subtotal intentionally can differ. That matches Pylon’s published formula: percent uses engine TCV; payable subtotal uses the order-form product scope.
4. Before / after numeric parity
| Opportunity | Engine TCV | Product subtotal after promotion | Buyout | Net in server + CEL |
|---|---|---|---|---|
| New business | $84,534 | $84,534 | 2% = $1,690.68 | $82,843.32 |
| Renewal | $300 | $84,534 | $0 | $84,534 |
| Amendment | −$500 | $1,900 | $0 | $1,900 |
5. UI, formulas, and configuration
The footer can point at buyoutAdjustment.netTcv and fall back to tcv.all for historical saved outputs.
Tooltip strings can interpolate pricing values using {pricing.buyoutAdjustment.subtotal}-style tokens.
The trace sheet shows a compact buyout row:
subtotal − buyoutDiscount = netTcv
It also clarifies the detailed pricing breakdown remains pre-buyout.
pylonPriceSummary and pylonBuyoutValue prefer saved input.pricing.buyoutDiscount.
When absent, they retain the legacy branch for older outputs.
apps/server/src/dealops3/__scripts__/enablePylonBuyoutAdjustments.tsis dry-run by default.- Requires a Neon development database host ending in
.neon.tech. - Targets Pylon Test Org.
- Publishes canonical seeded formula definitions instead of editing expression strings in place.
- Versions pricing config, amount validation, and footer config through V3/V2 conversion.
- Preserves existing quotes, unrelated formulas, and drafts.
6. Tests and validation
115 tests passed across buyout computation, ECM parity, input validation, Pylon formulas, runtime regressions, order-form input, and footer conversion.
11 existing amendment-credit tests passed. This matters because the buyout finalizer runs after amendment accounting.
Three Neon-backed scenarios covered create, preview, service recompute, repeated save/reload, edits, ECM lifecycle, frozen output, and pinned-spec behavior.
The unchanged 2% local Pylon page displays $82,843.32. Fixtures also cover Pylon’s title-case product tags.
repairMergeCoTermQuotes.main.ts. That script is not part of this PR.
7. What it doesn’t change
- No Prisma migration and no new database table or column.
- No production Pylon configuration change.
- No automatic backfill for historical quote outputs.
- No change to raw Pricy calculations, engine TCV, ARR, product revenue, credits, or promotion semantics.
- No change to approval or CRM input meaning.
- No second live-spec lookup on ordinary quote GET just to decorate saved output.
- No buyout grant for renewals, amendments, or unknown opportunity types.
- No rewrite of frozen outputs or pinned specs.
8. Risks / rollback / open questions
buyoutAdjustment.netTcv only where the payable Pylon headline is intended.
- Remove or stop publishing
pricingSpecData.buyoutAdjustment. - Point the footer metric back to
tcv.allor rely on its fallback. - Revert formula release if needed; historical saved outputs remain readable.
- Finalization happens exactly once in create/update/recompute/preview paths.
- GET does not mix saved output with live spec config.
- Renewal/amendment subtotals stay real while buyout is zero.
No explicit product open questions are listed in the PR. The main operational decision is when, if ever, to publish equivalent configuration outside the local Pylon test org.