Functional Weave
Code in TypeScript

subscriptions.next-billing-date@1.0.0

impl/typescript.ts

2,087 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 { addDays, epochDayFromIso, parseIsoDate } from "./dates_add_days.ts";  ← from dates.add-days ^1.0.0 · built alongside by fune
import { addMonths } from "./dates_add_months.ts";  ← from dates.add-months ^1.0.0 · built alongside by fune
import { type BillingInterval } from "./subscriptions_next_billing_date_types.ts";

const MONTHS = new Map<string, number>([["month", 1], ["quarter", 3], ["year", 12]]);
const DAYS = new Map<string, number>([["day", 1], ["week", 7]]);

/**
 * The first billing date strictly after `afterDate`: anchor + k intervals for
 * the smallest k >= 0 that lands after it. Each candidate is computed from the
 * anchor, never from the previous billing date, so the 31st stays on month
 * ends instead of decaying to the 28th after February.
 */
export function nextBillingDate(anchorDate: string, interval: BillingInterval, intervalCount: number, afterDate: string): string {
  if (!Number.isInteger(intervalCount) || intervalCount < 1) {
    throw new RangeError(`interval count must be 1 or more, received ${intervalCount}`);
  }
  if (!MONTHS.has(interval) && !DAYS.has(interval)) {
    throw new RangeError(`unknown billing interval "${interval}": expected day, week, month, quarter or year`);
  }
  const anchor = epochDayFromIso(anchorDate);
  const after = epochDayFromIso(afterDate);
  if (after < anchor) return anchorDate;

  const days = DAYS.get(interval);
  if (days !== undefined) {
    const step = days * intervalCount;
    const k = Math.floor((after - anchor) / step) + 1;
    return addDays(anchorDate, k * step);
  }

  // Jump straight to the period containing afterDate's month; the clamp can
  // only put that candidate on or before afterDate, so at most one or two
  // steps forward are needed from there.
  const step = (MONTHS.get(interval) as number) * intervalCount;
  const a = parseIsoDate(anchorDate);
  const b = parseIsoDate(afterDate);
  const monthsApart = (b.year - a.year) * 12 + (b.month - a.month);
  let k = Math.floor(monthsApart / step);
  let candidate = addMonths(anchorDate, k * step);
  while (epochDayFromIso(candidate) <= after) {
    k += 1;
    candidate = addMonths(anchorDate, k * step);
  }
  return candidate;
}