team rating
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
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<TeamRatingEvent> {
|
||||
this.events.push(event);
|
||||
return event;
|
||||
}
|
||||
|
||||
async findByTeamId(teamId: string): Promise<TeamRatingEvent[]> {
|
||||
return this.events.filter(e => e.teamId === teamId);
|
||||
}
|
||||
|
||||
async findByIds(ids: TeamRatingEventId[]): Promise<TeamRatingEvent[]> {
|
||||
return this.events.filter(e => ids.some(id => id.equals(e.id)));
|
||||
}
|
||||
|
||||
async getAllByTeamId(teamId: string): Promise<TeamRatingEvent[]> {
|
||||
return this.events.filter(e => e.teamId === teamId);
|
||||
}
|
||||
|
||||
async findEventsPaginated(teamId: string): Promise<any> {
|
||||
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<string, any> = new Map();
|
||||
|
||||
async findByTeamId(teamId: string): Promise<any | null> {
|
||||
return this.snapshots.get(teamId) || null;
|
||||
}
|
||||
|
||||
async save(snapshot: any): Promise<any> {
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user