Files
gridpilot.gg/core/racing/application/use-cases/CompleteRaceUseCaseWithRatings.ts
2026-01-16 19:46:49 +01:00

218 lines
8.1 KiB
TypeScript

import type { RaceResultsProvider } from '@core/identity/application/ports/RaceResultsProvider';
import { RatingUpdateService } from '@core/identity/domain/services/RatingUpdateService';
import { Result } from '@core/shared/domain/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import { Result as RaceResult } from '../../domain/entities/result/Result';
import { Standing } from '../../domain/entities/Standing';
import type { RaceRegistrationRepository } from '../../domain/repositories/RaceRegistrationRepository';
import type { RaceRepository } from '../../domain/repositories/RaceRepository';
import type { ResultRepository } from '../../domain/repositories/ResultRepository';
import type { StandingRepository } from '../../domain/repositories/StandingRepository';
import { RaceResultGenerator } from '../utils/RaceResultGenerator';
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<string, number>;
}
/**
* 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: RaceRepository,
private readonly raceRegistrationRepository: RaceRegistrationRepository,
private readonly resultRepository: ResultRepository,
private readonly standingRepository: StandingRepository,
private readonly driverRatingProvider: DriverRatingProvider,
private readonly ratingUpdateService: RatingUpdateService,
private readonly raceResultsProvider?: RaceResultsProvider, // Optional: for new ledger flow
) {}
async execute(command: CompleteRaceWithRatingsInput): Promise<
Result<CompleteRaceWithRatingsResult, ApplicationErrorCode<CompleteRaceWithRatingsErrorCode, { message: string }>>
> {
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);
const ratingsUpdatedForDriverIds: string[] = [];
// 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);
ratingsUpdatedForDriverIds.push(...registeredDriverIds);
} else {
ratingsUpdatedForDriverIds.push(...(ratingResult.driversUpdated || []));
}
} 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);
ratingsUpdatedForDriverIds.push(...registeredDriverIds);
}
} else {
// BACKWARD COMPATIBLE: Use legacy direct update approach
await this.updateDriverRatingsLegacy(results, registeredDriverIds.length);
ratingsUpdatedForDriverIds.push(...registeredDriverIds);
}
} 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);
return Result.ok({
raceId,
ratingsUpdatedForDriverIds,
});
} 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<void> {
const resultsByDriver = new Map<string, RaceResult[]>();
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<void> {
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);
}
}