278 lines
6.2 KiB
TypeScript
278 lines
6.2 KiB
TypeScript
/**
|
|
* 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<RateQuoteProps, 'id' | 'validUntil' | 'createdAt' | 'updatedAt'> & { 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],
|
|
};
|
|
}
|
|
}
|