rating
This commit is contained in:
140
core/identity/domain/entities/RatingEvent.ts
Normal file
140
core/identity/domain/entities/RatingEvent.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
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<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 implements IEntity<RatingEventId> {
|
||||
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<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,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user