Files
gridpilot.gg/core/identity/application/use-cases/AppendRatingEventsUseCase.ts
2026-01-16 19:46:49 +01:00

123 lines
4.0 KiB
TypeScript

import { RatingEvent, type RatingEventProps } from '../../domain/entities/RatingEvent';
import { RatingEventRepository } from '../../domain/repositories/RatingEventRepository';
import { UserRatingRepository } from '../../domain/repositories/UserRatingRepository';
import { RatingEventFactory } from '../../domain/services/RatingEventFactory';
import { RatingSnapshotCalculator } from '../../domain/services/RatingSnapshotCalculator';
import { RatingDelta } from '../../domain/value-objects/RatingDelta';
import { RatingDimensionKey } from '../../domain/value-objects/RatingDimensionKey';
import { RatingEventId } from '../../domain/value-objects/RatingEventId';
import { CreateRatingEventDto } from '../dtos/CreateRatingEventDto';
/**
* Input for AppendRatingEventsUseCase
*/
export interface AppendRatingEventsInput {
userId: string;
events?: CreateRatingEventDto[]; // Optional: direct event creation
// Alternative: raceId, penaltyId, etc. for factory-based creation
raceId?: string;
raceResults?: Array<{
position: number;
totalDrivers: number;
startPosition: number;
incidents: number;
fieldStrength: number;
status: 'finished' | 'dnf' | 'dns' | 'dsq' | 'afk';
}>;
}
/**
* Output for AppendRatingEventsUseCase
*/
export interface AppendRatingEventsOutput {
events: string[]; // Event IDs
snapshotUpdated: boolean;
}
/**
* Use Case: AppendRatingEventsUseCase
*
* Appends rating events to the ledger and recomputes the snapshot.
* Follows CQRS Light: command side operation.
*/
export class AppendRatingEventsUseCase {
constructor(
private readonly ratingEventRepository: RatingEventRepository,
private readonly userRatingRepository: UserRatingRepository,
) {}
async execute(input: AppendRatingEventsInput): Promise<AppendRatingEventsOutput> {
const eventsToSave = [];
// 1. Create events from direct input
if (input.events) {
for (const eventDto of input.events) {
const event = this.createEventFromDto(eventDto);
eventsToSave.push(event);
}
}
// 2. Create events from race results (using factory)
if (input.raceId && input.raceResults) {
for (const result of input.raceResults) {
const raceEvents = RatingEventFactory.createFromRaceFinish({
userId: input.userId,
raceId: input.raceId,
position: result.position,
totalDrivers: result.totalDrivers,
startPosition: result.startPosition,
incidents: result.incidents,
fieldStrength: result.fieldStrength,
status: result.status,
});
eventsToSave.push(...raceEvents);
}
}
// 3. Save all events to ledger
const savedEventIds: string[] = [];
for (const event of eventsToSave) {
await this.ratingEventRepository.save(event);
savedEventIds.push(event.id.value);
}
// 4. Recompute snapshot if events were saved
let snapshotUpdated = false;
if (savedEventIds.length > 0) {
const allEvents = await this.ratingEventRepository.getAllByUserId(input.userId);
const snapshot = RatingSnapshotCalculator.calculate(input.userId, allEvents);
await this.userRatingRepository.save(snapshot);
snapshotUpdated = true;
}
return {
events: savedEventIds,
snapshotUpdated,
};
}
private createEventFromDto(dto: CreateRatingEventDto) {
const props: RatingEventProps = {
id: RatingEventId.generate(),
userId: dto.userId,
dimension: RatingDimensionKey.create(dto.dimension),
delta: RatingDelta.create(dto.delta),
occurredAt: dto.occurredAt ? new Date(dto.occurredAt) : new Date(),
createdAt: new Date(),
source: { type: dto.sourceType, id: dto.sourceId },
reason: {
code: dto.reasonCode,
summary: dto.reasonSummary,
details: dto.reasonDetails || {},
},
visibility: { public: true, redactedFields: [] },
version: 1,
};
if (dto.weight !== undefined) {
props.weight = dto.weight;
}
return RatingEvent.create(props);
}
}