/** * Opportunity Domain - Helpers * * Utility functions for cancellation date calculations and validation. * Implements the "25th rule" and rental return deadline logic. */ import { CANCELLATION_DEADLINE_DAY, RENTAL_RETURN_DEADLINE_DAY, CANCELLATION_NOTICE, LINE_RETURN_STATUS, type CancellationFormData, type CancellationOpportunityData, type CancellationEligibility, type CancellationMonthOption, } from "./contract.js"; // ============================================================================ // Date Utilities // ============================================================================ /** * Get the last day of a month * @param year - Full year (e.g., 2025) * @param month - Month (1-12) * @returns Date string in YYYY-MM-DD format */ export function getLastDayOfMonth(year: number, month: number): string { // Day 0 of next month = last day of current month const lastDay = new Date(year, month, 0).getDate(); const monthStr = String(month).padStart(2, "0"); const dayStr = String(lastDay).padStart(2, "0"); return `${year}-${monthStr}-${dayStr}`; } /** * Get the rental return deadline (10th of following month) * @param year - Year of cancellation month * @param month - Month of cancellation (1-12) * @returns Date string in YYYY-MM-DD format */ export function getRentalReturnDeadline(year: number, month: number): string { // Move to next month let nextYear = year; let nextMonth = month + 1; if (nextMonth > 12) { nextMonth = 1; nextYear += 1; } const monthStr = String(nextMonth).padStart(2, "0"); const dayStr = String(RENTAL_RETURN_DEADLINE_DAY).padStart(2, "0"); return `${nextYear}-${monthStr}-${dayStr}`; } /** * Parse YYYY-MM format to year and month */ export function parseYearMonth(value: string): { year: number; month: number } | null { const match = value.match(/^(\d{4})-(0[1-9]|1[0-2])$/); if (!match) return null; return { year: parseInt(match[1], 10), month: parseInt(match[2], 10), }; } /** * Format a date as YYYY-MM */ export function formatYearMonth(date: Date): string { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); return `${year}-${month}`; } /** * Format a month for display (e.g., "January 2025") */ export function formatMonthLabel(year: number, month: number): string { const date = new Date(year, month - 1, 1); return date.toLocaleDateString("en-US", { year: "numeric", month: "long" }); } /** * Format a date for display (e.g., "January 31, 2025") */ export function formatDateLabel(dateStr: string): string { const date = new Date(dateStr); return date.toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric", }); } // ============================================================================ // Cancellation Deadline Logic // ============================================================================ /** * Check if current date is before the cancellation deadline for a given month * * Rule: Cancellation form must be received by the 25th of the cancellation month * * @param cancellationYear - Year of cancellation * @param cancellationMonth - Month of cancellation (1-12) * @param today - Current date (for testing, defaults to now) * @returns true if can still cancel for this month */ export function isBeforeCancellationDeadline( cancellationYear: number, cancellationMonth: number, today: Date = new Date() ): boolean { const deadlineDate = new Date( cancellationYear, cancellationMonth - 1, CANCELLATION_DEADLINE_DAY, 23, 59, 59 ); return today <= deadlineDate; } /** * Get the cancellation deadline date for a month * * @param year - Year * @param month - Month (1-12) * @returns Date string in YYYY-MM-DD format */ export function getCancellationDeadline(year: number, month: number): string { const monthStr = String(month).padStart(2, "0"); const dayStr = String(CANCELLATION_DEADLINE_DAY).padStart(2, "0"); return `${year}-${monthStr}-${dayStr}`; } /** * Calculate the earliest month that can be selected for cancellation * * If today is on or after the 25th, earliest is next month. * Otherwise, earliest is current month. * * @param today - Current date (for testing, defaults to now) * @returns YYYY-MM format string */ export function getEarliestCancellationMonth(today: Date = new Date()): string { const year = today.getFullYear(); const month = today.getMonth() + 1; // 1-indexed const day = today.getDate(); if (day > CANCELLATION_DEADLINE_DAY) { // After 25th - earliest is next month let nextMonth = month + 1; let nextYear = year; if (nextMonth > 12) { nextMonth = 1; nextYear += 1; } return `${nextYear}-${String(nextMonth).padStart(2, "0")}`; } else { // On or before 25th - can cancel this month return `${year}-${String(month).padStart(2, "0")}`; } } /** * Generate available cancellation months * * Returns 12 months starting from the earliest available month. * * @param today - Current date (for testing) * @returns Array of month options */ export function generateCancellationMonthOptions( today: Date = new Date() ): CancellationMonthOption[] { const earliestMonth = getEarliestCancellationMonth(today); const parsed = parseYearMonth(earliestMonth); if (!parsed) return []; let { year, month } = parsed; const currentYearMonth = formatYearMonth(today); const options: CancellationMonthOption[] = []; for (let i = 0; i < 12; i++) { const value = `${year}-${String(month).padStart(2, "0")}`; const serviceEndDate = getLastDayOfMonth(year, month); const rentalReturnDeadline = getRentalReturnDeadline(year, month); const isCurrentMonth = value === currentYearMonth; options.push({ value, label: formatMonthLabel(year, month), serviceEndDate, rentalReturnDeadline, isCurrentMonth, }); // Move to next month month += 1; if (month > 12) { month = 1; year += 1; } } return options; } /** * Get full cancellation eligibility information * * @param today - Current date (for testing) * @returns Cancellation eligibility details */ export function getCancellationEligibility(today: Date = new Date()): CancellationEligibility { const availableMonths = generateCancellationMonthOptions(today); const earliestMonth = getEarliestCancellationMonth(today); const day = today.getDate(); // Check if current month is still available const canCancelThisMonth = day <= CANCELLATION_DEADLINE_DAY; const currentMonthDeadline = canCancelThisMonth ? getCancellationDeadline(today.getFullYear(), today.getMonth() + 1) : null; return { canCancel: true, earliestCancellationMonth: earliestMonth, availableMonths, currentMonthDeadline, }; } /** * Validate that a selected cancellation month is allowed * * @param selectedMonth - YYYY-MM format * @param today - Current date (for testing) * @returns Validation result */ export function validateCancellationMonth( selectedMonth: string, today: Date = new Date() ): { valid: boolean; error?: string } { const parsed = parseYearMonth(selectedMonth); if (!parsed) { return { valid: false, error: "Invalid month format" }; } const earliestMonth = getEarliestCancellationMonth(today); const earliestParsed = parseYearMonth(earliestMonth); if (!earliestParsed) { return { valid: false, error: "Unable to determine earliest month" }; } // Compare dates const selectedDate = new Date(parsed.year, parsed.month - 1, 1); const earliestDate = new Date(earliestParsed.year, earliestParsed.month - 1, 1); if (selectedDate < earliestDate) { const deadline = CANCELLATION_DEADLINE_DAY; return { valid: false, error: `Cancellation requests for this month must be submitted by the ${deadline}th. The earliest available month is ${formatMonthLabel(earliestParsed.year, earliestParsed.month)}.`, }; } return { valid: true }; } // ============================================================================ // Data Transformation // ============================================================================ /** * Transform cancellation form data to Opportunity update data * * @param formData - Customer form submission * @returns Data to update on Opportunity */ export function transformCancellationFormToOpportunityData( formData: CancellationFormData ): CancellationOpportunityData { const parsed = parseYearMonth(formData.cancellationMonth); if (!parsed) { throw new Error("Invalid cancellation month format"); } const scheduledCancellationDate = getLastDayOfMonth(parsed.year, parsed.month); // NOTE: alternativeEmail and comments go to the Cancellation Case, not to Opportunity return { scheduledCancellationDate, cancellationNotice: CANCELLATION_NOTICE.RECEIVED, lineReturnStatus: LINE_RETURN_STATUS.NOT_YET, }; } /** * Calculate rental return deadline from scheduled cancellation date * * @param scheduledCancellationDate - End of cancellation month (YYYY-MM-DD) * @returns Rental return deadline (10th of following month) */ export function calculateRentalReturnDeadline(scheduledCancellationDate: string): string { const date = new Date(scheduledCancellationDate); const year = date.getFullYear(); const month = date.getMonth() + 1; // 1-indexed return getRentalReturnDeadline(year, month); } // ============================================================================ // Display Helpers // ============================================================================ /** * Get human-readable status for line return */ export function getLineReturnStatusLabel(status: string | undefined): { label: string; description: string; } { switch (status) { case LINE_RETURN_STATUS.NOT_YET: return { label: "Return Pending", description: "A return kit will be sent to you", }; case LINE_RETURN_STATUS.SENT_KIT: return { label: "Return Kit Sent", description: "Please return equipment using the provided kit", }; case LINE_RETURN_STATUS.PICKUP_SCHEDULED: return { label: "Pickup Scheduled", description: "Equipment pickup has been scheduled", }; case LINE_RETURN_STATUS.RETURNED: case "Returned1": return { label: "Returned", description: "Equipment has been returned successfully", }; case LINE_RETURN_STATUS.NTT_DISPATCH: return { label: "NTT Dispatch", description: "NTT will handle equipment return", }; case LINE_RETURN_STATUS.COMPENSATED: return { label: "Compensated", description: "Compensation fee has been charged for unreturned equipment", }; case LINE_RETURN_STATUS.NA: return { label: "Not Applicable", description: "No rental equipment to return", }; default: return { label: "Unknown", description: "", }; } } /** * Check if rental equipment is applicable for a product type */ export function hasRentalEquipment(productType: string): boolean { // Internet typically has rental equipment (router, modem) // SIM and VPN do not return productType === "Internet"; }