/** * RateQuote Entity * * Represents a shipping rate quote from a carrier * * Business Rules: * - Price must be positive * - ETA must be after ETD * - Transit days must be positive * - Rate quotes expire after 15 minutes (cache TTL) * - Availability must be between 0 and actual capacity */ export interface RouteSegment { portCode: string; portName: string; arrival?: Date; departure?: Date; vesselName?: string; voyageNumber?: string; } export interface Surcharge { type: string; // e.g., 'BAF', 'CAF', 'THC', 'ISPS' description: string; amount: number; currency: string; } export interface PriceBreakdown { baseFreight: number; surcharges: Surcharge[]; totalAmount: number; currency: string; } export interface RateQuoteProps { id: string; carrierId: string; carrierName: string; carrierCode: string; origin: { code: string; name: string; country: string; }; destination: { code: string; name: string; country: string; }; pricing: PriceBreakdown; containerType: string; // e.g., '20DRY', '40HC', '40REEFER' mode: 'FCL' | 'LCL'; etd: Date; // Estimated Time of Departure eta: Date; // Estimated Time of Arrival transitDays: number; route: RouteSegment[]; availability: number; // Available container slots frequency: string; // e.g., 'Weekly', 'Bi-weekly' vesselType?: string; // e.g., 'Container Ship', 'Ro-Ro' co2EmissionsKg?: number; // CO2 emissions in kg validUntil: Date; // When this quote expires (typically createdAt + 15 min) createdAt: Date; updatedAt: Date; } export class RateQuote { private readonly props: RateQuoteProps; private constructor(props: RateQuoteProps) { this.props = props; } /** * Factory method to create a new RateQuote */ static create( props: Omit & { id: string } ): RateQuote { const now = new Date(); const validUntil = new Date(now.getTime() + 15 * 60 * 1000); // 15 minutes // Validate pricing if (props.pricing.totalAmount <= 0) { throw new Error('Total price must be positive.'); } if (props.pricing.baseFreight <= 0) { throw new Error('Base freight must be positive.'); } // Validate dates if (props.eta <= props.etd) { throw new Error('ETA must be after ETD.'); } // Validate transit days if (props.transitDays <= 0) { throw new Error('Transit days must be positive.'); } // Validate availability if (props.availability < 0) { throw new Error('Availability cannot be negative.'); } // Validate route has at least origin and destination if (props.route.length < 2) { throw new Error('Route must have at least origin and destination ports.'); } return new RateQuote({ ...props, validUntil, createdAt: now, updatedAt: now, }); } /** * Factory method to reconstitute from persistence */ static fromPersistence(props: RateQuoteProps): RateQuote { return new RateQuote(props); } // Getters get id(): string { return this.props.id; } get carrierId(): string { return this.props.carrierId; } get carrierName(): string { return this.props.carrierName; } get carrierCode(): string { return this.props.carrierCode; } get origin(): { code: string; name: string; country: string } { return { ...this.props.origin }; } get destination(): { code: string; name: string; country: string } { return { ...this.props.destination }; } get pricing(): PriceBreakdown { return { ...this.props.pricing, surcharges: [...this.props.pricing.surcharges], }; } get containerType(): string { return this.props.containerType; } get mode(): 'FCL' | 'LCL' { return this.props.mode; } get etd(): Date { return this.props.etd; } get eta(): Date { return this.props.eta; } get transitDays(): number { return this.props.transitDays; } get route(): RouteSegment[] { return [...this.props.route]; } get availability(): number { return this.props.availability; } get frequency(): string { return this.props.frequency; } get vesselType(): string | undefined { return this.props.vesselType; } get co2EmissionsKg(): number | undefined { return this.props.co2EmissionsKg; } get validUntil(): Date { return this.props.validUntil; } get createdAt(): Date { return this.props.createdAt; } get updatedAt(): Date { return this.props.updatedAt; } // Business methods /** * Check if the rate quote is still valid (not expired) */ isValid(): boolean { return new Date() < this.props.validUntil; } /** * Check if the rate quote has expired */ isExpired(): boolean { return new Date() >= this.props.validUntil; } /** * Check if containers are available */ hasAvailability(): boolean { return this.props.availability > 0; } /** * Get total surcharges amount */ getTotalSurcharges(): number { return this.props.pricing.surcharges.reduce((sum, surcharge) => sum + surcharge.amount, 0); } /** * Get number of transshipments (route segments minus 2 for origin and destination) */ getTransshipmentCount(): number { return Math.max(0, this.props.route.length - 2); } /** * Check if this is a direct route (no transshipments) */ isDirectRoute(): boolean { return this.getTransshipmentCount() === 0; } /** * Get price per day (for comparison) */ getPricePerDay(): number { return this.props.pricing.totalAmount / this.props.transitDays; } /** * Convert to plain object for persistence */ toObject(): RateQuoteProps { return { ...this.props, origin: { ...this.props.origin }, destination: { ...this.props.destination }, pricing: { ...this.props.pricing, surcharges: [...this.props.pricing.surcharges], }, route: [...this.props.route], }; } }