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.
Plaid's "Committed Revenue" tile showed only monthly minimum × term, omitting fixed recurring platform fees that are equally contractual.
A new getNonMonthlyMinimumRecurringRevenueOverContract helper sums recurring fees over the contract and the summary adds it onto the minimum-commitment base.
Universal — no Plaid gate, no spec opt-in. Only Plaid renders this field today, but the math is org-agnostic.
1. Why this exists
Plaid's pricing flow surfaces a Committed Revenue field on the minimum-commitment panel. Until this PR, its tooltip read:
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:
2. The formula, before and after
monthlyMin × term
Ramp-aware on the minimum side (per-month rampedValues summed), but nothing else contributes.
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
| File | Change | Lines |
|---|---|---|
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.
Handled via filterUsageProductsNotInCommitmentBasedPricing at the top of the loop — these belong to multi-commitment quotes and are summed elsewhere.
Products with contributesToMonthlyMinimumRevenue: true already feed the MM floor. Including them here would double-count against monthlyMin × term.
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:
| Inputs | Correct | If 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
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.
| Test | Pins | Expected |
|---|---|---|
| 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:
ContractValue()
- Skips commitment-based pricing
- Skips MM-contributing products
- Includes one-time fees
- Powers Total Contract Value
RecurringRevenueOverContract()
- Skips commitment-based pricing
- Skips MM-contributing products
- Excludes one-time fees (extra filter)
- Powers Committed Revenue
9. What this does not change
- TCV math —
getNonMonthlyMinimumContractValueis untouched; one-time fees still flow into TCV. - MM-disabled quotes — committedRevenue still reflects monthly-usage fallback only, no recurring-fee stacking.
- Non-Plaid UI — the field is calculated for all orgs, but only Plaid's
pricingFlowSpec.jsonrenders it today. - Ramp handling on the MM base — preserved verbatim; the change is purely additive (hoisted into a
baseIIFE then augmented). - Per-org gating — there is none. No spec flag, no feature toggle.
10. Risks & open questions
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.
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 };