298 lines
6.6 KiB
TypeScript
298 lines
6.6 KiB
TypeScript
/**
|
|
* 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<BookingProps, 'bookingNumber' | 'status' | 'createdAt' | 'updatedAt'> & {
|
|
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<BookingContainer>): 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;
|
|
}
|
|
}
|