import type { DomainService } from '@core/shared/domain/Service'; import { RatingEvent } from '../entities/RatingEvent'; import type { RatingEventRepository } from '../repositories/RatingEventRepository'; import type { UserRatingRepository } from '../repositories/UserRatingRepository'; import { RatingDelta } from '../value-objects/RatingDelta'; import { RatingDimensionKey } from '../value-objects/RatingDimensionKey'; import { RatingEventId } from '../value-objects/RatingEventId'; import { RatingEventFactory } from './RatingEventFactory'; import { RatingSnapshotCalculator } from './RatingSnapshotCalculator'; /** * 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 DomainService { readonly serviceName = 'RatingUpdateService'; constructor( private readonly userRatingRepository: UserRatingRepository, private readonly ratingEventRepository: RatingEventRepository ) {} /** * 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 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); } }