122 lines
3.1 KiB
TypeScript
122 lines
3.1 KiB
TypeScript
/**
|
|
* 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';
|
|
|
|
export type TrackCategory = 'oval' | 'road' | 'street' | 'dirt';
|
|
export type TrackDifficulty = 'beginner' | 'intermediate' | 'advanced' | 'expert';
|
|
|
|
export class Track {
|
|
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;
|
|
readonly gameId: string;
|
|
|
|
private constructor(props: {
|
|
id: string;
|
|
name: string;
|
|
shortName: string;
|
|
country: string;
|
|
category: TrackCategory;
|
|
difficulty: TrackDifficulty;
|
|
lengthKm: number;
|
|
turns: number;
|
|
imageUrl?: string;
|
|
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);
|
|
|
|
return new Track({
|
|
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,
|
|
imageUrl: props.imageUrl,
|
|
gameId: props.gameId,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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`;
|
|
}
|
|
} |