Files
gridpilot.gg/core/identity/domain/entities/ExternalGameRatingProfile.ts
2026-01-16 19:46:49 +01:00

233 lines
6.0 KiB
TypeScript

import { Entity } from '@core/shared/domain/Entity';
import { IdentityDomainValidationError } from '../errors/IdentityDomainError';
import { ExternalRating } from '../value-objects/ExternalRating';
import { ExternalRatingProvenance } from '../value-objects/ExternalRatingProvenance';
import { GameKey } from '../value-objects/GameKey';
import { UserId } from '../value-objects/UserId';
export interface ExternalGameRatingProfileProps {
userId: UserId;
gameKey: GameKey;
ratings: Map<string, ExternalRating>; // type -> rating
provenance: ExternalRatingProvenance;
}
export class ExternalGameRatingProfile extends Entity<UserId> {
private readonly _userId: UserId;
private readonly _gameKey: GameKey;
private _ratings: Map<string, ExternalRating>;
private _provenance: ExternalRatingProvenance;
private constructor(props: ExternalGameRatingProfileProps) {
super(props.userId);
this._userId = props.userId;
this._gameKey = props.gameKey;
this._ratings = props.ratings;
this._provenance = props.provenance;
}
static create(props: ExternalGameRatingProfileProps): ExternalGameRatingProfile {
if (!props.userId || !props.gameKey || !props.ratings || !props.provenance) {
throw new IdentityDomainValidationError('All properties are required');
}
// Note: Empty ratings map is allowed for initial creation
// The entity can be created with no ratings and updated later
// Validate that all ratings match the gameKey
for (const [type, rating] of props.ratings.entries()) {
if (!rating.gameKey.equals(props.gameKey)) {
throw new IdentityDomainValidationError(
`Rating type ${type} has mismatched gameKey`
);
}
}
return new ExternalGameRatingProfile(props);
}
static restore(props: {
userId: string;
gameKey: string;
ratings: Array<{ type: string; gameKey: string; value: number }>;
provenance: {
source: string;
lastSyncedAt: Date;
verified?: boolean;
};
}): ExternalGameRatingProfile {
const userId = UserId.fromString(props.userId);
const gameKey = GameKey.create(props.gameKey);
const ratingsMap = new Map<string, ExternalRating>();
for (const ratingData of props.ratings) {
const ratingGameKey = GameKey.create(ratingData.gameKey);
const rating = ExternalRating.create(ratingGameKey, ratingData.type, ratingData.value);
ratingsMap.set(ratingData.type, rating);
}
const provenance = ExternalRatingProvenance.restore(props.provenance);
return new ExternalGameRatingProfile({
userId,
gameKey,
ratings: ratingsMap,
provenance,
});
}
get userId(): UserId {
return this._userId;
}
get gameKey(): GameKey {
return this._gameKey;
}
get ratings(): ReadonlyMap<string, ExternalRating> {
return new Map(this._ratings);
}
get provenance(): ExternalRatingProvenance {
return this._provenance;
}
/**
* Update ratings and provenance with latest data
*/
updateRatings(
newRatings: Map<string, ExternalRating>,
newProvenance: ExternalRatingProvenance
): void {
// Validate all new ratings match the gameKey
for (const [type, rating] of newRatings.entries()) {
if (!rating.gameKey.equals(this._gameKey)) {
throw new IdentityDomainValidationError(
`Rating type ${type} has mismatched gameKey`
);
}
}
this._ratings = newRatings;
this._provenance = newProvenance;
}
/**
* Get a specific rating by type
*/
getRatingByType(type: string): ExternalRating | undefined {
return this._ratings.get(type);
}
/**
* Get all rating types
*/
getRatingTypes(): string[] {
return Array.from(this._ratings.keys());
}
/**
* Check if profile has any ratings
*/
hasRatings(): boolean {
return this._ratings.size > 0;
}
/**
* Mark provenance as verified
*/
markVerified(): void {
this._provenance = this._provenance.markVerified();
}
/**
* Update last synced timestamp
*/
updateLastSyncedAt(date: Date): void {
this._provenance = this._provenance.updateLastSyncedAt(date);
}
/**
* Get summary for display
*/
toSummary(): {
userId: string;
gameKey: string;
ratingCount: number;
ratingTypes: string[];
source: string;
lastSyncedAt: Date;
verified: boolean;
} {
return {
userId: this._userId.toString(),
gameKey: this._gameKey.toString(),
ratingCount: this._ratings.size,
ratingTypes: this.getRatingTypes(),
source: this._provenance.source,
lastSyncedAt: this._provenance.lastSyncedAt,
verified: this._provenance.verified,
};
}
/**
* Serialize for storage
*/
toJSON(): {
userId: string;
gameKey: string;
ratings: Array<{ type: string; gameKey: string; value: number }>;
provenance: {
source: string;
lastSyncedAt: Date;
verified: boolean;
};
} {
return {
userId: this._userId.toString(),
gameKey: this._gameKey.toString(),
ratings: Array.from(this._ratings.entries()).map(([type, rating]) => ({
type,
gameKey: rating.gameKey.toString(),
value: rating.value,
})),
provenance: {
source: this._provenance.source,
lastSyncedAt: this._provenance.lastSyncedAt,
verified: this._provenance.verified,
},
};
}
equals(other: ExternalGameRatingProfile): boolean {
if (!(other instanceof ExternalGameRatingProfile)) {
return false;
}
if (!this._userId.equals(other._userId)) {
return false;
}
if (!this._gameKey.equals(other._gameKey)) {
return false;
}
if (!this._provenance.equals(other._provenance)) {
return false;
}
// Compare ratings maps
if (this._ratings.size !== other._ratings.size) {
return false;
}
for (const [type, rating] of this._ratings.entries()) {
const otherRating = other._ratings.get(type);
if (!otherRating || !rating.equals(otherRating)) {
return false;
}
}
return true;
}
}