/** * 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 type { RaceWithSOFOutputPort } from '../ports/output/RaceWithSOFOutputPort'; import { AverageStrengthOfFieldCalculator, type StrengthOfFieldCalculator, } from '../../domain/services/StrengthOfFieldCalculator'; export interface GetRaceWithSOFQueryParams { raceId: string; } type GetRaceWithSOFErrorCode = 'RACE_NOT_FOUND'; export class GetRaceWithSOFUseCase implements AsyncUseCase { private readonly sofCalculator: StrengthOfFieldCalculator; constructor( private readonly raceRepository: IRaceRepository, private readonly registrationRepository: IRaceRegistrationRepository, private readonly resultRepository: IResultRepository, private readonly getDriverRating: (input: GetDriverRatingInputPort) => Promise, sofCalculator?: StrengthOfFieldCalculator, ) { this.sofCalculator = sofCalculator ?? new AverageStrengthOfFieldCalculator(); } async execute(params: GetRaceWithSOFQueryParams): Promise>> { 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 outputPort: RaceWithSOFOutputPort = { id: race.id, leagueId: race.leagueId, scheduledAt: race.scheduledAt, track: race.track ?? '', car: race.car ?? '', status: race.status, strengthOfField, registeredCount: race.registeredCount ?? participantIds.length, maxParticipants: race.maxParticipants ?? participantIds.length, participantCount: participantIds.length, }; return Result.ok(outputPort); } }