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