import { Entity } from '@core/shared/domain/Entity'; import { RacingDomainInvariantError, RacingDomainValidationError } from '../errors/RacingDomainError'; import { TeamRatingDelta } from '../value-objects/TeamRatingDelta'; import { TeamRatingDimensionKey } from '../value-objects/TeamRatingDimensionKey'; import { TeamRatingEventId } from '../value-objects/TeamRatingEventId'; export interface TeamRatingEventSource { type: 'race' | 'penalty' | 'vote' | 'adminAction' | 'manualAdjustment'; id?: string; // e.g., raceId, penaltyId, voteId } export interface TeamRatingEventReason { code: string; description?: string; } export interface TeamRatingEventVisibility { public: boolean; } export interface TeamRatingEventProps { id: TeamRatingEventId; teamId: string; dimension: TeamRatingDimensionKey; delta: TeamRatingDelta; weight?: number; occurredAt: Date; createdAt: Date; source: TeamRatingEventSource; reason: TeamRatingEventReason; visibility: TeamRatingEventVisibility; version: number; } export class TeamRatingEvent extends Entity { readonly teamId: string; readonly dimension: TeamRatingDimensionKey; readonly delta: TeamRatingDelta; readonly weight: number | undefined; readonly occurredAt: Date; readonly createdAt: Date; readonly source: TeamRatingEventSource; readonly reason: TeamRatingEventReason; readonly visibility: TeamRatingEventVisibility; readonly version: number; private constructor(props: TeamRatingEventProps) { super(props.id); this.teamId = props.teamId; this.dimension = props.dimension; this.delta = props.delta; this.weight = props.weight; this.occurredAt = props.occurredAt; this.createdAt = props.createdAt; this.source = props.source; this.reason = props.reason; this.visibility = props.visibility; this.version = props.version; } /** * Factory method to create a new TeamRatingEvent. */ static create(props: { id: TeamRatingEventId; teamId: string; dimension: TeamRatingDimensionKey; delta: TeamRatingDelta; weight?: number; occurredAt: Date; createdAt: Date; source: TeamRatingEventSource; reason: TeamRatingEventReason; visibility: TeamRatingEventVisibility; version: number; }): TeamRatingEvent { // Validate required fields if (!props.teamId || props.teamId.trim().length === 0) { throw new RacingDomainValidationError('Team ID is required'); } if (!props.dimension) { throw new RacingDomainValidationError('Dimension is required'); } if (!props.delta) { throw new RacingDomainValidationError('Delta is required'); } if (!props.source) { throw new RacingDomainValidationError('Source is required'); } if (!props.reason) { throw new RacingDomainValidationError('Reason is required'); } if (!props.visibility) { throw new RacingDomainValidationError('Visibility is required'); } if (props.weight !== undefined && (typeof props.weight !== 'number' || props.weight <= 0)) { throw new RacingDomainValidationError('Weight must be a positive number if provided'); } const now = new Date(); if (props.occurredAt > now) { throw new RacingDomainValidationError('Occurrence date cannot be in the future'); } if (props.createdAt > now) { throw new RacingDomainValidationError('Creation date cannot be in the future'); } if (props.version < 1) { throw new RacingDomainValidationError('Version must be at least 1'); } // Validate invariants if (props.dimension.value === 'adminTrust' && props.source.type === 'race') { throw new RacingDomainInvariantError( 'adminTrust dimension cannot be updated from race events' ); } if (props.dimension.value === 'driving' && props.source.type === 'vote') { throw new RacingDomainInvariantError( 'driving dimension cannot be updated from vote events' ); } return new TeamRatingEvent(props); } /** * Rehydrate event from stored data (assumes data is already validated). */ static rehydrate(props: { id: TeamRatingEventId; teamId: string; dimension: TeamRatingDimensionKey; delta: TeamRatingDelta; weight?: number; occurredAt: Date; createdAt: Date; source: TeamRatingEventSource; reason: TeamRatingEventReason; visibility: TeamRatingEventVisibility; version: number; }): TeamRatingEvent { // Rehydration assumes data is already validated (from persistence) return new TeamRatingEvent(props); } /** * Compare with another event. */ equals(other: Entity): boolean { return this.id.equals(other.id); } /** * Return plain object representation for serialization. */ toJSON(): object { return { id: this.id.value, teamId: this.teamId, dimension: this.dimension.value, delta: this.delta.value, weight: this.weight, occurredAt: this.occurredAt.toISOString(), createdAt: this.createdAt.toISOString(), source: this.source, reason: this.reason, visibility: this.visibility, version: this.version, }; } }