/** * Container Entity * * Represents a shipping container in a booking * * Business Rules: * - Container number must follow ISO 6346 format (when provided) * - VGM (Verified Gross Mass) is required for export shipments * - Temperature must be within valid range for reefer containers */ export enum ContainerCategory { DRY = 'DRY', REEFER = 'REEFER', OPEN_TOP = 'OPEN_TOP', FLAT_RACK = 'FLAT_RACK', TANK = 'TANK', } export enum ContainerSize { TWENTY = '20', FORTY = '40', FORTY_FIVE = '45', } export enum ContainerHeight { STANDARD = 'STANDARD', HIGH_CUBE = 'HIGH_CUBE', } export interface ContainerProps { id: string; bookingId?: string; // Optional until container is assigned to a booking type: string; // e.g., '20DRY', '40HC', '40REEFER' category: ContainerCategory; size: ContainerSize; height: ContainerHeight; containerNumber?: string; // ISO 6346 format (assigned by carrier) sealNumber?: string; vgm?: number; // Verified Gross Mass in kg tareWeight?: number; // Empty container weight in kg maxGrossWeight?: number; // Maximum gross weight in kg temperature?: number; // For reefer containers (°C) humidity?: number; // For reefer containers (%) ventilation?: string; // For reefer containers isHazmat: boolean; imoClass?: string; // IMO hazmat class (if hazmat) cargoDescription?: string; createdAt: Date; updatedAt: Date; } export class Container { private readonly props: ContainerProps; private constructor(props: ContainerProps) { this.props = props; } /** * Factory method to create a new Container */ static create(props: Omit): Container { const now = new Date(); // Validate container number format if provided if (props.containerNumber && !Container.isValidContainerNumber(props.containerNumber)) { throw new Error('Invalid container number format. Must follow ISO 6346 standard.'); } // Validate VGM if provided if (props.vgm !== undefined && props.vgm <= 0) { throw new Error('VGM must be positive.'); } // Validate temperature for reefer containers if (props.category === ContainerCategory.REEFER) { if (props.temperature === undefined) { throw new Error('Temperature is required for reefer containers.'); } if (props.temperature < -40 || props.temperature > 40) { throw new Error('Temperature must be between -40°C and +40°C.'); } } // Validate hazmat if (props.isHazmat && !props.imoClass) { throw new Error('IMO class is required for hazmat containers.'); } return new Container({ ...props, createdAt: now, updatedAt: now, }); } /** * Factory method to reconstitute from persistence */ static fromPersistence(props: ContainerProps): Container { return new Container(props); } /** * Validate ISO 6346 container number format * Format: 4 letters (owner code) + 6 digits + 1 check digit * Example: MSCU1234567 */ private static isValidContainerNumber(containerNumber: string): boolean { const pattern = /^[A-Z]{4}\d{7}$/; if (!pattern.test(containerNumber)) { return false; } // Validate check digit (ISO 6346 algorithm) const ownerCode = containerNumber.substring(0, 4); const serialNumber = containerNumber.substring(4, 10); const checkDigit = parseInt(containerNumber.substring(10, 11), 10); // Convert letters to numbers (A=10, B=12, C=13, ..., Z=38) const letterValues: { [key: string]: number } = {}; 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('').forEach((letter, index) => { letterValues[letter] = 10 + index + Math.floor(index / 2); }); // Calculate sum let sum = 0; for (let i = 0; i < ownerCode.length; i++) { sum += letterValues[ownerCode[i]] * Math.pow(2, i); } for (let i = 0; i < serialNumber.length; i++) { sum += parseInt(serialNumber[i], 10) * Math.pow(2, i + 4); } // Check digit = sum % 11 (if 10, use 0) const calculatedCheckDigit = sum % 11 === 10 ? 0 : sum % 11; return calculatedCheckDigit === checkDigit; } // Getters get id(): string { return this.props.id; } get bookingId(): string | undefined { return this.props.bookingId; } get type(): string { return this.props.type; } get category(): ContainerCategory { return this.props.category; } get size(): ContainerSize { return this.props.size; } get height(): ContainerHeight { return this.props.height; } get containerNumber(): string | undefined { return this.props.containerNumber; } get sealNumber(): string | undefined { return this.props.sealNumber; } get vgm(): number | undefined { return this.props.vgm; } get tareWeight(): number | undefined { return this.props.tareWeight; } get maxGrossWeight(): number | undefined { return this.props.maxGrossWeight; } get temperature(): number | undefined { return this.props.temperature; } get humidity(): number | undefined { return this.props.humidity; } get ventilation(): string | undefined { return this.props.ventilation; } get isHazmat(): boolean { return this.props.isHazmat; } get imoClass(): string | undefined { return this.props.imoClass; } get cargoDescription(): string | undefined { return this.props.cargoDescription; } get createdAt(): Date { return this.props.createdAt; } get updatedAt(): Date { return this.props.updatedAt; } // Business methods isReefer(): boolean { return this.props.category === ContainerCategory.REEFER; } isDry(): boolean { return this.props.category === ContainerCategory.DRY; } isHighCube(): boolean { return this.props.height === ContainerHeight.HIGH_CUBE; } getTEU(): number { // Twenty-foot Equivalent Unit if (this.props.size === ContainerSize.TWENTY) { return 1; } else if ( this.props.size === ContainerSize.FORTY || this.props.size === ContainerSize.FORTY_FIVE ) { return 2; } return 0; } getPayload(): number | undefined { if (this.props.vgm !== undefined && this.props.tareWeight !== undefined) { return this.props.vgm - this.props.tareWeight; } return undefined; } assignContainerNumber(containerNumber: string): void { if (!Container.isValidContainerNumber(containerNumber)) { throw new Error('Invalid container number format.'); } this.props.containerNumber = containerNumber; this.props.updatedAt = new Date(); } assignSealNumber(sealNumber: string): void { this.props.sealNumber = sealNumber; this.props.updatedAt = new Date(); } setVGM(vgm: number): void { if (vgm <= 0) { throw new Error('VGM must be positive.'); } this.props.vgm = vgm; this.props.updatedAt = new Date(); } setTemperature(temperature: number): void { if (!this.isReefer()) { throw new Error('Cannot set temperature for non-reefer container.'); } if (temperature < -40 || temperature > 40) { throw new Error('Temperature must be between -40°C and +40°C.'); } this.props.temperature = temperature; this.props.updatedAt = new Date(); } setCargoDescription(description: string): void { this.props.cargoDescription = description; this.props.updatedAt = new Date(); } assignToBooking(bookingId: string): void { this.props.bookingId = bookingId; this.props.updatedAt = new Date(); } /** * Convert to plain object for persistence */ toObject(): ContainerProps { return { ...this.props }; } }