Server-owned promotion stamping and pricing discounts

This Dealops 2 server PR makes promotions trusted, priced, and booked into quote summaries without letting client-supplied stamps affect TCV, ACV, or ARR.

PR #7062 Author @mehulshinde Status Draft / open Stack DEA-7480 2/4 Area Dealops 2 server Files 11 Diff +1513 / -2 Tests 28 Mocha cases

What it adds

Three promotion services: stamping, discount computation, and summary booking.

The server now derives appliedPromotion from trusted inputs instead of accepting it from the client.

What it changes

PricingEngineSummary.calculateSummary runs a promotion pass after the response literal is built.

Promotion concessions reduce TCV, total contract revenue, and first-year ACV.

What it preserves

Quotes without promotion stamps return byte-identical summaries.

ARR, profit, margin, and negotiated dealDiscount stay gross by design.

Review focus

Validate the net/gross contract, the stacking rule for deal-volume promotions, and the amendment re-booking path.

selectedPromotionIds
appliedPromotion stamp
Promotion discount math
Pricing summary booking
Amendment override

1. Why this exists

After PR 1

Promotions can be authored, stored, selected, and included in approval attributes.

  • The model exists.
  • The quote can carry selected IDs.
  • No server pricing pass applies the discount yet.
This PR

The server becomes the source of truth for promotion eligibility and concession booking.

  • Client stamps are stripped or overwritten.
  • Discounts are computed from engine-valued revenue.
  • TCV, ACV, and ARR stop contradicting each other.
Stack context: this is DEA-7480 part 2/4. It sits between model/storage work and the upcoming admin UI, quote badges, persisted toggle, and Pylon startup-discount scenarios.

2. What changes

1. Seller selection

selectedPromotionIds is the only client-writable promotion state.

2. Server stamping

Server resolves live spec, segment, renewal, and stacking gates.

3. Discount math

Stamped lines are grouped by promotion and valued per contract year.

4. Summary booking

Promotion concession nets only the agreed output fields.

5. Amendments

Headline TCV is re-netted after amendment override recomputes it.

Files grouped by responsibility

Write paths
PricingQuoteService.ts stamps promotions during quote creation / seeded quote pricing.
trpc/router/pricingQuote/update.ts stamps promotions on normal recompute saves and on refreshOutput:false writes.
Summary
pricingEngineSummary.ts computes promotion discounts after the base response literal is built and applies the concession.
computeAmendmentContext.ts re-applies the concession when amendment normalization overwrites headline TCV.
Promotion services
stampQuotePromotions.ts derives line stamps from selected IDs and live specs.
computePromotionDiscounts.ts calculates year and month discount totals.
applyPromotionConcession.ts books the discount into the output summary.
Type boundary
packages/types/v2/pricingQuoteInput.ts adds apiProductInputSchema that omits appliedPromotion.
Tests
stampQuotePromotions.test.ts, computePromotionDiscounts.test.ts, and applyPromotionConcession.test.ts pin the behavioral contract.

Before / after at the API boundary

Before

A caller could send product lines that contained appliedPromotion.

That created room for stale or forged stamps to persist alongside selectedPromotionIds.

After

External quote input uses apiProductInputSchema, which omits appliedPromotion.

Zod strips the field; server write paths re-derive the real stamp.

3. How it works

Stamping

Inputs: selected IDs, live PromotionSpec, product list, quote use case, renewal flag.

Output: effective selection plus per-line appliedPromotion snapshots.

Discounting

Inputs: stamped lines, contract term, currency, engine-provided yearly revenue.

Output: promotionDiscounts with total, per-year, per-promotion, and internal per-month allocation.

Booking

Inputs: gross pricing summary plus computed discounts.

Output: net TCV / ACV fields with gross recurring run-rate fields preserved.

3.1 Server-owned stamping

Core rule

input.selectedPromotionIds is human-authored state; products[].appliedPromotion is derived state.

Gate What happens Why it matters
Live spec Dropped if the org no longer has the promotion spec. Prevents deleted or missing promotions from pricing.
Segment Dropped when quote use case does not match displayTargeting.segments. Prevents stale client state from bypassing presentation-only targeting.
Renewal Dropped unless carryIntoRenewal is true. Startup discount renewals re-price at full price by default.
Stacking Only the first deal-volume promotion survives. A deal-volume promotion already covers the whole recurring base.
const promotionStamping = await stampQuoteInputPromotions({
  organizationId,
  opportunityV2Id,
  products: finalMergedInput.products ?? [],
  selectedPromotionIds: finalMergedInput.selectedPromotionIds,
  pricingSpecData: summaryInputUpdate.pricingSpecData,
  useCase: finalMergedInput.useCase,
});

3.2 Pricing-summary promotion pass

Where it runs

The pass runs inside PricingEngineSummary.calculateSummary immediately after the response literal is built.

The calculator is kept pure by injecting engine valuation callbacks instead of reaching into PricingEngineService directly.

const promotionDiscounts = computePromotionDiscounts({
  products: pricingQuoteInput.products,
  subscriptionTerms: pricingQuoteInput.subscriptionTerms,
  currency: pricingQuoteInput.targetCurrency || 'USD',
  getYearlyRevenue: (product, yearIndex) =>
    pricingEngineService.getRevenue(
      { type: 'product', id: product.id, productSpecId: product.productSpecId ?? product.id },
      { type: 'absolute', timePeriod: 'year_idx', idx: yearIndex },
    ).value,
  getActiveMonths: promotionActiveMonths,
  isRecurring: (product) =>
    pricingSpecData.productSpecs[product.productSpecId ?? product.id]
      ?.calculationSpec?.contributesToRecurringRevenue !== false,
});

3.3 Discount math invariants

Aggregate cap

The ramped percent applies to the sum of eligible revenue, then clamps to the year cap.

This makes a flat cap behave like a true promotion cap, not a per-line cap.

Year-aware revenue

The pass uses year_idx, not month lookup, for the eligible base.

That prevents Pylon-style dated lines from being double-counted across years.

Recurring base

Deal-volume promotions include recurring lines only.

One-time fees such as implementation and services are excluded from the base.

Cent reconciliation

Discounts are allocated to months as integer cents using largest remainder.

TCV segments reconcile exactly instead of drifting by pennies.

3.4 What becomes net vs gross

Field family After promotion pass Reason
tcv.all, tcv.year1, tcv.year2, tcv.segments[].tcv Net They represent what the customer pays over the contract and per slice.
tcvAllValue, revenue.totalContract Net They mirror total contract value.
netNewAcv Net Promotion reduces first-year contract value.
netNewArr, arrAtScale, revenue.annual, revenue.monthly Gross They are recurring run-rate metrics; the promotion concession expires with the term.
dealDiscount, product discount fields, profit, margin Gross Negotiated rep concession stays separate from programmatic promotion concession.
Idempotency guard

applyPromotionConcession exits if summary.promotionDiscounts already exists, so repeated application does not double-discount.

if (summary.promotionDiscounts) return summary;
if (discounts.total.value <= 0) return summary;

summary.tcv.all = net(summary.tcv.all, discounts.total.value);
summary.netNewAcv = net(summary.netNewAcv, discountForYear(1));
summary.promotionDiscounts = output;

3.5 Amendment override repair

Why the helper exists

applyAmendmentSummaryOverride recomputes tcv.all from gross prior TCV plus change value.

This PR calls reapplyPromotionConcessionToTcvAll immediately after that overwrite, keeping the headline aligned with the already-booked promotion discount.

4. What it doesn't change

5. Tests and coverage shape

Stamping
8

Covers forged stamp removal, renewal carry gate, segment mismatch, no-spec stripping, no-op fast path, and stacking selection.

Discount math
15

Covers caps, year two at full price, repeat-last schedules, dated lines, deal-volume basis, exclusivity, and cent allocation.

Concession booking
5

Covers net fields, gross fields, idempotency, zero floor, and amendment re-application.

6. Risks / rollback / open questions

Primary risk: the promotion pass sits in the pricing summary path, so any mismatch in net/gross expectations affects downstream consumers of tcv, netNewAcv, and promotionDiscounts.