import type { IDomainService } from '@core/shared/domain'; import type { IUserRatingRepository } from '../repositories/IUserRatingRepository'; import type { IRatingEventRepository } from '../repositories/IRatingEventRepository'; import { RatingEventFactory } from './RatingEventFactory'; import { RatingSnapshotCalculator } from './RatingSnapshotCalculator'; import { RatingEvent } from '../entities/RatingEvent'; import { RatingEventId } from '../value-objects/RatingEventId'; import { RatingDimensionKey } from '../value-objects/RatingDimensionKey'; import { RatingDelta } from '../value-objects/RatingDelta'; /** * Domain Service: RatingUpdateService * * Handles updating user ratings based on various events and performance metrics. * Centralizes rating calculation logic and ensures consistency across the system. * * EVOLVED (Slice 7): Now uses event-driven approach with ledger pattern. * Records rating events and recomputes snapshots for transparency and auditability. */ export class RatingUpdateService implements IDomainService { readonly serviceName = 'RatingUpdateService'; constructor( private readonly userRatingRepository: IUserRatingRepository, private readonly ratingEventRepository: IRatingEventRepository ) {} /** * Record race rating events and update snapshots (NEW LEDGER APPROACH) * Replaces direct rating updates with event recording + snapshot recomputation */ async recordRaceRatingEvents(raceId: string, raceResults: Array<{ userId: string; startPos: number; finishPos: number; incidents: number; status: 'finished' | 'dnf' | 'dns' | 'dsq' | 'afk'; sof?: number; }>): Promise<{ success: boolean; eventsCreated: number; driversUpdated: string[] }> { try { // Use factory to create rating events from race results const eventsByUser = RatingEventFactory.createDrivingEventsFromRace({ raceId, results: raceResults, }); let totalEvents = 0; const driversUpdated: string[] = []; // Process each user's events for (const [userId, events] of eventsByUser) { if (events.length === 0) continue; // Save all events to ledger for (const event of events) { await this.ratingEventRepository.save(event); totalEvents++; } // Recompute snapshot from all events for this user const allEvents = await this.ratingEventRepository.getAllByUserId(userId); const snapshot = RatingSnapshotCalculator.calculate(userId, allEvents); await this.userRatingRepository.save(snapshot); driversUpdated.push(userId); } return { success: true, eventsCreated: totalEvents, driversUpdated, }; } catch (error) { console.error('[RatingUpdateService] Failed to record race rating events:', error); return { success: false, eventsCreated: 0, driversUpdated: [], }; } } /** * Update driver ratings after race completion (BACKWARD COMPATIBLE) * Still supported but now delegates to event-based approach internally */ async updateDriverRatingsAfterRace( driverResults: Array<{ driverId: string; position: number; totalDrivers: number; incidents: number; startPosition: number; }> ): Promise { // Convert to new format and use event-based approach const raceResults = driverResults.map(result => ({ userId: result.driverId, startPos: result.startPosition, finishPos: result.position, incidents: result.incidents, status: 'finished' as const, })); // Generate a synthetic race ID for backward compatibility const raceId = `backward-compat-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; const result = await this.recordRaceRatingEvents(raceId, raceResults); if (!result.success) { throw new Error('Failed to update ratings via event system'); } } /** * Update individual driver rating based on race result (LEGACY - DEPRECATED) * Kept for backward compatibility but now uses event-based approach */ private async updateDriverRating(result: { driverId: string; position: number; totalDrivers: number; incidents: number; startPosition: number; }): Promise { // Delegate to new event-based approach await this.updateDriverRatingsAfterRace([result]); } /** * Update trust score based on sportsmanship actions (USES LEDGER) */ async updateTrustScore(driverId: string, trustChange: number): Promise { // Create trust-related rating event using manual event creation const now = new Date(); const event = RatingEvent.create({ id: RatingEventId.generate(), userId: driverId, dimension: RatingDimensionKey.create('adminTrust'), delta: RatingDelta.create(trustChange), weight: 1, occurredAt: now, createdAt: now, source: { type: 'manualAdjustment', id: `trust-${now.getTime()}` }, reason: { code: trustChange > 0 ? 'TRUST_BONUS' : 'TRUST_PENALTY', summary: trustChange > 0 ? 'Positive sportsmanship' : 'Negative sportsmanship', details: { trustChange }, }, visibility: { public: true, redactedFields: [] }, version: 1, }); // Save event await this.ratingEventRepository.save(event); // Recompute snapshot const allEvents = await this.ratingEventRepository.getAllByUserId(driverId); const snapshot = RatingSnapshotCalculator.calculate(driverId, allEvents); await this.userRatingRepository.save(snapshot); } /** * Update steward rating based on protest handling quality (USES LEDGER) */ async updateStewardRating(stewardId: string, ratingChange: number): Promise { // Create steward-related rating event using manual event creation const now = new Date(); const event = RatingEvent.create({ id: RatingEventId.generate(), userId: stewardId, dimension: RatingDimensionKey.create('adminTrust'), delta: RatingDelta.create(ratingChange), weight: 1, occurredAt: now, createdAt: now, source: { type: 'manualAdjustment', id: `steward-${now.getTime()}` }, reason: { code: ratingChange > 0 ? 'STEWARD_BONUS' : 'STEWARD_PENALTY', summary: ratingChange > 0 ? 'Good protest handling' : 'Poor protest handling', details: { ratingChange }, }, visibility: { public: true, redactedFields: [] }, version: 1, }); // Save event await this.ratingEventRepository.save(event); // Recompute snapshot const allEvents = await this.ratingEventRepository.getAllByUserId(stewardId); const snapshot = RatingSnapshotCalculator.calculate(stewardId, allEvents); await this.userRatingRepository.save(snapshot); } /** * Calculate performance score based on finishing position and field strength * (Utility method kept for reference, but now handled by RatingEventFactory) */ private calculatePerformanceScore( position: number, totalDrivers: number, startPosition: number ): number { const positionScore = ((totalDrivers - position + 1) / totalDrivers) * 100; const positionsGained = startPosition - position; const gainBonus = Math.max(0, positionsGained * 2); const fieldStrengthMultiplier = 0.8 + (totalDrivers / 50); const rawScore = (positionScore + gainBonus) * fieldStrengthMultiplier; return Math.max(0, Math.min(100, rawScore)); } /** * Calculate fairness score based on incident involvement * (Utility method kept for reference, but now handled by RatingEventFactory) */ private calculateFairnessScore(incidents: number, totalDrivers: number): number { let fairnessScore = 100; fairnessScore -= incidents * 15; const incidentRate = incidents / totalDrivers; if (incidentRate > 0.5) { fairnessScore -= 20; } return Math.max(0, Math.min(100, fairnessScore)); } }