156 lines
5.0 KiB
TypeScript
Raw Permalink Normal View History

/**
* Cancellation Months Utility
*
* Generates available cancellation months following the "25th rule":
* - If current day is before or on the 25th, current month is available
* - If current day is after the 25th, only next month onwards is available
*
* Used by both SIM and Internet cancellation flows.
*/
/**
* Base cancellation month (value + label)
*/
export interface BaseCancellationMonth {
/** Month in YYYY-MM format */
value: string;
/** Human-readable label (e.g., "January 2025") */
label: string;
}
/**
* Extended cancellation month with runDate (for SIM/Freebit API)
*/
export interface CancellationMonthWithRunDate extends BaseCancellationMonth {
/** Freebit API run date in YYYYMMDD format (1st of next month) */
runDate: string;
}
/**
* Options for generating cancellation months
*/
export interface GenerateCancellationMonthsOptions {
/** Cutoff day of month (default: 25) */
cutoffDay?: number;
/** Number of months to generate (default: 12) */
monthCount?: number;
/** Whether to include runDate for Freebit API (default: false) */
includeRunDate?: boolean;
/** Override "today" for testing */
referenceDate?: Date;
}
/**
* Generate available cancellation months following the 25th rule.
*
* The 25th rule:
* - If submitting before or on the 25th, current month is available for cancellation
* - If submitting after the 25th, only next month onwards is available
*
* @example
* // Basic usage (Internet cancellation)
* const months = generateCancellationMonths();
* // Returns: [{ value: "2025-01", label: "January 2025" }, ...]
*
* @example
* // With runDate for SIM/Freebit API
* const months = generateCancellationMonths({ includeRunDate: true });
* // Returns: [{ value: "2025-01", label: "January 2025", runDate: "20250201" }, ...]
*/
export function generateCancellationMonths(
options?: GenerateCancellationMonthsOptions & { includeRunDate?: false }
): BaseCancellationMonth[];
export function generateCancellationMonths(
options: GenerateCancellationMonthsOptions & { includeRunDate: true }
): CancellationMonthWithRunDate[];
export function generateCancellationMonths(
options: GenerateCancellationMonthsOptions = {}
): BaseCancellationMonth[] | CancellationMonthWithRunDate[] {
const {
cutoffDay = 25,
monthCount = 12,
includeRunDate = false,
referenceDate = new Date(),
} = options;
const months: (BaseCancellationMonth | CancellationMonthWithRunDate)[] = [];
const dayOfMonth = referenceDate.getDate();
// Start from current month if before/on cutoff, otherwise next month
const startOffset = dayOfMonth <= cutoffDay ? 0 : 1;
for (let i = startOffset; i < startOffset + monthCount; i++) {
const date = new Date(referenceDate.getFullYear(), referenceDate.getMonth() + i, 1);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const monthStr = String(month).padStart(2, "0");
const monthEntry: BaseCancellationMonth = {
value: `${year}-${monthStr}`,
label: date.toLocaleDateString("en-US", { month: "long", year: "numeric" }),
};
if (includeRunDate) {
// runDate is the 1st of the NEXT month (cancellation takes effect at month end)
const nextMonth = new Date(year, month, 1);
const runYear = nextMonth.getFullYear();
const runMonth = String(nextMonth.getMonth() + 1).padStart(2, "0");
(monthEntry as CancellationMonthWithRunDate).runDate = `${runYear}${runMonth}01`;
}
months.push(monthEntry);
}
return months;
}
/**
* Calculate the last day of a month for cancellation effective date.
*
* @param yearMonth - Month in YYYY-MM format
* @returns Date string in YYYY-MM-DD format (last day of month)
*
* @example
* getCancellationEffectiveDate("2025-01");
* // Returns: "2025-01-31"
*/
export function getCancellationEffectiveDate(yearMonth: string): string {
const [year, month] = yearMonth.split("-").map(Number);
if (!year || !month) {
throw new Error(`Invalid year-month format: ${yearMonth}. Expected YYYY-MM`);
}
// month is 1-indexed, so new Date(year, month, 0) gives last day of that month
const lastDayOfMonth = new Date(year, month, 0);
return [
lastDayOfMonth.getFullYear(),
String(lastDayOfMonth.getMonth() + 1).padStart(2, "0"),
String(lastDayOfMonth.getDate()).padStart(2, "0"),
].join("-");
}
/**
* Calculate the Freebit API runDate from a cancellation month.
*
* @param yearMonth - Month in YYYY-MM format
* @returns Date string in YYYYMMDD format (1st of next month)
*
* @example
* getRunDateFromMonth("2025-01");
* // Returns: "20250201"
*/
export function getRunDateFromMonth(yearMonth: string): string {
const [year, month] = yearMonth.split("-").map(Number);
if (!year || !month) {
throw new Error(`Invalid year-month format: ${yearMonth}. Expected YYYY-MM`);
}
const nextMonth = new Date(year, month, 1);
const runYear = nextMonth.getFullYear();
const runMonth = String(nextMonth.getMonth() + 1).padStart(2, "0");
return `${runYear}${runMonth}01`;
}