import { TeamRatingEvent } from '../entities/TeamRatingEvent'; import { TeamRatingEventId } from '../value-objects/TeamRatingEventId'; import { TeamRatingDimensionKey } from '../value-objects/TeamRatingDimensionKey'; import { TeamRatingDelta } from '../value-objects/TeamRatingDelta'; import { TeamDrivingReasonCode } from '../value-objects/TeamDrivingReasonCode'; import { TeamDrivingRatingCalculator, TeamDrivingRaceResult, TeamDrivingQualifyingResult, TeamDrivingOvertakeStats } from './TeamDrivingRatingCalculator'; export interface TeamDrivingRaceFactsDto { raceId: string; teamId: string; results: Array<{ teamId: string; position: number; incidents: number; status: 'finished' | 'dnf' | 'dsq' | 'dns' | 'afk'; fieldSize: number; strengthOfField: number; pace?: number; consistency?: number; teamwork?: number; sportsmanship?: number; }>; } export interface TeamDrivingQualifyingFactsDto { raceId: string; results: Array<{ teamId: string; qualifyingPosition: number; fieldSize: number; }>; } export interface TeamDrivingOvertakeFactsDto { raceId: string; results: Array<{ teamId: string; overtakes: number; successfulDefenses: number; }>; } /** * Domain Service: TeamDrivingRatingEventFactory * * Factory for creating team driving rating events using the full TeamDrivingRatingCalculator. * Mirrors user slice 3 pattern in core/racing/. * * Pure domain logic - no persistence concerns. */ export class TeamDrivingRatingEventFactory { /** * Create rating events from a team's race finish. * Uses TeamDrivingRatingCalculator for comprehensive calculations. */ static createFromRaceFinish(input: { teamId: string; position: number; incidents: number; status: 'finished' | 'dnf' | 'dsq' | 'dns' | 'afk'; fieldSize: number; strengthOfField: number; raceId: string; pace?: number; consistency?: number; teamwork?: number; sportsmanship?: number; }): TeamRatingEvent[] { const result: TeamDrivingRaceResult = { teamId: input.teamId, position: input.position, incidents: input.incidents, status: input.status, fieldSize: input.fieldSize, strengthOfField: input.strengthOfField, raceId: input.raceId, pace: input.pace as number | undefined, consistency: input.consistency as number | undefined, teamwork: input.teamwork as number | undefined, sportsmanship: input.sportsmanship as number | undefined, }; return TeamDrivingRatingCalculator.calculateFromRaceFinish(result); } /** * Create rating events from multiple race results. * Returns events grouped by team ID. */ static createDrivingEventsFromRace(raceFacts: TeamDrivingRaceFactsDto): Map { const eventsByTeam = new Map(); for (const result of raceFacts.results) { const input: { teamId: string; position: number; incidents: number; status: 'finished' | 'dnf' | 'dsq' | 'dns' | 'afk'; fieldSize: number; strengthOfField: number; raceId: string; pace?: number; consistency?: number; teamwork?: number; sportsmanship?: number; } = { teamId: result.teamId, position: result.position, incidents: result.incidents, status: result.status, fieldSize: raceFacts.results.length, strengthOfField: result.strengthOfField, raceId: raceFacts.raceId, }; if (result.pace !== undefined) { input.pace = result.pace; } if (result.consistency !== undefined) { input.consistency = result.consistency; } if (result.teamwork !== undefined) { input.teamwork = result.teamwork; } if (result.sportsmanship !== undefined) { input.sportsmanship = result.sportsmanship; } const events = this.createFromRaceFinish(input); if (events.length > 0) { eventsByTeam.set(result.teamId, events); } } return eventsByTeam; } /** * Create rating events from qualifying results. * Uses TeamDrivingRatingCalculator for qualifying calculations. */ static createFromQualifying(input: { teamId: string; qualifyingPosition: number; fieldSize: number; raceId: string; }): TeamRatingEvent[] { const result: TeamDrivingQualifyingResult = { teamId: input.teamId, qualifyingPosition: input.qualifyingPosition, fieldSize: input.fieldSize, raceId: input.raceId, }; return TeamDrivingRatingCalculator.calculateFromQualifying(result); } /** * Create rating events from multiple qualifying results. * Returns events grouped by team ID. */ static createDrivingEventsFromQualifying(qualifyingFacts: TeamDrivingQualifyingFactsDto): Map { const eventsByTeam = new Map(); for (const result of qualifyingFacts.results) { const events = this.createFromQualifying({ teamId: result.teamId, qualifyingPosition: result.qualifyingPosition, fieldSize: result.fieldSize, raceId: qualifyingFacts.raceId, }); if (events.length > 0) { eventsByTeam.set(result.teamId, events); } } return eventsByTeam; } /** * Create rating events from overtake/defense statistics. * Uses TeamDrivingRatingCalculator for overtake calculations. */ static createFromOvertakeStats(input: { teamId: string; overtakes: number; successfulDefenses: number; raceId: string; }): TeamRatingEvent[] { const stats: TeamDrivingOvertakeStats = { teamId: input.teamId, overtakes: input.overtakes, successfulDefenses: input.successfulDefenses, raceId: input.raceId, }; return TeamDrivingRatingCalculator.calculateFromOvertakeStats(stats); } /** * Create rating events from multiple overtake stats. * Returns events grouped by team ID. */ static createDrivingEventsFromOvertakes(overtakeFacts: TeamDrivingOvertakeFactsDto): Map { const eventsByTeam = new Map(); for (const result of overtakeFacts.results) { const events = this.createFromOvertakeStats({ teamId: result.teamId, overtakes: result.overtakes, successfulDefenses: result.successfulDefenses, raceId: overtakeFacts.raceId, }); if (events.length > 0) { eventsByTeam.set(result.teamId, events); } } return eventsByTeam; } /** * Create rating events from a penalty. * Generates both driving and adminTrust events. * Uses TeamDrivingReasonCode for validation. */ static createFromPenalty(input: { teamId: string; penaltyType: 'minor' | 'major' | 'critical'; severity: 'low' | 'medium' | 'high'; incidentCount?: number; }): TeamRatingEvent[] { const now = new Date(); const events: TeamRatingEvent[] = []; // Driving dimension penalty const drivingDelta = this.calculatePenaltyDelta(input.penaltyType, input.severity, 'driving'); if (drivingDelta !== 0) { events.push( TeamRatingEvent.create({ id: TeamRatingEventId.generate(), teamId: input.teamId, dimension: TeamRatingDimensionKey.create('driving'), delta: TeamRatingDelta.create(drivingDelta), weight: 1, occurredAt: now, createdAt: now, source: { type: 'penalty', id: input.penaltyType }, reason: { code: TeamDrivingReasonCode.create('RACE_INCIDENTS').value, description: `${input.penaltyType} penalty for driving violations`, }, visibility: { public: true }, version: 1, }) ); } // AdminTrust dimension penalty const adminDelta = this.calculatePenaltyDelta(input.penaltyType, input.severity, 'adminTrust'); if (adminDelta !== 0) { events.push( TeamRatingEvent.create({ id: TeamRatingEventId.generate(), teamId: input.teamId, dimension: TeamRatingDimensionKey.create('adminTrust'), delta: TeamRatingDelta.create(adminDelta), weight: 1, occurredAt: now, createdAt: now, source: { type: 'penalty', id: input.penaltyType }, reason: { code: 'PENALTY_ADMIN', description: `${input.penaltyType} penalty for rule violations`, }, visibility: { public: true }, version: 1, }) ); } return events; } /** * Create rating events from a vote outcome. * Generates adminTrust events. */ static createFromVote(input: { teamId: string; outcome: 'positive' | 'negative'; voteCount: number; eligibleVoterCount: number; percentPositive: number; }): TeamRatingEvent[] { const now = new Date(); const events: TeamRatingEvent[] = []; // Calculate delta based on vote outcome const delta = this.calculateVoteDelta( input.outcome, input.eligibleVoterCount, input.voteCount, input.percentPositive ); if (delta !== 0) { events.push( TeamRatingEvent.create({ id: TeamRatingEventId.generate(), teamId: input.teamId, dimension: TeamRatingDimensionKey.create('adminTrust'), delta: TeamRatingDelta.create(delta), weight: input.voteCount, // Weight by number of votes occurredAt: now, createdAt: now, source: { type: 'vote', id: 'admin_vote' }, reason: { code: input.outcome === 'positive' ? 'VOTE_POSITIVE' : 'VOTE_NEGATIVE', description: `Admin vote outcome: ${input.outcome}`, }, visibility: { public: true }, version: 1, }) ); } return events; } /** * Create rating events from an admin action. * Generates adminTrust events. */ static createFromAdminAction(input: { teamId: string; actionType: 'bonus' | 'penalty' | 'warning'; severity?: 'low' | 'medium' | 'high'; }): TeamRatingEvent[] { const now = new Date(); const events: TeamRatingEvent[] = []; if (input.actionType === 'bonus') { events.push( TeamRatingEvent.create({ id: TeamRatingEventId.generate(), teamId: input.teamId, dimension: TeamRatingDimensionKey.create('adminTrust'), delta: TeamRatingDelta.create(5), weight: 1, occurredAt: now, createdAt: now, source: { type: 'adminAction', id: 'bonus' }, reason: { code: 'ADMIN_BONUS', description: 'Admin bonus for positive contribution', }, visibility: { public: true }, version: 1, }) ); } else if (input.actionType === 'penalty') { const delta = input.severity === 'high' ? -15 : input.severity === 'medium' ? -10 : -5; events.push( TeamRatingEvent.create({ id: TeamRatingEventId.generate(), teamId: input.teamId, dimension: TeamRatingDimensionKey.create('adminTrust'), delta: TeamRatingDelta.create(delta), weight: 1, occurredAt: now, createdAt: now, source: { type: 'adminAction', id: 'penalty' }, reason: { code: 'ADMIN_PENALTY', description: `Admin penalty (${input.severity} severity)`, }, visibility: { public: true }, version: 1, }) ); } else if (input.actionType === 'warning') { events.push( TeamRatingEvent.create({ id: TeamRatingEventId.generate(), teamId: input.teamId, dimension: TeamRatingDimensionKey.create('adminTrust'), delta: TeamRatingDelta.create(3), weight: 1, occurredAt: now, createdAt: now, source: { type: 'adminAction', id: 'warning' }, reason: { code: 'ADMIN_WARNING_RESPONSE', description: 'Response to admin warning', }, visibility: { public: true }, version: 1, }) ); } return events; } // Private helper methods private static calculatePenaltyDelta( penaltyType: 'minor' | 'major' | 'critical', severity: 'low' | 'medium' | 'high', dimension: 'driving' | 'adminTrust' ): number { const baseValues = { minor: { driving: -5, adminTrust: -3 }, major: { driving: -10, adminTrust: -8 }, critical: { driving: -20, adminTrust: -15 }, }; const severityMultipliers = { low: 1, medium: 1.5, high: 2, }; const base = baseValues[penaltyType][dimension]; const multiplier = severityMultipliers[severity]; return Math.round(base * multiplier); } private static calculateVoteDelta( outcome: 'positive' | 'negative', eligibleVoterCount: number, voteCount: number, percentPositive: number ): number { if (voteCount === 0) return 0; const participationRate = voteCount / eligibleVoterCount; const strength = (percentPositive / 100) * 2 - 1; // -1 to +1 // Base delta of +/- 10, scaled by participation and strength const baseDelta = outcome === 'positive' ? 10 : -10; const scaledDelta = baseDelta * participationRate * (0.5 + Math.abs(strength) * 0.5); return Math.round(scaledDelta * 10) / 10; } }