website refactor

This commit is contained in:
2026-01-16 18:21:06 +01:00
parent 2f53727702
commit 095885544b
146 changed files with 970 additions and 524 deletions

View File

@@ -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';

View File

@@ -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)],

View File

@@ -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();

View File

@@ -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>;

View File

@@ -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),

View File

@@ -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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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;
}
}

View File

@@ -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';

View File

@@ -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';

View File

@@ -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', () => {

View File

@@ -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);