Functional Weave
Code in TypeScript

subscriptions.mrr-arr@1.1.0

impl/typescript.ts

2,317 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 { type Rational, addRational, multiplyRational, rational, rationalToInteger } from "./math_rational.ts";  ← from math.rational ^1.0.0 · built alongside by fune
import { assertSameCurrency, money } from "./money_amount.ts";  ← from money.amount ^1.0.0 · built alongside by fune
import { type BillingInterval } from "./subscriptions_next_billing_date.ts";  ← from subscriptions.next-billing-date ^1.0.0 · built alongside by fune
import { type MrrArr, type RecurringCharge } from "./subscriptions_mrr_arr_types.ts";

// 1.0.0 declared its own BillingInterval, which callers imported from this
// module; it is now subscriptions.next-billing-date's, re-exported so they
// still can.
export type { BillingInterval };

const PER_YEAR = new Map<BillingInterval, number>([["day", 365], ["week", 52], ["month", 12], ["quarter", 4], ["year", 1]]);

/**
 * MRR and ARR from active and past-due subscriptions. Every charge becomes an
 * exact annual amount first, and each total is rounded once at the end, so a
 * list of annual plans does not lose a penny per plan.
 */
export function mrrArr(subscriptions: readonly RecurringCharge[], currency: string): MrrArr {
  const zero = money(0, currency);
  let annual: Rational = rational(0, 1);
  let counted = 0;
  let excluded = 0;
  for (const s of subscriptions) {
    assertSameCurrency(zero, s.amount);
    if (s.amount.minor < 0) {
      throw new RangeError(`recurring amount must not be negative, received ${s.amount.minor}`);
    }
    if (!Number.isInteger(s.intervalCount) || s.intervalCount < 1) {
      throw new RangeError(`interval count must be 1 or more, received ${s.intervalCount}`);
    }
    const perYear = PER_YEAR.get(s.interval);
    if (perYear === undefined) {
      throw new RangeError(`unknown billing interval "${s.interval}": expected day, week, month, quarter or year`);
    }
    if (s.status === "active" || s.status === "past-due") {
      counted += 1;
      annual = addRational(annual, rational(s.amount.minor * perYear, s.intervalCount));
    } else if (s.status === "trialing" || s.status === "paused" || s.status === "canceled") {
      excluded += 1;
    } else {
      throw new RangeError(`unknown subscription status "${s.status}": expected active, past-due, trialing, paused or canceled`);
    }
  }
  return {
    mrr: money(rationalToInteger(multiplyRational(annual, rational(1, 12)), "half-up"), currency),
    arr: money(rationalToInteger(annual, "half-up"), currency),
    counted,
    excluded,
  };
}