import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'; import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository'; import type { IDriverRepository } from '../../domain/repositories/IDriverRepository'; import type { IRaceRegistrationRepository } from '../../domain/repositories/IRaceRegistrationRepository'; import type { IResultRepository } from '../../domain/repositories/IResultRepository'; import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository'; import type { DriverRatingProvider } from '../ports/DriverRatingProvider'; import type { IImageService } from '../../domain/services/IImageService'; import type { IRaceDetailPresenter, RaceDetailViewModel, RaceDetailRaceViewModel, RaceDetailLeagueViewModel, RaceDetailEntryViewModel, RaceDetailUserResultViewModel, } from '../presenters/IRaceDetailPresenter'; /** * Use Case: GetRaceDetailUseCase * * Given a race id and current driver id: * - When the race exists, it builds a view model with race, league, entry list, registration flags and user result. * - When the race does not exist, it presents a view model with an error and no race data. * * Given a completed race with a result for the driver: * - When computing rating change, it applies the same position-based formula used in the legacy UI. */ export interface GetRaceDetailQueryParams { raceId: string; driverId: string; } export class GetRaceDetailUseCase { constructor( private readonly raceRepository: IRaceRepository, private readonly leagueRepository: ILeagueRepository, private readonly driverRepository: IDriverRepository, private readonly raceRegistrationRepository: IRaceRegistrationRepository, private readonly resultRepository: IResultRepository, private readonly leagueMembershipRepository: ILeagueMembershipRepository, private readonly driverRatingProvider: DriverRatingProvider, private readonly imageService: IImageService, public readonly presenter: IRaceDetailPresenter, ) {} async execute(params: GetRaceDetailQueryParams): Promise { const { raceId, driverId } = params; const race = await this.raceRepository.findById(raceId); if (!race) { const emptyViewModel: RaceDetailViewModel = { race: null, league: null, entryList: [], registration: { isUserRegistered: false, canRegister: false, }, userResult: null, error: 'Race not found', }; this.presenter.present(emptyViewModel); return; } const [league, registeredDriverIds, membership] = await Promise.all([ this.leagueRepository.findById(race.leagueId), this.raceRegistrationRepository.getRegisteredDrivers(race.id), this.leagueMembershipRepository.getMembership(race.leagueId, driverId), ]); const ratings = this.driverRatingProvider.getRatings(registeredDriverIds); const drivers = await Promise.all( registeredDriverIds.map(id => this.driverRepository.findById(id)), ); const entryList: RaceDetailEntryViewModel[] = drivers .filter((d): d is NonNullable => d !== null) .map(driver => ({ id: driver.id, name: driver.name, country: driver.country, avatarUrl: this.imageService.getDriverAvatar(driver.id), rating: ratings.get(driver.id) ?? null, isCurrentUser: driver.id === driverId, })); const isUserRegistered = registeredDriverIds.includes(driverId); const isUpcoming = race.status === 'scheduled' && race.scheduledAt > new Date(); const canRegister = !!membership && membership.status === 'active' && isUpcoming; let userResultView: RaceDetailUserResultViewModel | null = null; if (race.status === 'completed') { const results = await this.resultRepository.findByRaceId(race.id); const userResult = results.find(r => r.driverId === driverId) ?? null; if (userResult) { const ratingChange = this.calculateRatingChange(userResult.position); userResultView = { position: userResult.position, startPosition: userResult.startPosition, incidents: userResult.incidents, fastestLap: userResult.fastestLap, positionChange: userResult.getPositionChange(), isPodium: userResult.isPodium(), isClean: userResult.isClean(), ratingChange, }; } } const raceView: RaceDetailRaceViewModel = { id: race.id, leagueId: race.leagueId, track: race.track, car: race.car, scheduledAt: race.scheduledAt.toISOString(), sessionType: race.sessionType, status: race.status, strengthOfField: race.strengthOfField ?? null, registeredCount: race.registeredCount, maxParticipants: race.maxParticipants, }; const leagueView: RaceDetailLeagueViewModel | null = league ? { id: league.id, name: league.name, description: league.description, settings: { maxDrivers: league.settings.maxDrivers, qualifyingFormat: league.settings.qualifyingFormat, }, } : null; const viewModel: RaceDetailViewModel = { race: raceView, league: leagueView, entryList, registration: { isUserRegistered, canRegister, }, userResult: userResultView, }; this.presenter.present(viewModel); } private calculateRatingChange(position: number): number { const baseChange = position <= 3 ? 25 : position <= 10 ? 10 : -5; const positionBonus = Math.max(0, (20 - position) * 2); return baseChange + positionBonus; } }