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

206 lines
4.9 KiB
TypeScript

/**
* 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<PortProps, 'createdAt' | 'updatedAt'>): 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 },
};
}
}