Functional Weave
Code in Rust

subscriptions.churn@1.0.0

impl/typescript.ts

1,999 bytes · the TypeScript implementation · view raw

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),
  };
}