186 lines
5.7 KiB
TypeScript
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,
|
|
}));
|
|
}
|
|
} |