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

181 lines
5.1 KiB
TypeScript

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<TeamRatingEventId> {
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<TeamRatingEventId>): 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,
};
}
}