refactor racing use cases
This commit is contained in:
@@ -4,16 +4,52 @@ import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepos
|
||||
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
|
||||
import type { ITeamRepository } from '../../domain/repositories/ITeamRepository';
|
||||
import type { LeagueDriverSeasonStatsOutputPort } from '../ports/output/LeagueDriverSeasonStatsOutputPort';
|
||||
import type { AsyncUseCase } from '@core/shared/application';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
|
||||
import type { DriverRatingPort } from '../ports/DriverRatingPort';
|
||||
|
||||
export type DriverSeasonStats = {
|
||||
leagueId: string;
|
||||
driverId: string;
|
||||
position: number;
|
||||
driverName: string;
|
||||
teamId?: string;
|
||||
teamName?: string;
|
||||
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 implements AsyncUseCase<{ leagueId: string }, LeagueDriverSeasonStatsOutputPort, 'NO_ERROR'> {
|
||||
export class GetLeagueDriverSeasonStatsUseCase {
|
||||
constructor(
|
||||
private readonly standingRepository: IStandingRepository,
|
||||
private readonly resultRepository: IResultRepository,
|
||||
@@ -22,105 +58,137 @@ export class GetLeagueDriverSeasonStatsUseCase implements AsyncUseCase<{ leagueI
|
||||
private readonly driverRepository: IDriverRepository,
|
||||
private readonly teamRepository: ITeamRepository,
|
||||
private readonly driverRatingPort: DriverRatingPort,
|
||||
private readonly output: UseCaseOutputPort<GetLeagueDriverSeasonStatsResult>,
|
||||
) {}
|
||||
|
||||
async execute(params: { leagueId: string }): Promise<Result<LeagueDriverSeasonStatsOutputPort, never>> {
|
||||
const { leagueId } = params;
|
||||
async execute(
|
||||
input: GetLeagueDriverSeasonStatsInput,
|
||||
): Promise<
|
||||
Result<void, ApplicationErrorCode<GetLeagueDriverSeasonStatsErrorCode, { message: string }>>
|
||||
> {
|
||||
try {
|
||||
const { leagueId } = input;
|
||||
|
||||
// Get standings and races for the league
|
||||
const [standings, races] = await Promise.all([
|
||||
this.standingRepository.findByLeagueId(leagueId),
|
||||
this.raceRepository.findByLeagueId(leagueId),
|
||||
]);
|
||||
const [standings, races] = await Promise.all([
|
||||
this.standingRepository.findByLeagueId(leagueId),
|
||||
this.raceRepository.findByLeagueId(leagueId),
|
||||
]);
|
||||
|
||||
// Fetch all penalties for all races in the league
|
||||
const penaltiesArrays = await Promise.all(
|
||||
races.map(race => this.penaltyRepository.findByRaceId(race.id))
|
||||
);
|
||||
const penaltiesForLeague = penaltiesArrays.flat();
|
||||
|
||||
// Group penalties by driver for quick lookup
|
||||
const penaltiesByDriver = new Map<string, { baseDelta: number; bonusDelta: number }>();
|
||||
for (const p of penaltiesForLeague) {
|
||||
// Only count applied penalties
|
||||
if (p.status !== 'applied') continue;
|
||||
|
||||
const current = penaltiesByDriver.get(p.driverId) ?? { baseDelta: 0, bonusDelta: 0 };
|
||||
|
||||
// Convert penalty to points delta based on type
|
||||
if (p.type === 'points_deduction' && p.value) {
|
||||
// Points deductions are negative
|
||||
current.baseDelta -= p.value;
|
||||
if (!standings || standings.length === 0) {
|
||||
return Result.err({
|
||||
code: 'LEAGUE_NOT_FOUND',
|
||||
details: { message: 'League not found' },
|
||||
});
|
||||
}
|
||||
|
||||
penaltiesByDriver.set(p.driverId, current);
|
||||
}
|
||||
|
||||
// Collect driver ratings
|
||||
const driverRatings = new Map<string, { rating: number | null; ratingChange: number | null }>();
|
||||
for (const standing of standings) {
|
||||
const ratingInfo = this.driverRatingPort.getRating(standing.driverId);
|
||||
driverRatings.set(standing.driverId, ratingInfo);
|
||||
}
|
||||
|
||||
// Collect driver results
|
||||
const driverResults = new Map<string, Array<{ position: number }>>();
|
||||
for (const standing of standings) {
|
||||
const results = await this.resultRepository.findByDriverIdAndLeagueId(
|
||||
standing.driverId,
|
||||
leagueId,
|
||||
const penaltiesArrays = await Promise.all(
|
||||
races.map(race => this.penaltyRepository.findByRaceId(race.id)),
|
||||
);
|
||||
driverResults.set(standing.driverId, results);
|
||||
}
|
||||
const penaltiesForLeague = penaltiesArrays.flat();
|
||||
|
||||
// Fetch drivers and teams
|
||||
const driverIds = standings.map(s => s.driverId);
|
||||
const drivers = await Promise.all(driverIds.map(id => this.driverRepository.findById(id)));
|
||||
const driversMap = new Map(drivers.filter(d => d).map(d => [d!.id, d!]));
|
||||
const teamIds = Array.from(new Set(drivers.filter(d => d?.teamId).map(d => d!.teamId!)));
|
||||
const teams = await Promise.all(teamIds.map(id => this.teamRepository.findById(id)));
|
||||
const teamsMap = new Map(teams.filter(t => t).map(t => [t!.id, t!]));
|
||||
const penaltiesByDriver = new Map<string, { baseDelta: number; bonusDelta: number }>();
|
||||
for (const p of penaltiesForLeague) {
|
||||
if (p.status !== 'applied') continue;
|
||||
|
||||
// Compute stats
|
||||
const stats = standings.map(standing => {
|
||||
const driver = driversMap.get(standing.driverId);
|
||||
const team = driver?.teamId ? teamsMap.get(driver.teamId) : undefined;
|
||||
const penalties = penaltiesByDriver.get(standing.driverId) ?? { baseDelta: 0, bonusDelta: 0 };
|
||||
const results = driverResults.get(standing.driverId) ?? [];
|
||||
const rating = driverRatings.get(standing.driverId);
|
||||
const current = penaltiesByDriver.get(p.driverId) ?? { baseDelta: 0, bonusDelta: 0 };
|
||||
|
||||
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 pointsPerRace = racesStarted > 0 ? standing.points / racesStarted : 0;
|
||||
if (p.type === 'points_deduction' && p.value) {
|
||||
current.baseDelta -= p.value;
|
||||
}
|
||||
|
||||
return {
|
||||
penaltiesByDriver.set(p.driverId, current);
|
||||
}
|
||||
|
||||
const driverRatings = new Map<string, { rating: number | null; ratingChange: number | null }>();
|
||||
for (const standing of standings) {
|
||||
const driverId = String(standing.driverId);
|
||||
const ratingInfo = this.driverRatingPort.getRating(driverId);
|
||||
driverRatings.set(driverId, ratingInfo);
|
||||
}
|
||||
|
||||
const driverResults = new Map<string, Array<{ position: number }>>();
|
||||
for (const standing of standings) {
|
||||
const driverId = String(standing.driverId);
|
||||
const results = await this.resultRepository.findByDriverIdAndLeagueId(driverId, leagueId);
|
||||
driverResults.set(
|
||||
driverId,
|
||||
results.map(result => ({ position: Number((result as any).position) })),
|
||||
);
|
||||
}
|
||||
|
||||
const driverIds = standings.map(s => String(s.driverId));
|
||||
const drivers = await Promise.all(driverIds.map(id => this.driverRepository.findById(id)));
|
||||
const driversMap = new Map(drivers.filter(d => d).map(d => [String(d!.id), d!]));
|
||||
const teamIds = Array.from(
|
||||
new Set(
|
||||
drivers
|
||||
.filter(d => (d as any)?.teamId)
|
||||
.map(d => (d as any).teamId as string),
|
||||
),
|
||||
);
|
||||
const teams = await Promise.all(teamIds.map(id => this.teamRepository.findById(id)));
|
||||
const teamsMap = new Map(teams.filter(t => t).map(t => [String(t!.id), t!]));
|
||||
|
||||
const stats: DriverSeasonStats[] = standings.map(standing => {
|
||||
const driverId = String(standing.driverId);
|
||||
const driver = driversMap.get(driverId) as any;
|
||||
const teamId = driver?.teamId as string | undefined;
|
||||
const team = teamId ? teamsMap.get(String(teamId)) : undefined;
|
||||
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 = Number(standing.points);
|
||||
const pointsPerRace = racesStarted > 0 ? totalPoints / racesStarted : 0;
|
||||
|
||||
return {
|
||||
leagueId,
|
||||
driverId,
|
||||
position: Number(standing.position),
|
||||
driverName: String(driver?.name ?? ''),
|
||||
teamId,
|
||||
teamName: (team as any)?.name as string | 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,
|
||||
driverId: standing.driverId,
|
||||
position: standing.position,
|
||||
driverName: driver?.name ?? '',
|
||||
teamId: driver?.teamId ?? undefined,
|
||||
teamName: team?.name ?? undefined,
|
||||
totalPoints: standing.points,
|
||||
basePoints: standing.points - penalties.baseDelta,
|
||||
penaltyPoints: penalties.baseDelta,
|
||||
bonusPoints: penalties.bonusDelta,
|
||||
pointsPerRace,
|
||||
racesStarted,
|
||||
racesFinished,
|
||||
dnfs,
|
||||
noShows,
|
||||
avgFinish,
|
||||
rating: rating?.rating ?? null,
|
||||
ratingChange: rating?.ratingChange ?? null,
|
||||
stats,
|
||||
};
|
||||
});
|
||||
|
||||
return Result.ok({
|
||||
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 },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user