rating
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* Tests for GetLeagueEligibilityPreviewQuery
|
||||
*/
|
||||
|
||||
import { GetLeagueEligibilityPreviewQuery, GetLeagueEligibilityPreviewQueryHandler } from './GetLeagueEligibilityPreviewQuery';
|
||||
import { UserRating } from '../../domain/value-objects/UserRating';
|
||||
import { ExternalGameRatingProfile } from '../../domain/entities/ExternalGameRatingProfile';
|
||||
import { GameKey } from '../../domain/value-objects/GameKey';
|
||||
import { ExternalRating } from '../../domain/value-objects/ExternalRating';
|
||||
import { ExternalRatingProvenance } from '../../domain/value-objects/ExternalRatingProvenance';
|
||||
|
||||
describe('GetLeagueEligibilityPreviewQuery', () => {
|
||||
let mockUserRatingRepo: any;
|
||||
let mockExternalRatingRepo: any;
|
||||
let handler: GetLeagueEligibilityPreviewQueryHandler;
|
||||
|
||||
beforeEach(() => {
|
||||
mockUserRatingRepo = {
|
||||
findByUserId: jest.fn(),
|
||||
};
|
||||
mockExternalRatingRepo = {
|
||||
findByUserId: jest.fn(),
|
||||
};
|
||||
|
||||
handler = new GetLeagueEligibilityPreviewQueryHandler(
|
||||
mockUserRatingRepo,
|
||||
mockExternalRatingRepo
|
||||
);
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should evaluate simple platform eligibility - eligible', async () => {
|
||||
const userId = 'user-123';
|
||||
const leagueId = 'league-456';
|
||||
const rules = 'platform.driving >= 55';
|
||||
|
||||
const userRating = UserRating.create(userId);
|
||||
// Update driving to 65
|
||||
const updatedRating = userRating.updateDriverRating(65);
|
||||
mockUserRatingRepo.findByUserId.mockResolvedValue(updatedRating);
|
||||
mockExternalRatingRepo.findByUserId.mockResolvedValue([]);
|
||||
|
||||
const query: GetLeagueEligibilityPreviewQuery = {
|
||||
userId,
|
||||
leagueId,
|
||||
eligibilityRules: rules,
|
||||
};
|
||||
|
||||
const result = await handler.execute(query);
|
||||
|
||||
expect(result.eligible).toBe(true);
|
||||
expect(result.reasons).toHaveLength(1);
|
||||
expect(result.reasons[0].target).toBe('platform.driving');
|
||||
expect(result.reasons[0].failed).toBe(false);
|
||||
});
|
||||
|
||||
it('should evaluate simple platform eligibility - not eligible', async () => {
|
||||
const userId = 'user-123';
|
||||
const leagueId = 'league-456';
|
||||
const rules = 'platform.driving >= 75';
|
||||
|
||||
const userRating = UserRating.create(userId);
|
||||
// Driving is 50 by default
|
||||
mockUserRatingRepo.findByUserId.mockResolvedValue(userRating);
|
||||
mockExternalRatingRepo.findByUserId.mockResolvedValue([]);
|
||||
|
||||
const query: GetLeagueEligibilityPreviewQuery = {
|
||||
userId,
|
||||
leagueId,
|
||||
eligibilityRules: rules,
|
||||
};
|
||||
|
||||
const result = await handler.execute(query);
|
||||
|
||||
expect(result.eligible).toBe(false);
|
||||
expect(result.reasons[0].failed).toBe(true);
|
||||
});
|
||||
|
||||
it('should evaluate external rating eligibility', async () => {
|
||||
const userId = 'user-123';
|
||||
const leagueId = 'league-456';
|
||||
const rules = 'external.iracing.iRating between 2000 2500';
|
||||
|
||||
mockUserRatingRepo.findByUserId.mockResolvedValue(null);
|
||||
|
||||
const gameKey = GameKey.create('iracing');
|
||||
const profile = ExternalGameRatingProfile.create({
|
||||
userId: { toString: () => userId } as any,
|
||||
gameKey,
|
||||
ratings: new Map([
|
||||
['iRating', ExternalRating.create(gameKey, 'iRating', 2200)],
|
||||
]),
|
||||
provenance: ExternalRatingProvenance.create({ source: 'iRacing API', lastSyncedAt: new Date() }),
|
||||
});
|
||||
mockExternalRatingRepo.findByUserId.mockResolvedValue([profile]);
|
||||
|
||||
const query: GetLeagueEligibilityPreviewQuery = {
|
||||
userId,
|
||||
leagueId,
|
||||
eligibilityRules: rules,
|
||||
};
|
||||
|
||||
const result = await handler.execute(query);
|
||||
|
||||
expect(result.eligible).toBe(true);
|
||||
expect(result.reasons[0].target).toBe('external.iracing.iRating');
|
||||
});
|
||||
|
||||
it('should evaluate complex AND conditions', async () => {
|
||||
const userId = 'user-123';
|
||||
const leagueId = 'league-456';
|
||||
const rules = 'platform.driving >= 55 AND external.iracing.iRating >= 2000';
|
||||
|
||||
const userRating = UserRating.create(userId);
|
||||
const updatedRating = userRating.updateDriverRating(65);
|
||||
mockUserRatingRepo.findByUserId.mockResolvedValue(updatedRating);
|
||||
|
||||
const gameKey = GameKey.create('iracing');
|
||||
const profile = ExternalGameRatingProfile.create({
|
||||
userId: { toString: () => userId } as any,
|
||||
gameKey,
|
||||
ratings: new Map([
|
||||
['iRating', ExternalRating.create(gameKey, 'iRating', 2200)],
|
||||
]),
|
||||
provenance: ExternalRatingProvenance.create({ source: 'iRacing API', lastSyncedAt: new Date() }),
|
||||
});
|
||||
mockExternalRatingRepo.findByUserId.mockResolvedValue([profile]);
|
||||
|
||||
const query: GetLeagueEligibilityPreviewQuery = {
|
||||
userId,
|
||||
leagueId,
|
||||
eligibilityRules: rules,
|
||||
};
|
||||
|
||||
const result = await handler.execute(query);
|
||||
|
||||
expect(result.eligible).toBe(true);
|
||||
expect(result.reasons).toHaveLength(2);
|
||||
expect(result.reasons.every(r => !r.failed)).toBe(true);
|
||||
});
|
||||
|
||||
it('should evaluate OR conditions with at least one passing', async () => {
|
||||
const userId = 'user-123';
|
||||
const leagueId = 'league-456';
|
||||
const rules = 'platform.driving >= 75 OR external.iracing.iRating >= 2000';
|
||||
|
||||
const userRating = UserRating.create(userId);
|
||||
mockUserRatingRepo.findByUserId.mockResolvedValue(userRating);
|
||||
|
||||
const gameKey = GameKey.create('iracing');
|
||||
const profile = ExternalGameRatingProfile.create({
|
||||
userId: { toString: () => userId } as any,
|
||||
gameKey,
|
||||
ratings: new Map([
|
||||
['iRating', ExternalRating.create(gameKey, 'iRating', 2200)],
|
||||
]),
|
||||
provenance: ExternalRatingProvenance.create({ source: 'iRacing API', lastSyncedAt: new Date() }),
|
||||
});
|
||||
mockExternalRatingRepo.findByUserId.mockResolvedValue([profile]);
|
||||
|
||||
const query: GetLeagueEligibilityPreviewQuery = {
|
||||
userId,
|
||||
leagueId,
|
||||
eligibilityRules: rules,
|
||||
};
|
||||
|
||||
const result = await handler.execute(query);
|
||||
|
||||
expect(result.eligible).toBe(true);
|
||||
expect(result.reasons.filter(r => !r.failed)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should include context in metadata', async () => {
|
||||
const userId = 'user-123';
|
||||
const leagueId = 'league-456';
|
||||
const rules = 'platform.driving >= 55';
|
||||
|
||||
mockUserRatingRepo.findByUserId.mockResolvedValue(null);
|
||||
mockExternalRatingRepo.findByUserId.mockResolvedValue([]);
|
||||
|
||||
const query: GetLeagueEligibilityPreviewQuery = {
|
||||
userId,
|
||||
leagueId,
|
||||
eligibilityRules: rules,
|
||||
};
|
||||
|
||||
const result = await handler.execute(query);
|
||||
|
||||
expect(result.metadata?.userId).toBe(userId);
|
||||
expect(result.metadata?.filter).toBe(rules);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Query: GetLeagueEligibilityPreviewQuery
|
||||
*
|
||||
* Preview eligibility for a league based on DSL rules.
|
||||
* Uses EligibilityEvaluator to provide explainable results.
|
||||
*/
|
||||
|
||||
import { EvaluationResultDto } from '../dtos/EvaluationResultDto';
|
||||
import { EligibilityFilterDto } from '../dtos/EligibilityFilterDto';
|
||||
import { EligibilityEvaluator, RatingData } from '../../domain/services/EligibilityEvaluator';
|
||||
import { IUserRatingRepository } from '../../domain/repositories/IUserRatingRepository';
|
||||
import { IExternalGameRatingRepository } from '../../domain/repositories/IExternalGameRatingRepository';
|
||||
|
||||
export interface GetLeagueEligibilityPreviewQuery {
|
||||
userId: string;
|
||||
leagueId: string;
|
||||
eligibilityRules: string; // DSL expression
|
||||
}
|
||||
|
||||
export class GetLeagueEligibilityPreviewQueryHandler {
|
||||
private readonly evaluator: EligibilityEvaluator;
|
||||
|
||||
constructor(
|
||||
private readonly userRatingRepo: IUserRatingRepository,
|
||||
private readonly externalRatingRepo: IExternalGameRatingRepository
|
||||
) {
|
||||
this.evaluator = new EligibilityEvaluator();
|
||||
}
|
||||
|
||||
async execute(query: GetLeagueEligibilityPreviewQuery): Promise<EvaluationResultDto> {
|
||||
const { userId, leagueId, eligibilityRules } = query;
|
||||
|
||||
// Fetch user's rating data
|
||||
const userRating = await this.userRatingRepo.findByUserId(userId);
|
||||
const externalProfiles = await this.externalRatingRepo.findByUserId(userId);
|
||||
|
||||
// Build rating data for evaluation
|
||||
const ratingData: RatingData = {
|
||||
platform: {
|
||||
driving: userRating?.driver.value || 0,
|
||||
admin: userRating?.admin.value || 0,
|
||||
steward: userRating?.steward.value || 0,
|
||||
trust: userRating?.trust.value || 0,
|
||||
fairness: userRating?.fairness.value || 0,
|
||||
},
|
||||
external: {},
|
||||
};
|
||||
|
||||
// Add external ratings
|
||||
for (const profile of externalProfiles) {
|
||||
const gameKey = profile.gameKey.toString();
|
||||
ratingData.external[gameKey] = {};
|
||||
|
||||
// Convert Map to array and iterate
|
||||
const ratingsArray = Array.from(profile.ratings.entries());
|
||||
for (const [type, rating] of ratingsArray) {
|
||||
ratingData.external[gameKey][type] = rating.value;
|
||||
}
|
||||
}
|
||||
|
||||
// Evaluate eligibility
|
||||
const filter: EligibilityFilterDto = {
|
||||
dsl: eligibilityRules,
|
||||
context: {
|
||||
userId,
|
||||
leagueId,
|
||||
},
|
||||
};
|
||||
|
||||
return this.evaluator.evaluate(filter, ratingData);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Query: GetUserRatingLedgerQuery
|
||||
*
|
||||
* Paginated/filtered query for user rating events (ledger).
|
||||
*/
|
||||
|
||||
import { LedgerEntryDto, LedgerFilter, PaginatedLedgerResult } from '../dtos/LedgerEntryDto';
|
||||
import { IRatingEventRepository, PaginatedQueryOptions, RatingEventFilter } from '../../domain/repositories/IRatingEventRepository';
|
||||
|
||||
export interface GetUserRatingLedgerQuery {
|
||||
userId: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
filter?: LedgerFilter;
|
||||
}
|
||||
|
||||
export class GetUserRatingLedgerQueryHandler {
|
||||
constructor(
|
||||
private readonly ratingEventRepo: IRatingEventRepository
|
||||
) {}
|
||||
|
||||
async execute(query: GetUserRatingLedgerQuery): Promise<PaginatedLedgerResult> {
|
||||
const { userId, limit = 20, offset = 0, filter } = query;
|
||||
|
||||
// Build repo options
|
||||
const repoOptions: PaginatedQueryOptions = {
|
||||
limit,
|
||||
offset,
|
||||
};
|
||||
|
||||
// Add filter if provided
|
||||
if (filter) {
|
||||
const ratingEventFilter: RatingEventFilter = {};
|
||||
|
||||
if (filter.dimensions) {
|
||||
ratingEventFilter.dimensions = filter.dimensions;
|
||||
}
|
||||
if (filter.sourceTypes) {
|
||||
ratingEventFilter.sourceTypes = filter.sourceTypes;
|
||||
}
|
||||
if (filter.from) {
|
||||
ratingEventFilter.from = new Date(filter.from);
|
||||
}
|
||||
if (filter.to) {
|
||||
ratingEventFilter.to = new Date(filter.to);
|
||||
}
|
||||
if (filter.reasonCodes) {
|
||||
ratingEventFilter.reasonCodes = filter.reasonCodes;
|
||||
}
|
||||
|
||||
repoOptions.filter = ratingEventFilter;
|
||||
}
|
||||
|
||||
// Query repository
|
||||
const result = await this.ratingEventRepo.findEventsPaginated(userId, repoOptions);
|
||||
|
||||
// Convert domain entities to DTOs
|
||||
const entries: LedgerEntryDto[] = result.items.map(event => {
|
||||
const dto: LedgerEntryDto = {
|
||||
id: event.id.value,
|
||||
userId: event.userId,
|
||||
dimension: event.dimension.value,
|
||||
delta: event.delta.value,
|
||||
occurredAt: event.occurredAt.toISOString(),
|
||||
createdAt: event.createdAt.toISOString(),
|
||||
source: event.source,
|
||||
reason: event.reason,
|
||||
visibility: event.visibility,
|
||||
};
|
||||
if (event.weight !== undefined) {
|
||||
dto.weight = event.weight;
|
||||
}
|
||||
return dto;
|
||||
});
|
||||
|
||||
const nextOffset = result.nextOffset !== undefined ? result.nextOffset : null;
|
||||
|
||||
return {
|
||||
entries,
|
||||
pagination: {
|
||||
total: result.total,
|
||||
limit: result.limit,
|
||||
offset: result.offset,
|
||||
hasMore: result.hasMore,
|
||||
nextOffset,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* Tests for GetUserRatingsSummaryQuery
|
||||
*/
|
||||
|
||||
import { GetUserRatingsSummaryQuery, GetUserRatingsSummaryQueryHandler } from './GetUserRatingsSummaryQuery';
|
||||
import { UserRating } from '../../domain/value-objects/UserRating';
|
||||
import { ExternalGameRatingProfile } from '../../domain/entities/ExternalGameRatingProfile';
|
||||
import { GameKey } from '../../domain/value-objects/GameKey';
|
||||
import { ExternalRating } from '../../domain/value-objects/ExternalRating';
|
||||
import { ExternalRatingProvenance } from '../../domain/value-objects/ExternalRatingProvenance';
|
||||
import { RatingEvent } from '../../domain/entities/RatingEvent';
|
||||
import { RatingEventId } from '../../domain/value-objects/RatingEventId';
|
||||
import { RatingDimensionKey } from '../../domain/value-objects/RatingDimensionKey';
|
||||
import { RatingDelta } from '../../domain/value-objects/RatingDelta';
|
||||
|
||||
describe('GetUserRatingsSummaryQuery', () => {
|
||||
let mockUserRatingRepo: any;
|
||||
let mockExternalRatingRepo: any;
|
||||
let mockRatingEventRepo: any;
|
||||
let handler: GetUserRatingsSummaryQueryHandler;
|
||||
|
||||
beforeEach(() => {
|
||||
mockUserRatingRepo = {
|
||||
findByUserId: jest.fn(),
|
||||
};
|
||||
mockExternalRatingRepo = {
|
||||
findByUserId: jest.fn(),
|
||||
};
|
||||
mockRatingEventRepo = {
|
||||
getAllByUserId: jest.fn(),
|
||||
};
|
||||
|
||||
handler = new GetUserRatingsSummaryQueryHandler(
|
||||
mockUserRatingRepo,
|
||||
mockExternalRatingRepo,
|
||||
mockRatingEventRepo
|
||||
);
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should return summary with platform and external ratings', async () => {
|
||||
const userId = 'user-123';
|
||||
|
||||
// Mock user rating
|
||||
const userRating = UserRating.create(userId);
|
||||
mockUserRatingRepo.findByUserId.mockResolvedValue(userRating);
|
||||
|
||||
// Mock external ratings
|
||||
const gameKey = GameKey.create('iracing');
|
||||
const profile = ExternalGameRatingProfile.create({
|
||||
userId: { toString: () => userId } as any,
|
||||
gameKey,
|
||||
ratings: new Map([
|
||||
['iRating', ExternalRating.create(gameKey, 'iRating', 2200)],
|
||||
['safetyRating', ExternalRating.create(gameKey, 'safetyRating', 4.5)],
|
||||
]),
|
||||
provenance: ExternalRatingProvenance.create('iRacing API', new Date()),
|
||||
});
|
||||
mockExternalRatingRepo.findByUserId.mockResolvedValue([profile]);
|
||||
|
||||
// Mock rating events
|
||||
const event = RatingEvent.create({
|
||||
id: RatingEventId.create(),
|
||||
userId,
|
||||
dimension: RatingDimensionKey.create('driver'),
|
||||
delta: RatingDelta.create(5),
|
||||
occurredAt: new Date('2024-01-01'),
|
||||
createdAt: new Date('2024-01-01'),
|
||||
source: { type: 'race', id: 'race-123' },
|
||||
reason: { code: 'RACE_FINISH', summary: 'Good race', details: {} },
|
||||
visibility: { public: true, redactedFields: [] },
|
||||
version: 1,
|
||||
});
|
||||
mockRatingEventRepo.getAllByUserId.mockResolvedValue([event]);
|
||||
|
||||
const query: GetUserRatingsSummaryQuery = { userId };
|
||||
const result = await handler.execute(query);
|
||||
|
||||
expect(result.userId).toBe(userId);
|
||||
expect(result.platform.driving.value).toBe(50); // Default
|
||||
expect(result.platform.overallReputation).toBe(50);
|
||||
expect(result.external.iracing.iRating).toBe(2200);
|
||||
expect(result.external.iracing.safetyRating).toBe(4.5);
|
||||
expect(result.lastRatingEventAt).toBe('2024-01-01T00:00:00.000Z');
|
||||
});
|
||||
|
||||
it('should handle missing user rating gracefully', async () => {
|
||||
const userId = 'user-123';
|
||||
|
||||
mockUserRatingRepo.findByUserId.mockResolvedValue(null);
|
||||
mockExternalRatingRepo.findByUserId.mockResolvedValue([]);
|
||||
mockRatingEventRepo.getAllByUserId.mockResolvedValue([]);
|
||||
|
||||
const query: GetUserRatingsSummaryQuery = { userId };
|
||||
const result = await handler.execute(query);
|
||||
|
||||
expect(result.userId).toBe(userId);
|
||||
expect(result.platform.driving.value).toBe(0);
|
||||
expect(result.platform.overallReputation).toBe(0);
|
||||
expect(result.external).toEqual({});
|
||||
expect(result.lastRatingEventAt).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle multiple external game profiles', async () => {
|
||||
const userId = 'user-123';
|
||||
|
||||
mockUserRatingRepo.findByUserId.mockResolvedValue(UserRating.create(userId));
|
||||
|
||||
// Multiple game profiles
|
||||
const iracingProfile = ExternalGameRatingProfile.create({
|
||||
userId: { toString: () => userId } as any,
|
||||
gameKey: GameKey.create('iracing'),
|
||||
ratings: new Map([
|
||||
['iRating', ExternalRating.create(GameKey.create('iracing'), 'iRating', 2200)],
|
||||
]),
|
||||
provenance: ExternalRatingProvenance.create('iRacing API', new Date()),
|
||||
});
|
||||
|
||||
const assettoProfile = ExternalGameRatingProfile.create({
|
||||
userId: { toString: () => userId } as any,
|
||||
gameKey: GameKey.create('assetto'),
|
||||
ratings: new Map([
|
||||
['rating', ExternalRating.create(GameKey.create('assetto'), 'rating', 85)],
|
||||
]),
|
||||
provenance: ExternalRatingProvenance.create('Assetto API', new Date()),
|
||||
});
|
||||
|
||||
mockExternalRatingRepo.findByUserId.mockResolvedValue([iracingProfile, assettoProfile]);
|
||||
mockRatingEventRepo.getAllByUserId.mockResolvedValue([]);
|
||||
|
||||
const query: GetUserRatingsSummaryQuery = { userId };
|
||||
const result = await handler.execute(query);
|
||||
|
||||
expect(result.external.iracing.iRating).toBe(2200);
|
||||
expect(result.external.assetto.rating).toBe(85);
|
||||
});
|
||||
|
||||
it('should handle empty external ratings', async () => {
|
||||
const userId = 'user-123';
|
||||
|
||||
mockUserRatingRepo.findByUserId.mockResolvedValue(UserRating.create(userId));
|
||||
mockExternalRatingRepo.findByUserId.mockResolvedValue([]);
|
||||
mockRatingEventRepo.getAllByUserId.mockResolvedValue([]);
|
||||
|
||||
const query: GetUserRatingsSummaryQuery = { userId };
|
||||
const result = await handler.execute(query);
|
||||
|
||||
expect(result.external).toEqual({});
|
||||
});
|
||||
|
||||
it('should use current date for timestamps when no user rating exists', async () => {
|
||||
const userId = 'user-123';
|
||||
|
||||
mockUserRatingRepo.findByUserId.mockResolvedValue(null);
|
||||
mockExternalRatingRepo.findByUserId.mockResolvedValue([]);
|
||||
mockRatingEventRepo.getAllByUserId.mockResolvedValue([]);
|
||||
|
||||
const beforeQuery = new Date();
|
||||
const query: GetUserRatingsSummaryQuery = { userId };
|
||||
const result = await handler.execute(query);
|
||||
const afterQuery = new Date();
|
||||
|
||||
// Should have valid ISO date strings
|
||||
expect(new Date(result.createdAt).getTime()).toBeGreaterThanOrEqual(beforeQuery.getTime());
|
||||
expect(new Date(result.createdAt).getTime()).toBeLessThanOrEqual(afterQuery.getTime());
|
||||
expect(new Date(result.updatedAt).getTime()).toBeGreaterThanOrEqual(beforeQuery.getTime());
|
||||
expect(new Date(result.updatedAt).getTime()).toBeLessThanOrEqual(afterQuery.getTime());
|
||||
});
|
||||
});
|
||||
});
|
||||
118
core/identity/application/queries/GetUserRatingsSummaryQuery.ts
Normal file
118
core/identity/application/queries/GetUserRatingsSummaryQuery.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* Query: GetUserRatingsSummaryQuery
|
||||
*
|
||||
* Fast read query for user rating summary.
|
||||
* Combines platform snapshots and external game ratings.
|
||||
*/
|
||||
|
||||
import { RatingSummaryDto } from '../dtos/RatingSummaryDto';
|
||||
import { IUserRatingRepository } from '../../domain/repositories/IUserRatingRepository';
|
||||
import { IExternalGameRatingRepository } from '../../domain/repositories/IExternalGameRatingRepository';
|
||||
import { IRatingEventRepository } from '../../domain/repositories/IRatingEventRepository';
|
||||
|
||||
export interface GetUserRatingsSummaryQuery {
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export class GetUserRatingsSummaryQueryHandler {
|
||||
constructor(
|
||||
private readonly userRatingRepo: IUserRatingRepository,
|
||||
private readonly externalRatingRepo: IExternalGameRatingRepository,
|
||||
private readonly ratingEventRepo: IRatingEventRepository
|
||||
) {}
|
||||
|
||||
async execute(query: GetUserRatingsSummaryQuery): Promise<RatingSummaryDto> {
|
||||
const { userId } = query;
|
||||
|
||||
// Fetch platform rating snapshot
|
||||
const userRating = await this.userRatingRepo.findByUserId(userId);
|
||||
|
||||
// Fetch all external game ratings
|
||||
const externalProfiles = await this.externalRatingRepo.findByUserId(userId);
|
||||
|
||||
// Get last event timestamp if available
|
||||
let lastRatingEventAt: string | undefined;
|
||||
if (userRating) {
|
||||
// Get all events to find the most recent one
|
||||
const events = await this.ratingEventRepo.getAllByUserId(userId);
|
||||
if (events.length > 0) {
|
||||
const lastEvent = events[events.length - 1];
|
||||
if (lastEvent) {
|
||||
lastRatingEventAt = lastEvent.occurredAt.toISOString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build platform rating dimensions
|
||||
const platform = {
|
||||
driving: {
|
||||
value: userRating?.driver.value || 0,
|
||||
confidence: userRating?.driver.confidence || 0,
|
||||
sampleSize: userRating?.driver.sampleSize || 0,
|
||||
trend: userRating?.driver.trend || 'stable',
|
||||
lastUpdated: userRating?.driver.lastUpdated?.toISOString() || new Date(0).toISOString(),
|
||||
},
|
||||
admin: {
|
||||
value: userRating?.admin.value || 0,
|
||||
confidence: userRating?.admin.confidence || 0,
|
||||
sampleSize: userRating?.admin.sampleSize || 0,
|
||||
trend: userRating?.admin.trend || 'stable',
|
||||
lastUpdated: userRating?.admin.lastUpdated?.toISOString() || new Date(0).toISOString(),
|
||||
},
|
||||
steward: {
|
||||
value: userRating?.steward.value || 0,
|
||||
confidence: userRating?.steward.confidence || 0,
|
||||
sampleSize: userRating?.steward.sampleSize || 0,
|
||||
trend: userRating?.steward.trend || 'stable',
|
||||
lastUpdated: userRating?.steward.lastUpdated?.toISOString() || new Date(0).toISOString(),
|
||||
},
|
||||
trust: {
|
||||
value: userRating?.trust.value || 0,
|
||||
confidence: userRating?.trust.confidence || 0,
|
||||
sampleSize: userRating?.trust.sampleSize || 0,
|
||||
trend: userRating?.trust.trend || 'stable',
|
||||
lastUpdated: userRating?.trust.lastUpdated?.toISOString() || new Date(0).toISOString(),
|
||||
},
|
||||
fairness: {
|
||||
value: userRating?.fairness.value || 0,
|
||||
confidence: userRating?.fairness.confidence || 0,
|
||||
sampleSize: userRating?.fairness.sampleSize || 0,
|
||||
trend: userRating?.fairness.trend || 'stable',
|
||||
lastUpdated: userRating?.fairness.lastUpdated?.toISOString() || new Date(0).toISOString(),
|
||||
},
|
||||
overallReputation: userRating?.overallReputation || 0,
|
||||
};
|
||||
|
||||
// Build external ratings map
|
||||
const external: { [gameKey: string]: { [type: string]: number } } = {};
|
||||
|
||||
for (const profile of externalProfiles) {
|
||||
const gameKey = profile.gameKey.toString();
|
||||
external[gameKey] = {};
|
||||
|
||||
// Convert Map to array and iterate
|
||||
const ratingsArray = Array.from(profile.ratings.entries());
|
||||
for (const [type, rating] of ratingsArray) {
|
||||
external[gameKey][type] = rating.value;
|
||||
}
|
||||
}
|
||||
|
||||
// Get timestamps
|
||||
const createdAt = userRating?.createdAt?.toISOString() || new Date().toISOString();
|
||||
const updatedAt = userRating?.updatedAt?.toISOString() || new Date().toISOString();
|
||||
|
||||
const result: RatingSummaryDto = {
|
||||
userId,
|
||||
platform,
|
||||
external,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
};
|
||||
|
||||
if (lastRatingEventAt) {
|
||||
result.lastRatingEventAt = lastRatingEventAt;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
17
core/identity/application/queries/index.ts
Normal file
17
core/identity/application/queries/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Queries Index
|
||||
*
|
||||
* Export all query handlers and related types
|
||||
*/
|
||||
|
||||
// GetUserRatingsSummaryQuery
|
||||
export type { GetUserRatingsSummaryQuery } from './GetUserRatingsSummaryQuery';
|
||||
export { GetUserRatingsSummaryQueryHandler } from './GetUserRatingsSummaryQuery';
|
||||
|
||||
// GetUserRatingLedgerQuery
|
||||
export type { GetUserRatingLedgerQuery } from './GetUserRatingLedgerQuery';
|
||||
export { GetUserRatingLedgerQueryHandler } from './GetUserRatingLedgerQuery';
|
||||
|
||||
// GetLeagueEligibilityPreviewQuery
|
||||
export type { GetLeagueEligibilityPreviewQuery } from './GetLeagueEligibilityPreviewQuery';
|
||||
export { GetLeagueEligibilityPreviewQueryHandler } from './GetLeagueEligibilityPreviewQuery';
|
||||
Reference in New Issue
Block a user