Files
gridpilot.gg/core/racing/application/use-cases/RecalculateChampionshipStandingsUseCase.ts
2026-01-16 19:46:49 +01:00

175 lines
5.9 KiB
TypeScript

import { ChampionshipAggregator } from '@core/racing/domain/services/ChampionshipAggregator';
import { EventScoringService } from '@core/racing/domain/services/EventScoringService';
import type { ChampionshipConfig } from '@core/racing/domain/types/ChampionshipConfig';
import type { SessionType } from '@core/racing/domain/types/SessionType';
import { ChampionshipStanding } from '../../domain/entities/championship/ChampionshipStanding';
import type { Logger } from '@core/shared/domain/Logger';
import { Result } from '@core/shared/domain/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import { ChampionshipStandingRepository } from '../../domain/repositories/ChampionshipStandingRepository';
import { LeagueRepository } from '../../domain/repositories/LeagueRepository';
import { LeagueScoringConfigRepository } from '../../domain/repositories/LeagueScoringConfigRepository';
import { PenaltyRepository } from '../../domain/repositories/PenaltyRepository';
import { RaceRepository } from '../../domain/repositories/RaceRepository';
import { ResultRepository } from '../../domain/repositories/ResultRepository';
import { SeasonRepository } from '../../domain/repositories/SeasonRepository';
export type RecalculateChampionshipStandingsInput = {
leagueId: string;
seasonId: string;
};
export type ChampionshipStandingsEntry = {
driverId: string | null;
teamId: string | null;
position: number;
points: number;
};
export type RecalculateChampionshipStandingsResult = {
leagueId: string;
seasonId: string;
entries: ChampionshipStandingsEntry[];
};
export type RecalculateChampionshipStandingsErrorCode =
| 'LEAGUE_NOT_FOUND'
| 'SEASON_NOT_FOUND'
| 'REPOSITORY_ERROR';
export class RecalculateChampionshipStandingsUseCase {
constructor(private readonly leagueRepository: LeagueRepository,
private readonly seasonRepository: SeasonRepository,
private readonly leagueScoringConfigRepository: LeagueScoringConfigRepository,
private readonly raceRepository: RaceRepository,
private readonly resultRepository: ResultRepository,
private readonly penaltyRepository: PenaltyRepository,
private readonly championshipStandingRepository: ChampionshipStandingRepository,
private readonly eventScoringService: EventScoringService,
private readonly championshipAggregator: ChampionshipAggregator,
private readonly logger: Logger) {}
async execute(
input: RecalculateChampionshipStandingsInput,
): Promise<
Result<
RecalculateChampionshipStandingsResult,
ApplicationErrorCode<RecalculateChampionshipStandingsErrorCode, { message: string }>
>
> {
const { leagueId, seasonId } = input;
try {
const league = await this.leagueRepository.findById(leagueId);
if (!league) {
return Result.err({
code: 'LEAGUE_NOT_FOUND',
details: { message: `League not found: ${leagueId}` },
});
}
const season = await this.seasonRepository.findById(seasonId);
if (!season || season.leagueId !== leagueId) {
return Result.err({
code: 'SEASON_NOT_FOUND',
details: {
message: `Season not found for league: leagueId=${leagueId}, seasonId=${seasonId}`,
},
});
}
const leagueScoringConfig =
await this.leagueScoringConfigRepository.findBySeasonId(seasonId);
if (!leagueScoringConfig) {
throw new Error(`League scoring config not found for season: ${seasonId}`);
}
const championship = this.findChampionshipConfig(leagueScoringConfig.championships);
const races = await this.raceRepository.findByLeagueId(leagueId);
const eventPointsByEventId: Record<string, ReturnType<EventScoringService['scoreSession']>> =
{};
for (const race of races) {
const sessionType = this.mapRaceSessionType(String(race.sessionType));
if (!championship.sessionTypes.includes(sessionType)) {
continue;
}
const results = await this.resultRepository.findByRaceId(race.id);
const penalties = await this.penaltyRepository.findByRaceId(race.id);
const participantPoints = this.eventScoringService.scoreSession({
seasonId,
championship,
sessionType,
results,
penalties,
});
eventPointsByEventId[race.id] = participantPoints;
}
const standings: ChampionshipStanding[] = this.championshipAggregator.aggregate({
seasonId,
championship,
eventPointsByEventId,
});
await this.championshipStandingRepository.saveAll(standings);
const result: RecalculateChampionshipStandingsResult = {
leagueId,
seasonId,
entries: standings.map((standing) => ({
driverId: standing.participant?.id ?? null,
teamId: null,
position: standing.position.toNumber(),
points: standing.totalPoints.toNumber(),
})),
};
return Result.ok(result);
} catch (error) {
const err = error as Error;
this.logger.error('Failed to recalculate championship standings', err, {
leagueId,
seasonId,
});
return Result.err({
code: 'REPOSITORY_ERROR',
details: {
message:
err.message || 'Failed to recalculate championship standings',
},
});
}
}
private findChampionshipConfig(championships: ChampionshipConfig[]): ChampionshipConfig {
if (!championships || championships.length === 0) {
throw new Error('No championship configurations found');
}
return championships[0]!;
}
private mapRaceSessionType(sessionType: SessionType | string): SessionType {
if (sessionType === 'race') {
return 'main';
}
if (
sessionType === 'practice' ||
sessionType === 'qualifying' ||
sessionType === 'timeTrial'
) {
return sessionType;
}
return 'main';
}
}