Files
gridpilot.gg/packages/racing/domain/entities/Standing.ts
2025-12-12 01:11:36 +01:00

136 lines
3.3 KiB
TypeScript

/**
* Domain Entity: Standing
*
* Represents a championship standing in the GridPilot platform.
* Immutable entity with factory methods and domain validation.
*/
import { RacingDomainError, RacingDomainValidationError } from '../errors/RacingDomainError';
import type { IEntity } from '@gridpilot/shared/domain';
export class Standing implements IEntity<string> {
readonly id: string;
readonly leagueId: string;
readonly driverId: string;
readonly points: number;
readonly wins: number;
readonly position: number;
readonly racesCompleted: number;
private constructor(props: {
id: string;
leagueId: string;
driverId: string;
points: number;
wins: number;
position: number;
racesCompleted: number;
}) {
this.id = props.id;
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: {
id?: string;
leagueId: string;
driverId: string;
points?: number;
wins?: number;
position?: number;
racesCompleted?: number;
}): Standing {
this.validate(props);
const id = props.id && props.id.trim().length > 0
? props.id
: `${props.leagueId}:${props.driverId}`;
return new Standing({
id,
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: {
id?: string;
leagueId: string;
driverId: string;
}): void {
if (!props.leagueId || props.leagueId.trim().length === 0) {
throw new RacingDomainValidationError('League ID is required');
}
if (!props.driverId || props.driverId.trim().length === 0) {
throw new RacingDomainValidationError('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({
id: this.id,
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 RacingDomainValidationError('Position must be a positive integer');
}
return Standing.create({
id: this.id,
leagueId: this.leagueId,
driverId: this.driverId,
points: this.points,
wins: this.wins,
position,
racesCompleted: this.racesCompleted,
});
}
/**
* 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;
}
}