import { describe, it, expect } from 'vitest'; import { EventScoringService } from '@core/racing/domain/services/EventScoringService'; import type { BonusRule } from '@core/racing/domain/types/BonusRule'; import { Result } from '@core/racing/domain/entities/result/Result'; import type { Penalty } from '@core/racing/domain/entities/Penalty'; import type { ChampionshipConfig } from '@core/racing/domain/types/ChampionshipConfig'; import type { SessionType } from '@core/racing/domain/types/SessionType'; import { PointsTable } from '@core/racing/domain/value-objects/PointsTable'; const makeChampionshipConfig = (params: { id: string; name: string; sessionTypes: SessionType[]; mainPoints: number[]; sprintPoints?: number[]; mainBonusRules?: BonusRule[]; }): ChampionshipConfig => { const { id, name, sessionTypes, mainPoints, sprintPoints, mainBonusRules } = params; const pointsTableBySessionType: Record = {} as Record; sessionTypes.forEach((sessionType) => { if (sessionType === 'main') { pointsTableBySessionType[sessionType] = new PointsTable( mainPoints.reduce((acc, points, index) => { acc[index + 1] = points; return acc; }, {} as Record), ); } else if (sessionType === 'sprint' && sprintPoints) { pointsTableBySessionType[sessionType] = new PointsTable( sprintPoints.reduce((acc, points, index) => { acc[index + 1] = points; return acc; }, {} as Record), ); } else { pointsTableBySessionType[sessionType] = new PointsTable({}); } }); const bonusRulesBySessionType: Record = {} as Record; sessionTypes.forEach((sessionType) => { if (sessionType === 'main' && mainBonusRules) { bonusRulesBySessionType[sessionType] = mainBonusRules; } else { bonusRulesBySessionType[sessionType] = []; } }); return { id, name, type: 'driver', sessionTypes, pointsTableBySessionType, bonusRulesBySessionType, dropScorePolicy: { strategy: 'none', }, }; }; describe('EventScoringService', () => { const seasonId = 'season-1'; it('assigns base points based on finishing positions for a main race', () => { const service = new EventScoringService(); const championship = makeChampionshipConfig({ id: 'champ-1', name: 'Driver Championship', sessionTypes: ['main'], mainPoints: [25, 18, 15, 12, 10], }); const results: Result[] = [ Result.create({ id: 'result-1', raceId: 'race-1', driverId: 'driver-1', position: 1, fastestLap: 90000, incidents: 0, startPosition: 1, }), Result.create({ id: 'result-2', raceId: 'race-1', driverId: 'driver-2', position: 2, fastestLap: 90500, incidents: 0, startPosition: 2, }), Result.create({ id: 'result-3', raceId: 'race-1', driverId: 'driver-3', position: 3, fastestLap: 91000, incidents: 0, startPosition: 3, }), Result.create({ id: 'result-4', raceId: 'race-1', driverId: 'driver-4', position: 4, fastestLap: 91500, incidents: 0, startPosition: 4, }), Result.create({ id: 'result-5', raceId: 'race-1', driverId: 'driver-5', position: 5, fastestLap: 92000, incidents: 0, startPosition: 5, }), ]; const penalties: Penalty[] = []; const points = service.scoreSession({ seasonId, championship, sessionType: 'main', results, penalties, }); const byParticipant = new Map(points.map((p) => [p.participant.id, p])); expect(byParticipant.get('driver-1')?.basePoints).toBe(25); expect(byParticipant.get('driver-2')?.basePoints).toBe(18); expect(byParticipant.get('driver-3')?.basePoints).toBe(15); expect(byParticipant.get('driver-4')?.basePoints).toBe(12); expect(byParticipant.get('driver-5')?.basePoints).toBe(10); for (const entry of byParticipant.values()) { expect(entry.bonusPoints).toBe(0); expect(entry.penaltyPoints).toBe(0); expect(entry.totalPoints).toBe(entry.basePoints); } }); it('applies fastest lap bonus only when inside top 10', () => { const service = new EventScoringService(); const fastestLapBonus: BonusRule = { id: 'bonus-fastest-lap', type: 'fastestLap', points: 1, requiresFinishInTopN: 10, }; const championship = makeChampionshipConfig({ id: 'champ-1', name: 'Driver Championship', sessionTypes: ['main'], mainPoints: [25, 18, 15, 12, 10, 8, 6, 4, 2, 1], mainBonusRules: [fastestLapBonus], }); const baseResultTemplate = { raceId: 'race-1', incidents: 0, } as const; const resultsP11Fastest: Result[] = [ Result.create({ id: 'result-1', ...baseResultTemplate, driverId: 'driver-1', position: 1, startPosition: 1, fastestLap: 91000, }), Result.create({ id: 'result-2', ...baseResultTemplate, driverId: 'driver-2', position: 2, startPosition: 2, fastestLap: 90500, }), Result.create({ id: 'result-3', ...baseResultTemplate, driverId: 'driver-3', position: 11, startPosition: 15, fastestLap: 90000, }), ]; const penalties: Penalty[] = []; const pointsNoBonus = service.scoreSession({ seasonId, championship, sessionType: 'main', results: resultsP11Fastest, penalties, }); const mapNoBonus = new Map(pointsNoBonus.map((p) => [p.participant.id, p])); expect(mapNoBonus.get('driver-3')?.bonusPoints).toBe(0); const resultsP8Fastest: Result[] = [ Result.create({ id: 'result-1', ...baseResultTemplate, driverId: 'driver-1', position: 1, startPosition: 1, fastestLap: 91000, }), Result.create({ id: 'result-2', ...baseResultTemplate, driverId: 'driver-2', position: 2, startPosition: 2, fastestLap: 90500, }), Result.create({ id: 'result-3', ...baseResultTemplate, driverId: 'driver-3', position: 8, startPosition: 15, fastestLap: 90000, }), ]; const pointsWithBonus = service.scoreSession({ seasonId, championship, sessionType: 'main', results: resultsP8Fastest, penalties, }); const mapWithBonus = new Map(pointsWithBonus.map((p) => [p.participant.id, p])); expect(mapWithBonus.get('driver-3')?.bonusPoints).toBe(1); expect(mapWithBonus.get('driver-3')?.totalPoints).toBe( (mapWithBonus.get('driver-3')?.basePoints || 0) + (mapWithBonus.get('driver-3')?.bonusPoints || 0) - (mapWithBonus.get('driver-3')?.penaltyPoints || 0), ); }); });