refactor racing use cases

This commit is contained in:
2025-12-21 00:43:42 +01:00
parent e9d6f90bb2
commit c12656d671
308 changed files with 14401 additions and 7419 deletions

View File

@@ -4,6 +4,7 @@ import type { IRaceRepository } from '@core/racing/domain/repositories/IRaceRepo
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 { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository';
import type { ChampionshipConfig } from '@core/racing/domain/types/ChampionshipConfig';
import type { SessionType } from '@core/racing/domain/types/SessionType';
@@ -11,27 +12,36 @@ import type { ChampionshipStanding } from '@core/racing/domain/entities/champion
import { EventScoringService } from '@core/racing/domain/services/EventScoringService';
import { ChampionshipAggregator } from '@core/racing/domain/services/ChampionshipAggregator';
import type { ChampionshipStandingsOutputPort } from '../ports/output/ChampionshipStandingsOutputPort';
import type { ChampionshipStandingsRowOutputPort } from '../ports/output/ChampionshipStandingsRowOutputPort';
import type { AsyncUseCase } from '@core/shared/application/AsyncUseCase';
import type { UseCaseOutputPort, Logger } from '@core/shared/application';
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
export interface RecalculateChampionshipStandingsParams {
export type RecalculateChampionshipStandingsInput = {
leagueId: string;
seasonId: string;
championshipId: string;
}
};
type RecalculateChampionshipStandingsErrorCode =
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'
| 'LEAGUE_SCORING_CONFIG_NOT_FOUND'
| 'CHAMPIONSHIP_CONFIG_NOT_FOUND';
| 'REPOSITORY_ERROR';
export class RecalculateChampionshipStandingsUseCase
implements AsyncUseCase<RecalculateChampionshipStandingsParams, ChampionshipStandingsOutputPort, RecalculateChampionshipStandingsErrorCode>
{
export class RecalculateChampionshipStandingsUseCase {
constructor(
private readonly leagueRepository: ILeagueRepository,
private readonly seasonRepository: ISeasonRepository,
private readonly leagueScoringConfigRepository: ILeagueScoringConfigRepository,
private readonly raceRepository: IRaceRepository,
@@ -40,83 +50,120 @@ export class RecalculateChampionshipStandingsUseCase
private readonly championshipStandingRepository: IChampionshipStandingRepository,
private readonly eventScoringService: EventScoringService,
private readonly championshipAggregator: ChampionshipAggregator,
private readonly logger: Logger,
private readonly output: UseCaseOutputPort<RecalculateChampionshipStandingsResult>,
) {}
async execute(params: RecalculateChampionshipStandingsParams): Promise<Result<ChampionshipStandingsOutputPort, ApplicationErrorCode<RecalculateChampionshipStandingsErrorCode>>> {
const { seasonId, championshipId } = params;
async execute(
input: RecalculateChampionshipStandingsInput,
): Promise<
Result<
void,
ApplicationErrorCode<RecalculateChampionshipStandingsErrorCode, { message: string }>
>
> {
const { leagueId, seasonId } = input;
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;
try {
const league = await this.leagueRepository.findById(leagueId);
if (!league) {
return Result.err({
code: 'LEAGUE_NOT_FOUND',
details: { message: `League not found: ${leagueId}` },
});
}
const results = await this.resultRepository.findByRaceId(race.id);
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}`,
},
});
}
// Fetch penalties for this specific race
const penalties = await this.penaltyRepository.findByRaceId(race.id);
const leagueScoringConfig =
await this.leagueScoringConfigRepository.findBySeasonId(seasonId);
if (!leagueScoringConfig) {
throw new Error(`League scoring config not found for season: ${seasonId}`);
}
const participantPoints = this.eventScoringService.scoreSession({
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(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,
sessionType,
results,
penalties,
eventPointsByEventId,
});
eventPointsByEventId[race.id] = participantPoints;
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(),
})),
};
this.output.present(result);
return Result.ok(undefined);
} 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',
},
});
}
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: ChampionshipStandingsOutputPort = {
seasonId,
championshipId: championship.id,
championshipName: championship.name,
rows,
};
return Result.ok(dto);
}
private findChampionshipConfig(championships: ChampionshipConfig[]): ChampionshipConfig {
if (!championships || championships.length === 0) {
throw new Error('No championship configurations found');
}
private mapRaceSessionType(sessionType: string): SessionType {
return championships[0]!;
}
private mapRaceSessionType(sessionType: SessionType | string): SessionType {
if (sessionType === 'race') {
return 'main';
}
@@ -129,4 +176,4 @@ export class RecalculateChampionshipStandingsUseCase
}
return 'main';
}
}
}