rating
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
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<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,
|
||||
}));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user