This commit is contained in:
2025-12-08 23:52:36 +01:00
parent 2d0860d66c
commit 35f988f885
46 changed files with 4624 additions and 1041 deletions

View File

@@ -0,0 +1,99 @@
/**
* Application Query: GetLeagueStatsQuery
*
* Returns league statistics including average SOF across completed races.
*/
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
import type { IResultRepository } from '../../domain/repositories/IResultRepository';
import type { DriverRatingProvider } from '../ports/DriverRatingProvider';
import {
AverageStrengthOfFieldCalculator,
type StrengthOfFieldCalculator,
} from '../../domain/services/StrengthOfFieldCalculator';
export interface GetLeagueStatsQueryParams {
leagueId: string;
}
export interface LeagueStatsDTO {
leagueId: string;
totalRaces: number;
completedRaces: number;
scheduledRaces: number;
averageSOF: number | null;
highestSOF: number | null;
lowestSOF: number | null;
}
export class GetLeagueStatsQuery {
private readonly sofCalculator: StrengthOfFieldCalculator;
constructor(
private readonly leagueRepository: ILeagueRepository,
private readonly raceRepository: IRaceRepository,
private readonly resultRepository: IResultRepository,
private readonly driverRatingProvider: DriverRatingProvider,
sofCalculator?: StrengthOfFieldCalculator,
) {
this.sofCalculator = sofCalculator ?? new AverageStrengthOfFieldCalculator();
}
async execute(params: GetLeagueStatsQueryParams): Promise<LeagueStatsDTO | null> {
const { leagueId } = params;
const league = await this.leagueRepository.findById(leagueId);
if (!league) {
return null;
}
const races = await this.raceRepository.findByLeagueId(leagueId);
const completedRaces = races.filter(r => r.status === 'completed');
const scheduledRaces = races.filter(r => r.status === 'scheduled');
// Calculate SOF for each completed race
const sofValues: number[] = [];
for (const race of completedRaces) {
// Use stored SOF if available
if (race.strengthOfField) {
sofValues.push(race.strengthOfField);
continue;
}
// Otherwise calculate from results
const results = await this.resultRepository.findByRaceId(race.id);
if (results.length === 0) continue;
const driverIds = results.map(r => r.driverId);
const ratings = this.driverRatingProvider.getRatings(driverIds);
const driverRatings = driverIds
.filter(id => ratings.has(id))
.map(id => ({ driverId: id, rating: ratings.get(id)! }));
const sof = this.sofCalculator.calculate(driverRatings);
if (sof !== null) {
sofValues.push(sof);
}
}
// Calculate aggregate stats
const averageSOF = sofValues.length > 0
? Math.round(sofValues.reduce((a, b) => a + b, 0) / sofValues.length)
: null;
const highestSOF = sofValues.length > 0 ? Math.max(...sofValues) : null;
const lowestSOF = sofValues.length > 0 ? Math.min(...sofValues) : null;
return {
leagueId,
totalRaces: races.length,
completedRaces: completedRaces.length,
scheduledRaces: scheduledRaces.length,
averageSOF,
highestSOF,
lowestSOF,
};
}
}