178 lines
4.5 KiB
TypeScript
178 lines
4.5 KiB
TypeScript
/**
|
|
* Domain Entity: Standing
|
|
*
|
|
* Represents a championship standing in the GridPilot platform.
|
|
* Immutable entity with factory methods and domain validation.
|
|
*/
|
|
|
|
|
|
import { Entity } from '@core/shared/domain/Entity';
|
|
import { RacingDomainValidationError } from '../errors/RacingDomainError';
|
|
import { Points } from '../value-objects/Points';
|
|
import { Position } from './championship/Position';
|
|
import { DriverId } from './DriverId';
|
|
import { LeagueId } from './LeagueId';
|
|
|
|
export class Standing extends Entity<string> {
|
|
readonly leagueId: LeagueId;
|
|
readonly driverId: DriverId;
|
|
readonly points: Points;
|
|
readonly wins: number;
|
|
readonly position: Position;
|
|
readonly racesCompleted: number;
|
|
|
|
private constructor(props: {
|
|
id: string;
|
|
leagueId: LeagueId;
|
|
driverId: DriverId;
|
|
points: Points;
|
|
wins: number;
|
|
position: Position;
|
|
racesCompleted: number;
|
|
}) {
|
|
super(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 {
|
|
Standing.validate(props);
|
|
|
|
const id = props.id && props.id.trim().length > 0
|
|
? props.id
|
|
: `${props.leagueId}:${props.driverId}`;
|
|
|
|
const leagueId = LeagueId.create(props.leagueId);
|
|
const driverId = DriverId.create(props.driverId);
|
|
const points = Points.create(props.points ?? 0);
|
|
const position = Position.create(props.position ?? 1); // Default to 1 for position
|
|
|
|
return new Standing({
|
|
id,
|
|
leagueId,
|
|
driverId,
|
|
points,
|
|
wins: props.wins ?? 0,
|
|
position,
|
|
racesCompleted: props.racesCompleted ?? 0,
|
|
});
|
|
}
|
|
|
|
static rehydrate(props: {
|
|
id: string;
|
|
leagueId: string;
|
|
driverId: string;
|
|
points: number;
|
|
wins: number;
|
|
position: number;
|
|
racesCompleted: number;
|
|
}): Standing {
|
|
if (!props.id || props.id.trim().length === 0) {
|
|
throw new RacingDomainValidationError('Standing ID is required');
|
|
}
|
|
|
|
return new Standing({
|
|
id: props.id,
|
|
leagueId: LeagueId.create(props.leagueId),
|
|
driverId: DriverId.create(props.driverId),
|
|
points: Points.create(props.points),
|
|
wins: props.wins,
|
|
position: Position.create(props.position),
|
|
racesCompleted: props.racesCompleted,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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');
|
|
}
|
|
}
|
|
|
|
equals(other: Entity<string>): boolean {
|
|
if (!(other instanceof Standing)) {
|
|
return false;
|
|
}
|
|
return this.id === other.id;
|
|
}
|
|
|
|
/**
|
|
* Add points from a race result
|
|
*/
|
|
addRaceResult(position: number, pointsSystem: Record<number, number>): Standing {
|
|
const racePoints = pointsSystem[position] ?? 0;
|
|
const isWin = position === 1;
|
|
|
|
const newPoints = Points.create(this.points.toNumber() + racePoints);
|
|
const newPosition = this.position; // Position might be updated separately
|
|
|
|
return new Standing({
|
|
id: this.id,
|
|
leagueId: this.leagueId,
|
|
driverId: this.driverId,
|
|
points: newPoints,
|
|
wins: this.wins + (isWin ? 1 : 0),
|
|
position: newPosition,
|
|
racesCompleted: this.racesCompleted + 1,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Update championship position
|
|
*/
|
|
updatePosition(position: number): Standing {
|
|
const newPosition = Position.create(position);
|
|
|
|
return new Standing({
|
|
id: this.id,
|
|
leagueId: this.leagueId,
|
|
driverId: this.driverId,
|
|
points: this.points,
|
|
wins: this.wins,
|
|
position: newPosition,
|
|
racesCompleted: this.racesCompleted,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Calculate average points per race
|
|
*/
|
|
getAveragePoints(): number {
|
|
if (this.racesCompleted === 0) return 0;
|
|
return this.points.toNumber() / this.racesCompleted;
|
|
}
|
|
|
|
/**
|
|
* Calculate win percentage
|
|
*/
|
|
getWinPercentage(): number {
|
|
if (this.racesCompleted === 0) return 0;
|
|
return (this.wins / this.racesCompleted) * 100;
|
|
}
|
|
} |