Files
gridpilot.gg/core/racing/application/use-cases/GetLeagueDriverSeasonStatsUseCase.ts
2025-12-19 19:42:19 +01:00

126 lines
5.3 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 { IRaceRepository } from '../../domain/repositories/IRaceRepository';
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
import type { ITeamRepository } from '../../domain/repositories/ITeamRepository';
import type { LeagueDriverSeasonStatsOutputPort } from '../ports/output/LeagueDriverSeasonStatsOutputPort';
import type { AsyncUseCase } from '@core/shared/application';
import { Result } from '@core/shared/application/Result';
import type { DriverRatingPort } from '../ports/DriverRatingPort';
/**
* Use Case for retrieving league driver season statistics.
* Orchestrates domain logic and returns the result.
*/
export class GetLeagueDriverSeasonStatsUseCase implements AsyncUseCase<{ leagueId: string }, LeagueDriverSeasonStatsOutputPort, 'NO_ERROR'> {
constructor(
private readonly standingRepository: IStandingRepository,
private readonly resultRepository: IResultRepository,
private readonly penaltyRepository: IPenaltyRepository,
private readonly raceRepository: IRaceRepository,
private readonly driverRepository: IDriverRepository,
private readonly teamRepository: ITeamRepository,
private readonly driverRatingPort: DriverRatingPort,
) {}
async execute(params: { leagueId: string }): Promise<Result<LeagueDriverSeasonStatsOutputPort, never>> {
const { leagueId } = params;
// Get standings and races for the league
const [standings, races] = await Promise.all([
this.standingRepository.findByLeagueId(leagueId),
this.raceRepository.findByLeagueId(leagueId),
]);
// Fetch all penalties for all races in the league
const penaltiesArrays = await Promise.all(
races.map(race => this.penaltyRepository.findByRaceId(race.id))
);
const penaltiesForLeague = penaltiesArrays.flat();
// Group penalties by driver for quick lookup
const penaltiesByDriver = new Map<string, { baseDelta: number; bonusDelta: number }>();
for (const p of penaltiesForLeague) {
// Only count applied penalties
if (p.status !== 'applied') continue;
const current = penaltiesByDriver.get(p.driverId) ?? { baseDelta: 0, bonusDelta: 0 };
// Convert penalty to points delta based on type
if (p.type === 'points_deduction' && p.value) {
// Points deductions are negative
current.baseDelta -= p.value;
}
penaltiesByDriver.set(p.driverId, current);
}
// Collect driver ratings
const driverRatings = new Map<string, { rating: number | null; ratingChange: number | null }>();
for (const standing of standings) {
const ratingInfo = this.driverRatingPort.getRating(standing.driverId);
driverRatings.set(standing.driverId, ratingInfo);
}
// Collect driver results
const driverResults = new Map<string, Array<{ position: number }>>();
for (const standing of standings) {
const results = await this.resultRepository.findByDriverIdAndLeagueId(
standing.driverId,
leagueId,
);
driverResults.set(standing.driverId, results);
}
// Fetch drivers and teams
const driverIds = standings.map(s => s.driverId);
const drivers = await Promise.all(driverIds.map(id => this.driverRepository.findById(id)));
const driversMap = new Map(drivers.filter(d => d).map(d => [d!.id, d!]));
const teamIds = Array.from(new Set(drivers.filter(d => d?.teamId).map(d => d!.teamId!)));
const teams = await Promise.all(teamIds.map(id => this.teamRepository.findById(id)));
const teamsMap = new Map(teams.filter(t => t).map(t => [t!.id, t!]));
// Compute stats
const stats = standings.map(standing => {
const driver = driversMap.get(standing.driverId);
const team = driver?.teamId ? teamsMap.get(driver.teamId) : undefined;
const penalties = penaltiesByDriver.get(standing.driverId) ?? { baseDelta: 0, bonusDelta: 0 };
const results = driverResults.get(standing.driverId) ?? [];
const rating = driverRatings.get(standing.driverId);
const racesStarted = results.length;
const racesFinished = results.filter(r => r.position > 0).length;
const dnfs = results.filter(r => r.position === 0).length;
const noShows = races.length - racesStarted;
const avgFinish = results.length > 0 ? results.reduce((sum, r) => sum + r.position, 0) / results.length : null;
const pointsPerRace = racesStarted > 0 ? standing.points / racesStarted : 0;
return {
leagueId,
driverId: standing.driverId,
position: standing.position,
driverName: driver?.name ?? '',
teamId: driver?.teamId ?? undefined,
teamName: team?.name ?? undefined,
totalPoints: standing.points,
basePoints: standing.points - penalties.baseDelta,
penaltyPoints: penalties.baseDelta,
bonusPoints: penalties.bonusDelta,
pointsPerRace,
racesStarted,
racesFinished,
dnfs,
noShows,
avgFinish,
rating: rating?.rating ?? null,
ratingChange: rating?.ratingChange ?? null,
};
});
return Result.ok({
leagueId,
stats,
});
}
}