166 lines
5.3 KiB
TypeScript
166 lines
5.3 KiB
TypeScript
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<string, DriverDTO>;
|
|
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<void> {
|
|
await this.apiClient.applyPenalty(input);
|
|
}
|
|
|
|
/**
|
|
* Request protest defense
|
|
*/
|
|
async requestDefense(input: RequestProtestDefenseCommandDTO): Promise<void> {
|
|
await this.apiClient.requestDefense(input);
|
|
}
|
|
|
|
/**
|
|
* Review protest
|
|
*/
|
|
async reviewProtest(input: { protestId: string; stewardId: string; decision: string; decisionNotes: string }): Promise<void> {
|
|
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<any[]> {
|
|
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;
|
|
}
|
|
} |