import { RacesApiClient } from '../../api/races/RacesApiClient'; import { RaceResultsDetailViewModel } from '../../view-models/RaceResultsDetailViewModel'; import { RaceWithSOFViewModel } from '../../view-models/RaceWithSOFViewModel'; import { ImportRaceResultsSummaryViewModel } from '../../view-models/ImportRaceResultsSummaryViewModel'; import type { ImportRaceResultsDTO } from '../../types/generated/ImportRaceResultsDTO'; import { v4 as uuidv4 } from 'uuid'; // Define types type ImportRaceResultsInputDto = ImportRaceResultsDTO; type ImportRaceResultsSummaryDto = { success: boolean; raceId: string; driversProcessed: number; resultsRecorded: number; errors?: string[]; }; export interface ImportResultRowDTO { id: string; raceId: string; driverId: string; position: number; fastestLap: number; incidents: number; startPosition: number; } export interface CSVRow { driverId: string; position: number; fastestLap: number; incidents: number; startPosition: number; } /** * Race Results Service * * Orchestrates race results operations including viewing, importing, and SOF calculations. * All dependencies are injected via constructor. */ export class RaceResultsService { constructor( private readonly apiClient: RacesApiClient ) {} /** * Get race results detail with view model transformation */ async getResultsDetail(raceId: string, currentUserId?: string): Promise { const dto = await this.apiClient.getResultsDetail(raceId); return new RaceResultsDetailViewModel(dto, currentUserId || ''); } /** * Get race with strength of field calculation */ async getWithSOF(raceId: string): Promise { const dto = await this.apiClient.getWithSOF(raceId); return new RaceWithSOFViewModel(dto); } /** * Import race results and get summary */ async importResults(raceId: string, input: ImportRaceResultsInputDto): Promise { const dto = await this.apiClient.importResults(raceId, input); return new ImportRaceResultsSummaryViewModel(dto); } /** * Parse CSV content and validate results * @throws Error with descriptive message if validation fails */ parseCSV(content: string): CSVRow[] { const lines = content.trim().split('\n'); if (lines.length < 2) { throw new Error('CSV file is empty or invalid'); } const headerLine = lines[0]!; const header = headerLine.toLowerCase().split(',').map((h) => h.trim()); const requiredFields = ['driverid', 'position', 'fastestlap', 'incidents', 'startposition']; for (const field of requiredFields) { if (!header.includes(field)) { throw new Error(`Missing required field: ${field}`); } } const rows: CSVRow[] = []; for (let i = 1; i < lines.length; i++) { const line = lines[i]; if (!line) { continue; } const values = line.split(',').map((v) => v.trim()); if (values.length !== header.length) { throw new Error( `Invalid row ${i}: expected ${header.length} columns, got ${values.length}`, ); } const row: Record = {}; header.forEach((field, index) => { row[field] = values[index] ?? ''; }); const driverId = row['driverid'] ?? ''; const position = parseInt(row['position'] ?? '', 10); const fastestLap = parseFloat(row['fastestlap'] ?? ''); const incidents = parseInt(row['incidents'] ?? '', 10); const startPosition = parseInt(row['startposition'] ?? '', 10); if (!driverId || driverId.length === 0) { throw new Error(`Row ${i}: driverId is required`); } if (Number.isNaN(position) || position < 1) { throw new Error(`Row ${i}: position must be a positive integer`); } if (Number.isNaN(fastestLap) || fastestLap < 0) { throw new Error(`Row ${i}: fastestLap must be a non-negative number`); } if (Number.isNaN(incidents) || incidents < 0) { throw new Error(`Row ${i}: incidents must be a non-negative integer`); } if (Number.isNaN(startPosition) || startPosition < 1) { throw new Error(`Row ${i}: startPosition must be a positive integer`); } rows.push({ driverId, position, fastestLap, incidents, startPosition }); } const positions = rows.map((r) => r.position); const uniquePositions = new Set(positions); if (positions.length !== uniquePositions.size) { throw new Error('Duplicate positions found in CSV'); } const driverIds = rows.map((r) => r.driverId); const uniqueDrivers = new Set(driverIds); if (driverIds.length !== uniqueDrivers.size) { throw new Error('Duplicate driver IDs found in CSV'); } return rows; } /** * Transform parsed CSV rows into ImportResultRowDTO array */ transformToImportResults(rows: CSVRow[], raceId: string): ImportResultRowDTO[] { return rows.map((row) => ({ id: uuidv4(), raceId, driverId: row.driverId, position: row.position, fastestLap: row.fastestLap, incidents: row.incidents, startPosition: row.startPosition, })); } /** * Parse CSV file content and transform to import results * @throws Error with descriptive message if parsing or validation fails */ parseAndTransformCSV(content: string, raceId: string): ImportResultRowDTO[] { const rows = this.parseCSV(content); return this.transformToImportResults(rows, raceId); } }