Files
gridpilot.gg/core/racing/application/use-cases/GetLeagueDriverSeasonStatsUseCase.ts
2025-12-23 15:38:50 +01:00

186 lines
6.4 KiB
TypeScript

import { Result } from '@core/shared/application/Result';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository';
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
import type { IResultRepository } from '../../domain/repositories/IResultRepository';
import type { IStandingRepository } from '../../domain/repositories/IStandingRepository';
import type { DriverRatingPort } from '../ports/DriverRatingPort';
export type DriverSeasonStats = {
leagueId: string;
driverId: string;
position: number;
driverName: string;
teamId: string | undefined;
teamName: string | undefined;
totalPoints: number;
basePoints: number;
penaltyPoints: number;
bonusPoints: number;
pointsPerRace: number;
racesStarted: number;
racesFinished: number;
dnfs: number;
noShows: number;
avgFinish: number | null;
rating: number | null;
ratingChange: number | null;
};
export type GetLeagueDriverSeasonStatsInput = {
leagueId: string;
};
export type GetLeagueDriverSeasonStatsResult = {
leagueId: string;
stats: DriverSeasonStats[];
};
export type GetLeagueDriverSeasonStatsErrorCode =
| 'LEAGUE_NOT_FOUND'
| 'SEASON_NOT_FOUND'
| 'DRIVER_NOT_FOUND'
| 'REPOSITORY_ERROR';
/**
* Use Case for retrieving league driver season statistics.
* Orchestrates domain logic and returns the result.
*/
export class GetLeagueDriverSeasonStatsUseCase {
constructor(
private readonly standingRepository: IStandingRepository,
private readonly resultRepository: IResultRepository,
private readonly penaltyRepository: IPenaltyRepository,
private readonly raceRepository: IRaceRepository,
private readonly driverRepository: IDriverRepository,
private readonly driverRatingPort: DriverRatingPort,
private readonly output: UseCaseOutputPort<GetLeagueDriverSeasonStatsResult>,
) {}
async execute(
input: GetLeagueDriverSeasonStatsInput,
): Promise<
Result<void, ApplicationErrorCode<GetLeagueDriverSeasonStatsErrorCode, { message: string }>>
> {
try {
const { leagueId } = input;
const [standings, races] = await Promise.all([
this.standingRepository.findByLeagueId(leagueId),
this.raceRepository.findByLeagueId(leagueId),
]);
if (!standings || standings.length === 0) {
return Result.err({
code: 'LEAGUE_NOT_FOUND',
details: { message: 'League not found' },
});
}
const penaltiesArrays = await Promise.all(
races.map(race => this.penaltyRepository.findByRaceId(race.id)),
);
const penaltiesForLeague = penaltiesArrays.flat();
const penaltiesByDriver = new Map<string, { baseDelta: number; bonusDelta: number }>();
for (const p of penaltiesForLeague) {
if (p.status !== 'applied') continue;
const current = penaltiesByDriver.get(p.driverId) ?? { baseDelta: 0, bonusDelta: 0 };
if (p.type === 'points_deduction' && p.value) {
current.baseDelta -= p.value;
}
penaltiesByDriver.set(p.driverId, current);
}
const driverRatings = new Map<string, { rating: number | null; ratingChange: number | null }>();
for (const standing of standings) {
const driverId = standing.driverId.toString();
const rating = await this.driverRatingPort.getDriverRating(driverId);
driverRatings.set(driverId, { rating, ratingChange: null });
}
const driverResults = new Map<string, Array<{ position: number }>>();
for (const standing of standings) {
const driverId = standing.driverId.toString();
const results = await this.resultRepository.findByDriverIdAndLeagueId(driverId, leagueId);
driverResults.set(
driverId,
results.map(result => ({ position: result.position.toNumber() })),
);
}
const driverIds = standings.map(s => s.driverId.toString());
const drivers = await Promise.all(driverIds.map(id => this.driverRepository.findById(id)));
const driversMap = new Map(
drivers
.filter((driver): driver is NonNullable<typeof driver> => driver !== null)
.map(driver => [driver.id, driver]),
);
const stats: DriverSeasonStats[] = standings.map(standing => {
const driverId = standing.driverId.toString();
const driver = driversMap.get(driverId);
const penalties = penaltiesByDriver.get(driverId) ?? { baseDelta: 0, bonusDelta: 0 };
const results = driverResults.get(driverId) ?? [];
const rating = driverRatings.get(driverId);
const racesStarted = results.length;
const racesFinished = results.filter(r => r.position > 0).length;
const dnfs = results.filter(r => r.position === 0).length;
const noShows = races.length - racesStarted;
const avgFinish =
results.length > 0
? results.reduce((sum, r) => sum + r.position, 0) / results.length
: null;
const totalPoints = standing.points.toNumber();
const pointsPerRace = racesStarted > 0 ? totalPoints / racesStarted : 0;
return {
leagueId,
driverId,
position: standing.position.toNumber(),
driverName: driver ? driver.name.toString() : '',
teamId: undefined,
teamName: undefined,
totalPoints,
basePoints: totalPoints - penalties.baseDelta,
penaltyPoints: penalties.baseDelta,
bonusPoints: penalties.bonusDelta,
pointsPerRace,
racesStarted,
racesFinished,
dnfs,
noShows,
avgFinish,
rating: rating?.rating ?? null,
ratingChange: rating?.ratingChange ?? null,
};
});
const result: GetLeagueDriverSeasonStatsResult = {
leagueId,
stats,
};
this.output.present(result);
return Result.ok(undefined);
} catch (error) {
const message =
error instanceof Error && error.message
? error.message
: 'Failed to fetch league driver season stats';
return Result.err({
code: 'REPOSITORY_ERROR',
details: { message },
});
}
}
}