import { RecomputeTeamRatingSnapshotUseCase } from './RecomputeTeamRatingSnapshotUseCase'; import { ITeamRatingEventRepository } from '@core/racing/domain/repositories/ITeamRatingEventRepository'; import { ITeamRatingRepository } from '@core/racing/domain/repositories/ITeamRatingRepository'; import { TeamRatingEvent } from '@core/racing/domain/entities/TeamRatingEvent'; import { TeamRatingEventId } from '@core/racing/domain/value-objects/TeamRatingEventId'; import { TeamRatingDimensionKey } from '@core/racing/domain/value-objects/TeamRatingDimensionKey'; import { TeamRatingDelta } from '@core/racing/domain/value-objects/TeamRatingDelta'; // Mock repositories class MockTeamRatingEventRepository implements ITeamRatingEventRepository { private events: TeamRatingEvent[] = []; async save(event: TeamRatingEvent): Promise { this.events.push(event); return event; } async findByTeamId(teamId: string): Promise { return this.events.filter(e => e.teamId === teamId); } async findByIds(ids: TeamRatingEventId[]): Promise { return this.events.filter(e => ids.some(id => id.equals(e.id))); } async getAllByTeamId(teamId: string): Promise { return this.events.filter(e => e.teamId === teamId); } async findEventsPaginated(teamId: string): Promise { const events = await this.getAllByTeamId(teamId); return { items: events, total: events.length, limit: 10, offset: 0, hasMore: false, }; } setEvents(events: TeamRatingEvent[]) { this.events = events; } clear() { this.events = []; } } class MockTeamRatingRepository implements ITeamRatingRepository { private snapshots: Map = new Map(); async findByTeamId(teamId: string): Promise { return this.snapshots.get(teamId) || null; } async save(snapshot: any): Promise { this.snapshots.set(snapshot.teamId, snapshot); return snapshot; } getSnapshot(teamId: string) { return this.snapshots.get(teamId); } clear() { this.snapshots.clear(); } } describe('RecomputeTeamRatingSnapshotUseCase', () => { let useCase: RecomputeTeamRatingSnapshotUseCase; let mockEventRepo: MockTeamRatingEventRepository; let mockRatingRepo: MockTeamRatingRepository; beforeEach(() => { mockEventRepo = new MockTeamRatingEventRepository(); mockRatingRepo = new MockTeamRatingRepository(); useCase = new RecomputeTeamRatingSnapshotUseCase(mockEventRepo, mockRatingRepo); }); afterEach(() => { mockEventRepo.clear(); mockRatingRepo.clear(); }); describe('execute', () => { it('should create snapshot with default values when no events exist', async () => { await useCase.execute('team-123'); const snapshot = mockRatingRepo.getSnapshot('team-123'); expect(snapshot).toBeDefined(); expect(snapshot.teamId).toBe('team-123'); expect(snapshot.driving.value).toBe(50); expect(snapshot.adminTrust.value).toBe(50); expect(snapshot.overall).toBe(50); expect(snapshot.eventCount).toBe(0); }); it('should recompute snapshot from single event', async () => { const event = TeamRatingEvent.create({ id: TeamRatingEventId.generate(), teamId: 'team-123', dimension: TeamRatingDimensionKey.create('driving'), delta: TeamRatingDelta.create(10), occurredAt: new Date('2024-01-01T10:00:00Z'), createdAt: new Date('2024-01-01T10:00:00Z'), source: { type: 'race', id: 'race-456' }, reason: { code: 'RACE_FINISH', description: 'Finished 1st' }, visibility: { public: true }, version: 1, }); mockEventRepo.setEvents([event]); await useCase.execute('team-123'); const snapshot = mockRatingRepo.getSnapshot('team-123'); expect(snapshot).toBeDefined(); expect(snapshot.teamId).toBe('team-123'); expect(snapshot.driving.value).toBe(60); // 50 + 10 expect(snapshot.adminTrust.value).toBe(50); // Default expect(snapshot.overall).toBe(57); // 60 * 0.7 + 50 * 0.3 = 57 expect(snapshot.eventCount).toBe(1); }); it('should recompute snapshot from multiple events', async () => { const events = [ TeamRatingEvent.create({ id: TeamRatingEventId.generate(), teamId: 'team-123', dimension: TeamRatingDimensionKey.create('driving'), delta: TeamRatingDelta.create(10), weight: 1, occurredAt: new Date('2024-01-01T10:00:00Z'), createdAt: new Date('2024-01-01T10:00:00Z'), source: { type: 'race', id: 'race-456' }, reason: { code: 'RACE_FINISH', description: 'Finished 1st' }, visibility: { public: true }, version: 1, }), TeamRatingEvent.create({ id: TeamRatingEventId.generate(), teamId: 'team-123', dimension: TeamRatingDimensionKey.create('driving'), delta: TeamRatingDelta.create(5), weight: 2, occurredAt: new Date('2024-01-01T11:00:00Z'), createdAt: new Date('2024-01-01T11:00:00Z'), source: { type: 'race', id: 'race-457' }, reason: { code: 'RACE_FINISH', description: 'Finished 2nd' }, visibility: { public: true }, version: 1, }), TeamRatingEvent.create({ id: TeamRatingEventId.generate(), teamId: 'team-123', dimension: TeamRatingDimensionKey.create('adminTrust'), delta: TeamRatingDelta.create(3), weight: 1, occurredAt: new Date('2024-01-01T12:00:00Z'), createdAt: new Date('2024-01-01T12:00:00Z'), source: { type: 'adminAction', id: 'action-789' }, reason: { code: 'POSITIVE_ADMIN_ACTION', description: 'Helped organize event' }, visibility: { public: true }, version: 1, }), ]; mockEventRepo.setEvents(events); await useCase.execute('team-123'); const snapshot = mockRatingRepo.getSnapshot('team-123'); expect(snapshot).toBeDefined(); expect(snapshot.teamId).toBe('team-123'); // Driving: weighted average of (10*1 + 5*2) / (1+2) = 20/3 = 6.67, so 50 + 6.67 = 56.67 expect(snapshot.driving.value).toBeCloseTo(56.67, 1); // AdminTrust: 50 + 3 = 53 expect(snapshot.adminTrust.value).toBe(53); // Overall: 56.67 * 0.7 + 53 * 0.3 = 39.67 + 15.9 = 55.57 ≈ 55.6 expect(snapshot.overall).toBeCloseTo(55.6, 1); expect(snapshot.eventCount).toBe(3); }); it('should handle events with different dimensions correctly', async () => { const events = [ TeamRatingEvent.create({ id: TeamRatingEventId.generate(), teamId: 'team-456', dimension: TeamRatingDimensionKey.create('adminTrust'), delta: TeamRatingDelta.create(15), occurredAt: new Date('2024-01-01T10:00:00Z'), createdAt: new Date('2024-01-01T10:00:00Z'), source: { type: 'adminAction', id: 'action-123' }, reason: { code: 'EXCELLENT_ADMIN', description: 'Great leadership' }, visibility: { public: true }, version: 1, }), ]; mockEventRepo.setEvents(events); await useCase.execute('team-456'); const snapshot = mockRatingRepo.getSnapshot('team-456'); expect(snapshot).toBeDefined(); expect(snapshot.adminTrust.value).toBe(65); // 50 + 15 expect(snapshot.driving.value).toBe(50); // Default expect(snapshot.overall).toBe(54.5); // 50 * 0.7 + 65 * 0.3 = 54.5 }); it('should overwrite existing snapshot with recomputed values', async () => { // First, create an initial snapshot const initialEvent = TeamRatingEvent.create({ id: TeamRatingEventId.generate(), teamId: 'team-123', dimension: TeamRatingDimensionKey.create('driving'), delta: TeamRatingDelta.create(5), occurredAt: new Date('2024-01-01T10:00:00Z'), createdAt: new Date('2024-01-01T10:00:00Z'), source: { type: 'race', id: 'race-456' }, reason: { code: 'RACE_FINISH', description: 'Finished 1st' }, visibility: { public: true }, version: 1, }); mockEventRepo.setEvents([initialEvent]); await useCase.execute('team-123'); let snapshot = mockRatingRepo.getSnapshot('team-123'); expect(snapshot.driving.value).toBe(55); // Now add more events and recompute const additionalEvent = TeamRatingEvent.create({ id: TeamRatingEventId.generate(), teamId: 'team-123', dimension: TeamRatingDimensionKey.create('driving'), delta: TeamRatingDelta.create(10), occurredAt: new Date('2024-01-01T11:00:00Z'), createdAt: new Date('2024-01-01T11:00:00Z'), source: { type: 'race', id: 'race-457' }, reason: { code: 'RACE_FINISH', description: 'Finished 2nd' }, visibility: { public: true }, version: 1, }); mockEventRepo.setEvents([initialEvent, additionalEvent]); await useCase.execute('team-123'); snapshot = mockRatingRepo.getSnapshot('team-123'); expect(snapshot.driving.value).toBe(57.5); // Weighted average: (5 + 10) / 2 = 7.5, so 50 + 7.5 = 57.5 expect(snapshot.eventCount).toBe(2); }); }); });