Files
gridpilot.gg/core/identity/domain/entities/RatingEvent.ts
2026-01-16 16:46:57 +01:00

140 lines
4.0 KiB
TypeScript

import { Entity } from '@core/shared/domain/Entity';
import { IdentityDomainInvariantError, IdentityDomainValidationError } from '../errors/IdentityDomainError';
import { RatingDelta } from '../value-objects/RatingDelta';
import { RatingDimensionKey } from '../value-objects/RatingDimensionKey';
import { RatingEventId } from '../value-objects/RatingEventId';
export interface RatingEventSource {
type: 'race' | 'penalty' | 'vote' | 'adminAction' | 'manualAdjustment';
id: string;
}
export interface RatingEventReason {
code: string;
summary: string;
details: Record<string, unknown>;
}
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 extends Entity<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) {
super(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: Entity<RatingEventId>): boolean {
return this.id.equals(other.id);
}
toJSON(): Record<string, unknown> {
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,
};
}
}