/** * Domain Entity: RaceEvent (Aggregate Root) * * Represents a race event containing multiple sessions (practice, quali, race). * Immutable aggregate root with factory methods and domain validation. */ import { RacingDomainValidationError, RacingDomainInvariantError } from '../errors/RacingDomainError'; import type { IEntity } from '@core/shared/domain'; import type { Session } from './Session'; import { SessionType } from '../value-objects/SessionType'; export type RaceEventStatus = 'scheduled' | 'in_progress' | 'awaiting_stewarding' | 'closed' | 'cancelled'; export class RaceEvent implements IEntity { readonly id: string; readonly seasonId: string; readonly leagueId: string; readonly name: string; readonly sessions: readonly Session[]; readonly status: RaceEventStatus; readonly stewardingClosesAt: Date | undefined; private constructor(props: { id: string; seasonId: string; leagueId: string; name: string; sessions: readonly Session[]; status: RaceEventStatus; stewardingClosesAt?: Date; }) { this.id = props.id; this.seasonId = props.seasonId; this.leagueId = props.leagueId; this.name = props.name; this.sessions = props.sessions; this.status = props.status; this.stewardingClosesAt = props.stewardingClosesAt; } /** * Factory method to create a new RaceEvent entity */ static create(props: { id: string; seasonId: string; leagueId: string; name: string; sessions: Session[]; status?: RaceEventStatus; stewardingClosesAt?: Date; }): RaceEvent { this.validate(props); return new RaceEvent({ id: props.id, seasonId: props.seasonId, leagueId: props.leagueId, name: props.name, sessions: [...props.sessions], // Create immutable copy status: props.status ?? 'scheduled', ...(props.stewardingClosesAt !== undefined ? { stewardingClosesAt: props.stewardingClosesAt } : {}), }); } /** * Domain validation logic */ private static validate(props: { id: string; seasonId: string; leagueId: string; name: string; sessions: Session[]; }): void { if (!props.id || props.id.trim().length === 0) { throw new RacingDomainValidationError('RaceEvent ID is required'); } if (!props.seasonId || props.seasonId.trim().length === 0) { throw new RacingDomainValidationError('Season ID is required'); } if (!props.leagueId || props.leagueId.trim().length === 0) { throw new RacingDomainValidationError('League ID is required'); } if (!props.name || props.name.trim().length === 0) { throw new RacingDomainValidationError('RaceEvent name is required'); } if (!props.sessions || props.sessions.length === 0) { throw new RacingDomainValidationError('RaceEvent must have at least one session'); } // Validate all sessions belong to this race event const invalidSessions = props.sessions.filter(s => s.raceEventId !== props.id); if (invalidSessions.length > 0) { throw new RacingDomainValidationError('All sessions must belong to this race event'); } // Validate session types are unique const sessionTypes = props.sessions.map(s => s.sessionType.value); const uniqueTypes = new Set(sessionTypes); if (uniqueTypes.size !== sessionTypes.length) { throw new RacingDomainValidationError('Session types must be unique within a race event'); } // Validate at least one main race session exists const hasMainRace = props.sessions.some(s => s.sessionType.value === 'main'); if (!hasMainRace) { throw new RacingDomainValidationError('RaceEvent must have at least one main race session'); } } /** * Start the race event (move from scheduled to in_progress) */ start(): RaceEvent { if (this.status !== 'scheduled') { throw new RacingDomainInvariantError('Only scheduled race events can be started'); } return RaceEvent.create({ id: this.id, seasonId: this.seasonId, leagueId: this.leagueId, name: this.name, sessions: [...this.sessions], status: 'in_progress', ...(this.stewardingClosesAt !== undefined ? { stewardingClosesAt: this.stewardingClosesAt } : {}), }); } /** * Complete the main race session and move to awaiting_stewarding */ completeMainRace(): RaceEvent { if (this.status !== 'in_progress') { throw new RacingDomainInvariantError('Only in-progress race events can complete main race'); } const mainRaceSession = this.getMainRaceSession(); if (!mainRaceSession || mainRaceSession.status !== 'completed') { throw new RacingDomainInvariantError('Main race session must be completed first'); } return RaceEvent.create({ id: this.id, seasonId: this.seasonId, leagueId: this.leagueId, name: this.name, sessions: [...this.sessions], status: 'awaiting_stewarding', ...(this.stewardingClosesAt !== undefined ? { stewardingClosesAt: this.stewardingClosesAt } : {}), }); } /** * Close stewarding and finalize the race event */ closeStewarding(): RaceEvent { if (this.status !== 'awaiting_stewarding') { throw new RacingDomainInvariantError('Only race events awaiting stewarding can be closed'); } return RaceEvent.create({ id: this.id, seasonId: this.seasonId, leagueId: this.leagueId, name: this.name, sessions: [...this.sessions], status: 'closed', ...(this.stewardingClosesAt !== undefined ? { stewardingClosesAt: this.stewardingClosesAt } : {}), }); } /** * Cancel the race event */ cancel(): RaceEvent { if (this.status === 'closed') { throw new RacingDomainInvariantError('Cannot cancel a closed race event'); } if (this.status === 'cancelled') { return this; } return RaceEvent.create({ id: this.id, seasonId: this.seasonId, leagueId: this.leagueId, name: this.name, sessions: [...this.sessions], status: 'cancelled', ...(this.stewardingClosesAt !== undefined ? { stewardingClosesAt: this.stewardingClosesAt } : {}), }); } /** * Get the main race session (the one that counts for championship points) */ getMainRaceSession(): Session | undefined { return this.sessions.find(s => s.sessionType.equals(SessionType.main())); } /** * Get all sessions of a specific type */ getSessionsByType(sessionType: SessionType): Session[] { return this.sessions.filter(s => s.sessionType.equals(sessionType)); } /** * Get all completed sessions */ getCompletedSessions(): Session[] { return this.sessions.filter(s => s.status === 'completed'); } /** * Check if all sessions are completed */ areAllSessionsCompleted(): boolean { return this.sessions.every(s => s.status === 'completed'); } /** * Check if the main race is completed */ isMainRaceCompleted(): boolean { const mainRace = this.getMainRaceSession(); return mainRace ? mainRace.status === 'completed' : false; } /** * Check if stewarding window has expired */ hasStewardingExpired(): boolean { if (!this.stewardingClosesAt) return false; return new Date() > this.stewardingClosesAt; } /** * Check if race event is in the past */ isPast(): boolean { const latestSession = this.sessions.reduce((latest, session) => session.scheduledAt > latest.scheduledAt ? session : latest ); return latestSession.scheduledAt < new Date(); } /** * Check if race event is upcoming */ isUpcoming(): boolean { return this.status === 'scheduled' && !this.isPast(); } /** * Check if race event is currently running */ isLive(): boolean { return this.status === 'in_progress'; } /** * Check if race event is awaiting stewarding decisions */ isAwaitingStewarding(): boolean { return this.status === 'awaiting_stewarding'; } /** * Check if race event is closed (stewarding complete) */ isClosed(): boolean { return this.status === 'closed'; } }