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