import type { ValueObject } from '@core/shared/domain/ValueObject'; /** * Value Object: UserRating * * Multi-dimensional rating system for users covering: * - Driver skill: racing ability, lap times, consistency * - Admin competence: league management, event organization * - Steward fairness: protest handling, penalty consistency * - Trust score: reliability, sportsmanship, rule compliance * - Fairness score: clean racing, incident involvement */ export interface RatingDimension { value: number; // Current rating value (0-100 scale) confidence: number; // Confidence level based on sample size (0-1) sampleSize: number; // Number of events contributing to this rating trend: 'rising' | 'stable' | 'falling'; lastUpdated: Date; } export interface UserRatingProps { userId: string; driver: RatingDimension; admin: RatingDimension; steward: RatingDimension; trust: RatingDimension; fairness: RatingDimension; overallReputation: number; calculatorVersion?: string; createdAt: Date; updatedAt: Date; } const DEFAULT_DIMENSION: RatingDimension = { value: 50, confidence: 0, sampleSize: 0, trend: 'stable', lastUpdated: new Date(), }; export class UserRating implements ValueObject { readonly props: UserRatingProps; private constructor(props: UserRatingProps) { this.props = props; } get userId(): string { return this.props.userId; } get driver(): RatingDimension { return this.props.driver; } get admin(): RatingDimension { return this.props.admin; } get steward(): RatingDimension { return this.props.steward; } get trust(): RatingDimension { return this.props.trust; } get fairness(): RatingDimension { return this.props.fairness; } get overallReputation(): number { return this.props.overallReputation; } get createdAt(): Date { return this.props.createdAt; } get updatedAt(): Date { return this.props.updatedAt; } get calculatorVersion(): string | undefined { return this.props.calculatorVersion; } static create(userId: string): UserRating { if (!userId || userId.trim().length === 0) { throw new Error('UserRating userId is required'); } const now = new Date(); return new UserRating({ userId, driver: { ...DEFAULT_DIMENSION, lastUpdated: now }, admin: { ...DEFAULT_DIMENSION, lastUpdated: now }, steward: { ...DEFAULT_DIMENSION, lastUpdated: now }, trust: { ...DEFAULT_DIMENSION, lastUpdated: now }, fairness: { ...DEFAULT_DIMENSION, lastUpdated: now }, overallReputation: 50, calculatorVersion: '1.0', createdAt: now, updatedAt: now, }); } static restore(props: UserRatingProps): UserRating { return new UserRating(props); } equals(other: ValueObject): boolean { return this.props.userId === other.props.userId; } /** * Update driver rating based on race performance */ updateDriverRating( newValue: number, weight: number = 1 ): UserRating { const updated = this.updateDimension(this.driver, newValue, weight); return this.withUpdates({ driver: updated }); } /** * Update admin rating based on league management feedback */ updateAdminRating( newValue: number, weight: number = 1 ): UserRating { const updated = this.updateDimension(this.admin, newValue, weight); return this.withUpdates({ admin: updated }); } /** * Update steward rating based on protest handling feedback */ updateStewardRating( newValue: number, weight: number = 1 ): UserRating { const updated = this.updateDimension(this.steward, newValue, weight); return this.withUpdates({ steward: updated }); } /** * Update trust score based on reliability and sportsmanship */ updateTrustScore( newValue: number, weight: number = 1 ): UserRating { const updated = this.updateDimension(this.trust, newValue, weight); return this.withUpdates({ trust: updated }); } /** * Update fairness score based on clean racing incidents */ updateFairnessScore( newValue: number, weight: number = 1 ): UserRating { const updated = this.updateDimension(this.fairness, newValue, weight); return this.withUpdates({ fairness: updated }); } /** * Calculate weighted overall reputation */ calculateOverallReputation(): number { // Weight dimensions by confidence and importance const weights = { driver: 0.25 * this.driver.confidence, admin: 0.15 * this.admin.confidence, steward: 0.15 * this.steward.confidence, trust: 0.25 * this.trust.confidence, fairness: 0.20 * this.fairness.confidence, }; const totalWeight = Object.values(weights).reduce((sum, w) => sum + w, 0); if (totalWeight === 0) { return 50; // Default when no ratings yet } const weightedSum = this.driver.value * weights.driver + this.admin.value * weights.admin + this.steward.value * weights.steward + this.trust.value * weights.trust + this.fairness.value * weights.fairness; return Math.round(weightedSum / totalWeight); } /** * Get rating tier for display */ getDriverTier(): 'rookie' | 'amateur' | 'semi-pro' | 'pro' | 'elite' { if (this.driver.value >= 90) return 'elite'; if (this.driver.value >= 75) return 'pro'; if (this.driver.value >= 60) return 'semi-pro'; if (this.driver.value >= 40) return 'amateur'; return 'rookie'; } /** * Get trust level for matchmaking */ getTrustLevel(): 'unverified' | 'trusted' | 'highly-trusted' | 'community-leader' { if (this.trust.value >= 90 && this.trust.sampleSize >= 50) return 'community-leader'; if (this.trust.value >= 75 && this.trust.sampleSize >= 20) return 'highly-trusted'; if (this.trust.value >= 60 && this.trust.sampleSize >= 5) return 'trusted'; return 'unverified'; } /** * Check if user is eligible to be a steward */ canBeSteward(): boolean { return ( this.trust.value >= 70 && this.fairness.value >= 70 && this.trust.sampleSize >= 10 ); } /** * Check if user is eligible to be an admin */ canBeAdmin(): boolean { return ( this.trust.value >= 60 && this.trust.sampleSize >= 5 ); } private updateDimension( dimension: RatingDimension, newValue: number, weight: number ): RatingDimension { const clampedValue = Math.max(0, Math.min(100, newValue)); const newSampleSize = dimension.sampleSize + weight; // Exponential moving average with decay based on sample size const alpha = Math.min(0.3, 1 / (dimension.sampleSize + 1)); const updatedValue = dimension.value * (1 - alpha) + clampedValue * alpha; // Calculate confidence (asymptotic to 1) const confidence = 1 - Math.exp(-newSampleSize / 20); // Determine trend const valueDiff = updatedValue - dimension.value; let trend: 'rising' | 'stable' | 'falling' = 'stable'; if (valueDiff > 2) trend = 'rising'; if (valueDiff < -2) trend = 'falling'; return { value: Math.round(updatedValue * 10) / 10, confidence: Math.round(confidence * 100) / 100, sampleSize: newSampleSize, trend, lastUpdated: new Date(), }; } private withUpdates(updates: Partial): UserRating { const newRating = new UserRating({ ...this.props, ...updates, updatedAt: new Date(), }); // Recalculate overall reputation return new UserRating({ ...newRating.props, overallReputation: newRating.calculateOverallReputation(), }); } }