refactor racing use cases
This commit is contained in:
@@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user