311 lines
8.8 KiB
TypeScript
311 lines
8.8 KiB
TypeScript
/**
|
|
* Domain Entity: Session
|
|
*
|
|
* Represents a racing session within a race event.
|
|
* Immutable entity with factory methods and domain validation.
|
|
*/
|
|
|
|
import { Entity } from '@core/shared/domain/Entity';
|
|
import { RacingDomainInvariantError, RacingDomainValidationError } from '../errors/RacingDomainError';
|
|
import type { SessionType } from '../value-objects/SessionType';
|
|
|
|
export type SessionStatus = 'scheduled' | 'running' | 'completed' | 'cancelled';
|
|
|
|
export class Session extends Entity<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;
|
|
}) {
|
|
super(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();
|
|
}
|
|
} |