refactor
This commit is contained in:
45
core/racing/domain/entities/result/IncidentCount.test.ts
Normal file
45
core/racing/domain/entities/result/IncidentCount.test.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { IncidentCount } from './IncidentCount';
|
||||
import { RacingDomainValidationError } from '../../errors/RacingDomainError';
|
||||
|
||||
describe('IncidentCount', () => {
|
||||
describe('create', () => {
|
||||
it('should create an IncidentCount with valid non-negative integer', () => {
|
||||
const incidentCount = IncidentCount.create(5);
|
||||
expect(incidentCount.toNumber()).toBe(5);
|
||||
});
|
||||
|
||||
it('should create with zero', () => {
|
||||
const incidentCount = IncidentCount.create(0);
|
||||
expect(incidentCount.toNumber()).toBe(0);
|
||||
});
|
||||
|
||||
it('should throw error for negative number', () => {
|
||||
expect(() => IncidentCount.create(-1)).toThrow(RacingDomainValidationError);
|
||||
});
|
||||
|
||||
it('should throw error for non-integer', () => {
|
||||
expect(() => IncidentCount.create(1.5)).toThrow(RacingDomainValidationError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toNumber', () => {
|
||||
it('should return the number value', () => {
|
||||
const incidentCount = IncidentCount.create(3);
|
||||
expect(incidentCount.toNumber()).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('equals', () => {
|
||||
it('should return true for equal incident counts', () => {
|
||||
const ic1 = IncidentCount.create(2);
|
||||
const ic2 = IncidentCount.create(2);
|
||||
expect(ic1.equals(ic2)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for different incident counts', () => {
|
||||
const ic1 = IncidentCount.create(2);
|
||||
const ic2 = IncidentCount.create(3);
|
||||
expect(ic1.equals(ic2)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
20
core/racing/domain/entities/result/IncidentCount.ts
Normal file
20
core/racing/domain/entities/result/IncidentCount.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { RacingDomainValidationError } from '../../errors/RacingDomainError';
|
||||
|
||||
export class IncidentCount {
|
||||
private constructor(private readonly value: number) {}
|
||||
|
||||
static create(value: number): IncidentCount {
|
||||
if (!Number.isInteger(value) || value < 0) {
|
||||
throw new RacingDomainValidationError('Incident count must be a non-negative integer');
|
||||
}
|
||||
return new IncidentCount(value);
|
||||
}
|
||||
|
||||
toNumber(): number {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
equals(other: IncidentCount): boolean {
|
||||
return this.value === other.value;
|
||||
}
|
||||
}
|
||||
45
core/racing/domain/entities/result/LapTime.test.ts
Normal file
45
core/racing/domain/entities/result/LapTime.test.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { LapTime } from './LapTime';
|
||||
import { RacingDomainValidationError } from '../../errors/RacingDomainError';
|
||||
|
||||
describe('LapTime', () => {
|
||||
describe('create', () => {
|
||||
it('should create a LapTime with valid non-negative number', () => {
|
||||
const lapTime = LapTime.create(120.5);
|
||||
expect(lapTime.toNumber()).toBe(120.5);
|
||||
});
|
||||
|
||||
it('should create with zero', () => {
|
||||
const lapTime = LapTime.create(0);
|
||||
expect(lapTime.toNumber()).toBe(0);
|
||||
});
|
||||
|
||||
it('should throw error for negative number', () => {
|
||||
expect(() => LapTime.create(-1)).toThrow(RacingDomainValidationError);
|
||||
});
|
||||
|
||||
it('should throw error for NaN', () => {
|
||||
expect(() => LapTime.create(NaN)).toThrow(RacingDomainValidationError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toNumber', () => {
|
||||
it('should return the number value', () => {
|
||||
const lapTime = LapTime.create(95.2);
|
||||
expect(lapTime.toNumber()).toBe(95.2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('equals', () => {
|
||||
it('should return true for equal lap times', () => {
|
||||
const lt1 = LapTime.create(100.0);
|
||||
const lt2 = LapTime.create(100.0);
|
||||
expect(lt1.equals(lt2)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for different lap times', () => {
|
||||
const lt1 = LapTime.create(100.0);
|
||||
const lt2 = LapTime.create(101.0);
|
||||
expect(lt1.equals(lt2)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
20
core/racing/domain/entities/result/LapTime.ts
Normal file
20
core/racing/domain/entities/result/LapTime.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { RacingDomainValidationError } from '../../errors/RacingDomainError';
|
||||
|
||||
export class LapTime {
|
||||
private constructor(private readonly value: number) {}
|
||||
|
||||
static create(value: number): LapTime {
|
||||
if (typeof value !== 'number' || value < 0 || isNaN(value)) {
|
||||
throw new RacingDomainValidationError('Lap time must be a non-negative number');
|
||||
}
|
||||
return new LapTime(value);
|
||||
}
|
||||
|
||||
toNumber(): number {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
equals(other: LapTime): boolean {
|
||||
return this.value === other.value;
|
||||
}
|
||||
}
|
||||
44
core/racing/domain/entities/result/Position.test.ts
Normal file
44
core/racing/domain/entities/result/Position.test.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Position } from './Position';
|
||||
import { RacingDomainValidationError } from '../../errors/RacingDomainError';
|
||||
|
||||
describe('Position', () => {
|
||||
describe('create', () => {
|
||||
it('should create a Position with valid positive integer', () => {
|
||||
const position = Position.create(1);
|
||||
expect(position.toNumber()).toBe(1);
|
||||
});
|
||||
|
||||
it('should throw error for zero', () => {
|
||||
expect(() => Position.create(0)).toThrow(RacingDomainValidationError);
|
||||
});
|
||||
|
||||
it('should throw error for negative number', () => {
|
||||
expect(() => Position.create(-1)).toThrow(RacingDomainValidationError);
|
||||
});
|
||||
|
||||
it('should throw error for non-integer', () => {
|
||||
expect(() => Position.create(1.5)).toThrow(RacingDomainValidationError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toNumber', () => {
|
||||
it('should return the number value', () => {
|
||||
const position = Position.create(5);
|
||||
expect(position.toNumber()).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('equals', () => {
|
||||
it('should return true for equal positions', () => {
|
||||
const pos1 = Position.create(2);
|
||||
const pos2 = Position.create(2);
|
||||
expect(pos1.equals(pos2)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for different positions', () => {
|
||||
const pos1 = Position.create(2);
|
||||
const pos2 = Position.create(3);
|
||||
expect(pos1.equals(pos2)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
20
core/racing/domain/entities/result/Position.ts
Normal file
20
core/racing/domain/entities/result/Position.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { RacingDomainValidationError } from '../../errors/RacingDomainError';
|
||||
|
||||
export class Position {
|
||||
private constructor(private readonly value: number) {}
|
||||
|
||||
static create(value: number): Position {
|
||||
if (!Number.isInteger(value) || value <= 0) {
|
||||
throw new RacingDomainValidationError('Position must be a positive integer');
|
||||
}
|
||||
return new Position(value);
|
||||
}
|
||||
|
||||
toNumber(): number {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
equals(other: Position): boolean {
|
||||
return this.value === other.value;
|
||||
}
|
||||
}
|
||||
261
core/racing/domain/entities/result/Result.test.ts
Normal file
261
core/racing/domain/entities/result/Result.test.ts
Normal file
@@ -0,0 +1,261 @@
|
||||
import { Result } from './Result';
|
||||
import { RacingDomainValidationError } from '../../errors/RacingDomainError';
|
||||
import { RaceId } from '../RaceId';
|
||||
import { DriverId } from '../DriverId';
|
||||
import { Position } from './Position';
|
||||
import { LapTime } from './LapTime';
|
||||
import { IncidentCount } from './IncidentCount';
|
||||
|
||||
describe('Result', () => {
|
||||
const validId = 'result-123';
|
||||
const validRaceId = 'race-456';
|
||||
const validDriverId = 'driver-789';
|
||||
const validPosition = 1;
|
||||
const validFastestLap = 95.5;
|
||||
const validIncidents = 0;
|
||||
const validStartPosition = 2;
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a Result with valid props', () => {
|
||||
const props = {
|
||||
id: validId,
|
||||
raceId: validRaceId,
|
||||
driverId: validDriverId,
|
||||
position: validPosition,
|
||||
fastestLap: validFastestLap,
|
||||
incidents: validIncidents,
|
||||
startPosition: validStartPosition,
|
||||
};
|
||||
|
||||
const result = Result.create(props);
|
||||
|
||||
expect(result.id).toBe(validId);
|
||||
expect(result.raceId.toString()).toBe(validRaceId);
|
||||
expect(result.driverId.toString()).toBe(validDriverId);
|
||||
expect(result.position.toNumber()).toBe(validPosition);
|
||||
expect(result.fastestLap.toNumber()).toBe(validFastestLap);
|
||||
expect(result.incidents.toNumber()).toBe(validIncidents);
|
||||
expect(result.startPosition.toNumber()).toBe(validStartPosition);
|
||||
});
|
||||
|
||||
it('should throw error for empty id', () => {
|
||||
const props = {
|
||||
id: '',
|
||||
raceId: validRaceId,
|
||||
driverId: validDriverId,
|
||||
position: validPosition,
|
||||
fastestLap: validFastestLap,
|
||||
incidents: validIncidents,
|
||||
startPosition: validStartPosition,
|
||||
};
|
||||
|
||||
expect(() => Result.create(props)).toThrow(RacingDomainValidationError);
|
||||
});
|
||||
|
||||
it('should throw error for empty raceId', () => {
|
||||
const props = {
|
||||
id: validId,
|
||||
raceId: '',
|
||||
driverId: validDriverId,
|
||||
position: validPosition,
|
||||
fastestLap: validFastestLap,
|
||||
incidents: validIncidents,
|
||||
startPosition: validStartPosition,
|
||||
};
|
||||
|
||||
expect(() => Result.create(props)).toThrow(RacingDomainValidationError);
|
||||
});
|
||||
|
||||
it('should throw error for empty driverId', () => {
|
||||
const props = {
|
||||
id: validId,
|
||||
raceId: validRaceId,
|
||||
driverId: '',
|
||||
position: validPosition,
|
||||
fastestLap: validFastestLap,
|
||||
incidents: validIncidents,
|
||||
startPosition: validStartPosition,
|
||||
};
|
||||
|
||||
expect(() => Result.create(props)).toThrow(RacingDomainValidationError);
|
||||
});
|
||||
|
||||
it('should throw error for invalid position', () => {
|
||||
const props = {
|
||||
id: validId,
|
||||
raceId: validRaceId,
|
||||
driverId: validDriverId,
|
||||
position: 0,
|
||||
fastestLap: validFastestLap,
|
||||
incidents: validIncidents,
|
||||
startPosition: validStartPosition,
|
||||
};
|
||||
|
||||
expect(() => Result.create(props)).toThrow(RacingDomainValidationError);
|
||||
});
|
||||
|
||||
it('should throw error for invalid fastestLap', () => {
|
||||
const props = {
|
||||
id: validId,
|
||||
raceId: validRaceId,
|
||||
driverId: validDriverId,
|
||||
position: validPosition,
|
||||
fastestLap: -1,
|
||||
incidents: validIncidents,
|
||||
startPosition: validStartPosition,
|
||||
};
|
||||
|
||||
expect(() => Result.create(props)).toThrow(RacingDomainValidationError);
|
||||
});
|
||||
|
||||
it('should throw error for invalid incidents', () => {
|
||||
const props = {
|
||||
id: validId,
|
||||
raceId: validRaceId,
|
||||
driverId: validDriverId,
|
||||
position: validPosition,
|
||||
fastestLap: validFastestLap,
|
||||
incidents: -1,
|
||||
startPosition: validStartPosition,
|
||||
};
|
||||
|
||||
expect(() => Result.create(props)).toThrow(RacingDomainValidationError);
|
||||
});
|
||||
|
||||
it('should throw error for invalid startPosition', () => {
|
||||
const props = {
|
||||
id: validId,
|
||||
raceId: validRaceId,
|
||||
driverId: validDriverId,
|
||||
position: validPosition,
|
||||
fastestLap: validFastestLap,
|
||||
incidents: validIncidents,
|
||||
startPosition: 0,
|
||||
};
|
||||
|
||||
expect(() => Result.create(props)).toThrow(RacingDomainValidationError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('entity properties', () => {
|
||||
let result: Result;
|
||||
|
||||
beforeEach(() => {
|
||||
result = Result.create({
|
||||
id: validId,
|
||||
raceId: validRaceId,
|
||||
driverId: validDriverId,
|
||||
position: validPosition,
|
||||
fastestLap: validFastestLap,
|
||||
incidents: validIncidents,
|
||||
startPosition: validStartPosition,
|
||||
});
|
||||
});
|
||||
|
||||
it('should have readonly id', () => {
|
||||
expect(result.id).toBe(validId);
|
||||
});
|
||||
|
||||
it('should have readonly raceId as RaceId', () => {
|
||||
expect(result.raceId).toBeInstanceOf(RaceId);
|
||||
expect(result.raceId.toString()).toBe(validRaceId);
|
||||
});
|
||||
|
||||
it('should have readonly driverId as DriverId', () => {
|
||||
expect(result.driverId).toBeInstanceOf(DriverId);
|
||||
expect(result.driverId.toString()).toBe(validDriverId);
|
||||
});
|
||||
|
||||
it('should have readonly position as Position', () => {
|
||||
expect(result.position).toBeInstanceOf(Position);
|
||||
expect(result.position.toNumber()).toBe(validPosition);
|
||||
});
|
||||
|
||||
it('should have readonly fastestLap as LapTime', () => {
|
||||
expect(result.fastestLap).toBeInstanceOf(LapTime);
|
||||
expect(result.fastestLap.toNumber()).toBe(validFastestLap);
|
||||
});
|
||||
|
||||
it('should have readonly incidents as IncidentCount', () => {
|
||||
expect(result.incidents).toBeInstanceOf(IncidentCount);
|
||||
expect(result.incidents.toNumber()).toBe(validIncidents);
|
||||
});
|
||||
|
||||
it('should have readonly startPosition as Position', () => {
|
||||
expect(result.startPosition).toBeInstanceOf(Position);
|
||||
expect(result.startPosition.toNumber()).toBe(validStartPosition);
|
||||
});
|
||||
});
|
||||
|
||||
describe('domain methods', () => {
|
||||
it('should calculate position change correctly', () => {
|
||||
const result = Result.create({
|
||||
id: validId,
|
||||
raceId: validRaceId,
|
||||
driverId: validDriverId,
|
||||
position: 1,
|
||||
fastestLap: validFastestLap,
|
||||
incidents: validIncidents,
|
||||
startPosition: 3,
|
||||
});
|
||||
|
||||
expect(result.getPositionChange()).toBe(2); // 3 - 1
|
||||
});
|
||||
|
||||
it('should return true for podium finish', () => {
|
||||
const result = Result.create({
|
||||
id: validId,
|
||||
raceId: validRaceId,
|
||||
driverId: validDriverId,
|
||||
position: 3,
|
||||
fastestLap: validFastestLap,
|
||||
incidents: validIncidents,
|
||||
startPosition: validStartPosition,
|
||||
});
|
||||
|
||||
expect(result.isPodium()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for non-podium finish', () => {
|
||||
const result = Result.create({
|
||||
id: validId,
|
||||
raceId: validRaceId,
|
||||
driverId: validDriverId,
|
||||
position: 4,
|
||||
fastestLap: validFastestLap,
|
||||
incidents: validIncidents,
|
||||
startPosition: validStartPosition,
|
||||
});
|
||||
|
||||
expect(result.isPodium()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for clean race', () => {
|
||||
const result = Result.create({
|
||||
id: validId,
|
||||
raceId: validRaceId,
|
||||
driverId: validDriverId,
|
||||
position: validPosition,
|
||||
fastestLap: validFastestLap,
|
||||
incidents: 0,
|
||||
startPosition: validStartPosition,
|
||||
});
|
||||
|
||||
expect(result.isClean()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for race with incidents', () => {
|
||||
const result = Result.create({
|
||||
id: validId,
|
||||
raceId: validRaceId,
|
||||
driverId: validDriverId,
|
||||
position: validPosition,
|
||||
fastestLap: validFastestLap,
|
||||
incidents: 1,
|
||||
startPosition: validStartPosition,
|
||||
});
|
||||
|
||||
expect(result.isClean()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
120
core/racing/domain/entities/result/Result.ts
Normal file
120
core/racing/domain/entities/result/Result.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* Domain Entity: Result
|
||||
*
|
||||
* Represents a race result in the GridPilot platform.
|
||||
* Immutable entity with factory methods and domain validation.
|
||||
*/
|
||||
|
||||
import { RacingDomainValidationError } from '../../errors/RacingDomainError';
|
||||
import type { IEntity } from '@core/shared/domain';
|
||||
import { RaceId } from '../RaceId';
|
||||
import { DriverId } from '../DriverId';
|
||||
import { Position } from './Position';
|
||||
import { LapTime } from './LapTime';
|
||||
import { IncidentCount } from './IncidentCount';
|
||||
|
||||
export class Result implements IEntity<string> {
|
||||
readonly id: string;
|
||||
readonly raceId: RaceId;
|
||||
readonly driverId: DriverId;
|
||||
readonly position: Position;
|
||||
readonly fastestLap: LapTime;
|
||||
readonly incidents: IncidentCount;
|
||||
readonly startPosition: Position;
|
||||
|
||||
private constructor(props: {
|
||||
id: string;
|
||||
raceId: RaceId;
|
||||
driverId: DriverId;
|
||||
position: Position;
|
||||
fastestLap: LapTime;
|
||||
incidents: IncidentCount;
|
||||
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: number;
|
||||
startPosition: number;
|
||||
}): Result {
|
||||
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 incidents = IncidentCount.create(props.incidents);
|
||||
const startPosition = Position.create(props.startPosition);
|
||||
|
||||
return new Result({
|
||||
id: props.id,
|
||||
raceId,
|
||||
driverId,
|
||||
position,
|
||||
fastestLap,
|
||||
incidents,
|
||||
startPosition,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Domain validation logic
|
||||
*/
|
||||
private static validate(props: {
|
||||
id: string;
|
||||
raceId: string;
|
||||
driverId: string;
|
||||
position: number;
|
||||
fastestLap: number;
|
||||
incidents: number;
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (0 incidents)
|
||||
*/
|
||||
isClean(): boolean {
|
||||
return this.incidents.toNumber() === 0;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user