/** * Port Entity * * Represents a maritime port (based on UN/LOCODE standard) * * Business Rules: * - Port code must follow UN/LOCODE format (2-letter country + 3-letter location) * - Coordinates must be valid latitude/longitude */ export interface PortCoordinates { latitude: number; longitude: number; } export interface PortProps { id: string; code: string; // UN/LOCODE (e.g., 'NLRTM' for Rotterdam) name: string; // Port name city: string; country: string; // ISO 3166-1 alpha-2 country code countryName: string; // Full country name coordinates: PortCoordinates; timezone?: string; // IANA timezone (e.g., 'Europe/Amsterdam') isActive: boolean; createdAt: Date; updatedAt: Date; } export class Port { private readonly props: PortProps; private constructor(props: PortProps) { this.props = props; } /** * Factory method to create a new Port */ static create(props: Omit): Port { const now = new Date(); // Validate UN/LOCODE format if (!Port.isValidUNLOCODE(props.code)) { throw new Error('Invalid port code format. Must follow UN/LOCODE format (e.g., NLRTM).'); } // Validate country code if (!Port.isValidCountryCode(props.country)) { throw new Error('Invalid country code. Must be ISO 3166-1 alpha-2 format (e.g., NL).'); } // Validate coordinates if (!Port.isValidCoordinates(props.coordinates)) { throw new Error('Invalid coordinates.'); } return new Port({ ...props, createdAt: now, updatedAt: now, }); } /** * Factory method to reconstitute from persistence */ static fromPersistence(props: PortProps): Port { return new Port(props); } /** * Validate UN/LOCODE format (5 characters: 2-letter country code + 3-letter location code) */ private static isValidUNLOCODE(code: string): boolean { const unlocodePattern = /^[A-Z]{2}[A-Z0-9]{3}$/; return unlocodePattern.test(code); } /** * Validate ISO 3166-1 alpha-2 country code */ private static isValidCountryCode(code: string): boolean { const countryCodePattern = /^[A-Z]{2}$/; return countryCodePattern.test(code); } /** * Validate coordinates */ private static isValidCoordinates(coords: PortCoordinates): boolean { const { latitude, longitude } = coords; return latitude >= -90 && latitude <= 90 && longitude >= -180 && longitude <= 180; } // Getters get id(): string { return this.props.id; } get code(): string { return this.props.code; } get name(): string { return this.props.name; } get city(): string { return this.props.city; } get country(): string { return this.props.country; } get countryName(): string { return this.props.countryName; } get coordinates(): PortCoordinates { return { ...this.props.coordinates }; } get timezone(): string | undefined { return this.props.timezone; } get isActive(): boolean { return this.props.isActive; } get createdAt(): Date { return this.props.createdAt; } get updatedAt(): Date { return this.props.updatedAt; } // Business methods /** * Get display name (e.g., "Rotterdam, Netherlands (NLRTM)") */ getDisplayName(): string { return `${this.props.name}, ${this.props.countryName} (${this.props.code})`; } /** * Calculate distance to another port (Haversine formula) * Returns distance in kilometers */ distanceTo(otherPort: Port): number { const R = 6371; // Earth's radius in kilometers const lat1 = this.toRadians(this.props.coordinates.latitude); const lat2 = this.toRadians(otherPort.coordinates.latitude); const deltaLat = this.toRadians( otherPort.coordinates.latitude - this.props.coordinates.latitude ); const deltaLon = this.toRadians( otherPort.coordinates.longitude - this.props.coordinates.longitude ); const a = Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) + Math.cos(lat1) * Math.cos(lat2) * Math.sin(deltaLon / 2) * Math.sin(deltaLon / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; } private toRadians(degrees: number): number { return degrees * (Math.PI / 180); } updateCoordinates(coordinates: PortCoordinates): void { if (!Port.isValidCoordinates(coordinates)) { throw new Error('Invalid coordinates.'); } this.props.coordinates = { ...coordinates }; this.props.updatedAt = new Date(); } updateTimezone(timezone: string): void { this.props.timezone = timezone; this.props.updatedAt = new Date(); } deactivate(): void { this.props.isActive = false; this.props.updatedAt = new Date(); } activate(): void { this.props.isActive = true; this.props.updatedAt = new Date(); } /** * Convert to plain object for persistence */ toObject(): PortProps { return { ...this.props, coordinates: { ...this.props.coordinates }, }; } }