import { IRaceResultsProvider } from '../ports/IRaceResultsProvider'; import { IRatingEventRepository } from '../../domain/repositories/IRatingEventRepository'; import { IUserRatingRepository } from '../../domain/repositories/IUserRatingRepository'; import { RatingEventFactory } from '../../domain/services/RatingEventFactory'; import { DrivingRatingCalculator } from '../../domain/services/DrivingRatingCalculator'; import { RatingSnapshotCalculator } from '../../domain/services/RatingSnapshotCalculator'; import { AppendRatingEventsUseCase } from './AppendRatingEventsUseCase'; import { RecordRaceRatingEventsInput, RecordRaceRatingEventsOutput } from '../dtos/RecordRaceRatingEventsDto'; /** * Use Case: RecordRaceRatingEventsUseCase * * Records rating events for a completed race. * Follows CQRS Light: command side operation. * * Flow: * 1. Load race results from racing context * 2. Compute SoF if needed (platform-only) * 3. Factory creates rating events * 4. Append to ledger via AppendRatingEventsUseCase * 5. Recompute snapshots * * Per plans section 7.1.1 and 10.1 */ export class RecordRaceRatingEventsUseCase { constructor( private readonly raceResultsProvider: IRaceResultsProvider, private readonly ratingEventRepository: IRatingEventRepository, private readonly userRatingRepository: IUserRatingRepository, private readonly appendRatingEventsUseCase: AppendRatingEventsUseCase, ) {} async execute(input: RecordRaceRatingEventsInput): Promise { const errors: string[] = []; const driversUpdated: string[] = []; let totalEventsCreated = 0; try { // 1. Load race results const raceResults = await this.raceResultsProvider.getRaceResults(input.raceId); if (!raceResults) { return { success: false, raceId: input.raceId, eventsCreated: 0, driversUpdated: [], errors: ['Race results not found'], }; } if (raceResults.results.length === 0) { return { success: false, raceId: input.raceId, eventsCreated: 0, driversUpdated: [], errors: ['No results found for race'], }; } // 2. Compute SoF if not provided (platform-only approach) // Use existing user ratings as platform strength indicators const resultsWithSof = await this.computeSoFIfNeeded(raceResults); // 3. Create rating events using factory const eventsByUser = RatingEventFactory.createDrivingEventsFromRace({ raceId: input.raceId, results: resultsWithSof, }); if (eventsByUser.size === 0) { return { success: true, raceId: input.raceId, eventsCreated: 0, driversUpdated: [], errors: [], }; } // 4. Process each driver's events for (const [userId, events] of eventsByUser) { try { // Use AppendRatingEventsUseCase to handle ledger and snapshot const result = await this.appendRatingEventsUseCase.execute({ userId, events: events.map(event => ({ userId: event.userId, dimension: event.dimension.value, delta: event.delta.value, weight: event.weight || 1, sourceType: event.source.type, sourceId: event.source.id, reasonCode: event.reason.code, reasonSummary: event.reason.summary, reasonDetails: event.reason.details, occurredAt: event.occurredAt.toISOString(), })), }); if (result.snapshotUpdated) { driversUpdated.push(userId); totalEventsCreated += result.events.length; } } catch (error) { const errorMsg = `Failed to process events for user ${userId}: ${error instanceof Error ? error.message : 'Unknown error'}`; errors.push(errorMsg); } } return { success: errors.length === 0, raceId: input.raceId, eventsCreated: totalEventsCreated, driversUpdated, errors, }; } catch (error) { const errorMsg = `Failed to record race rating events: ${error instanceof Error ? error.message : 'Unknown error'}`; errors.push(errorMsg); return { success: false, raceId: input.raceId, eventsCreated: 0, driversUpdated: [], errors, }; } } /** * Compute Strength of Field if not provided in results * Uses platform driving ratings (existing user ratings) */ private async computeSoFIfNeeded(raceResults: { raceId: string; results: Array<{ userId: string; startPos: number; finishPos: number; incidents: number; status: 'finished' | 'dnf' | 'dns' | 'dsq' | 'afk'; sof?: number; }>; }): Promise> { // Check if all results have sof const hasAllSof = raceResults.results.every(r => r.sof !== undefined); if (hasAllSof) { return raceResults.results.map(r => ({ ...r, sof: r.sof! })); } // Need to compute SoF - get user ratings for all drivers const userIds = raceResults.results.map(r => r.userId); const ratingsMap = new Map(); // Get ratings for all drivers for (const userId of userIds) { const userRating = await this.userRatingRepository.findByUserId(userId); if (userRating) { ratingsMap.set(userId, userRating.driver.value); } else { // Default rating for new drivers ratingsMap.set(userId, 50); } } // Calculate average rating as SoF const ratings = Array.from(ratingsMap.values()); const sof = ratings.length > 0 ? Math.round(ratings.reduce((sum, r) => sum + r, 0) / ratings.length) : 50; // Add SoF to each result return raceResults.results.map(r => ({ ...r, sof: r.sof !== undefined ? r.sof : sof, })); } }