Files
gridpilot.gg/core/racing/application/use-cases/RecalculateChampionshipStandingsUseCase.ts
2025-12-17 00:33:13 +01:00

134 lines
5.2 KiB
TypeScript

import type { ISeasonRepository } from '@core/racing/domain/repositories/ISeasonRepository';
import type { ILeagueScoringConfigRepository } from '@core/racing/domain/repositories/ILeagueScoringConfigRepository';
import type { IRaceRepository } from '@core/racing/domain/repositories/IRaceRepository';
import type { IResultRepository } from '@core/racing/domain/repositories/IResultRepository';
import type { IPenaltyRepository } from '@core/racing/domain/repositories/IPenaltyRepository';
import type { IChampionshipStandingRepository } from '@core/racing/domain/repositories/IChampionshipStandingRepository';
import type { ChampionshipConfig } from '@core/racing/domain/types/ChampionshipConfig';
import type { SessionType } from '@core/racing/domain/types/SessionType';
import type { ChampionshipStanding } from '@core/racing/domain/entities/championship/ChampionshipStanding';
import { EventScoringService } from '@core/racing/domain/services/EventScoringService';
import { ChampionshipAggregator } from '@core/racing/domain/services/ChampionshipAggregator';
import type {
ChampionshipStandingsDTO,
ChampionshipStandingsRowDTO,
} from '../dto/ChampionshipStandingsDTO';
import type { AsyncUseCase } from '@core/shared/application/AsyncUseCase';
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
export interface RecalculateChampionshipStandingsParams {
seasonId: string;
championshipId: string;
}
type RecalculateChampionshipStandingsErrorCode =
| 'SEASON_NOT_FOUND'
| 'LEAGUE_SCORING_CONFIG_NOT_FOUND'
| 'CHAMPIONSHIP_CONFIG_NOT_FOUND';
export class RecalculateChampionshipStandingsUseCase
implements AsyncUseCase<RecalculateChampionshipStandingsParams, ChampionshipStandingsDTO, RecalculateChampionshipStandingsErrorCode>
{
constructor(
private readonly seasonRepository: ISeasonRepository,
private readonly leagueScoringConfigRepository: ILeagueScoringConfigRepository,
private readonly raceRepository: IRaceRepository,
private readonly resultRepository: IResultRepository,
private readonly penaltyRepository: IPenaltyRepository,
private readonly championshipStandingRepository: IChampionshipStandingRepository,
private readonly eventScoringService: EventScoringService,
private readonly championshipAggregator: ChampionshipAggregator,
) {}
async execute(params: RecalculateChampionshipStandingsParams): Promise<Result<ChampionshipStandingsDTO, ApplicationErrorCode<RecalculateChampionshipStandingsErrorCode>>> {
const { seasonId, championshipId } = params;
const season = await this.seasonRepository.findById(seasonId);
if (!season) {
return Result.err({ code: 'SEASON_NOT_FOUND', details: { message: `Season not found: ${seasonId}` } });
}
const leagueScoringConfig =
await this.leagueScoringConfigRepository.findBySeasonId(seasonId);
if (!leagueScoringConfig) {
return Result.err({ code: 'LEAGUE_SCORING_CONFIG_NOT_FOUND', details: { message: `League scoring config not found for season: ${seasonId}` } });
}
const championship = leagueScoringConfig.championships.find((c) => c.id === championshipId);
if (!championship) {
return Result.err({ code: 'CHAMPIONSHIP_CONFIG_NOT_FOUND', details: { message: `Championship config not found: ${championshipId}` } });
}
const races = await this.raceRepository.findByLeagueId(season.leagueId);
const eventPointsByEventId: Record<string, ReturnType<EventScoringService['scoreSession']>> =
{};
for (const race of races) {
// Map existing Race.sessionType into scoring SessionType where possible.
const sessionType = this.mapRaceSessionType(race.sessionType);
if (!championship.sessionTypes.includes(sessionType)) {
continue;
}
const results = await this.resultRepository.findByRaceId(race.id);
// Fetch penalties for this specific race
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 rows: ChampionshipStandingsRowDTO[] = standings.map((s) => ({
participant: s.participant,
position: s.position.toNumber(),
totalPoints: s.totalPoints.toNumber(),
resultsCounted: s.resultsCounted.toNumber(),
resultsDropped: s.resultsDropped.toNumber(),
}));
const dto: ChampionshipStandingsDTO = {
seasonId,
championshipId: championship.id,
championshipName: championship.name,
rows,
};
return Result.ok(dto);
}
private mapRaceSessionType(sessionType: string): SessionType {
if (sessionType === 'race') {
return 'main';
}
if (
sessionType === 'practice' ||
sessionType === 'qualifying' ||
sessionType === 'timeTrial'
) {
return sessionType;
}
return 'main';
}
}