Files
cfw-autumn/shared/utils/billingUtils/intervalUtils/intervalArithmetic.ts
2025-12-13 12:18:06 +00:00

102 lines
2.6 KiB
TypeScript

import { UTCDate } from "@date-fns/utc";
import { BillingInterval } from "@models/productModels/intervals/billingInterval";
import { EntInterval } from "@models/productModels/intervals/entitlementInterval";
import {
addDays,
addHours,
addMinutes,
addMonths,
addWeeks,
addYears,
getDate,
} from "date-fns";
// Union type for all intervals
type Interval = BillingInterval | EntInterval;
// Intervals that support end-of-month preservation
const MONTH_BASED_INTERVALS = [
BillingInterval.Month,
BillingInterval.Quarter,
BillingInterval.SemiAnnual,
BillingInterval.Year,
EntInterval.Month,
EntInterval.Quarter,
EntInterval.SemiAnnual,
EntInterval.Year,
] as const;
/**
* Adds an interval to a timestamp.
* For month-based intervals, preserves the anchor day (Stripe-compatible end-of-month behavior).
*
* @param from - Unix timestamp in milliseconds
* @param interval - BillingInterval or EntInterval
* @param intervalCount - Number of intervals to add (default: 1)
* @returns Unix timestamp in milliseconds
*/
export const addInterval = ({
from,
interval,
intervalCount = 1,
}: {
from: number;
interval: Interval;
intervalCount?: number;
}): number => {
const fromDate = new UTCDate(from);
const anchorDay = getDate(fromDate);
const isMonthBased = (MONTH_BASED_INTERVALS as readonly string[]).includes(
interval,
);
let result: UTCDate;
switch (interval) {
// Fine-grained intervals (EntInterval only)
case EntInterval.Minute:
return addMinutes(fromDate, intervalCount).getTime();
case EntInterval.Hour:
return addHours(fromDate, intervalCount).getTime();
case EntInterval.Day:
return addDays(fromDate, intervalCount).getTime();
// Week (no end-of-month handling needed)
case BillingInterval.Week:
case EntInterval.Week:
return addWeeks(fromDate, intervalCount).getTime();
// Month-based intervals (with end-of-month preservation)
case BillingInterval.Month:
case EntInterval.Month:
result = new UTCDate(addMonths(fromDate, intervalCount).getTime());
break;
case BillingInterval.Quarter:
case EntInterval.Quarter:
result = new UTCDate(addMonths(fromDate, 3 * intervalCount).getTime());
break;
case BillingInterval.SemiAnnual:
case EntInterval.SemiAnnual:
result = new UTCDate(addMonths(fromDate, 6 * intervalCount).getTime());
break;
case BillingInterval.Year:
case EntInterval.Year:
result = new UTCDate(addYears(fromDate, intervalCount).getTime());
break;
// Non-recurring intervals
case BillingInterval.OneOff:
case EntInterval.Lifetime:
return from; // No change for non-recurring
default:
throw new Error(`Invalid interval: ${interval}`);
}
return result.getTime();
};