Files
gridpilot.gg/core/identity/application/use-cases/UpsertExternalGameRatingUseCase.ts
2025-12-29 22:27:33 +01:00

186 lines
5.9 KiB
TypeScript

import { IExternalGameRatingRepository } from '../../domain/repositories/IExternalGameRatingRepository';
import { ExternalGameRatingProfile } from '../../domain/entities/ExternalGameRatingProfile';
import { UserId } from '../../domain/value-objects/UserId';
import { GameKey } from '../../domain/value-objects/GameKey';
import { ExternalRating } from '../../domain/value-objects/ExternalRating';
import { ExternalRatingProvenance } from '../../domain/value-objects/ExternalRatingProvenance';
import { UpsertExternalGameRatingInput, UpsertExternalGameRatingOutput } from '../dtos/UpsertExternalGameRatingDto';
/**
* Use Case: UpsertExternalGameRatingUseCase
*
* Upserts external game rating profile with latest data and provenance.
* Follows CQRS Light: command side operation.
*
* Store/display only, no compute.
*
* Flow:
* 1. Validate input
* 2. Check if profile exists
* 3. Create or update profile with latest ratings
* 4. Save to repository
* 5. Return result
*/
export class UpsertExternalGameRatingUseCase {
constructor(
private readonly externalGameRatingRepository: IExternalGameRatingRepository
) {}
async execute(input: UpsertExternalGameRatingInput): Promise<UpsertExternalGameRatingOutput> {
try {
// 1. Validate input
const validationError = this.validateInput(input);
if (validationError) {
return {
success: false,
profile: {
userId: input.userId,
gameKey: input.gameKey,
ratingCount: 0,
ratingTypes: [],
source: input.provenance.source,
lastSyncedAt: input.provenance.lastSyncedAt,
verified: input.provenance.verified ?? false,
},
action: 'created',
errors: [validationError],
};
}
// 2. Check if profile exists
const existingProfile = await this.externalGameRatingRepository.findByUserIdAndGameKey(
input.userId,
input.gameKey
);
// 3. Create or update profile
let profile: ExternalGameRatingProfile;
let action: 'created' | 'updated';
if (existingProfile) {
// Update existing profile
const ratingsMap = this.createRatingsMap(input.ratings, input.gameKey);
const provenance = ExternalRatingProvenance.create({
source: input.provenance.source,
lastSyncedAt: new Date(input.provenance.lastSyncedAt),
verified: input.provenance.verified ?? false,
});
existingProfile.updateRatings(ratingsMap, provenance);
profile = existingProfile;
action = 'updated';
} else {
// Create new profile
profile = this.createProfile(input);
action = 'created';
}
// 4. Save to repository
const savedProfile = await this.externalGameRatingRepository.save(profile);
// 5. Return result
const summary = savedProfile.toSummary();
return {
success: true,
profile: {
userId: summary.userId,
gameKey: summary.gameKey,
ratingCount: summary.ratingCount,
ratingTypes: summary.ratingTypes,
source: summary.source,
lastSyncedAt: summary.lastSyncedAt.toISOString(),
verified: summary.verified,
},
action,
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return {
success: false,
profile: {
userId: input.userId,
gameKey: input.gameKey,
ratingCount: 0,
ratingTypes: [],
source: input.provenance.source,
lastSyncedAt: input.provenance.lastSyncedAt,
verified: input.provenance.verified ?? false,
},
action: 'created',
errors: [errorMessage],
};
}
}
private validateInput(input: UpsertExternalGameRatingInput): string | null {
if (!input.userId || input.userId.trim().length === 0) {
return 'User ID is required';
}
if (!input.gameKey || input.gameKey.trim().length === 0) {
return 'Game key is required';
}
if (!input.ratings || input.ratings.length === 0) {
return 'Ratings are required';
}
for (const rating of input.ratings) {
if (!rating.type || rating.type.trim().length === 0) {
return 'Rating type cannot be empty';
}
if (typeof rating.value !== 'number' || isNaN(rating.value)) {
return `Rating value for type ${rating.type} must be a valid number`;
}
}
if (!input.provenance.source || input.provenance.source.trim().length === 0) {
return 'Provenance source is required';
}
const syncDate = new Date(input.provenance.lastSyncedAt);
if (isNaN(syncDate.getTime())) {
return 'Provenance lastSyncedAt must be a valid date';
}
return null;
}
private createRatingsMap(
ratingsData: Array<{ type: string; value: number }>,
gameKeyString: string
): Map<string, ExternalRating> {
const gameKey = GameKey.create(gameKeyString);
const ratingsMap = new Map<string, ExternalRating>();
for (const ratingData of ratingsData) {
const rating = ExternalRating.create(
gameKey,
ratingData.type.trim(),
ratingData.value
);
ratingsMap.set(ratingData.type.trim(), rating);
}
return ratingsMap;
}
private createProfile(input: UpsertExternalGameRatingInput): ExternalGameRatingProfile {
const userId = UserId.fromString(input.userId);
const gameKey = GameKey.create(input.gameKey);
const ratingsMap = this.createRatingsMap(input.ratings, input.gameKey);
const provenance = ExternalRatingProvenance.create({
source: input.provenance.source,
lastSyncedAt: new Date(input.provenance.lastSyncedAt),
verified: input.provenance.verified ?? false,
});
return ExternalGameRatingProfile.create({
userId,
gameKey,
ratings: ratingsMap,
provenance,
});
}
}