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

300 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;
}
}