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 { IImageServicePort } from '../ports/IImageServicePort'; import type { RaceDetailViewModel, RaceDetailRaceViewModel, RaceDetailLeagueViewModel, RaceDetailEntryViewModel, RaceDetailUserResultViewModel, } from '../presenters/IRaceDetailPresenter'; import type { AsyncUseCase } from '@core/shared/application/AsyncUseCase'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; /** * 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; } type GetRaceDetailErrorCode = 'RACE_NOT_FOUND'; export class GetRaceDetailUseCase implements AsyncUseCase { 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: IImageServicePort, ) {} async execute(params: GetRaceDetailQueryParams): Promise>> { const { raceId, driverId } = params; const race = await this.raceRepository.findById(raceId); if (!race) { return Result.err({ code: 'RACE_NOT_FOUND' }); } 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, ...(race.registeredCount !== undefined ? { registeredCount: race.registeredCount } : {}), ...(race.maxParticipants !== undefined ? { maxParticipants: race.maxParticipants } : {}), }; const leagueView: RaceDetailLeagueViewModel | null = league ? { id: league.id, name: league.name, description: league.description, settings: { ...(league.settings.maxDrivers !== undefined ? { maxDrivers: league.settings.maxDrivers } : {}), ...(league.settings.qualifyingFormat !== undefined ? { qualifyingFormat: league.settings.qualifyingFormat } : {}), }, } : null; const viewModel: RaceDetailViewModel = { race: raceView, league: leagueView, entryList, registration: { isUserRegistered, canRegister, }, userResult: userResultView, }; return Result.ok(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; } }