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 { Result as RaceResult } from '../../domain/entities/result/Result'; import { Standing } from '../../domain/entities/Standing'; import { RaceResultGenerator } from '../utils/RaceResultGenerator'; import { RatingUpdateService } from '@core/identity/domain/services/RatingUpdateService'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort'; import type { IRaceResultsProvider } from '@core/identity/application/ports/IRaceResultsProvider'; export interface CompleteRaceWithRatingsInput { raceId: string; } export type CompleteRaceWithRatingsResult = { raceId: string; ratingsUpdatedForDriverIds: string[]; }; export type CompleteRaceWithRatingsErrorCode = | 'RACE_NOT_FOUND' | 'NO_REGISTERED_DRIVERS' | 'ALREADY_COMPLETED' | 'RATING_UPDATE_FAILED' | 'REPOSITORY_ERROR'; interface DriverRatingProvider { getRatings(driverIds: string[]): Map; } /** * Enhanced CompleteRaceUseCase that includes rating updates. * EVOLVED (Slice 7): Now uses ledger-based rating updates for transparency and auditability. */ export class CompleteRaceUseCaseWithRatings { constructor( private readonly raceRepository: IRaceRepository, private readonly raceRegistrationRepository: IRaceRegistrationRepository, private readonly resultRepository: IResultRepository, private readonly standingRepository: IStandingRepository, private readonly driverRatingProvider: DriverRatingProvider, private readonly ratingUpdateService: RatingUpdateService, private readonly output: UseCaseOutputPort, private readonly raceResultsProvider?: IRaceResultsProvider, // Optional: for new ledger flow ) {} async execute(command: CompleteRaceWithRatingsInput): Promise< Result> > { try { const { raceId } = command; const race = await this.raceRepository.findById(raceId); if (!race) { return Result.err({ code: 'RACE_NOT_FOUND', details: { message: 'Race not found' } }); } const raceStatus = (race as unknown as { status?: unknown }).status; const isCompleted = typeof raceStatus === 'string' ? raceStatus === 'completed' : typeof (raceStatus as { isCompleted?: unknown })?.isCompleted === 'function' ? (raceStatus as { isCompleted: () => boolean }).isCompleted() : typeof (raceStatus as { toString?: unknown })?.toString === 'function' ? (raceStatus as { toString: () => string }).toString() === 'completed' : false; if (isCompleted) { return Result.err({ code: 'ALREADY_COMPLETED', details: { message: 'Race already completed' } }); } const registeredDriverIds = await this.raceRegistrationRepository.getRegisteredDrivers(raceId); if (registeredDriverIds.length === 0) { return Result.err({ code: 'NO_REGISTERED_DRIVERS', details: { message: 'No registered drivers' } }); } const driverRatings = this.driverRatingProvider.getRatings(registeredDriverIds); const results = RaceResultGenerator.generateRaceResults(raceId, registeredDriverIds, driverRatings); for (const result of results) { await this.resultRepository.create(result); } await this.updateStandings(race.leagueId, results); // SLICE 7: Use new ledger-based approach if raceResultsProvider is available // This provides backward compatibility while evolving to event-driven architecture try { if (this.raceResultsProvider) { // NEW LEDGER APPROACH: Use RecordRaceRatingEventsUseCase via RatingUpdateService const raceResultsData = { raceId, results: results.map(result => ({ userId: result.driverId.toString(), startPos: result.startPosition.toNumber(), finishPos: result.position.toNumber(), incidents: result.incidents.toNumber(), status: 'finished' as const, // RaceResultGenerator only generates finished results })), }; try { const ratingResult = await this.ratingUpdateService.recordRaceRatingEvents( raceId, raceResultsData.results ); if (!ratingResult.success) { console.warn(`[Slice 7] Ledger-based rating update failed for race ${raceId}, falling back to legacy method`); await this.updateDriverRatingsLegacy(results, registeredDriverIds.length); } } catch (error) { // If ledger approach throws error, fall back to legacy method console.warn(`[Slice 7] Ledger-based rating update threw error for race ${raceId}, falling back to legacy method: ${error instanceof Error ? error.message : 'Unknown error'}`); await this.updateDriverRatingsLegacy(results, registeredDriverIds.length); } } else { // BACKWARD COMPATIBLE: Use legacy direct update approach await this.updateDriverRatingsLegacy(results, registeredDriverIds.length); } } catch (error) { return Result.err({ code: 'RATING_UPDATE_FAILED', details: { message: error instanceof Error ? error.message : 'Failed to update driver ratings', }, }); } const completedRace = race.complete(); await this.raceRepository.update(completedRace); this.output.present({ raceId, ratingsUpdatedForDriverIds: registeredDriverIds, }); return Result.ok(undefined); } catch (error) { return Result.err({ code: 'REPOSITORY_ERROR', details: { message: error instanceof Error ? error.message : 'Unknown error', }, }); } } private async updateStandings(leagueId: string, results: RaceResult[]): Promise { const resultsByDriver = new Map(); for (const result of results) { const driverIdStr = result.driverId.toString(); const existing = resultsByDriver.get(driverIdStr) || []; existing.push(result); resultsByDriver.set(driverIdStr, existing); } for (const [driverIdStr, driverResults] of resultsByDriver) { let standing = await this.standingRepository.findByDriverIdAndLeagueId(driverIdStr, leagueId); if (!standing) { standing = Standing.create({ leagueId, driverId: driverIdStr, }); } for (const result of driverResults) { standing = standing.addRaceResult(result.position.toNumber(), { 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); } } /** * Legacy rating update method (BACKWARD COMPATIBLE) * Uses direct updates via RatingUpdateService */ private async updateDriverRatingsLegacy(results: RaceResult[], totalDrivers: number): Promise { const driverResults = results.map((result) => ({ driverId: result.driverId.toString(), position: result.position.toNumber(), totalDrivers, incidents: result.incidents.toNumber(), startPosition: result.startPosition.toNumber(), })); await this.ratingUpdateService.updateDriverRatingsAfterRace(driverResults); } }