186 lines
5.9 KiB
TypeScript
186 lines
5.9 KiB
TypeScript
import { ExternalGameRatingRepository } from '../../domain/repositories/ExternalGameRatingRepository';
|
|
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: ExternalGameRatingRepository
|
|
) {}
|
|
|
|
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,
|
|
});
|
|
}
|
|
} |