156 lines
5.0 KiB
TypeScript
156 lines
5.0 KiB
TypeScript
|
|
/**
|
||
|
|
* 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`;
|
||
|
|
}
|