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 { // Get all races for this league const leagueRaces = await this.raceService.findByLeagueId(leagueId); // Get protests and penalties for each race const protestsMap: Record = {}; const penaltiesMap: Record = {}; const driverIds = new Set(); 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 = {}; 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 { 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 = { 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 { await this.protestService.reviewProtest(input); } /** * Apply a penalty */ async applyPenalty(input: any): Promise { await this.penaltyService.applyPenalty(input); } }