Files
gridpilot.gg/packages/racing/domain/entities/Race.ts
2025-12-08 23:52:36 +01:00

200 lines
4.5 KiB
TypeScript

/**
* Domain Entity: Race
*
* Represents a race/session in the GridPilot platform.
* Immutable entity with factory methods and domain validation.
*/
export type SessionType = 'practice' | 'qualifying' | 'race';
export type RaceStatus = 'scheduled' | 'running' | 'completed' | 'cancelled';
export class Race {
readonly id: string;
readonly leagueId: string;
readonly scheduledAt: Date;
readonly track: string;
readonly trackId?: string;
readonly car: string;
readonly carId?: string;
readonly sessionType: SessionType;
readonly status: RaceStatus;
readonly strengthOfField?: number;
readonly registeredCount?: number;
readonly maxParticipants?: number;
private constructor(props: {
id: string;
leagueId: string;
scheduledAt: Date;
track: string;
trackId?: string;
car: string;
carId?: string;
sessionType: SessionType;
status: RaceStatus;
strengthOfField?: number;
registeredCount?: number;
maxParticipants?: number;
}) {
this.id = props.id;
this.leagueId = props.leagueId;
this.scheduledAt = props.scheduledAt;
this.track = props.track;
this.trackId = props.trackId;
this.car = props.car;
this.carId = props.carId;
this.sessionType = props.sessionType;
this.status = props.status;
this.strengthOfField = props.strengthOfField;
this.registeredCount = props.registeredCount;
this.maxParticipants = props.maxParticipants;
}
/**
* Factory method to create a new Race entity
*/
static create(props: {
id: string;
leagueId: string;
scheduledAt: Date;
track: string;
trackId?: string;
car: string;
carId?: string;
sessionType?: SessionType;
status?: RaceStatus;
strengthOfField?: number;
registeredCount?: number;
maxParticipants?: number;
}): Race {
this.validate(props);
return new Race({
id: props.id,
leagueId: props.leagueId,
scheduledAt: props.scheduledAt,
track: props.track,
trackId: props.trackId,
car: props.car,
carId: props.carId,
sessionType: props.sessionType ?? 'race',
status: props.status ?? 'scheduled',
strengthOfField: props.strengthOfField,
registeredCount: props.registeredCount,
maxParticipants: props.maxParticipants,
});
}
/**
* Domain validation logic
*/
private static validate(props: {
id: string;
leagueId: string;
scheduledAt: Date;
track: string;
car: string;
}): void {
if (!props.id || props.id.trim().length === 0) {
throw new Error('Race ID is required');
}
if (!props.leagueId || props.leagueId.trim().length === 0) {
throw new Error('League ID is required');
}
if (!props.scheduledAt || !(props.scheduledAt instanceof Date)) {
throw new Error('Valid scheduled date is required');
}
if (!props.track || props.track.trim().length === 0) {
throw new Error('Track is required');
}
if (!props.car || props.car.trim().length === 0) {
throw new Error('Car is required');
}
}
/**
* Start the race (move from scheduled to running)
*/
start(): Race {
if (this.status !== 'scheduled') {
throw new Error('Only scheduled races can be started');
}
return new Race({
...this,
status: 'running',
});
}
/**
* Mark race as completed
*/
complete(): Race {
if (this.status === 'completed') {
throw new Error('Race is already completed');
}
if (this.status === 'cancelled') {
throw new Error('Cannot complete a cancelled race');
}
return new Race({
...this,
status: 'completed',
});
}
/**
* Cancel the race
*/
cancel(): Race {
if (this.status === 'completed') {
throw new Error('Cannot cancel a completed race');
}
if (this.status === 'cancelled') {
throw new Error('Race is already cancelled');
}
return new Race({
...this,
status: 'cancelled',
});
}
/**
* Update SOF and participant count
*/
updateField(strengthOfField: number, registeredCount: number): Race {
return new Race({
...this,
strengthOfField,
registeredCount,
});
}
/**
* Check if race is in the past
*/
isPast(): boolean {
return this.scheduledAt < new Date();
}
/**
* Check if race is upcoming
*/
isUpcoming(): boolean {
return this.status === 'scheduled' && !this.isPast();
}
/**
* Check if race is live/running
*/
isLive(): boolean {
return this.status === 'running';
}
}