import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'; import type { IRaceRegistrationRepository } from '../../domain/repositories/IRaceRegistrationRepository'; import type { IResultRepository } from '../../domain/repositories/IResultRepository'; import type { IStandingRepository } from '../../domain/repositories/IStandingRepository'; import type { DriverRatingProvider } from '../ports/DriverRatingProvider'; import { Result } from '../../domain/entities/Result'; import { Standing } from '../../domain/entities/Standing'; import type { AsyncUseCase } from '@core/shared/application'; import type { Logger } from '@core/shared/application'; /** * Use Case: CompleteRaceUseCase * * Encapsulates the workflow for completing a race: * - loads the race by id * - throws if the race does not exist * - delegates completion rules to the Race domain entity * - automatically generates realistic results for registered drivers * - updates league standings * - persists all changes via repositories. */ export interface CompleteRaceCommandDTO { raceId: string; } export class CompleteRaceUseCase implements AsyncUseCase { constructor( private readonly raceRepository: IRaceRepository, private readonly raceRegistrationRepository: IRaceRegistrationRepository, private readonly resultRepository: IResultRepository, private readonly standingRepository: IStandingRepository, private readonly driverRatingProvider: DriverRatingProvider, private readonly logger: Logger, ) {} async execute(command: CompleteRaceCommandDTO): Promise { this.logger.debug(`Executing CompleteRaceUseCase for raceId: ${command.raceId}`); const { raceId } = command; try { const race = await this.raceRepository.findById(raceId); if (!race) { this.logger.error(`Race with id ${raceId} not found.`); throw new Error('Race not found'); } this.logger.debug(`Race ${raceId} found. Status: ${race.status}`); // Get registered drivers for this race const registeredDriverIds = await this.raceRegistrationRepository.getRegisteredDrivers(raceId); if (registeredDriverIds.length === 0) { this.logger.warn(`No registered drivers found for race ${raceId}.`); throw new Error('Cannot complete race with no registered drivers'); } this.logger.info(`${registeredDriverIds.length} drivers registered for race ${raceId}. Generating results.`); // Get driver ratings const driverRatings = this.driverRatingProvider.getRatings(registeredDriverIds); this.logger.debug(`Driver ratings fetched for ${registeredDriverIds.length} drivers.`); // Generate realistic race results const results = this.generateRaceResults(raceId, registeredDriverIds, driverRatings); this.logger.debug(`Generated ${results.length} race results for race ${raceId}.`); // Save results for (const result of results) { await this.resultRepository.create(result); } this.logger.info(`Persisted ${results.length} race results for race ${raceId}.`); // Update standings await this.updateStandings(race.leagueId, results); this.logger.info(`Standings updated for league ${race.leagueId}.`); // Complete the race const completedRace = race.complete(); await this.raceRepository.update(completedRace); this.logger.info(`Race ${raceId} successfully completed and updated.`); } catch (error) { this.logger.error(`Failed to complete race ${raceId}: ${error.message}`, error as Error); throw error; } } private generateRaceResults( raceId: string, driverIds: string[], driverRatings: Map ): Result[] { this.logger.debug(`Generating race results for race ${raceId} with ${driverIds.length} drivers.`); // Create driver performance data const driverPerformances = driverIds.map(driverId => ({ driverId, rating: driverRatings.get(driverId) ?? 1500, // Default rating randomFactor: Math.random() - 0.5, // -0.5 to +0.5 randomization })); // Sort by performance (rating + randomization) driverPerformances.sort((a, b) => { const perfA = a.rating + (a.randomFactor * 200); // ±100 rating points randomization const perfB = b.rating + (b.randomFactor * 200); return perfB - perfA; // Higher performance first }); this.logger.debug(`Driver performances sorted for race ${raceId}.`); // Generate qualifying results for start positions (similar but different from race results) const qualiPerformances = driverPerformances.map(p => ({ ...p, randomFactor: Math.random() - 0.5, // New randomization for quali })); qualiPerformances.sort((a, b) => { const perfA = a.rating + (a.randomFactor * 150); const perfB = b.rating + (b.randomFactor * 150); return perfB - perfA; }); this.logger.debug(`Qualifying performances generated for race ${raceId}.`); // Generate results const results: Result[] = []; for (let i = 0; i < driverPerformances.length; i++) { const { driverId } = driverPerformances[i]; const position = i + 1; const startPosition = qualiPerformances.findIndex(p => p.driverId === driverId) + 1; // Generate realistic lap times (90-120 seconds for a lap) const baseLapTime = 90000 + Math.random() * 30000; const positionBonus = (position - 1) * 500; // Winners are faster const fastestLap = Math.round(baseLapTime + positionBonus + Math.random() * 5000); // Generate incidents (0-3, higher for lower positions) const incidentProbability = Math.min(0.8, position / driverPerformances.length); const incidents = Math.random() < incidentProbability ? Math.floor(Math.random() * 3) + 1 : 0; results.push( Result.create({ id: `${raceId}-${driverId}`, raceId, driverId, position, startPosition, fastestLap, incidents, }) ); } this.logger.debug(`Individual results created for race ${raceId}.`); return results; } private async updateStandings(leagueId: string, results: Result[]): Promise { this.logger.debug(`Updating standings for league ${leagueId} with ${results.length} results.`); // Group results by driver const resultsByDriver = new Map(); for (const result of results) { const existing = resultsByDriver.get(result.driverId) || []; existing.push(result); resultsByDriver.set(result.driverId, existing); } this.logger.debug(`Results grouped by driver for league ${leagueId}.`); // Update or create standings for each driver for (const [driverId, driverResults] of resultsByDriver) { let standing = await this.standingRepository.findByDriverIdAndLeagueId(driverId, leagueId); if (!standing) { standing = Standing.create({ leagueId, driverId, }); this.logger.debug(`Created new standing for driver ${driverId} in league ${leagueId}.`); } else { this.logger.debug(`Found existing standing for driver ${driverId} in league ${leagueId}.`); } // Add all results for this driver (should be just one for this race) for (const result of driverResults) { standing = standing.addRaceResult(result.position, { 1: 25, 2: 18, 3: 15, 4: 12, 5: 10, 6: 8, 7: 6, 8: 4, 9: 2, 10: 1 }); } await this.standingRepository.save(standing); this.logger.debug(`Standing saved for driver ${driverId} in league ${leagueId}.`); } this.logger.info(`Standings update complete for league ${leagueId}.`); } }