192 lines
4.9 KiB
TypeScript
192 lines
4.9 KiB
TypeScript
/**
|
|
* Enhanced Result entity with detailed incident tracking
|
|
*/
|
|
|
|
import { RacingDomainValidationError } from '../errors/RacingDomainError';
|
|
import type { IEntity } from '@core/shared/domain';
|
|
import { RaceIncidents, type IncidentRecord } from '../value-objects/RaceIncidents';
|
|
import { RaceId } from './RaceId';
|
|
import { DriverId } from './DriverId';
|
|
import { Position } from './result/Position';
|
|
import { LapTime } from './result/LapTime';
|
|
|
|
export class ResultWithIncidents implements IEntity<string> {
|
|
readonly id: string;
|
|
readonly raceId: RaceId;
|
|
readonly driverId: DriverId;
|
|
readonly position: Position;
|
|
readonly fastestLap: LapTime;
|
|
readonly incidents: RaceIncidents;
|
|
readonly startPosition: Position;
|
|
|
|
private constructor(props: {
|
|
id: string;
|
|
raceId: RaceId;
|
|
driverId: DriverId;
|
|
position: Position;
|
|
fastestLap: LapTime;
|
|
incidents: RaceIncidents;
|
|
startPosition: Position;
|
|
}) {
|
|
this.id = props.id;
|
|
this.raceId = props.raceId;
|
|
this.driverId = props.driverId;
|
|
this.position = props.position;
|
|
this.fastestLap = props.fastestLap;
|
|
this.incidents = props.incidents;
|
|
this.startPosition = props.startPosition;
|
|
}
|
|
|
|
/**
|
|
* Factory method to create a new Result entity
|
|
*/
|
|
static create(props: {
|
|
id: string;
|
|
raceId: string;
|
|
driverId: string;
|
|
position: number;
|
|
fastestLap: number;
|
|
incidents: RaceIncidents;
|
|
startPosition: number;
|
|
}): ResultWithIncidents {
|
|
this.validate(props);
|
|
const raceId = RaceId.create(props.raceId);
|
|
const driverId = DriverId.create(props.driverId);
|
|
const position = Position.create(props.position);
|
|
const fastestLap = LapTime.create(props.fastestLap);
|
|
const startPosition = Position.create(props.startPosition);
|
|
return new ResultWithIncidents({
|
|
id: props.id,
|
|
raceId,
|
|
driverId,
|
|
position,
|
|
fastestLap,
|
|
incidents: props.incidents,
|
|
startPosition,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create from legacy Result data (with incidents as number)
|
|
*/
|
|
static fromLegacy(props: {
|
|
id: string;
|
|
raceId: string;
|
|
driverId: string;
|
|
position: number;
|
|
fastestLap: number;
|
|
incidents: number;
|
|
startPosition: number;
|
|
}): ResultWithIncidents {
|
|
const raceIncidents = RaceIncidents.fromLegacyIncidentsCount(props.incidents);
|
|
return ResultWithIncidents.create({
|
|
...props,
|
|
incidents: raceIncidents,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Domain validation logic
|
|
*/
|
|
private static validate(props: {
|
|
id: string;
|
|
raceId: string;
|
|
driverId: string;
|
|
position: number;
|
|
fastestLap: number;
|
|
incidents: RaceIncidents;
|
|
startPosition: number;
|
|
}): void {
|
|
if (!props.id || props.id.trim().length === 0) {
|
|
throw new RacingDomainValidationError('Result ID is required');
|
|
}
|
|
|
|
if (!props.raceId || props.raceId.trim().length === 0) {
|
|
throw new RacingDomainValidationError('Race ID is required');
|
|
}
|
|
|
|
if (!props.driverId || props.driverId.trim().length === 0) {
|
|
throw new RacingDomainValidationError('Driver ID is required');
|
|
}
|
|
|
|
if (!Number.isInteger(props.position) || props.position < 1) {
|
|
throw new RacingDomainValidationError('Position must be a positive integer');
|
|
}
|
|
|
|
if (props.fastestLap < 0) {
|
|
throw new RacingDomainValidationError('Fastest lap cannot be negative');
|
|
}
|
|
|
|
if (!Number.isInteger(props.startPosition) || props.startPosition < 1) {
|
|
throw new RacingDomainValidationError('Start position must be a positive integer');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calculate positions gained/lost
|
|
*/
|
|
getPositionChange(): number {
|
|
return this.startPosition.toNumber() - this.position.toNumber();
|
|
}
|
|
|
|
/**
|
|
* Check if driver finished on podium
|
|
*/
|
|
isPodium(): boolean {
|
|
return this.position.toNumber() <= 3;
|
|
}
|
|
|
|
/**
|
|
* Check if driver had a clean race (no incidents)
|
|
*/
|
|
isClean(): boolean {
|
|
return this.incidents.isClean();
|
|
}
|
|
|
|
/**
|
|
* Get total incident count (for backward compatibility)
|
|
*/
|
|
getTotalIncidents(): number {
|
|
return this.incidents.getTotalCount();
|
|
}
|
|
|
|
/**
|
|
* Get incident severity score
|
|
*/
|
|
getIncidentSeverityScore(): number {
|
|
return this.incidents.getSeverityScore();
|
|
}
|
|
|
|
/**
|
|
* Get human-readable incident summary
|
|
*/
|
|
getIncidentSummary(): string {
|
|
return this.incidents.getSummary();
|
|
}
|
|
|
|
/**
|
|
* Add an incident to this result
|
|
*/
|
|
addIncident(incident: IncidentRecord): ResultWithIncidents {
|
|
const updatedIncidents = this.incidents.addIncident(incident);
|
|
return new ResultWithIncidents({
|
|
...this,
|
|
incidents: updatedIncidents,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Convert to legacy format (for backward compatibility)
|
|
*/
|
|
toLegacyFormat() {
|
|
return {
|
|
id: this.id,
|
|
raceId: this.raceId,
|
|
driverId: this.driverId,
|
|
position: this.position,
|
|
fastestLap: this.fastestLap,
|
|
incidents: this.getTotalIncidents(),
|
|
startPosition: this.startPosition,
|
|
};
|
|
}
|
|
} |