Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { roundDiv } from "./math_round_div.ts"; ← from math.round-div ^1.0.0 · built alongside by fune
import { type Money, assertSameCurrency } from "./money_amount.ts"; ← from money.amount ^1.0.0 · built alongside by fune
import { type ChurnMetrics } from "./subscriptions_churn_types.ts";
function rate(numerator: number, denominator: number): number | null {
return denominator === 0 ? null : roundDiv(numerator * 10000, denominator, "half-up");
}
/**
* Logo churn, gross and net revenue churn, and gross and net revenue
* retention for one period, each as basis points of the starting cohort.
*/
export function churnMetrics(
customersAtStart: number,
customersLost: number,
mrrAtStart: Money,
expansionMrr: Money,
contractionMrr: Money,
churnedMrr: Money,
): ChurnMetrics {
if (!Number.isInteger(customersAtStart) || customersAtStart < 0 || !Number.isInteger(customersLost) || customersLost < 0) {
throw new RangeError(`customer counts must be whole numbers of 0 or more, received ${customersAtStart} and ${customersLost}`);
}
if (customersLost > customersAtStart) {
throw new RangeError(`customers lost must not exceed customers at start, received ${customersLost} of ${customersAtStart}`);
}
for (const amount of [expansionMrr, contractionMrr, churnedMrr]) assertSameCurrency(mrrAtStart, amount);
const start = mrrAtStart.minor;
const expansion = expansionMrr.minor;
const lost = contractionMrr.minor + churnedMrr.minor;
if (start < 0 || expansion < 0 || contractionMrr.minor < 0 || churnedMrr.minor < 0) {
throw new RangeError("MRR amounts must not be negative");
}
if (lost > start) {
throw new RangeError(`contraction and churned MRR must not exceed MRR at start, received ${lost} of ${start}`);
}
return {
logoChurnBasisPoints: rate(customersLost, customersAtStart),
grossRevenueChurnBasisPoints: rate(lost, start),
netRevenueChurnBasisPoints: rate(lost - expansion, start),
grossRevenueRetentionBasisPoints: rate(start - lost, start),
netRevenueRetentionBasisPoints: rate(start + expansion - lost, start),
};
}