Files
gridpilot.gg/core/racing/application/use-cases/GetRaceWithSOFUseCase.ts
2025-12-19 15:07:53 +01:00

114 lines
4.1 KiB
TypeScript

/**
* Use Case: GetRaceWithSOFUseCase
*
* Returns race details enriched with calculated Strength of Field (SOF).
* SOF is calculated from participant ratings if not already stored on the race.
*/
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { AsyncUseCase } from '@core/shared/application/AsyncUseCase';
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
import type { IRaceRegistrationRepository } from '../../domain/repositories/IRaceRegistrationRepository';
import type { IResultRepository } from '../../domain/repositories/IResultRepository';
import type { GetDriverRatingInputPort } from '../ports/input/GetDriverRatingInputPort';
import type { GetDriverRatingOutputPort } from '../ports/output/GetDriverRatingOutputPort';
import {
AverageStrengthOfFieldCalculator,
type StrengthOfFieldCalculator,
} from '../../domain/services/StrengthOfFieldCalculator';
export interface GetRaceWithSOFQueryParams {
raceId: string;
}
export interface RaceWithSOFResultDTO {
raceId: string;
leagueId: string;
scheduledAt: Date;
track: string;
trackId: string;
car: string;
carId: string;
sessionType: string;
status: string;
strengthOfField: number | null;
registeredCount: number;
maxParticipants: number;
participantCount: number;
}
type GetRaceWithSOFErrorCode = 'RACE_NOT_FOUND';
export class GetRaceWithSOFUseCase implements AsyncUseCase<GetRaceWithSOFQueryParams, RaceWithSOFResultDTO, GetRaceWithSOFErrorCode> {
private readonly sofCalculator: StrengthOfFieldCalculator;
constructor(
private readonly raceRepository: IRaceRepository,
private readonly registrationRepository: IRaceRegistrationRepository,
private readonly resultRepository: IResultRepository,
private readonly getDriverRating: (input: GetDriverRatingInputPort) => Promise<GetDriverRatingOutputPort>,
sofCalculator?: StrengthOfFieldCalculator,
) {
this.sofCalculator = sofCalculator ?? new AverageStrengthOfFieldCalculator();
}
async execute(params: GetRaceWithSOFQueryParams): Promise<Result<RaceWithSOFResultDTO, ApplicationErrorCode<GetRaceWithSOFErrorCode>>> {
const { raceId } = params;
const race = await this.raceRepository.findById(raceId);
if (!race) {
return Result.err({ code: 'RACE_NOT_FOUND' });
}
// Get participant IDs based on race status
let participantIds: string[] = [];
if (race.status === 'completed') {
// For completed races, use results
const results = await this.resultRepository.findByRaceId(raceId);
participantIds = results.map(r => r.driverId);
} else {
// For upcoming/running races, use registrations
participantIds = await this.registrationRepository.getRegisteredDrivers(raceId);
}
// Use stored SOF if available, otherwise calculate
let strengthOfField = race.strengthOfField ?? null;
if (strengthOfField === null && participantIds.length > 0) {
// Get ratings for all participants using clean ports
const ratingPromises = participantIds.map(driverId =>
this.getDriverRating({ driverId })
);
const ratingResults = await Promise.all(ratingPromises);
const driverRatings = participantIds
.filter((_, index) => ratingResults[index].rating !== null)
.map((driverId, index) => ({
driverId,
rating: ratingResults[index].rating!
}));
strengthOfField = this.sofCalculator.calculate(driverRatings);
}
const dto: RaceWithSOFResultDTO = {
raceId: race.id,
leagueId: race.leagueId,
scheduledAt: race.scheduledAt,
track: race.track ?? '',
trackId: race.trackId ?? '',
car: race.car ?? '',
carId: race.carId ?? '',
sessionType: race.sessionType.props,
status: race.status,
strengthOfField,
registeredCount: race.registeredCount ?? participantIds.length,
maxParticipants: race.maxParticipants ?? participantIds.length,
participantCount: participantIds.length,
};
return Result.ok(dto);
}
}