/** * Domain Entity: Race * * Represents a race/session in the GridPilot platform. * Immutable entity with factory methods and domain validation. */ import { RacingDomainValidationError, RacingDomainInvariantError } from '../errors/RacingDomainError'; import type { IEntity } from '@gridpilot/shared/domain'; export type SessionType = 'practice' | 'qualifying' | 'race'; export type RaceStatus = 'scheduled' | 'running' | 'completed' | 'cancelled'; export class Race implements IEntity { readonly id: string; readonly leagueId: string; readonly scheduledAt: Date; readonly track: string; readonly trackId: string | undefined; readonly car: string; readonly carId: string | undefined; readonly sessionType: SessionType; readonly status: RaceStatus; readonly strengthOfField: number | undefined; readonly registeredCount: number | undefined; readonly maxParticipants: number | undefined; 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, ...(props.trackId !== undefined ? { trackId: props.trackId } : {}), car: props.car, ...(props.carId !== undefined ? { carId: props.carId } : {}), sessionType: props.sessionType ?? 'race', status: props.status ?? 'scheduled', ...(props.strengthOfField !== undefined ? { strengthOfField: props.strengthOfField } : {}), ...(props.registeredCount !== undefined ? { registeredCount: props.registeredCount } : {}), ...(props.maxParticipants !== undefined ? { 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 RacingDomainValidationError('Race ID is required'); } if (!props.leagueId || props.leagueId.trim().length === 0) { throw new RacingDomainValidationError('League ID is required'); } if (!props.scheduledAt || !(props.scheduledAt instanceof Date)) { throw new RacingDomainValidationError('Valid scheduled date is required'); } if (!props.track || props.track.trim().length === 0) { throw new RacingDomainValidationError('Track is required'); } if (!props.car || props.car.trim().length === 0) { throw new RacingDomainValidationError('Car is required'); } } /** * Start the race (move from scheduled to running) */ start(): Race { if (this.status !== 'scheduled') { throw new RacingDomainInvariantError('Only scheduled races can be started'); } const base = { id: this.id, leagueId: this.leagueId, scheduledAt: this.scheduledAt, track: this.track, car: this.car, sessionType: this.sessionType, status: 'running' as RaceStatus, }; const withTrackId = this.trackId !== undefined ? { ...base, trackId: this.trackId } : base; const withCarId = this.carId !== undefined ? { ...withTrackId, carId: this.carId } : withTrackId; const withSof = this.strengthOfField !== undefined ? { ...withCarId, strengthOfField: this.strengthOfField } : withCarId; const withRegistered = this.registeredCount !== undefined ? { ...withSof, registeredCount: this.registeredCount } : withSof; const props = this.maxParticipants !== undefined ? { ...withRegistered, maxParticipants: this.maxParticipants } : withRegistered; return Race.create(props); } /** * Mark race as completed */ complete(): Race { if (this.status === 'completed') { throw new RacingDomainInvariantError('Race is already completed'); } if (this.status === 'cancelled') { throw new RacingDomainInvariantError('Cannot complete a cancelled race'); } const base = { id: this.id, leagueId: this.leagueId, scheduledAt: this.scheduledAt, track: this.track, car: this.car, sessionType: this.sessionType, status: 'completed' as RaceStatus, }; const withTrackId = this.trackId !== undefined ? { ...base, trackId: this.trackId } : base; const withCarId = this.carId !== undefined ? { ...withTrackId, carId: this.carId } : withTrackId; const withSof = this.strengthOfField !== undefined ? { ...withCarId, strengthOfField: this.strengthOfField } : withCarId; const withRegistered = this.registeredCount !== undefined ? { ...withSof, registeredCount: this.registeredCount } : withSof; const props = this.maxParticipants !== undefined ? { ...withRegistered, maxParticipants: this.maxParticipants } : withRegistered; return Race.create(props); } /** * Cancel the race */ cancel(): Race { if (this.status === 'completed') { throw new RacingDomainInvariantError('Cannot cancel a completed race'); } if (this.status === 'cancelled') { throw new RacingDomainInvariantError('Race is already cancelled'); } const base = { id: this.id, leagueId: this.leagueId, scheduledAt: this.scheduledAt, track: this.track, car: this.car, sessionType: this.sessionType, status: 'cancelled' as RaceStatus, }; const withTrackId = this.trackId !== undefined ? { ...base, trackId: this.trackId } : base; const withCarId = this.carId !== undefined ? { ...withTrackId, carId: this.carId } : withTrackId; const withSof = this.strengthOfField !== undefined ? { ...withCarId, strengthOfField: this.strengthOfField } : withCarId; const withRegistered = this.registeredCount !== undefined ? { ...withSof, registeredCount: this.registeredCount } : withSof; const props = this.maxParticipants !== undefined ? { ...withRegistered, maxParticipants: this.maxParticipants } : withRegistered; return Race.create(props); } /** * Update SOF and participant count */ updateField(strengthOfField: number, registeredCount: number): Race { const base = { id: this.id, leagueId: this.leagueId, scheduledAt: this.scheduledAt, track: this.track, car: this.car, sessionType: this.sessionType, status: this.status, strengthOfField, registeredCount, }; const withTrackId = this.trackId !== undefined ? { ...base, trackId: this.trackId } : base; const withCarId = this.carId !== undefined ? { ...withTrackId, carId: this.carId } : withTrackId; const props = this.maxParticipants !== undefined ? { ...withCarId, maxParticipants: this.maxParticipants } : withCarId; return Race.create(props); } /** * 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'; } }