import { DrivingRatingCalculator, DrivingRaceFactsDto } from './DrivingRatingCalculator'; import { RatingEvent } from '../entities/RatingEvent'; import { RatingEventId } from '../value-objects/RatingEventId'; import { RatingDimensionKey } from '../value-objects/RatingDimensionKey'; import { RatingDelta } from '../value-objects/RatingDelta'; describe('DrivingRatingCalculator', () => { describe('calculateFromRaceFacts', () => { it('should calculate delta for finished race with good performance', () => { const facts: DrivingRaceFactsDto = { raceId: 'race-123', results: [ { userId: 'user-123', startPos: 5, finishPos: 2, incidents: 0, status: 'finished', sof: 2500, }, { userId: 'user-456', startPos: 3, finishPos: 8, incidents: 0, status: 'finished', sof: 2500, }, ], }; const results = DrivingRatingCalculator.calculateFromRaceFacts(facts); expect(results.has('user-123')).toBe(true); const result = results.get('user-123')!; expect(result.userId).toBe('user-123'); expect(result.delta).toBeGreaterThan(0); // Positive for good performance expect(result.events.length).toBeGreaterThan(0); // Should have performance event const performanceEvent = result.events.find(e => e.reasonCode === 'DRIVING_FINISH_STRENGTH_GAIN'); expect(performanceEvent).toBeDefined(); expect(performanceEvent?.delta).toBeGreaterThan(0); }); it('should calculate delta for finished race with poor performance', () => { const facts: DrivingRaceFactsDto = { raceId: 'race-123', results: [ { userId: 'user-123', startPos: 2, finishPos: 8, incidents: 0, status: 'finished', sof: 2500, }, { userId: 'user-456', startPos: 5, finishPos: 2, incidents: 0, status: 'finished', sof: 2500, }, ], }; const results = DrivingRatingCalculator.calculateFromRaceFacts(facts); const result = results.get('user-123')!; expect(result.delta).toBeLessThan(0); // Negative for poor performance }); it('should add incident penalties', () => { const facts: DrivingRaceFactsDto = { raceId: 'race-123', results: [ { userId: 'user-123', startPos: 5, finishPos: 5, incidents: 3, status: 'finished', sof: 2500, }, ], }; const results = DrivingRatingCalculator.calculateFromRaceFacts(facts); const result = results.get('user-123')!; const incidentEvent = result.events.find(e => e.reasonCode === 'DRIVING_INCIDENTS_PENALTY'); expect(incidentEvent).toBeDefined(); expect(incidentEvent?.delta).toBeLessThan(0); expect(result.delta).toBeLessThan(0); }); it('should apply DNS penalty', () => { const facts: DrivingRaceFactsDto = { raceId: 'race-123', results: [ { userId: 'user-123', startPos: 5, finishPos: 10, incidents: 0, status: 'dns', sof: 2500, }, ], }; const results = DrivingRatingCalculator.calculateFromRaceFacts(facts); const result = results.get('user-123')!; const dnsEvent = result.events.find(e => e.reasonCode === 'DRIVING_DNS_PENALTY'); expect(dnsEvent).toBeDefined(); expect(dnsEvent?.delta).toBe(-15); expect(result.delta).toBeLessThan(0); }); it('should apply DNF penalty', () => { const facts: DrivingRaceFactsDto = { raceId: 'race-123', results: [ { userId: 'user-123', startPos: 5, finishPos: 10, incidents: 0, status: 'dnf', sof: 2500, }, ], }; const results = DrivingRatingCalculator.calculateFromRaceFacts(facts); const result = results.get('user-123')!; const dnfEvent = result.events.find(e => e.reasonCode === 'DRIVING_DNF_PENALTY'); expect(dnfEvent).toBeDefined(); expect(dnfEvent?.delta).toBe(-10); }); it('should apply DSQ penalty', () => { const facts: DrivingRaceFactsDto = { raceId: 'race-123', results: [ { userId: 'user-123', startPos: 5, finishPos: 10, incidents: 0, status: 'dsq', sof: 2500, }, ], }; const results = DrivingRatingCalculator.calculateFromRaceFacts(facts); const result = results.get('user-123')!; const dsqEvent = result.events.find(e => e.reasonCode === 'DRIVING_DSQ_PENALTY'); expect(dsqEvent).toBeDefined(); expect(dsqEvent?.delta).toBe(-25); }); it('should apply AFK penalty', () => { const facts: DrivingRaceFactsDto = { raceId: 'race-123', results: [ { userId: 'user-123', startPos: 5, finishPos: 10, incidents: 0, status: 'afk', sof: 2500, }, ], }; const results = DrivingRatingCalculator.calculateFromRaceFacts(facts); const result = results.get('user-123')!; const afkEvent = result.events.find(e => e.reasonCode === 'DRIVING_AFK_PENALTY'); expect(afkEvent).toBeDefined(); expect(afkEvent?.delta).toBe(-20); }); it('should calculate positions gained bonus', () => { const facts: DrivingRaceFactsDto = { raceId: 'race-123', results: [ { userId: 'user-123', startPos: 10, finishPos: 3, incidents: 0, status: 'finished', sof: 2500, }, { userId: 'user-456', startPos: 5, finishPos: 8, incidents: 0, status: 'finished', sof: 2500, }, ], }; const results = DrivingRatingCalculator.calculateFromRaceFacts(facts); const result = results.get('user-123')!; const gainEvent = result.events.find(e => e.reasonCode === 'DRIVING_POSITIONS_GAINED_BONUS'); expect(gainEvent).toBeDefined(); expect(gainEvent?.delta).toBeGreaterThan(0); }); it('should handle multiple drivers in race', () => { const facts: DrivingRaceFactsDto = { raceId: 'race-123', results: [ { userId: 'user-123', startPos: 5, finishPos: 2, incidents: 0, status: 'finished', sof: 2500, }, { userId: 'user-456', startPos: 3, finishPos: 8, incidents: 2, status: 'finished', sof: 2500, }, { userId: 'user-789', startPos: 5, finishPos: 5, incidents: 0, status: 'dns', sof: 2500, }, ], }; const results = DrivingRatingCalculator.calculateFromRaceFacts(facts); expect(results.has('user-123')).toBe(true); expect(results.has('user-456')).toBe(true); expect(results.has('user-789')).toBe(true); // user-123 should have positive delta expect(results.get('user-123')!.delta).toBeGreaterThan(0); // user-456 should have negative delta (poor position + incidents) expect(results.get('user-456')!.delta).toBeLessThan(0); // user-789 should have negative delta (DNS) expect(results.get('user-789')!.delta).toBeLessThan(0); }); it('should calculate SoF if not provided', () => { const facts: DrivingRaceFactsDto = { raceId: 'race-123', results: [ { userId: 'user-123', startPos: 5, finishPos: 2, incidents: 0, status: 'finished', // No sof provided }, { userId: 'user-456', startPos: 3, finishPos: 8, incidents: 0, status: 'finished', // No sof provided }, ], }; const results = DrivingRatingCalculator.calculateFromRaceFacts(facts); // Should still calculate without errors expect(results.size).toBe(2); expect(results.get('user-123')!.events.length).toBeGreaterThan(0); }); it('should handle empty results array', () => { const facts: DrivingRaceFactsDto = { raceId: 'race-123', results: [], }; const results = DrivingRatingCalculator.calculateFromRaceFacts(facts); expect(results.size).toBe(0); }); }); describe('calculate', () => { it('should sum events with component weights', () => { const events: RatingEvent[] = [ RatingEvent.create({ id: RatingEventId.generate(), userId: 'user-123', dimension: RatingDimensionKey.create('driving'), delta: RatingDelta.create(10), occurredAt: new Date(), createdAt: new Date(), source: { type: 'race', id: 'race-123' }, reason: { code: 'DRIVING_FINISH_STRENGTH_GAIN', summary: 'Good finish', details: {}, }, visibility: { public: true, redactedFields: [] }, version: 1, }), RatingEvent.create({ id: RatingEventId.generate(), userId: 'user-123', dimension: RatingDimensionKey.create('driving'), delta: RatingDelta.create(-5), occurredAt: new Date(), createdAt: new Date(), source: { type: 'race', id: 'race-123' }, reason: { code: 'DRIVING_INCIDENTS_PENALTY', summary: '1 incident', details: {}, }, visibility: { public: true, redactedFields: [] }, version: 1, }), ]; const result = DrivingRatingCalculator.calculate(events); // Should apply weights: 10 * 0.5 + (-5) * 0.3 = 5 - 1.5 = 3.5 // Then normalized by total weight (1 + 1 = 2) expect(result).toBeGreaterThan(0); expect(result).toBeLessThan(5); }); it('should handle empty events array', () => { const result = DrivingRatingCalculator.calculate([]); expect(result).toBe(0); }); it('should apply reliability weight to penalty events', () => { const events: RatingEvent[] = [ RatingEvent.create({ id: RatingEventId.generate(), userId: 'user-123', dimension: RatingDimensionKey.create('driving'), delta: RatingDelta.create(-15), occurredAt: new Date(), createdAt: new Date(), source: { type: 'race', id: 'race-123' }, reason: { code: 'DRIVING_DNS_PENALTY', summary: 'Did not start', details: {}, }, visibility: { public: true, redactedFields: [] }, version: 1, }), ]; const result = DrivingRatingCalculator.calculate(events); // Should apply reliability weight (0.2) expect(result).toBe(-15 * 0.2); }); it('should normalize by total weight', () => { const events: RatingEvent[] = [ RatingEvent.create({ id: RatingEventId.generate(), userId: 'user-123', dimension: RatingDimensionKey.create('driving'), delta: RatingDelta.create(20), occurredAt: new Date(), createdAt: new Date(), source: { type: 'race', id: 'race-123' }, reason: { code: 'DRIVING_FINISH_STRENGTH_GAIN', summary: 'Test', details: {}, }, visibility: { public: true, redactedFields: [] }, version: 1, }), RatingEvent.create({ id: RatingEventId.generate(), userId: 'user-123', dimension: RatingDimensionKey.create('driving'), delta: RatingDelta.create(-10), occurredAt: new Date(), createdAt: new Date(), source: { type: 'race', id: 'race-123' }, reason: { code: 'DRIVING_DNS_PENALTY', summary: 'Test', details: {}, }, visibility: { public: true, redactedFields: [] }, version: 1, }), ]; const result = DrivingRatingCalculator.calculate(events); // 20 * 0.5 + (-10) * 0.2 = 10 - 2 = 8 // Normalized by (1 + 1) = 2 // Result = 8 / 2 = 4 expect(result).toBe(4); }); it('should handle events with custom weights', () => { const events: RatingEvent[] = [ RatingEvent.create({ id: RatingEventId.generate(), userId: 'user-123', dimension: RatingDimensionKey.create('driving'), delta: RatingDelta.create(10), weight: 2, occurredAt: new Date(), createdAt: new Date(), source: { type: 'race', id: 'race-123' }, reason: { code: 'DRIVING_FINISH_STRENGTH_GAIN', summary: 'Test', details: {}, }, visibility: { public: true, redactedFields: [] }, version: 1, }), ]; const result = DrivingRatingCalculator.calculate(events); // Should consider event weight expect(result).toBeGreaterThan(0); }); }); });