import { ExternalGameRatingProfile } from '../../domain/entities/ExternalGameRatingProfile'; import { ExternalGameRatingRepository } from '../../domain/repositories/ExternalGameRatingRepository'; import { ExternalRating } from '../../domain/value-objects/ExternalRating'; import { ExternalRatingProvenance } from '../../domain/value-objects/ExternalRatingProvenance'; import { GameKey } from '../../domain/value-objects/GameKey'; import { UserId } from '../../domain/value-objects/UserId'; 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: ExternalGameRatingRepository ) {} async execute(input: UpsertExternalGameRatingInput): Promise { 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 { const gameKey = GameKey.create(gameKeyString); const ratingsMap = new Map(); 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, }); } }