2025-12-22 18:59:38 +09:00
/ * *
* 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 ( 10 th 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])$/ ) ;
2026-01-15 11:28:25 +09:00
if ( ! match || ! match [ 1 ] || ! match [ 2 ] ) return null ;
2025-12-22 18:59:38 +09:00
return {
2026-01-15 11:28:25 +09:00
year : Number.parseInt ( match [ 1 ] , 10 ) ,
month : Number.parseInt ( match [ 2 ] , 10 ) ,
2025-12-22 18:59:38 +09:00
} ;
}
/ * *
* 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 25 th 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 25 th , 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 ( 10 th 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" ;
}