Plaid v2: MM checkbox count scales TCV (dealops-3uy)
Wires v1's "monthly minimum type" checkbox-count multiplier into v2's contractual outputs (ramped TCV, flat-MM TCV, displayed minimum-commitment block) — narrowly scoped so non-Plaid orgs stay byte-identical.
A single multiplier reader getMonthlyMinimumMultiplier() plus two new TCV methods getFlatMMTCVContractValue and getFlatMMTCVForMonthRange. The summary layer reads them via a 3-step fallback chain.
For Plaid (flag-on), ramped and flat-MM TCV, plus the displayed minimumCommitment.* block, now scale by checked-box count. $120k → $240k → $360k as N goes 1 → 2 → 3.
Core math (getMonthlyMinimumPercentage, commitment rollups, applyMonthlyMinimumDiscount) is byte-identical on main for every non-Plaid org and for flag-off Plaid.
New spec flag scaleByAdditionalProductCodes. Default false → multiplier always 1. Plaid's pricingSpec.json opts in.
1. Why this exists
In v1 (penguin), each checked "Monthly minimum type" box multiplied the contractual MM floor by the checkbox count. The rep picks 2 boxes, the MM floor doubles in TCV. v2 ignored this entirely — the term only fed the effectiveMonthlyMinimum formula variable used for SF writeback / approval-tier matching, but never landed in TCV or the displayed commitment block.
getRampedTCVContractValue ?? getTCV, and flat-MM quotes hit unscaled getTCV.
The narrow-scope contract
The PR explicitly carves the work into two pieces. This PR ships the contractual display + TCV piece. Per-product / aggregate revenue scaling (the applyMonthlyMinimumDiscount chokepoint) is deferred to dealops-9za so this PR doesn't touch core pricing-engine math for any non-Plaid org.
2. What changes
2.1 The fallback chain (the central idea)
Every TCV read in calculateSummary and buildTcvSegments now goes through a 3-link resolver. Each link returns null on quotes it doesn't apply to, so non-Plaid orgs fall through to plain getTCV byte-identically.
Pre-existing. Live TCV path for ramped Plaid quotes. Gated to PER_MONTH_MINIMUM_RAMP_ORG_NAMES (Plaid Test Org only, prod Plaid joins later).
Flat-MM (non-ramped) Plaid path. Adds additive marginal (N−1) × floor × months to getTCV. Gated by scaleByAdditionalProductCodes — covers test + prod Plaid.
Unmodified getTCV. All non-Plaid orgs land here; the two upstream methods return null and the result is byte-identical to main.
2.2 The additive marginal (the math)
For flat-MM Plaid quotes with multiplier mult > 1:
marginal = monthlyMinimumAmount × (mult − 1) × monthsInRange
result = getTCV(year).value + marginal
This is additive, not multiplicative or max. It mirrors v1's SF writeback, which emits N separate MM QLIs summed alongside usage QLIs — no max-with-usage clamp anywhere. v2's applyMonthlyMinimumDiscount clamps semantics differ; closing that pre-existing v1↔v2 gap is dealops-j4u, not this PR.
2.3 The screenshot scenario — concrete numbers
Scenario: binding flat-MM quote, $10,000/mo floor, 12-month term, $1,000/mo usage → MM binds, base TCV = $120k. After fix-up, TCV scales linearly with N.
2.4 Engine: new methods and their gates
All three methods sit on PricingEngineService. Each has a tight set of early returns so non-Plaid orgs are provably unaffected.
Single source of truth for the multiplier. Reads the additional_monthly_minimum_product_codes term; falls back to the singular monthlyMinimumProductCode when no term is seeded.
getMonthlyMinimumMultiplier(): number {
const scale = pricingSpecData.pricingEngineSpec
?.minimumCommitment?.scaleByAdditionalProductCodes;
if (!scale) return 1; // non-Plaid identity
const term = terms.find(t => t.id === ADDITIONAL_MM_TERM_ID);
if (term?.userValue.type === 'array') {
return term.userValue.value.filter(x => typeof x === 'string').length;
}
// CPQ-import fallback: singular code counts as 1
const imported = minimumCommitment?.monthlyMinimumProductCode;
if (typeof imported === 'string' && imported.length > 0) return 1;
return 0;
}
Returns null — i.e. defers to getTCV — in every case that would otherwise change non-Plaid behavior.
- Flag off / non-Plaid → multiplier returns 1 → null
- MM disabled on the quote → null
- Ramp present → null (upstream
getRampedTCVContractValuehandles it) monthlyMinimumAmountis 0 or missing → nullmult ≤ 1→ null (covers both N=1 identity and N=0; N=0 is dealops-j4u territory)
Otherwise: baseTCV.value + flat × (mult − 1) × monthsInRange.
Per-month-range sibling. Same gates, same additive marginal, applied over an arbitrary [start, start + count) window clamped to subscriptionTerms.
Used by buildTcvSegments so multi-commitment periods and year-3+ year-loop segments carry their proportional share. Without this, segments would sum to unscaled while tcvAll carries the full marginal — tripping the reconciliation tolerance and either showing an arithmetic-wrong "Σ segments" in the calc trace or firing production log noise on every Plaid calc.
2.5 Summary: where scaling lives
The summary scales response.minimumCommitment.{monthlyMinimumAmount, annualMinimumAmount, effectivePercentage} at the display boundary, not inside the engine's commitment readers. This keeps core math untouched.
const mmMultiplier = pricingEngineService.getMonthlyMinimumMultiplier();
const mmEnabledOnQuote = pricingQuoteInput.minimumCommitment?.enabled === true;
const commitmentScale = mmEnabledOnQuote ? mmMultiplier : 1;
const monthlyMinimum = { ...rawMonthlyMinimum, value: rawMonthlyMinimum.value * commitmentScale };
const annualMinimum = { ...rawAnnualMinimum, value: rawAnnualMinimum.value * commitmentScale };
The mmEnabledOnQuote guard is a carve-out: when MM is disabled on the quote, the commitment readers return the usage-revenue fallback, which must not be scaled. Same carve-out the engine-internal version had.
2.6 CPQ-import fallback
CPQ import + renewal init carry the imported MM SKU on minimumCommitment.monthlyMinimumProductCode (singular), but never seed an additional_monthly_minimum_product_codes term. With flag on and no term, multiplier would be 0 → silent $0 TCV.
Source side: assemblePricingQuoteInput materializes the singular code as a single-element term. Engine side: getMonthlyMinimumMultiplier defensively counts the singular code as 1 when the term is absent. Terms always win when both are present.
2.7 Ancillary: the cycle break
The constant ADDITIONAL_MONTHLY_MINIMUM_PRODUCT_CODES_TERM_ID moved into a new zero-import sibling file PlaidEffectiveMonthlyMinimum.constants.ts. Importing it from …attr.ts would re-enter the plaid/registry ↔ attr ↔ formulaUtils ↔ evaluationEngine ↔ variables/registry ↔ plaid/registry init cycle and hit a TDZ error at module-load time. The .attr.ts file re-exports the constant for backward compat.
3. The PlaidWriteBack atomicity rework (sibling change in this PR)
This PR also carries a substantial rework to PlaidWriteBack.ts (+216/−211) under dealops-1q8. It's logically separate from the multiplier work but ships in the same PR.
Phase 1 preprocessOpportunityForPlaid fired two standalone SF writes (un-primary PATCH + OLI batch DELETE) before the composite request. A composite failure left the opp half-preprocessed: un-primaried quote, OLIs gone, no new quote.
Phase 1 is now pure-read discovery (discoverPlaidPreprocessRequirements). The un-primary PATCH and per-OLI DELETEs are folded into the composite/graph before the opp PATCH. SF's atomic rollback now covers them on failure.
Composite shape (with 1 primary quote + 3 OLIs)
| # | Method | Target | referenceId |
|---|---|---|---|
| 0 | PATCH | SBQQ__Quote__c/Q-old → {SBQQ__Primary__c: false} | preprocess_unprimary |
| 1–3 | DELETE | OpportunityLineItem/oli-{n} | preprocess_oli_delete_{n} |
| 4 | PATCH | Opportunity/{id} | updateOpportunity |
| 5 | POST | SBQQ__Quote__c (new) | dealops_new_quote |
| 6… | POST | QLIs | dealops_{n} |
Other guardrails added: a single-attempt contract (no retry — partial-success risk on Ampersand timeouts outweighs benefit), and a budget guard that throws PLAID_COMPOSITE_GRAPH_TOO_LARGE before any Ampersand call when sub-request count would exceed SF's 500-cap.
4. What it doesn't change
- Core engine math.
getMonthlyMinimumPercentage,getMonthlyMinimumPercentageForMonth,getAnnualCommitment,getMonthlyCommitment,applyMonthlyMinimumDiscount— all byte-identical to main across N=1/2/3. - Per-product / aggregate revenue.
revenue.annualstays ×1 for Plaid flag-on quotes; the per-product revenue chart still shows the unscaled product total. Tracked as dealops-9za. - Production Plaid ramped path.
PER_MONTH_MINIMUM_RAMP_ORG_NAMESstill only containsPlaid Test Org. Production Plaid joins in a separate ramp-rollout follow-on. The flat-MM path added here covers test + prod Plaid via the opt-in flag. - Plaid SF writeback.
input.minimumCommitmentis never mutated. The pre-existing v1↔v2 parity gap (v1 emits N MM-segment QLIs, v2 emits one) is now user-visible (display ×N, SF ×1) and tracked as dealops-ydo. - FE checkbox UX bug in
useMonthlyMinimumTypes.ts:121-148(0↔1 transition destroys sibling state) — pre-existing, tracked as dealops-99g.
5. Risks & intentional consequences
tcv.* and minimumCommitment.* scaled by N but the per-product revenue chart and revenue.annual stay at ×1. Reviewers comparing the two columns side-by-side will see the discrepancy. This is documented and accepted.
buildTcvSegments uses the same fallback chain for both year-loop and multi-commitment branches, so Σ segments = tcvAll for Plaid flag-on quotes across both 36-month year-based and 2×6-month multi-commitment shapes. Without the getFlatMMTCVForMonthRange threading, the trace would print an arithmetic-incorrect "Σ segments = tcvAll" equation or fire production log noise on every calc.
applyMonthlyMinimumDiscount applies multiplicative scaling that already reduces revenue below usage. The flat-MM additive marginal preserves this gap (it's constant in N, so N=1 and N=2 differ by the same −1 × floor × T amount as on main). Closing it is dealops-j4u.
6. Test coverage
One new spec file: pricingEngineMonthlyMinimumMultiplier.spec.ts (1345 lines, 47 tests, 4 describe blocks).
Reader correctness across flag on/off × 0/1/2/3 codes × CPQ-import fallback; core math invariance across N; getRampedTCVContractValue scaling.
Display-block scaling for flag on × 0/1/2/3 codes; flag-off identity; MM-disabled carve-out (usage-revenue fallback stays unscaled).
Binding/non-binding × N=2/3; all early-return gates; partial-final-year clamp; sister getFlatMMTCVForMonthRange with overshoot clamping.
12-mo binding tcv.all scaling; 36-mo year-loop Y3+ coverage; 2×6-mo multi-commitment reconciliation (the critical Σ segments = tcvAll invariant).
Plus a new PlaidWriteBack.atomic.test.ts (886 lines, TDD-red contract tests for dealops-1q8 GAP-ATOMIC) and 276 lines of obsolete preprocess-phase tests removed from PlaidWriteBack.test.ts.
Regression validation: 465 tests pass across 7 touchpoint suites (pricingEngineService, pricingEngineServiceProratedAndOthers, monthlyMinimum, tcv, dealDiscount, evaluationEngine, and the new MM-multiplier spec).
7. Follow-up bead map
| Bead | Scope | Priority |
|---|---|---|
dealops-9za | Per-product / aggregate revenue scaling via applyMonthlyMinimumDiscount | Open |
dealops-ydo | SF writeback parity — emit N MM-segment QLIs (mirrors beadops-bvv) | Open |
dealops-j4u | Absolute v1↔v2 TCV alignment (commitment-clamp vs floor-plus-extras) | P1 |
dealops-99g | FE checkbox 0↔1 transition destroys sibling state | Open |
| (unbeaded) | Add production Plaid to PER_MONTH_MINIMUM_RAMP_ORG_NAMES | Ramp rollout |