Files
gridpilot.gg/core/identity/application/use-cases/RecordRaceRatingEventsUseCase.ts
2026-01-16 21:35:35 +01:00

186 lines
5.7 KiB
TypeScript

import { UserRatingRepository } from '../../domain/repositories/UserRatingRepository';
import { RatingEventFactory } from '../../domain/services/RatingEventFactory';
import { RecordRaceRatingEventsInput, RecordRaceRatingEventsOutput } from '../dtos/RecordRaceRatingEventsDto';
import { RaceResultsProvider } from '../ports/RaceResultsProvider';
import { AppendRatingEventsUseCase } from './AppendRatingEventsUseCase';
/**
* 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: RaceResultsProvider,
private readonly userRatingRepository: UserRatingRepository,
private readonly appendRatingEventsUseCase: AppendRatingEventsUseCase,
) {}
async execute(input: RecordRaceRatingEventsInput): Promise<RecordRaceRatingEventsOutput> {
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<Array<{
userId: string;
startPos: number;
finishPos: number;
incidents: number;
status: 'finished' | 'dnf' | 'dns' | 'dsq' | 'afk';
sof: number;
}>> {
// 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<string, number>();
// 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,
}));
}
}