200 lines
4.5 KiB
TypeScript
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';
|
|
}
|
|
} |