import { ProtestsApiClient } from '../../api/protests/ProtestsApiClient'; import { ProtestViewModel } from '../../view-models/ProtestViewModel'; import { RaceViewModel } from '../../view-models/RaceViewModel'; import { ProtestDriverViewModel } from '../../view-models/ProtestDriverViewModel'; import type { LeagueAdminProtestsDTO } from '../../types/generated/LeagueAdminProtestsDTO'; import type { ApplyPenaltyCommandDTO } from '../../types/generated/ApplyPenaltyCommandDTO'; import type { RequestProtestDefenseCommandDTO } from '../../types/generated/RequestProtestDefenseCommandDTO'; import type { ReviewProtestCommandDTO } from '../../types/generated/ReviewProtestCommandDTO'; import type { DriverDTO } from '../../types/generated/DriverDTO'; import type { FileProtestCommandDTO } from '../../types/generated/FileProtestCommandDTO'; import type { ProtestIncidentDTO } from '../../types/generated/ProtestIncidentDTO'; export interface ProtestParticipant { id: string; name: string; } export interface FileProtestInput { raceId: string; leagueId?: string; protestingDriverId: string; accusedDriverId: string; lap: string; timeInRace?: string; description: string; comment?: string; proofVideoUrl?: string; } /** * Protest Service * * Orchestrates protest operations by coordinating API calls and view model creation. * All dependencies are injected via constructor. */ export class ProtestService { constructor( private readonly apiClient: ProtestsApiClient ) {} /** * Get protests for a league with view model transformation */ async getLeagueProtests(leagueId: string): Promise<{ protests: ProtestViewModel[]; racesById: LeagueAdminProtestsDTO['racesById']; driversById: LeagueAdminProtestsDTO['driversById']; }> { const dto = await this.apiClient.getLeagueProtests(leagueId); return { protests: dto.protests.map(protest => new ProtestViewModel(protest)), racesById: dto.racesById, driversById: dto.driversById, }; } /** * Get a single protest by ID from league protests */ async getProtestById(leagueId: string, protestId: string): Promise<{ protest: ProtestViewModel; race: RaceViewModel; protestingDriver: ProtestDriverViewModel; accusedDriver: ProtestDriverViewModel; } | null> { const dto = await this.apiClient.getLeagueProtest(leagueId, protestId); const protest = dto.protests[0]; if (!protest) return null; const race = Object.values(dto.racesById)[0]; if (!race) return null; // Cast to the correct type for indexing const driversById = dto.driversById as unknown as Record; const protestingDriver = driversById[protest.protestingDriverId]; const accusedDriver = driversById[protest.accusedDriverId]; if (!protestingDriver || !accusedDriver) return null; return { protest: new ProtestViewModel(protest), race: new RaceViewModel(race), protestingDriver: new ProtestDriverViewModel(protestingDriver), accusedDriver: new ProtestDriverViewModel(accusedDriver), }; } /** * Apply a penalty */ async applyPenalty(input: ApplyPenaltyCommandDTO): Promise { await this.apiClient.applyPenalty(input); } /** * Request protest defense */ async requestDefense(input: RequestProtestDefenseCommandDTO): Promise { await this.apiClient.requestDefense(input); } /** * Review protest */ async reviewProtest(input: { protestId: string; stewardId: string; decision: string; decisionNotes: string }): Promise { const normalizedDecision = input.decision.toLowerCase() === 'upheld' ? 'uphold' : input.decision.toLowerCase(); const command: ReviewProtestCommandDTO = { protestId: input.protestId, stewardId: input.stewardId, decision: normalizedDecision, decisionNotes: input.decisionNotes, }; await this.apiClient.reviewProtest(command); } /** * Find protests by race ID */ async findByRaceId(raceId: string): Promise { const dto = await this.apiClient.getRaceProtests(raceId); return dto.protests; } /** * Validate file protest input * @throws Error with descriptive message if validation fails */ validateFileProtestInput(input: FileProtestInput): void { if (!input.accusedDriverId) { throw new Error('Please select the driver you are protesting against.'); } if (!input.lap || parseInt(input.lap, 10) < 0) { throw new Error('Please enter a valid lap number.'); } if (!input.description.trim()) { throw new Error('Please describe what happened.'); } } /** * Construct file protest command from input */ constructFileProtestCommand(input: FileProtestInput): FileProtestCommandDTO { this.validateFileProtestInput(input); const incident: ProtestIncidentDTO = { lap: parseInt(input.lap, 10), description: input.description.trim(), ...(input.timeInRace ? { timeInRace: parseInt(input.timeInRace, 10) } : {}), }; const command: FileProtestCommandDTO = { raceId: input.raceId, protestingDriverId: input.protestingDriverId, accusedDriverId: input.accusedDriverId, incident, ...(input.comment?.trim() ? { comment: input.comment.trim() } : {}), ...(input.proofVideoUrl?.trim() ? { proofVideoUrl: input.proofVideoUrl.trim() } : {}), }; return command; } }