import type { IValueObject } from '@core/shared/domain'; /** * Value Object: TeamRating * * Multi-dimensional rating system for teams covering: * - Driving: racing ability, performance, consistency * - AdminTrust: reliability, leadership, community contribution */ export interface TeamRatingDimension { 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 TeamRatingProps { teamId: string; driving: TeamRatingDimension; adminTrust: TeamRatingDimension; overall: number; calculatorVersion?: string; createdAt: Date; updatedAt: Date; } const DEFAULT_DIMENSION: TeamRatingDimension = { value: 50, confidence: 0, sampleSize: 0, trend: 'stable', lastUpdated: new Date(), }; export class TeamRating implements IValueObject { readonly props: TeamRatingProps; private constructor(props: TeamRatingProps) { this.props = props; } get teamId(): string { return this.props.teamId; } get driving(): TeamRatingDimension { return this.props.driving; } get adminTrust(): TeamRatingDimension { return this.props.adminTrust; } get overall(): number { return this.props.overall; } get createdAt(): Date { return this.props.createdAt; } get updatedAt(): Date { return this.props.updatedAt; } get calculatorVersion(): string | undefined { return this.props.calculatorVersion; } static create(teamId: string): TeamRating { if (!teamId || teamId.trim().length === 0) { throw new Error('TeamRating teamId is required'); } const now = new Date(); return new TeamRating({ teamId, driving: { ...DEFAULT_DIMENSION, lastUpdated: now }, adminTrust: { ...DEFAULT_DIMENSION, lastUpdated: now }, overall: 50, calculatorVersion: '1.0', createdAt: now, updatedAt: now, }); } static restore(props: TeamRatingProps): TeamRating { return new TeamRating(props); } equals(other: IValueObject): boolean { return this.props.teamId === other.props.teamId; } /** * Update driving rating based on race performance */ updateDrivingRating( newValue: number, weight: number = 1 ): TeamRating { const updated = this.updateDimension(this.driving, newValue, weight); return this.withUpdates({ driving: updated }); } /** * Update admin trust rating based on league management feedback */ updateAdminTrustRating( newValue: number, weight: number = 1 ): TeamRating { const updated = this.updateDimension(this.adminTrust, newValue, weight); return this.withUpdates({ adminTrust: updated }); } /** * Calculate weighted overall rating */ calculateOverall(): number { // Weight dimensions by confidence const weights = { driving: 0.7 * this.driving.confidence, adminTrust: 0.3 * this.adminTrust.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.driving.value * weights.driving + this.adminTrust.value * weights.adminTrust; return Math.round(weightedSum / totalWeight); } private updateDimension( dimension: TeamRatingDimension, newValue: number, weight: number ): TeamRatingDimension { 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): TeamRating { const newRating = new TeamRating({ ...this.props, ...updates, updatedAt: new Date(), }); // Recalculate overall return new TeamRating({ ...newRating.props, overall: newRating.calculateOverall(), }); } }