179 lines
6.4 KiB
TypeScript
179 lines
6.4 KiB
TypeScript
import { RaceService } from '../races/RaceService';
|
|
import { ProtestService } from '../protests/ProtestService';
|
|
import { PenaltyService } from '../penalties/PenaltyService';
|
|
import { DriverService } from '../drivers/DriverService';
|
|
import { LeagueMembershipService } from './LeagueMembershipService';
|
|
import { LeagueStewardingViewModel, RaceWithProtests } from '../../view-models/LeagueStewardingViewModel';
|
|
import type { ProtestDetailViewModel } from '../../view-models/ProtestDetailViewModel';
|
|
|
|
/**
|
|
* League Stewarding Service
|
|
*
|
|
* Orchestrates league stewarding operations by coordinating calls to race, protest, penalty, driver, and membership services.
|
|
* All dependencies are injected via constructor.
|
|
*/
|
|
export class LeagueStewardingService {
|
|
private getPenaltyValueLabel(valueKind: string): string {
|
|
switch (valueKind) {
|
|
case 'seconds':
|
|
return 'seconds';
|
|
case 'grid_positions':
|
|
return 'positions';
|
|
case 'points':
|
|
return 'points';
|
|
case 'races':
|
|
return 'races';
|
|
case 'none':
|
|
return '';
|
|
default:
|
|
return '';
|
|
}
|
|
}
|
|
|
|
private getFallbackDefaultPenaltyValue(valueKind: string): number {
|
|
switch (valueKind) {
|
|
case 'seconds':
|
|
return 5;
|
|
case 'grid_positions':
|
|
return 3;
|
|
case 'points':
|
|
return 5;
|
|
case 'races':
|
|
return 1;
|
|
case 'none':
|
|
return 0;
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
constructor(
|
|
private readonly raceService: RaceService,
|
|
private readonly protestService: ProtestService,
|
|
private readonly penaltyService: PenaltyService,
|
|
private readonly driverService: DriverService,
|
|
private readonly leagueMembershipService: LeagueMembershipService
|
|
) {}
|
|
|
|
/**
|
|
* Get league stewarding data for all races in a league
|
|
*/
|
|
async getLeagueStewardingData(leagueId: string): Promise<LeagueStewardingViewModel> {
|
|
// Get all races for this league
|
|
const leagueRaces = await this.raceService.findByLeagueId(leagueId);
|
|
|
|
// Get protests and penalties for each race
|
|
const protestsMap: Record<string, any[]> = {};
|
|
const penaltiesMap: Record<string, any[]> = {};
|
|
const driverIds = new Set<string>();
|
|
|
|
for (const race of leagueRaces) {
|
|
const raceProtests = await this.protestService.findByRaceId(race.id);
|
|
const racePenalties = await this.penaltyService.findByRaceId(race.id);
|
|
|
|
protestsMap[race.id] = raceProtests;
|
|
penaltiesMap[race.id] = racePenalties;
|
|
|
|
// Collect driver IDs
|
|
raceProtests.forEach((p: any) => {
|
|
driverIds.add(p.protestingDriverId);
|
|
driverIds.add(p.accusedDriverId);
|
|
});
|
|
racePenalties.forEach((p: any) => {
|
|
driverIds.add(p.driverId);
|
|
});
|
|
}
|
|
|
|
// Load driver info
|
|
const driverEntities = await this.driverService.findByIds(Array.from(driverIds));
|
|
const driverMap: Record<string, any> = {};
|
|
driverEntities.forEach((driver) => {
|
|
if (driver) {
|
|
driverMap[driver.id] = driver;
|
|
}
|
|
});
|
|
|
|
// Compute race data with protest/penalty info
|
|
const racesWithData: RaceWithProtests[] = leagueRaces.map(race => {
|
|
const protests = protestsMap[race.id] || [];
|
|
const penalties = penaltiesMap[race.id] || [];
|
|
return {
|
|
race: {
|
|
id: race.id,
|
|
track: race.track,
|
|
scheduledAt: new Date(race.scheduledAt),
|
|
},
|
|
pendingProtests: protests.filter(p => p.status === 'pending' || p.status === 'under_review'),
|
|
resolvedProtests: protests.filter(p => p.status === 'upheld' || p.status === 'dismissed' || p.status === 'withdrawn'),
|
|
penalties
|
|
};
|
|
}).sort((a, b) => b.race.scheduledAt.getTime() - a.race.scheduledAt.getTime());
|
|
|
|
return new LeagueStewardingViewModel(racesWithData, driverMap);
|
|
}
|
|
|
|
/**
|
|
* Get protest review details as a page-ready view model
|
|
*/
|
|
async getProtestDetailViewModel(leagueId: string, protestId: string): Promise<ProtestDetailViewModel> {
|
|
const [protestData, penaltyTypesReference] = await Promise.all([
|
|
this.protestService.getProtestById(leagueId, protestId),
|
|
this.penaltyService.getPenaltyTypesReference(),
|
|
]);
|
|
|
|
if (!protestData) {
|
|
throw new Error('Protest not found');
|
|
}
|
|
|
|
const penaltyUiDefaults: Record<string, { label: string; description: string; defaultValue: number }> = {
|
|
time_penalty: { label: 'Time Penalty', description: 'Add seconds to race result', defaultValue: 5 },
|
|
grid_penalty: { label: 'Grid Penalty', description: 'Grid positions for next race', defaultValue: 3 },
|
|
points_deduction: { label: 'Points Deduction', description: 'Deduct championship points', defaultValue: 5 },
|
|
disqualification: { label: 'Disqualification', description: 'Disqualify from race', defaultValue: 0 },
|
|
warning: { label: 'Warning', description: 'Official warning only', defaultValue: 0 },
|
|
license_points: { label: 'License Points', description: 'Safety rating penalty', defaultValue: 2 },
|
|
};
|
|
|
|
const penaltyTypes = (penaltyTypesReference?.penaltyTypes ?? []).map((ref: any) => {
|
|
const ui = penaltyUiDefaults[ref.type];
|
|
const valueLabel = this.getPenaltyValueLabel(String(ref.valueKind ?? 'none'));
|
|
const defaultValue = ui?.defaultValue ?? this.getFallbackDefaultPenaltyValue(String(ref.valueKind ?? 'none'));
|
|
|
|
return {
|
|
type: String(ref.type),
|
|
label: ui?.label ?? String(ref.type).replaceAll('_', ' '),
|
|
description: ui?.description ?? '',
|
|
requiresValue: Boolean(ref.requiresValue),
|
|
valueLabel,
|
|
defaultValue,
|
|
};
|
|
});
|
|
|
|
const timePenalty = penaltyTypes.find((p) => p.type === 'time_penalty');
|
|
const initial = timePenalty ?? penaltyTypes[0];
|
|
|
|
return {
|
|
protest: protestData.protest,
|
|
race: protestData.race,
|
|
protestingDriver: protestData.protestingDriver,
|
|
accusedDriver: protestData.accusedDriver,
|
|
penaltyTypes,
|
|
defaultReasons: penaltyTypesReference?.defaultReasons ?? { upheld: '', dismissed: '' },
|
|
initialPenaltyType: initial?.type ?? null,
|
|
initialPenaltyValue: initial?.defaultValue ?? 0,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Review a protest
|
|
*/
|
|
async reviewProtest(input: { protestId: string; stewardId: string; decision: string; decisionNotes: string }): Promise<void> {
|
|
await this.protestService.reviewProtest(input);
|
|
}
|
|
|
|
/**
|
|
* Apply a penalty
|
|
*/
|
|
async applyPenalty(input: any): Promise<void> {
|
|
await this.penaltyService.applyPenalty(input);
|
|
}
|
|
} |