/** * Domain Entity: Session * * Represents a racing session within a race event. * Immutable entity with factory methods and domain validation. */ import { RacingDomainValidationError, RacingDomainInvariantError } from '../errors/RacingDomainError'; import type { IEntity } from '@core/shared/domain'; import type { SessionType } from '../value-objects/SessionType'; export type SessionStatus = 'scheduled' | 'running' | 'completed' | 'cancelled'; export class Session implements IEntity { readonly id: string; readonly raceEventId: string; readonly scheduledAt: Date; readonly track: string; readonly trackId: string | undefined; readonly car: string; readonly carId: string | undefined; readonly sessionType: SessionType; readonly status: SessionStatus; readonly strengthOfField: number | undefined; readonly registeredCount: number | undefined; readonly maxParticipants: number | undefined; private constructor(props: { id: string; raceEventId: string; scheduledAt: Date; track: string; trackId?: string; car: string; carId?: string; sessionType: SessionType; status: SessionStatus; strengthOfField?: number; registeredCount?: number; maxParticipants?: number; }) { this.id = props.id; this.raceEventId = props.raceEventId; 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 Session entity */ static create(props: { id: string; raceEventId: string; scheduledAt: Date; track: string; trackId?: string; car: string; carId?: string; sessionType: SessionType; status?: SessionStatus; strengthOfField?: number; registeredCount?: number; maxParticipants?: number; }): Session { this.validate(props); return new Session({ id: props.id, raceEventId: props.raceEventId, scheduledAt: props.scheduledAt, track: props.track, ...(props.trackId !== undefined ? { trackId: props.trackId } : {}), car: props.car, ...(props.carId !== undefined ? { carId: props.carId } : {}), sessionType: props.sessionType, 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; raceEventId: string; scheduledAt: Date; track: string; car: string; sessionType: SessionType; }): void { if (!props.id || props.id.trim().length === 0) { throw new RacingDomainValidationError('Session ID is required'); } if (!props.raceEventId || props.raceEventId.trim().length === 0) { throw new RacingDomainValidationError('Race Event 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'); } if (!props.sessionType) { throw new RacingDomainValidationError('Session type is required'); } } /** * Start the session (move from scheduled to running) */ start(): Session { if (this.status !== 'scheduled') { throw new RacingDomainInvariantError('Only scheduled sessions can be started'); } const base = { id: this.id, raceEventId: this.raceEventId, scheduledAt: this.scheduledAt, track: this.track, car: this.car, sessionType: this.sessionType, status: 'running' as SessionStatus, }; 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 Session.create(props); } /** * Mark session as completed */ complete(): Session { if (this.status === 'completed') { throw new RacingDomainInvariantError('Session is already completed'); } if (this.status === 'cancelled') { throw new RacingDomainInvariantError('Cannot complete a cancelled session'); } const base = { id: this.id, raceEventId: this.raceEventId, scheduledAt: this.scheduledAt, track: this.track, car: this.car, sessionType: this.sessionType, status: 'completed' as SessionStatus, }; 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 Session.create(props); } /** * Cancel the session */ cancel(): Session { if (this.status === 'completed') { throw new RacingDomainInvariantError('Cannot cancel a completed session'); } if (this.status === 'cancelled') { throw new RacingDomainInvariantError('Session is already cancelled'); } const base = { id: this.id, raceEventId: this.raceEventId, scheduledAt: this.scheduledAt, track: this.track, car: this.car, sessionType: this.sessionType, status: 'cancelled' as SessionStatus, }; 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 Session.create(props); } /** * Update SOF and participant count */ updateField(strengthOfField: number, registeredCount: number): Session { const base = { id: this.id, raceEventId: this.raceEventId, 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 Session.create(props); } /** * Check if session is in the past */ isPast(): boolean { return this.scheduledAt < new Date(); } /** * Check if session is upcoming */ isUpcoming(): boolean { return this.status === 'scheduled' && !this.isPast(); } /** * Check if session is live/running */ isLive(): boolean { return this.status === 'running'; } /** * Check if this session counts for championship points */ countsForPoints(): boolean { return this.sessionType.countsForPoints(); } /** * Check if this session determines grid positions */ determinesGrid(): boolean { return this.sessionType.determinesGrid(); } }