import type { RaceDetailOutputPort } from '@core/racing/application/ports/output/RaceDetailOutputPort'; import type { DriverRatingProvider } from '@core/racing/application/ports/DriverRatingProvider'; import type { IImageServicePort } from '@core/racing/application/ports/IImageServicePort'; import type { GetRaceDetailParamsDTO } from '../dtos/GetRaceDetailParamsDTO'; import type { RaceDetailDTO } from '../dtos/RaceDetailDTO'; import type { RaceDetailRaceDTO } from '../dtos/RaceDetailRaceDTO'; import type { RaceDetailLeagueDTO } from '../dtos/RaceDetailLeagueDTO'; import type { RaceDetailEntryDTO } from '../dtos/RaceDetailEntryDTO'; import type { RaceDetailRegistrationDTO } from '../dtos/RaceDetailRegistrationDTO'; import type { RaceDetailUserResultDTO } from '../dtos/RaceDetailUserResultDTO'; export class RaceDetailPresenter { private result: RaceDetailDTO | null = null; constructor( private readonly driverRatingProvider: DriverRatingProvider, private readonly imageService: IImageServicePort, ) {} async present(outputPort: RaceDetailOutputPort, params: GetRaceDetailParamsDTO): Promise { const raceDTO: RaceDetailRaceDTO | null = outputPort.race ? { id: outputPort.race.id, leagueId: outputPort.race.leagueId, track: outputPort.race.track, car: outputPort.race.car, scheduledAt: outputPort.race.scheduledAt.toISOString(), sessionType: outputPort.race.sessionType, status: outputPort.race.status, strengthOfField: outputPort.race.strengthOfField ?? null, registeredCount: outputPort.race.registeredCount ?? undefined, maxParticipants: outputPort.race.maxParticipants ?? undefined, } : null; const leagueDTO: RaceDetailLeagueDTO | null = outputPort.league ? { id: outputPort.league.id.toString(), name: outputPort.league.name.toString(), description: outputPort.league.description.toString(), settings: { maxDrivers: outputPort.league.settings.maxDrivers ?? undefined, qualifyingFormat: outputPort.league.settings.qualifyingFormat ?? undefined, }, } : null; const entryListDTO: RaceDetailEntryDTO[] = await Promise.all( outputPort.drivers.map(async driver => { const ratingResult = await this.driverRatingProvider.getDriverRating({ driverId: driver.id }); const avatarResult = await this.imageService.getDriverAvatar({ driverId: driver.id }); return { id: driver.id, name: driver.name.toString(), country: driver.country.toString(), avatarUrl: avatarResult.avatarUrl, rating: ratingResult.rating, isCurrentUser: driver.id === params.driverId, }; }), ); const registrationDTO: RaceDetailRegistrationDTO = { isUserRegistered: outputPort.isUserRegistered, canRegister: outputPort.canRegister, }; const userResultDTO: RaceDetailUserResultDTO | null = outputPort.userResult ? { position: outputPort.userResult.position.toNumber(), startPosition: outputPort.userResult.startPosition.toNumber(), incidents: outputPort.userResult.incidents.toNumber(), fastestLap: outputPort.userResult.fastestLap.toNumber(), positionChange: outputPort.userResult.getPositionChange(), isPodium: outputPort.userResult.isPodium(), isClean: outputPort.userResult.isClean(), ratingChange: this.calculateRatingChange(outputPort.userResult.position.toNumber()), } : null; this.result = { race: raceDTO, league: leagueDTO, entryList: entryListDTO, registration: registrationDTO, userResult: userResultDTO, } as RaceDetailDTO; } getViewModel(): RaceDetailDTO | null { return this.result; } get viewModel(): RaceDetailDTO { if (!this.result) { throw new Error('Presenter not presented'); } return this.result; } 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; } }