import type { IEntity } from '@core/shared/domain'; import { RatingEventId } from '../value-objects/RatingEventId'; import { RatingDimensionKey } from '../value-objects/RatingDimensionKey'; import { RatingDelta } from '../value-objects/RatingDelta'; import { IdentityDomainValidationError, IdentityDomainInvariantError } from '../errors/IdentityDomainError'; export interface RatingEventSource { type: 'race' | 'penalty' | 'vote' | 'adminAction' | 'manualAdjustment'; id: string; } export interface RatingEventReason { code: string; summary: string; details: Record; } export interface RatingEventVisibility { public: boolean; redactedFields: string[]; } export interface RatingEventProps { id: RatingEventId; userId: string; dimension: RatingDimensionKey; delta: RatingDelta; weight?: number; occurredAt: Date; createdAt: Date; source: RatingEventSource; reason: RatingEventReason; visibility: RatingEventVisibility; version: number; } export class RatingEvent implements IEntity { readonly id: RatingEventId; readonly userId: string; readonly dimension: RatingDimensionKey; readonly delta: RatingDelta; readonly weight: number | undefined; readonly occurredAt: Date; readonly createdAt: Date; readonly source: RatingEventSource; readonly reason: RatingEventReason; readonly visibility: RatingEventVisibility; readonly version: number; private constructor(props: RatingEventProps) { this.id = props.id; this.userId = props.userId; 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; } static create(props: RatingEventProps): RatingEvent { // Validate required fields if (!props.userId || props.userId.trim().length === 0) { throw new IdentityDomainValidationError('userId is required'); } if (!props.dimension) { throw new IdentityDomainValidationError('dimension is required'); } if (!props.delta) { throw new IdentityDomainValidationError('delta is required'); } if (!props.source) { throw new IdentityDomainValidationError('source is required'); } if (!props.reason) { throw new IdentityDomainValidationError('reason is required'); } if (!props.visibility) { throw new IdentityDomainValidationError('visibility is required'); } if (!props.version || props.version < 1) { throw new IdentityDomainValidationError('version must be a positive integer'); } // Validate dates const now = new Date(); if (props.occurredAt > now) { throw new IdentityDomainValidationError('occurredAt cannot be in the future'); } if (props.createdAt > now) { throw new IdentityDomainValidationError('createdAt cannot be in the future'); } if (props.occurredAt > props.createdAt) { throw new IdentityDomainInvariantError('occurredAt must be before or equal to createdAt'); } // Validate weight if provided if (props.weight !== undefined && (props.weight <= 0 || !Number.isFinite(props.weight))) { throw new IdentityDomainValidationError('weight must be a positive number'); } return new RatingEvent(props); } static rehydrate(props: RatingEventProps): RatingEvent { // Rehydration assumes data is already validated (from persistence) return new RatingEvent(props); } equals(other: IEntity): boolean { return this.id.equals(other.id); } toJSON(): Record { return { id: this.id.value, userId: this.userId, 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, }; } }