import { PortCode } from '../value-objects/port-code.vo'; import { ContainerType } from '../value-objects/container-type.vo'; import { DateRange } from '../value-objects/date-range.vo'; export type DgSurchargeValue = number | 'ON REQUEST' | 'NOT ACCEPTED'; export type HandlingUnit = 'W' | 'UP'; // W = tonne revenue (max CBM/T), UP = per CBM export type FrequencyType = 'Weekly' | 'Bi-Weekly' | 'Bi-Monthly' | 'Monthly'; export interface FreightPricing { freightCurrency: string; freightRatePerCBM: number; // 0.0 = included/to negotiate freightMinimum: number; } export interface FobCharges { fobCurrency: string; fobDocumentation: number; fobISPS: number; fobHandling: number; fobHandlingUnit: HandlingUnit; fobHandlingMinimum: number; fobSolas: number; fobCustoms: number; fobAMS_ACI: number; fobISF5: number; fobDGAdmin: number; // Only if DG shipment } export interface DgSurchargeInfo { dgSurchargeCurrency: string; dgSurchargeRate: DgSurchargeValue; dgSurchargeUnit: 'UP' | 'LS' | '%'; // per CBM, lump sum, or percentage dgSurchargeMin: DgSurchargeValue; } /** * CsvRate — Shipping rate from a consolidator CSV file. * * Business Rules: * - Route matching uses originCode + destinationCode (UN/LOCODE) * - Price = max(freightRatePerCBM×V, freightMinimum) + FOB fixed + handling * - FOB and freight may be in different currencies * - DG surcharge applies only when hasDangerousGoods = true */ export class CsvRate { constructor( // Supplier identity public readonly companyName: string, public readonly companyEmail: string, // Route geography public readonly originCFS: string, public readonly originCode: PortCode, public readonly portOfLoading: string, public readonly routing: string, public readonly destinationCFS: string, public readonly destinationCode: PortCode, public readonly destinationCountry: string, // Container public readonly containerType: ContainerType, // Pricing public readonly freight: FreightPricing, public readonly fob: FobCharges, public readonly dgSurcharge: DgSurchargeInfo, // Metadata public readonly remarks: string, public readonly frequency: FrequencyType, public readonly transitDays: number, public readonly validity: DateRange ) { this.validate(); } private validate(): void { if (!this.companyName?.trim()) throw new Error('Company name is required'); if (!this.companyEmail?.trim()) throw new Error('Company email is required'); if (this.transitDays <= 0) throw new Error('Transit days must be positive'); if (this.freight.freightMinimum < 0) throw new Error('Freight minimum cannot be negative'); if (this.fob.fobHandling < 0) throw new Error('FOB handling cannot be negative'); } isValidForDate(date: Date): boolean { return this.validity.contains(date); } isCurrentlyValid(): boolean { return this.validity.isCurrentRange(); } matchesRoute(origin: PortCode, destination: PortCode): boolean { return this.originCode.equals(origin) && this.destinationCode.equals(destination); } isDgAccepted(): boolean { return this.dgSurcharge.dgSurchargeRate !== 'NOT ACCEPTED'; } isDgOnRequest(): boolean { return this.dgSurcharge.dgSurchargeRate === 'ON REQUEST'; } isDirectRoute(): boolean { return this.routing.trim().toLowerCase() === 'direct'; } getFrequencyScore(): number { switch (this.frequency) { case 'Weekly': return 4; case 'Bi-Weekly': return 3; case 'Bi-Monthly': return 2; case 'Monthly': return 1; default: return 2; } } getRouteDescription(): string { return `${this.originCode.getValue()} → ${this.destinationCode.getValue()}`; } getSummary(): string { return `${this.companyName}: ${this.getRouteDescription()} (${this.containerType.getValue()})`; } toString(): string { return this.getSummary(); } }