/** * DateRange Value Object * * Encapsulates ETD/ETA date range with validation * * Business Rules: * - End date must be after start date * - Dates cannot be in the past (for new shipments) * - Date range is immutable */ export class DateRange { private readonly startDate: Date; private readonly endDate: Date; private constructor(startDate: Date, endDate: Date) { this.startDate = startDate; this.endDate = endDate; } static create(startDate: Date, endDate: Date, allowPastDates = false): DateRange { if (!startDate || !endDate) { throw new Error('Start date and end date are required.'); } if (endDate <= startDate) { throw new Error('End date must be after start date.'); } if (!allowPastDates) { const now = new Date(); now.setHours(0, 0, 0, 0); // Reset time to start of day if (startDate < now) { throw new Error('Start date cannot be in the past.'); } } return new DateRange(new Date(startDate), new Date(endDate)); } /** * Create from ETD and transit days */ static fromTransitDays(etd: Date, transitDays: number): DateRange { if (transitDays <= 0) { throw new Error('Transit days must be positive.'); } const eta = new Date(etd); eta.setDate(eta.getDate() + transitDays); return DateRange.create(etd, eta, true); } getStartDate(): Date { return new Date(this.startDate); } getEndDate(): Date { return new Date(this.endDate); } getDurationInDays(): number { const diffTime = this.endDate.getTime() - this.startDate.getTime(); return Math.ceil(diffTime / (1000 * 60 * 60 * 24)); } getDurationInHours(): number { const diffTime = this.endDate.getTime() - this.startDate.getTime(); return Math.ceil(diffTime / (1000 * 60 * 60)); } contains(date: Date): boolean { return date >= this.startDate && date <= this.endDate; } overlaps(other: DateRange): boolean { return ( this.startDate <= other.endDate && this.endDate >= other.startDate ); } isFutureRange(): boolean { const now = new Date(); return this.startDate > now; } isPastRange(): boolean { const now = new Date(); return this.endDate < now; } isCurrentRange(): boolean { const now = new Date(); return this.contains(now); } equals(other: DateRange): boolean { return ( this.startDate.getTime() === other.startDate.getTime() && this.endDate.getTime() === other.endDate.getTime() ); } toString(): string { return `${this.formatDate(this.startDate)} - ${this.formatDate(this.endDate)}`; } private formatDate(date: Date): string { return date.toISOString().split('T')[0]; } toObject(): { startDate: Date; endDate: Date } { return { startDate: new Date(this.startDate), endDate: new Date(this.endDate), }; } }