/** * Domain Entity: Track * * Represents a racing track/circuit in the GridPilot platform. * Immutable entity with factory methods and domain validation. */ import { RacingDomainValidationError } from '../errors/RacingDomainError'; import type { IEntity } from '@gridpilot/shared/domain'; export type TrackCategory = 'oval' | 'road' | 'street' | 'dirt'; export type TrackDifficulty = 'beginner' | 'intermediate' | 'advanced' | 'expert'; export class Track implements IEntity { readonly id: string; readonly name: string; readonly shortName: string; readonly country: string; readonly category: TrackCategory; readonly difficulty: TrackDifficulty; readonly lengthKm: number; readonly turns: number; readonly imageUrl: string | undefined; readonly gameId: string; private constructor(props: { id: string; name: string; shortName: string; country: string; category: TrackCategory; difficulty: TrackDifficulty; lengthKm: number; turns: number; imageUrl?: string | undefined; gameId: string; }) { this.id = props.id; this.name = props.name; this.shortName = props.shortName; this.country = props.country; this.category = props.category; this.difficulty = props.difficulty; this.lengthKm = props.lengthKm; this.turns = props.turns; this.imageUrl = props.imageUrl; this.gameId = props.gameId; } /** * Factory method to create a new Track entity */ static create(props: { id: string; name: string; shortName?: string; country: string; category?: TrackCategory; difficulty?: TrackDifficulty; lengthKm: number; turns: number; imageUrl?: string; gameId: string; }): Track { this.validate(props); const base = { id: props.id, name: props.name, shortName: props.shortName ?? props.name.slice(0, 3).toUpperCase(), country: props.country, category: props.category ?? 'road', difficulty: props.difficulty ?? 'intermediate', lengthKm: props.lengthKm, turns: props.turns, gameId: props.gameId, }; const withImage = props.imageUrl !== undefined ? { ...base, imageUrl: props.imageUrl } : base; return new Track(withImage); } /** * Domain validation logic */ private static validate(props: { id: string; name: string; country: string; lengthKm: number; turns: number; gameId: string; }): void { if (!props.id || props.id.trim().length === 0) { throw new RacingDomainValidationError('Track ID is required'); } if (!props.name || props.name.trim().length === 0) { throw new RacingDomainValidationError('Track name is required'); } if (!props.country || props.country.trim().length === 0) { throw new RacingDomainValidationError('Track country is required'); } if (props.lengthKm <= 0) { throw new RacingDomainValidationError('Track length must be positive'); } if (props.turns < 0) { throw new RacingDomainValidationError('Track turns cannot be negative'); } if (!props.gameId || props.gameId.trim().length === 0) { throw new RacingDomainValidationError('Game ID is required'); } } /** * Get formatted length string */ getFormattedLength(): string { return `${this.lengthKm.toFixed(2)} km`; } }