Files
gridpilot.gg/packages/racing/application/use-cases/GetLeagueDriverSeasonStatsQuery.ts
2025-12-04 23:31:55 +01:00

97 lines
3.7 KiB
TypeScript

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<LeagueDriverSeasonStatsDTO[]> {
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<string, { baseDelta: number; bonusDelta: number }>();
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<string, LeagueDriverSeasonStatsDTO>();
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;
}
}