102 lines
2.6 KiB
TypeScript
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();
|
|
};
|