team rating
This commit is contained in:
290
core/racing/domain/services/TeamRatingSnapshotCalculator.test.ts
Normal file
290
core/racing/domain/services/TeamRatingSnapshotCalculator.test.ts
Normal file
@@ -0,0 +1,290 @@
|
||||
import { TeamRatingSnapshotCalculator } from './TeamRatingSnapshotCalculator';
|
||||
import { TeamRatingEvent } from '../entities/TeamRatingEvent';
|
||||
import { TeamRatingEventId } from '../value-objects/TeamRatingEventId';
|
||||
import { TeamRatingDimensionKey } from '../value-objects/TeamRatingDimensionKey';
|
||||
import { TeamRatingDelta } from '../value-objects/TeamRatingDelta';
|
||||
import { TeamRatingValue } from '../value-objects/TeamRatingValue';
|
||||
|
||||
describe('TeamRatingSnapshotCalculator', () => {
|
||||
describe('calculate', () => {
|
||||
it('should return default ratings for empty events', () => {
|
||||
const snapshot = TeamRatingSnapshotCalculator.calculate('team-123', []);
|
||||
|
||||
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 calculate single dimension rating', () => {
|
||||
const events = [
|
||||
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-1' },
|
||||
reason: { code: 'RACE_FINISH', description: 'Finished 1st' },
|
||||
visibility: { public: true },
|
||||
version: 1,
|
||||
}),
|
||||
];
|
||||
|
||||
const snapshot = TeamRatingSnapshotCalculator.calculate('team-123', events);
|
||||
|
||||
expect(snapshot.driving.value).toBe(60); // 50 + 10
|
||||
expect(snapshot.adminTrust.value).toBe(50); // Default
|
||||
expect(snapshot.overall).toBeCloseTo(57, 1); // 60 * 0.7 + 50 * 0.3 = 57
|
||||
});
|
||||
|
||||
it('should calculate multiple events with weights', () => {
|
||||
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-1' },
|
||||
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-2' },
|
||||
reason: { code: 'RACE_PENALTY', description: 'Incident penalty' },
|
||||
visibility: { public: true },
|
||||
version: 1,
|
||||
}),
|
||||
];
|
||||
|
||||
const snapshot = TeamRatingSnapshotCalculator.calculate('team-123', events);
|
||||
|
||||
// Weighted average: (10*1 + (-5)*2) / (1+2) = 0/3 = 0
|
||||
// So driving = 50 + 0 = 50
|
||||
expect(snapshot.driving.value).toBe(50);
|
||||
});
|
||||
|
||||
it('should calculate mixed dimensions', () => {
|
||||
const events = [
|
||||
TeamRatingEvent.create({
|
||||
id: TeamRatingEventId.generate(),
|
||||
teamId: 'team-123',
|
||||
dimension: TeamRatingDimensionKey.create('driving'),
|
||||
delta: TeamRatingDelta.create(15),
|
||||
occurredAt: new Date('2024-01-01T10:00:00Z'),
|
||||
createdAt: new Date('2024-01-01T10:00:00Z'),
|
||||
source: { type: 'race', id: 'race-1' },
|
||||
reason: { code: 'RACE_FINISH', description: 'Finished 1st' },
|
||||
visibility: { public: true },
|
||||
version: 1,
|
||||
}),
|
||||
TeamRatingEvent.create({
|
||||
id: TeamRatingEventId.generate(),
|
||||
teamId: 'team-123',
|
||||
dimension: TeamRatingDimensionKey.create('adminTrust'),
|
||||
delta: TeamRatingDelta.create(5),
|
||||
occurredAt: new Date('2024-01-01T10:00:00Z'),
|
||||
createdAt: new Date('2024-01-01T10:00:00Z'),
|
||||
source: { type: 'adminAction', id: 'action-1' },
|
||||
reason: { code: 'ADMIN_BONUS', description: 'Helpful admin work' },
|
||||
visibility: { public: true },
|
||||
version: 1,
|
||||
}),
|
||||
];
|
||||
|
||||
const snapshot = TeamRatingSnapshotCalculator.calculate('team-123', events);
|
||||
|
||||
expect(snapshot.driving.value).toBe(65); // 50 + 15
|
||||
expect(snapshot.adminTrust.value).toBe(55); // 50 + 5
|
||||
expect(snapshot.overall).toBeCloseTo(62, 1); // 65 * 0.7 + 55 * 0.3 = 62
|
||||
});
|
||||
|
||||
it('should clamp values between 0 and 100', () => {
|
||||
const events = [
|
||||
TeamRatingEvent.create({
|
||||
id: TeamRatingEventId.generate(),
|
||||
teamId: 'team-123',
|
||||
dimension: TeamRatingDimensionKey.create('driving'),
|
||||
delta: TeamRatingDelta.create(60), // Would make it 110
|
||||
occurredAt: new Date('2024-01-01T10:00:00Z'),
|
||||
createdAt: new Date('2024-01-01T10:00:00Z'),
|
||||
source: { type: 'race', id: 'race-1' },
|
||||
reason: { code: 'RACE_FINISH', description: 'Finished 1st' },
|
||||
visibility: { public: true },
|
||||
version: 1,
|
||||
}),
|
||||
];
|
||||
|
||||
const snapshot = TeamRatingSnapshotCalculator.calculate('team-123', events);
|
||||
|
||||
expect(snapshot.driving.value).toBe(100); // Clamped
|
||||
});
|
||||
|
||||
it('should track last updated date', () => {
|
||||
const events = [
|
||||
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-1' },
|
||||
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(3),
|
||||
occurredAt: new Date('2024-01-02T10:00:00Z'),
|
||||
createdAt: new Date('2024-01-02T10:00:00Z'),
|
||||
source: { type: 'race', id: 'race-2' },
|
||||
reason: { code: 'RACE_FINISH', description: 'Finished 2nd' },
|
||||
visibility: { public: true },
|
||||
version: 1,
|
||||
}),
|
||||
];
|
||||
|
||||
const snapshot = TeamRatingSnapshotCalculator.calculate('team-123', events);
|
||||
|
||||
expect(snapshot.lastUpdated).toEqual(new Date('2024-01-02T10:00:00Z'));
|
||||
expect(snapshot.eventCount).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateDimensionChange', () => {
|
||||
it('should calculate net change for a dimension', () => {
|
||||
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-1' },
|
||||
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-2' },
|
||||
reason: { code: 'RACE_PENALTY', description: 'Incident penalty' },
|
||||
visibility: { public: true },
|
||||
version: 1,
|
||||
}),
|
||||
];
|
||||
|
||||
const change = TeamRatingSnapshotCalculator.calculateDimensionChange(
|
||||
TeamRatingDimensionKey.create('driving'),
|
||||
events
|
||||
);
|
||||
|
||||
// (10*1 + (-5)*2) / (1+2) = 0/3 = 0
|
||||
expect(change).toBe(0);
|
||||
});
|
||||
|
||||
it('should return 0 for no events', () => {
|
||||
const change = TeamRatingSnapshotCalculator.calculateDimensionChange(
|
||||
TeamRatingDimensionKey.create('driving'),
|
||||
[]
|
||||
);
|
||||
|
||||
expect(change).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateOverWindow', () => {
|
||||
it('should calculate ratings for a time window', () => {
|
||||
const allEvents = [
|
||||
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-1' },
|
||||
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),
|
||||
occurredAt: new Date('2024-01-02T10:00:00Z'),
|
||||
createdAt: new Date('2024-01-02T10:00:00Z'),
|
||||
source: { type: 'race', id: 'race-2' },
|
||||
reason: { code: 'RACE_FINISH', description: 'Finished 2nd' },
|
||||
visibility: { public: true },
|
||||
version: 1,
|
||||
}),
|
||||
];
|
||||
|
||||
const snapshot = TeamRatingSnapshotCalculator.calculateOverWindow(
|
||||
'team-123',
|
||||
allEvents,
|
||||
new Date('2024-01-01T00:00:00Z'),
|
||||
new Date('2024-01-01T23:59:59Z')
|
||||
);
|
||||
|
||||
// Only first event in window
|
||||
expect(snapshot.driving.value).toBe(60); // 50 + 10
|
||||
expect(snapshot.eventCount).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateDelta', () => {
|
||||
it('should calculate differences between snapshots', () => {
|
||||
const before = {
|
||||
teamId: 'team-123',
|
||||
driving: TeamRatingValue.create(50),
|
||||
adminTrust: TeamRatingValue.create(50),
|
||||
overall: 50,
|
||||
lastUpdated: new Date('2024-01-01'),
|
||||
eventCount: 10,
|
||||
};
|
||||
|
||||
const after = {
|
||||
teamId: 'team-123',
|
||||
driving: TeamRatingValue.create(65),
|
||||
adminTrust: TeamRatingValue.create(55),
|
||||
overall: 62,
|
||||
lastUpdated: new Date('2024-01-02'),
|
||||
eventCount: 15,
|
||||
};
|
||||
|
||||
const delta = TeamRatingSnapshotCalculator.calculateDelta(before, after);
|
||||
|
||||
expect(delta.driving).toBe(15);
|
||||
expect(delta.adminTrust).toBe(5);
|
||||
expect(delta.overall).toBe(12);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user