import { describe, it, expect } from 'vitest'; import { Race } from '@core/racing/domain/entities/Race'; import { League } from '@core/racing/domain/entities/League'; import { Result } from '@core/racing/domain/entities/Result'; import { Penalty } from '@core/racing/domain/entities/Penalty'; import { Standing } from '@core/racing/domain/entities/Standing'; import { GetRaceResultsDetailUseCase } from '@core/racing/application/use-cases/GetRaceResultsDetailUseCase'; import { ImportRaceResultsUseCase } from '@core/racing/application/use-cases/ImportRaceResultsUseCase'; import type { IRaceResultsDetailPresenter, RaceResultsDetailViewModel, } from '@core/racing/application/presenters/IRaceResultsDetailPresenter'; import type { IImportRaceResultsPresenter, ImportRaceResultsSummaryViewModel, } from '@core/racing/application/presenters/IImportRaceResultsPresenter'; class FakeRaceResultsDetailPresenter implements IRaceResultsDetailPresenter { viewModel: RaceResultsDetailViewModel | null = null; reset(): void { this.viewModel = null; } present(viewModel: RaceResultsDetailViewModel): RaceResultsDetailViewModel { this.viewModel = viewModel; return viewModel; } getViewModel(): RaceResultsDetailViewModel | null { return this.viewModel; } } class FakeImportRaceResultsPresenter implements IImportRaceResultsPresenter { viewModel: ImportRaceResultsSummaryViewModel | null = null; present(viewModel: ImportRaceResultsSummaryViewModel): ImportRaceResultsSummaryViewModel { this.viewModel = viewModel; return viewModel; } getViewModel(): ImportRaceResultsSummaryViewModel | null { return this.viewModel; } } describe('ImportRaceResultsUseCase', () => { it('imports results and triggers standings recalculation for the league', async () => { // Given a league, a race, empty results, and a standing repository const league = League.create({ id: 'league-1', name: 'Import League', description: 'League for import tests', ownerId: 'owner-1', }); const race = Race.create({ id: 'race-1', leagueId: league.id, scheduledAt: new Date(), track: 'Import Circuit', car: 'GT3', sessionType: 'race', status: 'completed', }); const races = new Map(); races.set(race.id, race); const leagues = new Map(); leagues.set(league.id, league); const storedResults: Result[] = []; let existsByRaceIdCalled = false; const recalcCalls: string[] = []; const raceRepository = { findById: async (id: string): Promise => races.get(id) ?? null, findAll: async (): Promise => [], findByLeagueId: async (): Promise => [], findUpcomingByLeagueId: async (): Promise => [], findCompletedByLeagueId: async (): Promise => [], findByStatus: async (): Promise => [], findByDateRange: async (): Promise => [], create: async (): Promise => { throw new Error('Not implemented'); }, update: async (): Promise => { throw new Error('Not implemented'); }, delete: async (): Promise => { throw new Error('Not implemented'); }, exists: async (): Promise => false, }; const leagueRepository = { findById: async (id: string): Promise => leagues.get(id) ?? null, findAll: async (): Promise => [], findByOwnerId: async (): Promise => [], create: async (): Promise => { throw new Error('Not implemented'); }, update: async (): Promise => { throw new Error('Not implemented'); }, delete: async (): Promise => { throw new Error('Not implemented'); }, exists: async (): Promise => false, searchByName: async (): Promise => [], }; const resultRepository = { findById: async (): Promise => null, findAll: async (): Promise => [], findByRaceId: async (): Promise => [], findByDriverId: async (): Promise => [], findByDriverIdAndLeagueId: async (): Promise => [], create: async (): Promise => { throw new Error('Not implemented'); }, createMany: async (results: Result[]): Promise => { storedResults.push(...results); return results; }, update: async (): Promise => { throw new Error('Not implemented'); }, delete: async (): Promise => { throw new Error('Not implemented'); }, deleteByRaceId: async (): Promise => { throw new Error('Not implemented'); }, exists: async (): Promise => false, existsByRaceId: async (raceId: string): Promise => { existsByRaceIdCalled = true; return storedResults.some((r) => r.raceId === raceId); }, }; const standingRepository = { findByLeagueId: async (): Promise => [], findByDriverIdAndLeagueId: async (): Promise => null, findAll: async (): Promise => [], save: async (): Promise => { throw new Error('Not implemented'); }, saveMany: async (): Promise => { throw new Error('Not implemented'); }, delete: async (): Promise => { throw new Error('Not implemented'); }, deleteByLeagueId: async (): Promise => { throw new Error('Not implemented'); }, exists: async (): Promise => false, recalculate: async (leagueId: string): Promise => { recalcCalls.push(leagueId); return []; }, }; const presenter = new FakeImportRaceResultsPresenter(); const useCase = new ImportRaceResultsUseCase( raceRepository, leagueRepository, resultRepository, standingRepository, presenter, ); const importedResults = [ Result.create({ id: 'result-1', raceId: race.id, driverId: 'driver-1', position: 1, fastestLap: 90.123, incidents: 0, startPosition: 3, }), Result.create({ id: 'result-2', raceId: race.id, driverId: 'driver-2', position: 2, fastestLap: 91.456, incidents: 2, startPosition: 1, }), ]; // When executing the import await useCase.execute({ raceId: race.id, results: importedResults, }); // Then new Result entries are persisted expect(existsByRaceIdCalled).toBe(true); expect(storedResults.length).toBe(2); expect(storedResults.map((r) => r.id)).toEqual(['result-1', 'result-2']); // And standings are recalculated exactly once for the correct league expect(recalcCalls).toEqual([league.id]); // And the presenter receives a summary const viewModel = presenter.getViewModel(); expect(viewModel).not.toBeNull(); expect(viewModel!.importedCount).toBe(2); expect(viewModel!.standingsRecalculated).toBe(true); }); it('rejects import when results already exist for the race', async () => { const league = League.create({ id: 'league-2', name: 'Existing Results League', description: 'League with existing results', ownerId: 'owner-2', }); const race = Race.create({ id: 'race-2', leagueId: league.id, scheduledAt: new Date(), track: 'Existing Circuit', car: 'GT4', sessionType: 'race', status: 'completed', }); const races = new Map([[race.id, race]]); const leagues = new Map([[league.id, league]]); const storedResults: Result[] = [ Result.create({ id: 'existing', raceId: race.id, driverId: 'driver-x', position: 1, fastestLap: 90.0, incidents: 1, startPosition: 1, }), ]; const raceRepository = { findById: async (id: string): Promise => races.get(id) ?? null, findAll: async (): Promise => [], findByLeagueId: async (): Promise => [], findUpcomingByLeagueId: async (): Promise => [], findCompletedByLeagueId: async (): Promise => [], findByStatus: async (): Promise => [], findByDateRange: async (): Promise => [], create: async (): Promise => { throw new Error('Not implemented'); }, update: async (): Promise => { throw new Error('Not implemented'); }, delete: async (): Promise => { throw new Error('Not implemented'); }, exists: async (): Promise => false, }; const leagueRepository = { findById: async (id: string): Promise => leagues.get(id) ?? null, findAll: async (): Promise => [], findByOwnerId: async (): Promise => [], create: async (): Promise => { throw new Error('Not implemented'); }, update: async (): Promise => { throw new Error('Not implemented'); }, delete: async (): Promise => { throw new Error('Not implemented'); }, exists: async (): Promise => false, searchByName: async (): Promise => [], }; const resultRepository = { findById: async (): Promise => null, findAll: async (): Promise => [], findByRaceId: async (): Promise => [], findByDriverId: async (): Promise => [], findByDriverIdAndLeagueId: async (): Promise => [], create: async (): Promise => { throw new Error('Not implemented'); }, createMany: async (_results: Result[]): Promise => { throw new Error('Should not be called when results already exist'); }, update: async (): Promise => { throw new Error('Not implemented'); }, delete: async (): Promise => { throw new Error('Not implemented'); }, deleteByRaceId: async (): Promise => { throw new Error('Not implemented'); }, exists: async (): Promise => false, existsByRaceId: async (raceId: string): Promise => { return storedResults.some((r) => r.raceId === raceId); }, }; const standingRepository = { findByLeagueId: async (): Promise => [], findByDriverIdAndLeagueId: async (): Promise => null, findAll: async (): Promise => [], save: async (): Promise => { throw new Error('Not implemented'); }, saveMany: async (): Promise => { throw new Error('Not implemented'); }, delete: async (): Promise => { throw new Error('Not implemented'); }, deleteByLeagueId: async (): Promise => { throw new Error('Not implemented'); }, exists: async (): Promise => false, recalculate: async (_leagueId: string): Promise => { throw new Error('Should not be called when results already exist'); }, }; const presenter = new FakeImportRaceResultsPresenter(); const driverRepository = { findById: async (): Promise => null, findByIRacingId: async (iracingId: string): Promise => { // Mock finding driver by iracingId if (iracingId === 'driver-1') { return Driver.create({ id: 'driver-1', iracingId: 'driver-1', name: 'Driver One', country: 'US' }); } if (iracingId === 'driver-2') { return Driver.create({ id: 'driver-2', iracingId: 'driver-2', name: 'Driver Two', country: 'GB' }); } return null; }, findAll: async (): Promise => [], create: async (): Promise => { throw new Error('Not implemented'); }, update: async (): Promise => { throw new Error('Not implemented'); }, delete: async (): Promise => { throw new Error('Not implemented'); }, exists: async (): Promise => false, existsByIRacingId: async (): Promise => false, }; const useCase = new ImportRaceResultsUseCase( raceRepository, leagueRepository, resultRepository, driverRepository, standingRepository, presenter, ); const importedResults = [ Result.create({ id: 'new-result', raceId: race.id, driverId: 'driver-1', position: 2, fastestLap: 91.0, incidents: 0, startPosition: 2, }), ]; await expect( useCase.execute({ raceId: race.id, results: importedResults, }), ).rejects.toThrow('Results already exist for this race'); }); }); describe('GetRaceResultsDetailUseCase', () => { it('computes points system from league settings and identifies fastest lap', async () => { // Given a league with default scoring configuration and two results const league = League.create({ id: 'league-scoring', name: 'Scoring League', description: 'League with scoring settings', ownerId: 'owner-scoring', }); const race = Race.create({ id: 'race-scoring', leagueId: league.id, scheduledAt: new Date(), track: 'Scoring Circuit', car: 'Prototype', sessionType: 'race', status: 'completed', }); const driver1: { id: string; name: string; country: string } = { id: 'driver-a', name: 'Driver A', country: 'US', }; const driver2: { id: string; name: string; country: string } = { id: 'driver-b', name: 'Driver B', country: 'GB', }; const result1 = Result.create({ id: 'r1', raceId: race.id, driverId: driver1.id, position: 1, fastestLap: 90.123, incidents: 0, startPosition: 3, }); const result2 = Result.create({ id: 'r2', raceId: race.id, driverId: driver2.id, position: 2, fastestLap: 88.456, incidents: 2, startPosition: 1, }); const races = new Map([[race.id, race]]); const leagues = new Map([[league.id, league]]); const results = [result1, result2]; const drivers = [driver1, driver2]; const raceRepository = { findById: async (id: string): Promise => races.get(id) ?? null, findAll: async (): Promise => [], findByLeagueId: async (): Promise => [], findUpcomingByLeagueId: async (): Promise => [], findCompletedByLeagueId: async (): Promise => [], findByStatus: async (): Promise => [], findByDateRange: async (): Promise => [], create: async (): Promise => { throw new Error('Not implemented'); }, update: async (): Promise => { throw new Error('Not implemented'); }, delete: async (): Promise => { throw new Error('Not implemented'); }, exists: async (): Promise => false, }; const leagueRepository = { findById: async (id: string): Promise => leagues.get(id) ?? null, findAll: async (): Promise => [], findByOwnerId: async (): Promise => [], create: async (): Promise => { throw new Error('Not implemented'); }, update: async (): Promise => { throw new Error('Not implemented'); }, delete: async (): Promise => { throw new Error('Not implemented'); }, exists: async (): Promise => false, searchByName: async (): Promise => [], }; const resultRepository = { findById: async (): Promise => null, findAll: async (): Promise => [], findByRaceId: async (raceId: string): Promise => results.filter((r) => r.raceId === raceId), findByDriverId: async (): Promise => [], findByDriverIdAndLeagueId: async (): Promise => [], create: async (): Promise => { throw new Error('Not implemented'); }, createMany: async (): Promise => { throw new Error('Not implemented'); }, update: async (): Promise => { throw new Error('Not implemented'); }, delete: async (): Promise => { throw new Error('Not implemented'); }, deleteByRaceId: async (): Promise => { throw new Error('Not implemented'); }, exists: async (): Promise => false, existsByRaceId: async (): Promise => false, }; const driverRepository = { findById: async (): Promise => null, findByIRacingId: async (): Promise => null, findAll: async (): Promise => drivers.map(d => Driver.create({ id: d.id, iracingId: '123', name: d.name, country: d.country })), create: async (): Promise => { throw new Error('Not implemented'); }, update: async (): Promise => { throw new Error('Not implemented'); }, delete: async (): Promise => { throw new Error('Not implemented'); }, exists: async (): Promise => false, existsByIRacingId: async (): Promise => false, }; const penaltyRepository = { findById: async (): Promise => null, findByRaceId: async (): Promise => [] as Penalty[], findByDriverId: async (): Promise => [], findByProtestId: async (): Promise => [], findPending: async (): Promise => [], findIssuedBy: async (): Promise => [], create: async (): Promise => { throw new Error('Not implemented'); }, update: async (): Promise => { throw new Error('Not implemented'); }, exists: async (): Promise => false, }; const presenter = new FakeRaceResultsDetailPresenter(); const useCase = new GetRaceResultsDetailUseCase( raceRepository, leagueRepository, resultRepository, driverRepository, penaltyRepository, ); // When executing the query await useCase.execute({ raceId: race.id }, presenter); const viewModel = presenter.getViewModel(); expect(viewModel).not.toBeNull(); // Then points system matches the default F1-style configuration expect(viewModel!.pointsSystem?.[1]).toBe(25); expect(viewModel!.pointsSystem?.[2]).toBe(18); // And fastest lap is identified correctly expect(viewModel!.fastestLapTime).toBeCloseTo(88.456, 3); }); it('builds race results view model including penalties', async () => { // Given a race with one result and one applied penalty const league = League.create({ id: 'league-penalties', name: 'Penalty League', description: 'League with penalties', ownerId: 'owner-penalties', }); const race = Race.create({ id: 'race-penalties', leagueId: league.id, scheduledAt: new Date(), track: 'Penalty Circuit', car: 'Touring', sessionType: 'race', status: 'completed', }); const driver: { id: string; name: string; country: string } = { id: 'driver-pen', name: 'Penalty Driver', country: 'DE', }; const result = Result.create({ id: 'res-pen', raceId: race.id, driverId: driver.id, position: 3, fastestLap: 95.0, incidents: 4, startPosition: 5, }); const penalty = Penalty.create({ id: 'pen-1', leagueId: league.id, raceId: race.id, driverId: driver.id, type: 'points_deduction', value: 3, reason: 'Track limits', issuedBy: 'steward-1', status: 'applied', issuedAt: new Date(), }); const races = new Map([[race.id, race]]); const leagues = new Map([[league.id, league]]); const results = [result]; const drivers = [driver]; const penalties = [penalty]; const raceRepository = { findById: async (id: string): Promise => races.get(id) ?? null, findAll: async (): Promise => [], findByLeagueId: async (): Promise => [], findUpcomingByLeagueId: async (): Promise => [], findCompletedByLeagueId: async (): Promise => [], findByStatus: async (): Promise => [], findByDateRange: async (): Promise => [], create: async (): Promise => { throw new Error('Not implemented'); }, update: async (): Promise => { throw new Error('Not implemented'); }, delete: async (): Promise => { throw new Error('Not implemented'); }, exists: async (): Promise => false, }; const leagueRepository = { findById: async (id: string): Promise => leagues.get(id) ?? null, findAll: async (): Promise => [], findByOwnerId: async (): Promise => [], create: async (): Promise => { throw new Error('Not implemented'); }, update: async (): Promise => { throw new Error('Not implemented'); }, delete: async (): Promise => { throw new Error('Not implemented'); }, exists: async (): Promise => false, searchByName: async (): Promise => [], }; const resultRepository = { findById: async (): Promise => null, findAll: async (): Promise => [], findByRaceId: async (raceId: string): Promise => results.filter((r) => r.raceId === raceId), findByDriverId: async (): Promise => [], findByDriverIdAndLeagueId: async (): Promise => [], create: async (): Promise => { throw new Error('Not implemented'); }, createMany: async (): Promise => { throw new Error('Not implemented'); }, update: async (): Promise => { throw new Error('Not implemented'); }, delete: async (): Promise => { throw new Error('Not implemented'); }, deleteByRaceId: async (): Promise => { throw new Error('Not implemented'); }, exists: async (): Promise => false, existsByRaceId: async (): Promise => false, }; const driverRepository = { findById: async (): Promise => null, findByIRacingId: async (): Promise => null, findAll: async (): Promise => drivers.map(d => Driver.create({ id: d.id, iracingId: '123', name: d.name, country: d.country })), create: async (): Promise => { throw new Error('Not implemented'); }, update: async (): Promise => { throw new Error('Not implemented'); }, delete: async (): Promise => { throw new Error('Not implemented'); }, exists: async (): Promise => false, existsByIRacingId: async (): Promise => false, }; const penaltyRepository = { findById: async (): Promise => null, findByRaceId: async (raceId: string): Promise => penalties.filter((p) => p.raceId === raceId), findByDriverId: async (): Promise => [], findByProtestId: async (): Promise => [], findPending: async (): Promise => [], findIssuedBy: async (): Promise => [], create: async (): Promise => { throw new Error('Not implemented'); }, update: async (): Promise => { throw new Error('Not implemented'); }, exists: async (): Promise => false, }; const presenter = new FakeRaceResultsDetailPresenter(); const useCase = new GetRaceResultsDetailUseCase( raceRepository, leagueRepository, resultRepository, driverRepository, penaltyRepository, ); // When await useCase.execute({ raceId: race.id }, presenter); const viewModel = presenter.getViewModel(); expect(viewModel).not.toBeNull(); // Then header and league info are present expect(viewModel!.race).not.toBeNull(); expect(viewModel!.race!.id).toBe(race.id); expect(viewModel!.league).not.toBeNull(); expect(viewModel!.league!.id).toBe(league.id); // And classification and penalties match the underlying data expect(viewModel!.results.length).toBe(1); expect(viewModel!.results[0]!.id).toBe(result.id); expect(viewModel!.penalties.length).toBe(1); expect(viewModel!.penalties[0]!.driverId).toBe(driver.id); expect(viewModel!.penalties[0]!.type).toBe('points_deduction'); expect(viewModel!.penalties[0]!.value).toBe(3); }); it('presents an error when race does not exist', async () => { // Given repositories without the requested race const raceRepository = { findById: async (): Promise => null, findAll: async (): Promise => [], findByLeagueId: async (): Promise => [], findUpcomingByLeagueId: async (): Promise => [], findCompletedByLeagueId: async (): Promise => [], findByStatus: async (): Promise => [], findByDateRange: async (): Promise => [], create: async (): Promise => { throw new Error('Not implemented'); }, update: async (): Promise => { throw new Error('Not implemented'); }, delete: async (): Promise => { throw new Error('Not implemented'); }, exists: async (): Promise => false, }; const leagueRepository = { findById: async (): Promise => null, findAll: async (): Promise => [], findByOwnerId: async (): Promise => [], create: async (): Promise => { throw new Error('Not implemented'); }, update: async (): Promise => { throw new Error('Not implemented'); }, delete: async (): Promise => { throw new Error('Not implemented'); }, exists: async (): Promise => false, searchByName: async (): Promise => [], }; const resultRepository = { findById: async (): Promise => null, findAll: async (): Promise => [], findByRaceId: async (): Promise => [] as Result[], findByDriverId: async (): Promise => [], findByDriverIdAndLeagueId: async (): Promise => [], create: async (): Promise => { throw new Error('Not implemented'); }, createMany: async (): Promise => { throw new Error('Not implemented'); }, update: async (): Promise => { throw new Error('Not implemented'); }, delete: async (): Promise => { throw new Error('Not implemented'); }, deleteByRaceId: async (): Promise => { throw new Error('Not implemented'); }, exists: async (): Promise => false, existsByRaceId: async (): Promise => false, }; const driverRepository = { findById: async (): Promise => null, findByIRacingId: async (): Promise => null, findAll: async (): Promise => [], create: async (): Promise => { throw new Error('Not implemented'); }, update: async (): Promise => { throw new Error('Not implemented'); }, delete: async (): Promise => { throw new Error('Not implemented'); }, exists: async (): Promise => false, existsByIRacingId: async (): Promise => false, }; const penaltyRepository = { findById: async (): Promise => null, findByRaceId: async (): Promise => [] as Penalty[], findByDriverId: async (): Promise => [], findByProtestId: async (): Promise => [], findPending: async (): Promise => [], findIssuedBy: async (): Promise => [], create: async (): Promise => { throw new Error('Not implemented'); }, update: async (): Promise => { throw new Error('Not implemented'); }, exists: async (): Promise => false, }; const presenter = new FakeRaceResultsDetailPresenter(); const useCase = new GetRaceResultsDetailUseCase( raceRepository, leagueRepository, resultRepository, driverRepository, penaltyRepository, ); // When await useCase.execute({ raceId: 'missing-race' }, presenter); const viewModel = presenter.getViewModel(); expect(viewModel).not.toBeNull(); expect(viewModel!.race).toBeNull(); expect(viewModel!.error).toBe('Race not found'); }); });