119 lines
2.8 KiB
TypeScript
119 lines
2.8 KiB
TypeScript
/**
|
|
* Domain Entity: Standing
|
|
*
|
|
* Represents a championship standing in the GridPilot platform.
|
|
* Immutable entity with factory methods and domain validation.
|
|
*/
|
|
|
|
import { RacingDomainValidationError } from '../errors/RacingDomainError';
|
|
|
|
export class Standing {
|
|
readonly leagueId: string;
|
|
readonly driverId: string;
|
|
readonly points: number;
|
|
readonly wins: number;
|
|
readonly position: number;
|
|
readonly racesCompleted: number;
|
|
|
|
private constructor(props: {
|
|
leagueId: string;
|
|
driverId: string;
|
|
points: number;
|
|
wins: number;
|
|
position: number;
|
|
racesCompleted: number;
|
|
}) {
|
|
this.leagueId = props.leagueId;
|
|
this.driverId = props.driverId;
|
|
this.points = props.points;
|
|
this.wins = props.wins;
|
|
this.position = props.position;
|
|
this.racesCompleted = props.racesCompleted;
|
|
}
|
|
|
|
/**
|
|
* Factory method to create a new Standing entity
|
|
*/
|
|
static create(props: {
|
|
leagueId: string;
|
|
driverId: string;
|
|
points?: number;
|
|
wins?: number;
|
|
position?: number;
|
|
racesCompleted?: number;
|
|
}): Standing {
|
|
this.validate(props);
|
|
|
|
return new Standing({
|
|
leagueId: props.leagueId,
|
|
driverId: props.driverId,
|
|
points: props.points ?? 0,
|
|
wins: props.wins ?? 0,
|
|
position: props.position ?? 0,
|
|
racesCompleted: props.racesCompleted ?? 0,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Domain validation logic
|
|
*/
|
|
private static validate(props: {
|
|
leagueId: string;
|
|
driverId: string;
|
|
}): void {
|
|
if (!props.leagueId || props.leagueId.trim().length === 0) {
|
|
throw new RacingDomainError('League ID is required');
|
|
}
|
|
|
|
if (!props.driverId || props.driverId.trim().length === 0) {
|
|
throw new RacingDomainError('Driver ID is required');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add points from a race result
|
|
*/
|
|
addRaceResult(position: number, pointsSystem: Record<number, number>): Standing {
|
|
const racePoints = pointsSystem[position] ?? 0;
|
|
const isWin = position === 1;
|
|
|
|
return new Standing({
|
|
leagueId: this.leagueId,
|
|
driverId: this.driverId,
|
|
points: this.points + racePoints,
|
|
wins: this.wins + (isWin ? 1 : 0),
|
|
position: this.position,
|
|
racesCompleted: this.racesCompleted + 1,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Update championship position
|
|
*/
|
|
updatePosition(position: number): Standing {
|
|
if (!Number.isInteger(position) || position < 1) {
|
|
throw new RacingDomainError('Position must be a positive integer');
|
|
}
|
|
|
|
return new Standing({
|
|
...this,
|
|
position,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Calculate average points per race
|
|
*/
|
|
getAveragePoints(): number {
|
|
if (this.racesCompleted === 0) return 0;
|
|
return this.points / this.racesCompleted;
|
|
}
|
|
|
|
/**
|
|
* Calculate win percentage
|
|
*/
|
|
getWinPercentage(): number {
|
|
if (this.racesCompleted === 0) return 0;
|
|
return (this.wins / this.racesCompleted) * 100;
|
|
}
|
|
} |