/** * 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'; } }