Committed Revenue now includes recurring fees

The minimumCommitment.committedRevenue figure becomes monthly minimum × term + recurring fees × term, matching Plaid's contractual semantic and applying universally.

PRdealops#6160 Author@yijunz166 Files4 +/−+356 / −24 Areadealops2 / pricingengine StatusOpen

Problem

Plaid's "Committed Revenue" tile showed only monthly minimum × term, omitting fixed recurring platform fees that are equally contractual.

Fix

A new getNonMonthlyMinimumRecurringRevenueOverContract helper sums recurring fees over the contract and the summary adds it onto the minimum-commitment base.

Scope

Universal — no Plaid gate, no spec opt-in. Only Plaid renders this field today, but the math is org-agnostic.

Monthly minimum (MM)
Recurring fees (new)
One-time fees (excluded)
Year-1-only recurring

1. Why this exists

Plaid's pricing flow surfaces a Committed Revenue field on the minimum-commitment panel. Until this PR, its tooltip read:

Minimum commitment multiplied by contract length

That definition matched the math (monthlyMinimum × contractLengthMonths) but not the underlying contractual reality: most Plaid deals carry a fixed monthly recurring platform fee alongside the usage-floor minimum, and the customer is committed to both. The tile under-reported the obligation, sometimes by hundreds of thousands of dollars.

The fix is to bake recurring fees into the field at calculation time so the displayed figure is the full contractual ask. New tooltip:

Monthly minimum × contract length, plus recurring fees × contract length

2. The formula, before and after

Before
committedRevenue =
  monthlyMin × term

Ramp-aware on the minimum side (per-month rampedValues summed), but nothing else contributes.

After
committedRevenue =
  monthlyMin × term +
  Σ recurring-fee revenue 
  one-time fees MM-contributing products

Augmentation is gated on the minimum commitment actually being enabled; otherwise the base is a usage fallback and stacking recurring fees would over-state.

3. What changes, by file

FileChangeLines
pricingEngineService.ts New public method getNonMonthlyMinimumRecurringRevenueOverContract(). Iterates products, filters down to non-MM recurring SKUs, sums revenue via the engine's absolute/total_contract path. +60 / −0
pricingEngineSummary.ts Refactors committedRevenue into a base IIFE (unchanged math, just hoisted) then calls the new helper and adds extra.value on top. Two early returns guard MM-disabled and zero-extra cases. +43 / −23
pricingFlowSpec.json (Plaid) Tooltip string update only. No structural change. +1 / −1
pricingEngineMonthlyMinimumMultiplier.spec.ts New describe block with 7 cases pinning the new behavior; helper makeProductSpec gains calculationSpecOverrides for spec flexibility. +252 / −0

4. The product filter

The new helper walks all products and excludes three categories. Getting this filter right is the whole PR — the math itself is a one-line addition.

Excluded: commitment-based-pricing usage

Handled via filterUsageProductsNotInCommitmentBasedPricing at the top of the loop — these belong to multi-commitment quotes and are summed elsewhere.

Excluded: contributes to monthly minimum

Products with contributesToMonthlyMinimumRevenue: true already feed the MM floor. Including them here would double-count against monthlyMin × term.

Excluded: one-time fees

Products with contributesToRecurringRevenue: false (e.g. flat_fee_one_time implementation costs). This is the new filter vs. the TCV helper this was modeled on.

The filter is summarized in code as:

if (
  isNil(productSpec) ||
  productSpec.calculationSpec?.contributesToMonthlyMinimumRevenue ||
  !productSpec.calculationSpec?.contributesToRecurringRevenue
) {
  continue;
}

5. Why absolute / total_contract?

The helper sums revenue with this selector:

this.getRevenue(
  { type: 'product', id: product.id, productSpecId: ... },
  { type: 'absolute', timePeriod: 'total_contract' },
).value;

The choice isn't cosmetic. The engine respects a per-product contributesToYear1RecurringRevenueOnly gate only on the absolute path. If the helper used recurring/monthly × subscriptionTerms instead, a year-1-only platform fee would be multiplied across the full contract — a 24-month deal with a $500/mo year-1-only fee would over-count by $6,000.

This is pinned by the year-1-only test case:

InputsCorrectIf using recurring/monthly × term
24-mo term, $1,000 MM, $500/mo Y1-only recurring $30,000
= 1,000×24 + 500×12
$36,000
= 1,000×24 + 500×24

6. The MM-disabled gate

Subtle correctness point. When minimumCommitment.enabled is false, getMonthlyCommitment returns the monthly usage revenue as a fallback — non-null, but no longer a contractual floor. Stacking recurring fees on top would conflate usage with platform fees and badly over-state the field. The summary therefore short-circuits:
if (!base || !minimumCommitment?.enabled) return base;
This keeps the MM-disabled path byte-identical to pre-change behavior. The MM disabled: augmentation suppressed test pins it.

7. Test matrix

Seven cases under the new describe block. Each one isolates a single dimension of the behavior.

TestPinsExpected
No non-MM products Regression guard — pre-change behavior preserved when nothing to add 10,000 × 24 = 240,000
Non-MM recurring product present Happy path — addition fires 10,000×24 + 5,000×24 = 360,000
Year-1-only recurring Engine's Y1 gate is honored (the reason for absolute/total_contract) 1,000×24 + 500×12 = 30,000
One-time fee excluded New contributesToRecurringRevenue filter works 1,000×12 + 800×12 = 21,600
($50K impl fee dropped)
Ramped MM + non-MM recurring Ramp summation on base + flat add on extra 1,400 + 250×5 = 2,650
MM disabled Augmentation gate (would catch 12,000 → 72,000 regression) 1,000 × 12 = 12,000
Non-Plaid org Behavior is universal, not Plaid-gated 1,000×12 + 300×12 = 15,600

8. Comparison with the TCV helper

The PR explicitly models the new helper on getNonMonthlyMinimumContractValue (which powers TCV) and notes the one structural difference. Reviewers should not assume these two helpers are interchangeable:

TCV helper
getNonMonthlyMinimum
ContractValue()
  • Skips commitment-based pricing
  • Skips MM-contributing products
  • Includes one-time fees
  • Powers Total Contract Value
New helper
getNonMonthlyMinimum
RecurringRevenueOverContract()
  • Skips commitment-based pricing
  • Skips MM-contributing products
  • Excludes one-time fees (extra filter)
  • Powers Committed Revenue

9. What this does not change

10. Risks & open questions

Universal application is intentional but worth flagging. Any org that wires up a committedRevenue field in its pricingFlowSpec will see the new math automatically. If another org's contracts treat "committed revenue" as floor-only, the displayed figure will jump on the first calculation after deploy. Today only Plaid surfaces this field, so the blast radius is limited — but the assumption should be revisited if a second org adopts the field.
Backward compatibility on MM-disabled quotes is explicitly tested. The MM disabled: augmentation suppressed case exists specifically to catch a regression flagged by the PR-review bot — a previous iteration would have turned 12,000 into 72,000 on usage-only quotes. The current gate at if (!base || !minimumCommitment?.enabled) return base closes that hole.
Full diff: the summary-side refactor
const base = ((): NumberCurrency | null => {
  if (minimumCommitment?.enabled && ramp && ramp.rampedValues.length > 0) {
    const fallbackMonthlyMinimum =
      minimumCommitment.monthlyMinimumAmount?.value ?? 0;
    let total = 0;
    for (let monthIdx = 0; monthIdx < contractLengthMonths; monthIdx++) {
      total +=
        (ramp.rampedValues[monthIdx]?.value ?? fallbackMonthlyMinimum) *
        commitmentScale;
    }
    return { ...rawMonthlyMinimum, value: total };
  }
  return multiplyCurrency(monthlyMinimum, contractLengthMonths);
})();

if (!base || !minimumCommitment?.enabled) return base;
const extra =
  pricingEngineService.getNonMonthlyMinimumRecurringRevenueOverContract();
if (extra.value === 0) return base;
return { ...base, value: base.value + extra.value };