xpeditis2.0/apps/backend/src/domain/entities/rate-quote.entity.ts
David-Henri ARNAUD 1044900e98 feature phase
2025-10-08 16:56:27 +02:00

278 lines
5.9 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],
};
}
}