- Implemented FormStep component for user input (name, email, address). - Created OtpStep component for OTP verification. - Developed SuccessStep component to display success messages based on account creation. - Introduced eligibility-check.store for managing state throughout the eligibility check process. - Added commitlint configuration for standardized commit messages. - Configured knip for workspace management and project structure.
382 lines
11 KiB
TypeScript
382 lines
11 KiB
TypeScript
/**
|
|
* 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 || !match[1] || !match[2]) return null;
|
|
return {
|
|
year: Number.parseInt(match[1], 10),
|
|
month: Number.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";
|
|
}
|