import type { IStandingRepository } from '../../domain/repositories/IStandingRepository'; import type { IResultRepository } from '../../domain/repositories/IResultRepository'; import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository'; import type { LeagueDriverSeasonStatsDTO } from '../dto/LeagueDriverSeasonStatsDTO'; export interface DriverRatingPort { getRating(driverId: string): { rating: number | null; ratingChange: number | null }; } export interface GetLeagueDriverSeasonStatsQueryParamsDTO { leagueId: string; } export class GetLeagueDriverSeasonStatsQuery { constructor( private readonly standingRepository: IStandingRepository, private readonly resultRepository: IResultRepository, private readonly penaltyRepository: IPenaltyRepository, private readonly driverRatingPort: DriverRatingPort, ) {} async execute(params: GetLeagueDriverSeasonStatsQueryParamsDTO): Promise { const { leagueId } = params; const [standings, penaltiesForLeague] = await Promise.all([ this.standingRepository.findByLeagueId(leagueId), this.penaltyRepository.findByLeagueId(leagueId), ]); // Group penalties by driver for quick lookup const penaltiesByDriver = new Map(); for (const p of penaltiesForLeague) { const current = penaltiesByDriver.get(p.driverId) ?? { baseDelta: 0, bonusDelta: 0 }; if (p.pointsDelta < 0) { current.baseDelta += p.pointsDelta; } else { current.bonusDelta += p.pointsDelta; } penaltiesByDriver.set(p.driverId, current); } // Build basic stats per driver from standings const statsByDriver = new Map(); for (const standing of standings) { const penalty = penaltiesByDriver.get(standing.driverId) ?? { baseDelta: 0, bonusDelta: 0 }; const totalPenaltyPoints = penalty.baseDelta; const bonusPoints = penalty.bonusDelta; const racesCompleted = standing.racesCompleted; const pointsPerRace = racesCompleted > 0 ? standing.points / racesCompleted : 0; const ratingInfo = this.driverRatingPort.getRating(standing.driverId); const dto: LeagueDriverSeasonStatsDTO = { leagueId, driverId: standing.driverId, position: standing.position, driverName: '', teamId: undefined, teamName: undefined, totalPoints: standing.points + totalPenaltyPoints + bonusPoints, basePoints: standing.points, penaltyPoints: Math.abs(totalPenaltyPoints), bonusPoints, pointsPerRace, racesStarted: racesCompleted, racesFinished: racesCompleted, dnfs: 0, noShows: 0, avgFinish: null, rating: ratingInfo.rating, ratingChange: ratingInfo.ratingChange, }; statsByDriver.set(standing.driverId, dto); } // Enhance stats with basic finish-position-based avgFinish from results for (const [driverId, dto] of statsByDriver.entries()) { const driverResults = await this.resultRepository.findByDriverIdAndLeagueId(driverId, leagueId); if (driverResults.length > 0) { const totalPositions = driverResults.reduce((sum, r) => sum + r.position, 0); const avgFinish = totalPositions / driverResults.length; dto.avgFinish = Number.isFinite(avgFinish) ? Number(avgFinish.toFixed(2)) : null; dto.racesStarted = driverResults.length; dto.racesFinished = driverResults.length; } statsByDriver.set(driverId, dto); } // Ensure ordering by position const result = Array.from(statsByDriver.values()).sort((a, b) => a.position - b.position); return result; } }