/** * 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`; }