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