website refactor
This commit is contained in:
@@ -5,8 +5,7 @@
|
||||
* Uses EligibilityEvaluator to provide explainable results.
|
||||
*/
|
||||
|
||||
import { EvaluationResultDto } from '../dtos/EvaluationResultDto';
|
||||
import { EligibilityFilterDto } from '../dtos/EligibilityFilterDto';
|
||||
import { EvaluationResultDto, EligibilityFilterDto } from '../../domain/types/Eligibility';
|
||||
import { EligibilityEvaluator, RatingData } from '../../domain/services/EligibilityEvaluator';
|
||||
import { UserRatingRepository } from '../../domain/repositories/UserRatingRepository';
|
||||
import { ExternalGameRatingRepository } from '../../domain/repositories/ExternalGameRatingRepository';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Tests for GetUserRatingsSummaryQuery
|
||||
*/
|
||||
|
||||
import { describe, expect, it, beforeEach, vi } from 'vitest';
|
||||
import { describe, expect, it, beforeEach, vi, type Mock } from 'vitest';
|
||||
import { GetUserRatingsSummaryQuery, GetUserRatingsSummaryQueryHandler } from './GetUserRatingsSummaryQuery';
|
||||
import { UserRating } from '../../domain/value-objects/UserRating';
|
||||
import { ExternalGameRatingProfile } from '../../domain/entities/ExternalGameRatingProfile';
|
||||
@@ -13,11 +13,16 @@ 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';
|
||||
import { UserRatingRepository } from '../../domain/repositories/UserRatingRepository';
|
||||
import { ExternalGameRatingRepository } from '../../domain/repositories/ExternalGameRatingRepository';
|
||||
import { RatingEventRepository } from '../../domain/repositories/RatingEventRepository';
|
||||
|
||||
import { UserId } from '../../domain/value-objects/UserId';
|
||||
|
||||
describe('GetUserRatingsSummaryQuery', () => {
|
||||
let mockUserRatingRepo: any;
|
||||
let mockExternalRatingRepo: any;
|
||||
let mockRatingEventRepo: any;
|
||||
let mockUserRatingRepo: { findByUserId: Mock };
|
||||
let mockExternalRatingRepo: { findByUserId: Mock };
|
||||
let mockRatingEventRepo: { getAllByUserId: Mock };
|
||||
let handler: GetUserRatingsSummaryQueryHandler;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -32,9 +37,9 @@ describe('GetUserRatingsSummaryQuery', () => {
|
||||
};
|
||||
|
||||
handler = new GetUserRatingsSummaryQueryHandler(
|
||||
mockUserRatingRepo,
|
||||
mockExternalRatingRepo,
|
||||
mockRatingEventRepo
|
||||
mockUserRatingRepo as unknown as UserRatingRepository,
|
||||
mockExternalRatingRepo as unknown as ExternalGameRatingRepository,
|
||||
mockRatingEventRepo as unknown as RatingEventRepository
|
||||
);
|
||||
});
|
||||
|
||||
@@ -49,7 +54,7 @@ describe('GetUserRatingsSummaryQuery', () => {
|
||||
// Mock external ratings
|
||||
const gameKey = GameKey.create('iracing');
|
||||
const profile = ExternalGameRatingProfile.create({
|
||||
userId: { toString: () => userId } as any,
|
||||
userId: UserId.fromString(userId),
|
||||
gameKey,
|
||||
ratings: new Map([
|
||||
['iRating', ExternalRating.create(gameKey, 'iRating', 2200)],
|
||||
@@ -109,7 +114,7 @@ describe('GetUserRatingsSummaryQuery', () => {
|
||||
|
||||
// Multiple game profiles
|
||||
const iracingProfile = ExternalGameRatingProfile.create({
|
||||
userId: { toString: () => userId } as any,
|
||||
userId: UserId.fromString(userId),
|
||||
gameKey: GameKey.create('iracing'),
|
||||
ratings: new Map([
|
||||
['iRating', ExternalRating.create(GameKey.create('iracing'), 'iRating', 2200)],
|
||||
@@ -118,7 +123,7 @@ describe('GetUserRatingsSummaryQuery', () => {
|
||||
});
|
||||
|
||||
const assettoProfile = ExternalGameRatingProfile.create({
|
||||
userId: { toString: () => userId } as any,
|
||||
userId: UserId.fromString(userId),
|
||||
gameKey: GameKey.create('assetto'),
|
||||
ratings: new Map([
|
||||
['rating', ExternalRating.create(GameKey.create('assetto'), 'rating', 85)],
|
||||
|
||||
@@ -134,14 +134,16 @@ class MockUserRatingRepository {
|
||||
}
|
||||
}
|
||||
|
||||
import { CreateRatingEventDto } from '../dtos/CreateRatingEventDto';
|
||||
|
||||
// Mock AppendRatingEventsUseCase
|
||||
class MockAppendRatingEventsUseCase {
|
||||
constructor(
|
||||
private ratingEventRepository: any,
|
||||
private userRatingRepository: any
|
||||
private ratingEventRepository: MockRatingEventRepository,
|
||||
private userRatingRepository: MockUserRatingRepository
|
||||
) {}
|
||||
|
||||
async execute(input: any): Promise<any> {
|
||||
async execute(input: { userId: string; events: CreateRatingEventDto[] }): Promise<{ events: string[]; snapshotUpdated: boolean }> {
|
||||
const events: RatingEvent[] = [];
|
||||
|
||||
// Create events from input
|
||||
@@ -195,7 +197,6 @@ describe('Admin Vote Session Use Cases', () => {
|
||||
// Use dates relative to current time so close() works
|
||||
const now = new Date(Date.now() - 86400000); // Yesterday
|
||||
const tomorrow = new Date(Date.now() + 86400000); // Tomorrow
|
||||
const dayAfter = new Date(Date.now() + 86400000 * 2); // Day after tomorrow
|
||||
|
||||
beforeEach(() => {
|
||||
mockSessionRepo = new MockAdminVoteSessionRepository();
|
||||
|
||||
@@ -3,9 +3,6 @@ import { describe, expect, it, vi, beforeEach } from 'vitest';
|
||||
import { AppendRatingEventsUseCase, AppendRatingEventsInput } from './AppendRatingEventsUseCase';
|
||||
import { RatingEventRepository } from '../../domain/repositories/RatingEventRepository';
|
||||
import { UserRatingRepository } from '../../domain/repositories/UserRatingRepository';
|
||||
import { RatingEvent } from '../../domain/entities/RatingEvent';
|
||||
import { UserRating } from '../../domain/value-objects/UserRating';
|
||||
import { RatingEventId } from '../../domain/value-objects/RatingEventId';
|
||||
|
||||
describe('AppendRatingEventsUseCase', () => {
|
||||
let mockEventRepo: Partial<RatingEventRepository>;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { RatingEventRepository } from '../../domain/repositories/RatingEventRepo
|
||||
import { UserRatingRepository } from '../../domain/repositories/UserRatingRepository';
|
||||
import { RatingEventFactory } from '../../domain/services/RatingEventFactory';
|
||||
import { RatingSnapshotCalculator } from '../../domain/services/RatingSnapshotCalculator';
|
||||
import { RatingEvent } from '../../domain/entities/RatingEvent';
|
||||
import { RatingEvent, type RatingEventProps } 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';
|
||||
@@ -97,7 +97,7 @@ export class AppendRatingEventsUseCase {
|
||||
}
|
||||
|
||||
private createEventFromDto(dto: CreateRatingEventDto) {
|
||||
const props: any = {
|
||||
const props: RatingEventProps = {
|
||||
id: RatingEventId.generate(),
|
||||
userId: dto.userId,
|
||||
dimension: RatingDimensionKey.create(dto.dimension),
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { AdminVoteSessionRepository } from '../../domain/repositories/AdminVoteSessionRepository';
|
||||
import { RatingEventRepository } from '../../domain/repositories/RatingEventRepository';
|
||||
import { UserRatingRepository } from '../../domain/repositories/UserRatingRepository';
|
||||
import { AdminTrustRatingCalculator } from '../../domain/services/AdminTrustRatingCalculator';
|
||||
import { RatingSnapshotCalculator } from '../../domain/services/RatingSnapshotCalculator';
|
||||
import { RatingEventFactory } from '../../domain/services/RatingEventFactory';
|
||||
import { CloseAdminVoteSessionInput, CloseAdminVoteSessionOutput } from '../dtos/AdminVoteSessionDto';
|
||||
import { AdminVoteSession, AdminVoteOutcome } from '../../domain/entities/AdminVoteSession';
|
||||
|
||||
/**
|
||||
* Use Case: CloseAdminVoteSessionUseCase
|
||||
@@ -26,7 +26,6 @@ export class CloseAdminVoteSessionUseCase {
|
||||
private readonly adminVoteSessionRepository: AdminVoteSessionRepository,
|
||||
private readonly ratingEventRepository: RatingEventRepository,
|
||||
private readonly userRatingRepository: UserRatingRepository,
|
||||
private readonly appendRatingEventsUseCase: any, // Will be typed properly in integration
|
||||
) {}
|
||||
|
||||
async execute(input: CloseAdminVoteSessionInput): Promise<CloseAdminVoteSessionOutput> {
|
||||
@@ -117,7 +116,7 @@ export class CloseAdminVoteSessionUseCase {
|
||||
* Events are created for the admin being voted on
|
||||
* Per plans: no events are created for tie outcomes
|
||||
*/
|
||||
private async createRatingEvents(session: any, outcome: any): Promise<number> {
|
||||
private async createRatingEvents(session: AdminVoteSession, outcome: AdminVoteOutcome): Promise<number> {
|
||||
let eventsCreated = 0;
|
||||
|
||||
// Don't create events for tie outcomes
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Result } from '@/shared/domain/Result';
|
||||
import { Result } from '@core/shared/domain/Result';
|
||||
import type { Logger } from '@core/shared/domain/Logger';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import { User } from '../../domain/entities/User';
|
||||
import { UserRepository } from '../../domain/repositories/UserRepository';
|
||||
|
||||
export type GetCurrentSessionInput = {
|
||||
userId: string;
|
||||
|
||||
@@ -4,11 +4,12 @@
|
||||
* Authenticates a user with email and password.
|
||||
*/
|
||||
|
||||
import { PasswordHash } from '@/identity/domain/value-objects/PasswordHash';
|
||||
import { PasswordHash } from '../../domain/value-objects/PasswordHash';
|
||||
import type { Logger } from '@core/shared/domain/Logger';
|
||||
import { Result } from '@core/shared/domain/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { IdentitySessionPort } from '../ports/IdentitySessionPort';
|
||||
import { UserRepository } from '../../domain/repositories/UserRepository';
|
||||
|
||||
export type LoginWithEmailInput = {
|
||||
email: string;
|
||||
|
||||
@@ -6,8 +6,6 @@ import { AppendRatingEventsUseCase } from './AppendRatingEventsUseCase';
|
||||
import { UserRating } from '../../domain/value-objects/UserRating';
|
||||
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';
|
||||
|
||||
// In-memory implementations for integration testing
|
||||
class InMemoryRaceResultsProvider implements RaceResultsProvider {
|
||||
|
||||
@@ -6,8 +6,7 @@ import { AppendRatingEventsUseCase } from './AppendRatingEventsUseCase';
|
||||
import { UserRating } from '../../domain/value-objects/UserRating';
|
||||
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';
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
|
||||
// Mock implementations
|
||||
class MockRaceResultsProvider implements RaceResultsProvider {
|
||||
@@ -17,11 +16,11 @@ class MockRaceResultsProvider implements RaceResultsProvider {
|
||||
this.results = results;
|
||||
}
|
||||
|
||||
async getRaceResults(raceId: string): Promise<RaceResultsData | null> {
|
||||
async getRaceResults(_raceId: string): Promise<RaceResultsData | null> {
|
||||
return this.results;
|
||||
}
|
||||
|
||||
async hasRaceResults(raceId: string): Promise<boolean> {
|
||||
async hasRaceResults(_raceId: string): Promise<boolean> {
|
||||
return this.results !== null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ import { RaceResultsProvider } from '../ports/RaceResultsProvider';
|
||||
import { RatingEventRepository } from '../../domain/repositories/RatingEventRepository';
|
||||
import { UserRatingRepository } from '../../domain/repositories/UserRatingRepository';
|
||||
import { RatingEventFactory } from '../../domain/services/RatingEventFactory';
|
||||
import { DrivingRatingCalculator } from '../../domain/services/DrivingRatingCalculator';
|
||||
import { RatingSnapshotCalculator } from '../../domain/services/RatingSnapshotCalculator';
|
||||
import { AppendRatingEventsUseCase } from './AppendRatingEventsUseCase';
|
||||
import { RecordRaceRatingEventsInput, RecordRaceRatingEventsOutput } from '../dtos/RecordRaceRatingEventsDto';
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Result } from '@core/shared/domain/Result';
|
||||
import type { Logger } from '@core/shared/domain/Logger';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { StoredUser } from '../../domain/repositories/UserRepository';
|
||||
import type { StoredUser, UserRepository } from '../../domain/repositories/UserRepository';
|
||||
import type { AuthenticatedUser } from '../ports/IdentityProviderPort';
|
||||
import type { IdentitySessionPort } from '../ports/IdentitySessionPort';
|
||||
|
||||
|
||||
@@ -1,27 +1,30 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { UpsertExternalGameRatingUseCase } from './UpsertExternalGameRatingUseCase';
|
||||
import { UpsertExternalGameRatingInput } from '../dtos/UpsertExternalGameRatingDto';
|
||||
import { ExternalGameRatingProfile } from '../../domain/entities/ExternalGameRatingProfile';
|
||||
import { ExternalGameRatingRepository } from '../../domain/repositories/ExternalGameRatingRepository';
|
||||
|
||||
// Mock repository for integration test
|
||||
class MockExternalGameRatingRepository {
|
||||
private profiles = new Map<string, any>();
|
||||
private profiles = new Map<string, ExternalGameRatingProfile>();
|
||||
|
||||
private getKey(userId: string, gameKey: string): string {
|
||||
return `${userId}|${gameKey}`;
|
||||
}
|
||||
|
||||
async findByUserIdAndGameKey(userId: string, gameKey: string): Promise<any | null> {
|
||||
async findByUserIdAndGameKey(userId: string, gameKey: string): Promise<ExternalGameRatingProfile | null> {
|
||||
return this.profiles.get(this.getKey(userId, gameKey)) || null;
|
||||
}
|
||||
|
||||
async findByUserId(userId: string): Promise<any[]> {
|
||||
return Array.from(this.profiles.values()).filter((p: any) => p.userId.toString() === userId);
|
||||
async findByUserId(userId: string): Promise<ExternalGameRatingProfile[]> {
|
||||
return Array.from(this.profiles.values()).filter((p: ExternalGameRatingProfile) => p.userId.toString() === userId);
|
||||
}
|
||||
|
||||
async findByGameKey(gameKey: string): Promise<any[]> {
|
||||
return Array.from(this.profiles.values()).filter((p: any) => p.gameKey.toString() === gameKey);
|
||||
async findByGameKey(gameKey: string): Promise<ExternalGameRatingProfile[]> {
|
||||
return Array.from(this.profiles.values()).filter((p: ExternalGameRatingProfile) => p.gameKey.toString() === gameKey);
|
||||
}
|
||||
|
||||
async save(profile: any): Promise<any> {
|
||||
async save(profile: ExternalGameRatingProfile): Promise<ExternalGameRatingProfile> {
|
||||
const key = this.getKey(profile.userId.toString(), profile.gameKey.toString());
|
||||
this.profiles.set(key, profile);
|
||||
return profile;
|
||||
@@ -50,7 +53,7 @@ describe('UpsertExternalGameRatingUseCase - Integration', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
repository = new MockExternalGameRatingRepository();
|
||||
useCase = new UpsertExternalGameRatingUseCase(repository as any);
|
||||
useCase = new UpsertExternalGameRatingUseCase(repository as unknown as ExternalGameRatingRepository);
|
||||
});
|
||||
|
||||
describe('Full upsert flow', () => {
|
||||
|
||||
@@ -6,11 +6,19 @@ import { GameKey } from '../../domain/value-objects/GameKey';
|
||||
import { ExternalRating } from '../../domain/value-objects/ExternalRating';
|
||||
import { ExternalRatingProvenance } from '../../domain/value-objects/ExternalRatingProvenance';
|
||||
import { UpsertExternalGameRatingInput } from '../dtos/UpsertExternalGameRatingDto';
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
||||
import { vi, describe, it, expect, beforeEach, type Mock } from 'vitest';
|
||||
|
||||
describe('UpsertExternalGameRatingUseCase', () => {
|
||||
let useCase: UpsertExternalGameRatingUseCase;
|
||||
let mockRepository: ExternalGameRatingRepository;
|
||||
let mockRepository: {
|
||||
findByUserIdAndGameKey: Mock;
|
||||
findByUserId: Mock;
|
||||
findByGameKey: Mock;
|
||||
save: Mock;
|
||||
saveMany: Mock;
|
||||
delete: Mock;
|
||||
exists: Mock;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockRepository = {
|
||||
@@ -21,9 +29,17 @@ describe('UpsertExternalGameRatingUseCase', () => {
|
||||
saveMany: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
exists: vi.fn(),
|
||||
} as any;
|
||||
} as unknown as {
|
||||
findByUserIdAndGameKey: Mock;
|
||||
findByUserId: Mock;
|
||||
findByGameKey: Mock;
|
||||
save: Mock;
|
||||
saveMany: Mock;
|
||||
delete: Mock;
|
||||
exists: Mock;
|
||||
};
|
||||
|
||||
useCase = new UpsertExternalGameRatingUseCase(mockRepository);
|
||||
useCase = new UpsertExternalGameRatingUseCase(mockRepository as unknown as ExternalGameRatingRepository);
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
@@ -42,8 +58,8 @@ describe('UpsertExternalGameRatingUseCase', () => {
|
||||
},
|
||||
};
|
||||
|
||||
(mockRepository.findByUserIdAndGameKey as any).mockResolvedValue(null);
|
||||
(mockRepository.save as any).mockImplementation(async (profile: any) => profile);
|
||||
mockRepository.findByUserIdAndGameKey.mockResolvedValue(null);
|
||||
mockRepository.save.mockImplementation(async (profile: ExternalGameRatingProfile) => profile);
|
||||
|
||||
const result = await useCase.execute(input);
|
||||
|
||||
@@ -61,8 +77,8 @@ describe('UpsertExternalGameRatingUseCase', () => {
|
||||
|
||||
it('should update existing profile', async () => {
|
||||
const existingProfile = createTestProfile('user-123', 'iracing');
|
||||
(mockRepository.findByUserIdAndGameKey as any).mockResolvedValue(existingProfile);
|
||||
(mockRepository.save as any).mockImplementation(async (profile: any) => profile);
|
||||
mockRepository.findByUserIdAndGameKey.mockResolvedValue(existingProfile);
|
||||
mockRepository.save.mockImplementation(async (profile: ExternalGameRatingProfile) => profile);
|
||||
|
||||
const input: UpsertExternalGameRatingInput = {
|
||||
userId: 'user-123',
|
||||
@@ -211,7 +227,7 @@ describe('UpsertExternalGameRatingUseCase', () => {
|
||||
},
|
||||
};
|
||||
|
||||
(mockRepository.findByUserIdAndGameKey as any).mockRejectedValue(new Error('Database connection failed'));
|
||||
mockRepository.findByUserIdAndGameKey.mockRejectedValue(new Error('Database connection failed'));
|
||||
|
||||
const result = await useCase.execute(input);
|
||||
|
||||
@@ -232,8 +248,8 @@ describe('UpsertExternalGameRatingUseCase', () => {
|
||||
},
|
||||
};
|
||||
|
||||
(mockRepository.findByUserIdAndGameKey as any).mockResolvedValue(null);
|
||||
(mockRepository.save as any).mockImplementation(async (profile: any) => profile);
|
||||
mockRepository.findByUserIdAndGameKey.mockResolvedValue(null);
|
||||
mockRepository.save.mockImplementation(async (profile: ExternalGameRatingProfile) => profile);
|
||||
|
||||
const result = await useCase.execute(input);
|
||||
|
||||
@@ -254,8 +270,8 @@ describe('UpsertExternalGameRatingUseCase', () => {
|
||||
},
|
||||
};
|
||||
|
||||
(mockRepository.findByUserIdAndGameKey as any).mockResolvedValue(null);
|
||||
(mockRepository.save as any).mockImplementation(async (profile: any) => profile);
|
||||
mockRepository.findByUserIdAndGameKey.mockResolvedValue(null);
|
||||
mockRepository.save.mockImplementation(async (profile: ExternalGameRatingProfile) => profile);
|
||||
|
||||
const result = await useCase.execute(input);
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { DomainError } from '@core/shared/errors/DomainError';
|
||||
import type { CommonDomainErrorKind } from '@core/shared/errors/DomainError';
|
||||
import type { DomainError, CommonDomainErrorKind } from '@core/shared/errors/DomainError';
|
||||
|
||||
export abstract class IdentityDomainError extends Error implements DomainError<CommonDomainErrorKind> {
|
||||
readonly type = 'domain' as const;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { ExternalGameRatingRepository } from './ExternalGameRatingRepository';
|
||||
import { ExternalGameRatingProfile } from '../entities/ExternalGameRatingProfile';
|
||||
import { UserId } from '../value-objects/UserId';
|
||||
@@ -182,11 +183,6 @@ describe('ExternalGameRatingRepository', () => {
|
||||
await repository.save(profile);
|
||||
|
||||
// Update the profile
|
||||
const updatedProvenance = ExternalRatingProvenance.create({
|
||||
source: 'iracing',
|
||||
lastSyncedAt: new Date('2024-01-02'),
|
||||
verified: true,
|
||||
});
|
||||
profile.updateLastSyncedAt(new Date('2024-01-02'));
|
||||
profile.markVerified();
|
||||
|
||||
@@ -290,7 +286,7 @@ describe('ExternalGameRatingRepository', () => {
|
||||
lastSyncedAt: new Date('2024-01-01'),
|
||||
verified: false,
|
||||
});
|
||||
profile2.updateRatings(profile2.ratings, profile2Provenance);
|
||||
profile2.updateRatings(new Map(profile2.ratings), profile2Provenance);
|
||||
|
||||
await repository.saveMany([profile1, profile2]);
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import { RatingEvent } from '../entities/RatingEvent';
|
||||
import { RatingEventId } from '../value-objects/RatingEventId';
|
||||
import { RatingDimensionKey } from '../value-objects/RatingDimensionKey';
|
||||
import { RatingDelta } from '../value-objects/RatingDelta';
|
||||
import { AdminVoteOutcome } from '../entities/AdminVoteSession';
|
||||
|
||||
describe('AdminTrustRatingCalculator', () => {
|
||||
describe('calculate', () => {
|
||||
@@ -316,13 +315,12 @@ describe('AdminTrustRatingCalculator', () => {
|
||||
|
||||
it('should default to zero for unknown action type', () => {
|
||||
const input: SystemSignalInput = {
|
||||
actionType: 'sla_response' as any,
|
||||
actionType: 'unknown_type' as unknown as SystemSignalInput['actionType'],
|
||||
details: {},
|
||||
};
|
||||
|
||||
// Override for test
|
||||
const delta = AdminTrustRatingCalculator.calculateFromSystemSignal(input);
|
||||
expect(delta.value).toBe(5); // Known type
|
||||
expect(delta.value).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { RatingEvent } from '../entities/RatingEvent';
|
||||
import { DrivingReasonCode } from '../value-objects/DrivingReasonCode';
|
||||
import { RatingDelta } from '../value-objects/RatingDelta';
|
||||
|
||||
/**
|
||||
* Input DTO for driving rating calculation from race facts
|
||||
@@ -71,7 +69,7 @@ export class DrivingRatingCalculator {
|
||||
const fieldStrength = facts.results.length > 0
|
||||
? (facts.results
|
||||
.filter(r => r.status === 'finished')
|
||||
.reduce((sum, r) => sum + (r.sof || this.estimateDriverRating(r.userId)), 0) /
|
||||
.reduce((sum, r) => sum + (r.sof || this.estimateDriverRating()), 0) /
|
||||
Math.max(1, facts.results.filter(r => r.status === 'finished').length))
|
||||
: 0;
|
||||
|
||||
@@ -297,7 +295,7 @@ export class DrivingRatingCalculator {
|
||||
* Estimate driver rating for SoF calculation
|
||||
* This is a placeholder - in real implementation, would query user rating snapshot
|
||||
*/
|
||||
private static estimateDriverRating(userId: string): number {
|
||||
private static estimateDriverRating(): number {
|
||||
// Default rating for new drivers
|
||||
return 50;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
import { EligibilityEvaluator, RatingData } from './EligibilityEvaluator';
|
||||
import { EligibilityFilterDto } from '../../application/dtos/EligibilityFilterDto';
|
||||
import { EligibilityFilterDto } from '../types/Eligibility';
|
||||
|
||||
describe('EligibilityEvaluator', () => {
|
||||
let evaluator: EligibilityEvaluator;
|
||||
|
||||
@@ -6,8 +6,13 @@
|
||||
* Provides explainable results with detailed reasons.
|
||||
*/
|
||||
|
||||
import { EvaluationResultDto, EvaluationReason } from '../../application/dtos/EvaluationResultDto';
|
||||
import { EligibilityFilterDto, ParsedEligibilityFilter, EligibilityCondition } from '../../application/dtos/EligibilityFilterDto';
|
||||
import {
|
||||
EvaluationResultDto,
|
||||
EvaluationReason,
|
||||
EligibilityFilterDto,
|
||||
ParsedEligibilityFilter,
|
||||
EligibilityCondition
|
||||
} from '../types/Eligibility';
|
||||
|
||||
export interface RatingData {
|
||||
platform: {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, vi, type Mock } from 'vitest';
|
||||
import { RatingUpdateService } from './RatingUpdateService';
|
||||
import type { UserRatingRepository } from '../repositories/UserRatingRepository';
|
||||
import type { RatingEventRepository } from '../repositories/RatingEventRepository';
|
||||
@@ -10,8 +10,8 @@ import { RatingDelta } from '../value-objects/RatingDelta';
|
||||
|
||||
describe('RatingUpdateService - Slice 7 Evolution', () => {
|
||||
let service: RatingUpdateService;
|
||||
let userRatingRepository: any;
|
||||
let ratingEventRepository: any;
|
||||
let userRatingRepository: { findByUserId: Mock; save: Mock };
|
||||
let ratingEventRepository: { save: Mock; getAllByUserId: Mock };
|
||||
|
||||
beforeEach(() => {
|
||||
userRatingRepository = {
|
||||
@@ -24,7 +24,10 @@ describe('RatingUpdateService - Slice 7 Evolution', () => {
|
||||
getAllByUserId: vi.fn(),
|
||||
};
|
||||
|
||||
service = new RatingUpdateService(userRatingRepository, ratingEventRepository);
|
||||
service = new RatingUpdateService(
|
||||
userRatingRepository as unknown as UserRatingRepository,
|
||||
ratingEventRepository as unknown as RatingEventRepository
|
||||
);
|
||||
});
|
||||
|
||||
describe('recordRaceRatingEvents - Ledger-based approach', () => {
|
||||
@@ -150,7 +153,7 @@ describe('RatingUpdateService - Slice 7 Evolution', () => {
|
||||
|
||||
// Verify DNF penalty event was created
|
||||
const savedEvents = ratingEventRepository.save.mock.calls.map(call => call[0]);
|
||||
const hasDnfPenalty = savedEvents.some((event: any) =>
|
||||
const hasDnfPenalty = savedEvents.some((event: RatingEvent) =>
|
||||
event.reason.code === 'DRIVING_DNF_PENALTY'
|
||||
);
|
||||
expect(hasDnfPenalty).toBe(true);
|
||||
|
||||
55
core/identity/domain/types/Eligibility.ts
Normal file
55
core/identity/domain/types/Eligibility.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Domain Types: Eligibility
|
||||
*
|
||||
* DSL-based eligibility filter and evaluation results.
|
||||
*/
|
||||
|
||||
export interface EligibilityFilterDto {
|
||||
/**
|
||||
* DSL expression for eligibility rules
|
||||
*/
|
||||
dsl: string;
|
||||
|
||||
/**
|
||||
* Optional context for evaluation
|
||||
*/
|
||||
context?: {
|
||||
userId?: string;
|
||||
leagueId?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
export interface EligibilityCondition {
|
||||
target: 'platform' | 'external';
|
||||
dimension?: string;
|
||||
game?: string;
|
||||
operator: string;
|
||||
expected: number | [number, number];
|
||||
}
|
||||
|
||||
export interface ParsedEligibilityFilter {
|
||||
conditions: EligibilityCondition[];
|
||||
logicalOperator: 'AND' | 'OR';
|
||||
}
|
||||
|
||||
export interface EvaluationReason {
|
||||
target: string;
|
||||
operator: string;
|
||||
expected: number | [number, number];
|
||||
actual: number;
|
||||
failed: boolean;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface EvaluationResultDto {
|
||||
eligible: boolean;
|
||||
reasons: EvaluationReason[];
|
||||
summary: string;
|
||||
evaluatedAt: string;
|
||||
metadata?: {
|
||||
userId?: string;
|
||||
filter?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { AdminTrustReasonCode } from './AdminTrustReasonCode';
|
||||
import { AdminTrustReasonCode, type AdminTrustReasonCodeValue } from './AdminTrustReasonCode';
|
||||
import { IdentityDomainValidationError } from '../errors/IdentityDomainError';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('AdminTrustReasonCode', () => {
|
||||
describe('create', () => {
|
||||
@@ -71,7 +72,7 @@ describe('AdminTrustReasonCode', () => {
|
||||
'ADMIN_VOTE_OUTCOME_NEGATIVE',
|
||||
];
|
||||
voteCodes.forEach(codeStr => {
|
||||
const code = AdminTrustReasonCode.fromValue(codeStr as any);
|
||||
const code = AdminTrustReasonCode.fromValue(codeStr as AdminTrustReasonCodeValue);
|
||||
expect(code.isVoteOutcome()).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -82,7 +83,7 @@ describe('AdminTrustReasonCode', () => {
|
||||
'ADMIN_ACTION_REVERSAL_PENALTY',
|
||||
];
|
||||
nonVoteCodes.forEach(codeStr => {
|
||||
const code = AdminTrustReasonCode.fromValue(codeStr as any);
|
||||
const code = AdminTrustReasonCode.fromValue(codeStr as AdminTrustReasonCodeValue);
|
||||
expect(code.isVoteOutcome()).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -97,7 +98,7 @@ describe('AdminTrustReasonCode', () => {
|
||||
'ADMIN_ACTION_ABUSE_REPORT_PENALTY',
|
||||
];
|
||||
systemCodes.forEach(codeStr => {
|
||||
const code = AdminTrustReasonCode.fromValue(codeStr as any);
|
||||
const code = AdminTrustReasonCode.fromValue(codeStr as AdminTrustReasonCodeValue);
|
||||
expect(code.isSystemSignal()).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -108,7 +109,7 @@ describe('AdminTrustReasonCode', () => {
|
||||
'ADMIN_VOTE_OUTCOME_NEGATIVE',
|
||||
];
|
||||
nonSystemCodes.forEach(codeStr => {
|
||||
const code = AdminTrustReasonCode.fromValue(codeStr as any);
|
||||
const code = AdminTrustReasonCode.fromValue(codeStr as AdminTrustReasonCodeValue);
|
||||
expect(code.isSystemSignal()).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -122,7 +123,7 @@ describe('AdminTrustReasonCode', () => {
|
||||
'ADMIN_ACTION_RULE_CLARITY_BONUS',
|
||||
];
|
||||
positiveCodes.forEach(codeStr => {
|
||||
const code = AdminTrustReasonCode.fromValue(codeStr as any);
|
||||
const code = AdminTrustReasonCode.fromValue(codeStr as AdminTrustReasonCodeValue);
|
||||
expect(code.isPositive()).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -134,7 +135,7 @@ describe('AdminTrustReasonCode', () => {
|
||||
'ADMIN_ACTION_ABUSE_REPORT_PENALTY',
|
||||
];
|
||||
nonPositiveCodes.forEach(codeStr => {
|
||||
const code = AdminTrustReasonCode.fromValue(codeStr as any);
|
||||
const code = AdminTrustReasonCode.fromValue(codeStr as AdminTrustReasonCodeValue);
|
||||
expect(code.isPositive()).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -148,7 +149,7 @@ describe('AdminTrustReasonCode', () => {
|
||||
'ADMIN_ACTION_ABUSE_REPORT_PENALTY',
|
||||
];
|
||||
negativeCodes.forEach(codeStr => {
|
||||
const code = AdminTrustReasonCode.fromValue(codeStr as any);
|
||||
const code = AdminTrustReasonCode.fromValue(codeStr as AdminTrustReasonCodeValue);
|
||||
expect(code.isNegative()).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -160,7 +161,7 @@ describe('AdminTrustReasonCode', () => {
|
||||
'ADMIN_ACTION_RULE_CLARITY_BONUS',
|
||||
];
|
||||
nonNegativeCodes.forEach(codeStr => {
|
||||
const code = AdminTrustReasonCode.fromValue(codeStr as any);
|
||||
const code = AdminTrustReasonCode.fromValue(codeStr as AdminTrustReasonCodeValue);
|
||||
expect(code.isNegative()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { DrivingReasonCode } from './DrivingReasonCode';
|
||||
import { DrivingReasonCode, type DrivingReasonCodeValue } from './DrivingReasonCode';
|
||||
import { IdentityDomainValidationError } from '../errors/IdentityDomainError';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('DrivingReasonCode', () => {
|
||||
describe('create', () => {
|
||||
@@ -77,7 +78,7 @@ describe('DrivingReasonCode', () => {
|
||||
'DRIVING_PACE_RELATIVE_GAIN',
|
||||
];
|
||||
performanceCodes.forEach(codeStr => {
|
||||
const code = DrivingReasonCode.fromValue(codeStr as any);
|
||||
const code = DrivingReasonCode.fromValue(codeStr as DrivingReasonCodeValue);
|
||||
expect(code.isPerformance()).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -89,7 +90,7 @@ describe('DrivingReasonCode', () => {
|
||||
'DRIVING_SEASON_ATTENDANCE_BONUS',
|
||||
];
|
||||
nonPerformanceCodes.forEach(codeStr => {
|
||||
const code = DrivingReasonCode.fromValue(codeStr as any);
|
||||
const code = DrivingReasonCode.fromValue(codeStr as DrivingReasonCodeValue);
|
||||
expect(code.isPerformance()).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -103,7 +104,7 @@ describe('DrivingReasonCode', () => {
|
||||
'DRIVING_PENALTY_INVOLVEMENT_PENALTY',
|
||||
];
|
||||
cleanDrivingCodes.forEach(codeStr => {
|
||||
const code = DrivingReasonCode.fromValue(codeStr as any);
|
||||
const code = DrivingReasonCode.fromValue(codeStr as DrivingReasonCodeValue);
|
||||
expect(code.isCleanDriving()).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -115,7 +116,7 @@ describe('DrivingReasonCode', () => {
|
||||
'DRIVING_SEASON_ATTENDANCE_BONUS',
|
||||
];
|
||||
nonCleanDrivingCodes.forEach(codeStr => {
|
||||
const code = DrivingReasonCode.fromValue(codeStr as any);
|
||||
const code = DrivingReasonCode.fromValue(codeStr as DrivingReasonCodeValue);
|
||||
expect(code.isCleanDriving()).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -131,7 +132,7 @@ describe('DrivingReasonCode', () => {
|
||||
'DRIVING_SEASON_ATTENDANCE_BONUS',
|
||||
];
|
||||
reliabilityCodes.forEach(codeStr => {
|
||||
const code = DrivingReasonCode.fromValue(codeStr as any);
|
||||
const code = DrivingReasonCode.fromValue(codeStr as DrivingReasonCodeValue);
|
||||
expect(code.isReliability()).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -142,7 +143,7 @@ describe('DrivingReasonCode', () => {
|
||||
'DRIVING_INCIDENTS_PENALTY',
|
||||
];
|
||||
nonReliabilityCodes.forEach(codeStr => {
|
||||
const code = DrivingReasonCode.fromValue(codeStr as any);
|
||||
const code = DrivingReasonCode.fromValue(codeStr as DrivingReasonCodeValue);
|
||||
expect(code.isReliability()).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -160,7 +161,7 @@ describe('DrivingReasonCode', () => {
|
||||
'DRIVING_AFK_PENALTY',
|
||||
];
|
||||
penaltyCodes.forEach(codeStr => {
|
||||
const code = DrivingReasonCode.fromValue(codeStr as any);
|
||||
const code = DrivingReasonCode.fromValue(codeStr as DrivingReasonCodeValue);
|
||||
expect(code.isPenalty()).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -172,7 +173,7 @@ describe('DrivingReasonCode', () => {
|
||||
'DRIVING_SEASON_ATTENDANCE_BONUS',
|
||||
];
|
||||
nonPenaltyCodes.forEach(codeStr => {
|
||||
const code = DrivingReasonCode.fromValue(codeStr as any);
|
||||
const code = DrivingReasonCode.fromValue(codeStr as DrivingReasonCodeValue);
|
||||
expect(code.isPenalty()).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -187,7 +188,7 @@ describe('DrivingReasonCode', () => {
|
||||
'DRIVING_PACE_RELATIVE_GAIN',
|
||||
];
|
||||
bonusCodes.forEach(codeStr => {
|
||||
const code = DrivingReasonCode.fromValue(codeStr as any);
|
||||
const code = DrivingReasonCode.fromValue(codeStr as DrivingReasonCodeValue);
|
||||
expect(code.isBonus()).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -198,7 +199,7 @@ describe('DrivingReasonCode', () => {
|
||||
'DRIVING_DNS_PENALTY',
|
||||
];
|
||||
nonBonusCodes.forEach(codeStr => {
|
||||
const code = DrivingReasonCode.fromValue(codeStr as any);
|
||||
const code = DrivingReasonCode.fromValue(codeStr as DrivingReasonCodeValue);
|
||||
expect(code.isBonus()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user