/** * Booking Entity * * Represents a freight booking * * Business Rules: * - Must have valid rate quote * - Shipper and consignee are required * - Status transitions must follow allowed paths * - Containers can be added/updated until confirmed * - Cannot modify confirmed bookings (except status) */ import { BookingNumber } from '../value-objects/booking-number.vo'; import { BookingStatus } from '../value-objects/booking-status.vo'; export interface Address { street: string; city: string; postalCode: string; country: string; } export interface Party { name: string; address: Address; contactName: string; contactEmail: string; contactPhone: string; } export interface BookingContainer { id: string; type: string; containerNumber?: string; vgm?: number; // Verified Gross Mass in kg temperature?: number; // For reefer containers sealNumber?: string; } export interface BookingProps { id: string; bookingNumber: BookingNumber; userId: string; organizationId: string; rateQuoteId: string; status: BookingStatus; shipper: Party; consignee: Party; cargoDescription: string; containers: BookingContainer[]; specialInstructions?: string; createdAt: Date; updatedAt: Date; } export class Booking { private readonly props: BookingProps; private constructor(props: BookingProps) { this.props = props; } /** * Factory method to create a new Booking */ static create( props: Omit & { id: string; bookingNumber?: BookingNumber; status?: BookingStatus; } ): Booking { const now = new Date(); const bookingProps: BookingProps = { ...props, bookingNumber: props.bookingNumber || BookingNumber.generate(), status: props.status || BookingStatus.create('draft'), createdAt: now, updatedAt: now, }; // Validate business rules Booking.validate(bookingProps); return new Booking(bookingProps); } /** * Validate business rules */ private static validate(props: BookingProps): void { if (!props.userId) { throw new Error('User ID is required'); } if (!props.organizationId) { throw new Error('Organization ID is required'); } if (!props.rateQuoteId) { throw new Error('Rate quote ID is required'); } if (!props.shipper || !props.shipper.name) { throw new Error('Shipper information is required'); } if (!props.consignee || !props.consignee.name) { throw new Error('Consignee information is required'); } if (!props.cargoDescription || props.cargoDescription.length < 10) { throw new Error('Cargo description must be at least 10 characters'); } } // Getters get id(): string { return this.props.id; } get bookingNumber(): BookingNumber { return this.props.bookingNumber; } get userId(): string { return this.props.userId; } get organizationId(): string { return this.props.organizationId; } get rateQuoteId(): string { return this.props.rateQuoteId; } get status(): BookingStatus { return this.props.status; } get shipper(): Party { return { ...this.props.shipper }; } get consignee(): Party { return { ...this.props.consignee }; } get cargoDescription(): string { return this.props.cargoDescription; } get containers(): BookingContainer[] { return [...this.props.containers]; } get specialInstructions(): string | undefined { return this.props.specialInstructions; } get createdAt(): Date { return this.props.createdAt; } get updatedAt(): Date { return this.props.updatedAt; } /** * Update booking status */ updateStatus(newStatus: BookingStatus): Booking { if (!this.status.canTransitionTo(newStatus)) { throw new Error(`Cannot transition from ${this.status.value} to ${newStatus.value}`); } return new Booking({ ...this.props, status: newStatus, updatedAt: new Date(), }); } /** * Add container to booking */ addContainer(container: BookingContainer): Booking { if (!this.status.canBeModified()) { throw new Error('Cannot modify containers after booking is confirmed'); } return new Booking({ ...this.props, containers: [...this.props.containers, container], updatedAt: new Date(), }); } /** * Update container information */ updateContainer(containerId: string, updates: Partial): Booking { if (!this.status.canBeModified()) { throw new Error('Cannot modify containers after booking is confirmed'); } const containerIndex = this.props.containers.findIndex(c => c.id === containerId); if (containerIndex === -1) { throw new Error(`Container ${containerId} not found`); } const updatedContainers = [...this.props.containers]; updatedContainers[containerIndex] = { ...updatedContainers[containerIndex], ...updates, }; return new Booking({ ...this.props, containers: updatedContainers, updatedAt: new Date(), }); } /** * Remove container from booking */ removeContainer(containerId: string): Booking { if (!this.status.canBeModified()) { throw new Error('Cannot modify containers after booking is confirmed'); } return new Booking({ ...this.props, containers: this.props.containers.filter(c => c.id !== containerId), updatedAt: new Date(), }); } /** * Update cargo description */ updateCargoDescription(description: string): Booking { if (!this.status.canBeModified()) { throw new Error('Cannot modify cargo description after booking is confirmed'); } if (description.length < 10) { throw new Error('Cargo description must be at least 10 characters'); } return new Booking({ ...this.props, cargoDescription: description, updatedAt: new Date(), }); } /** * Update special instructions */ updateSpecialInstructions(instructions: string): Booking { return new Booking({ ...this.props, specialInstructions: instructions, updatedAt: new Date(), }); } /** * Check if booking can be cancelled */ canBeCancelled(): boolean { return !this.status.isFinal(); } /** * Cancel booking */ cancel(): Booking { if (!this.canBeCancelled()) { throw new Error('Cannot cancel booking in final state'); } return this.updateStatus(BookingStatus.create('cancelled')); } /** * Equality check */ equals(other: Booking): boolean { return this.id === other.id; } }