diff --git a/adapters/bootstrap/LeagueScoringPresets.ts b/adapters/bootstrap/LeagueScoringPresets.ts new file mode 100644 index 000000000..0b5e81eeb --- /dev/null +++ b/adapters/bootstrap/LeagueScoringPresets.ts @@ -0,0 +1,239 @@ +import { PointsTable } from '@core/racing/domain/value-objects/PointsTable'; +import type { ChampionshipConfig } from '@core/racing/domain/types/ChampionshipConfig'; +import type { SessionType } from '@core/racing/domain/types/SessionType'; +import type { BonusRule } from '@core/racing/domain/types/BonusRule'; +import type { DropScorePolicy } from '@core/racing/domain/types/DropScorePolicy'; +import type { ChampionshipType } from '@core/racing/domain/types/ChampionshipType'; +import { LeagueScoringConfig } from '@core/racing/domain/entities/LeagueScoringConfig'; + +export type LeagueScoringPresetPrimaryChampionshipType = + | 'driver' + | 'team' + | 'nations' + | 'trophy'; + +export interface LeagueScoringPreset { + id: string; + name: string; + description: string; + primaryChampionshipType: LeagueScoringPresetPrimaryChampionshipType; + dropPolicySummary: string; + sessionSummary: string; + bonusSummary: string; + createConfig: (options: { seasonId: string }) => LeagueScoringConfig; +} + +const mainPointsSprintMain = new PointsTable({ + 1: 25, + 2: 18, + 3: 15, + 4: 12, + 5: 10, + 6: 8, + 7: 6, + 8: 4, + 9: 2, + 10: 1, +}); + +const sprintPointsSprintMain = new PointsTable({ + 1: 8, + 2: 7, + 3: 6, + 4: 5, + 5: 4, + 6: 3, + 7: 2, + 8: 1, +}); + +const clubMainPoints = new PointsTable({ + 1: 20, + 2: 15, + 3: 12, + 4: 10, + 5: 8, + 6: 6, + 7: 4, + 8: 2, + 9: 1, +}); + +const enduranceMainPoints = new PointsTable({ + 1: 50, + 2: 36, + 3: 30, + 4: 24, + 5: 20, + 6: 16, + 7: 12, + 8: 8, + 9: 4, + 10: 2, +}); + +export const leagueScoringPresets: LeagueScoringPreset[] = [ + { + id: 'sprint-main-driver', + name: 'Sprint + Main', + description: + 'Short sprint race plus main race; sprint gives fewer points.', + primaryChampionshipType: 'driver', + dropPolicySummary: 'Best 6 results of 8 count towards the championship.', + sessionSummary: 'Sprint + Main', + bonusSummary: 'Fastest lap +1 point in main race if finishing P10 or better.', + createConfig: ({ seasonId }) => { + const fastestLapBonus: BonusRule = { + id: 'fastest-lap-main', + type: 'fastestLap', + points: 1, + requiresFinishInTopN: 10, + }; + + const sessionTypes: SessionType[] = ['sprint', 'main']; + + const pointsTableBySessionType: Record = { + sprint: sprintPointsSprintMain, + main: mainPointsSprintMain, + practice: new PointsTable({}), + qualifying: new PointsTable({}), + q1: new PointsTable({}), + q2: new PointsTable({}), + q3: new PointsTable({}), + timeTrial: new PointsTable({}), + }; + + const bonusRulesBySessionType: Record = { + sprint: [], + main: [fastestLapBonus], + practice: [], + qualifying: [], + q1: [], + q2: [], + q3: [], + timeTrial: [], + }; + + const dropScorePolicy: DropScorePolicy = { + strategy: 'bestNResults', + count: 6, + }; + + const championship: ChampionshipConfig = { + id: 'driver-champ-sprint-main', + name: 'Driver Championship', + type: 'driver' as ChampionshipType, + sessionTypes, + pointsTableBySessionType, + bonusRulesBySessionType, + dropScorePolicy, + }; + + return LeagueScoringConfig.create({ + id: `lsc-${seasonId}-sprint-main-driver`, + seasonId, + scoringPresetId: 'sprint-main-driver', + championships: [championship], + }); + }, + }, + { + id: 'club-default', + name: 'Club ladder', + description: + 'Simple club ladder with a single main race and no bonuses or drop scores.', + primaryChampionshipType: 'driver', + dropPolicySummary: 'All race results count, no drop scores.', + sessionSummary: 'Main race only', + bonusSummary: 'No bonus points.', + createConfig: ({ seasonId }) => { + const sessionTypes: SessionType[] = ['main']; + + const pointsTableBySessionType: Record = { + sprint: new PointsTable({}), + main: clubMainPoints, + practice: new PointsTable({}), + qualifying: new PointsTable({}), + q1: new PointsTable({}), + q2: new PointsTable({}), + q3: new PointsTable({}), + timeTrial: new PointsTable({}), + }; + + const dropScorePolicy: DropScorePolicy = { + strategy: 'none', + }; + + const championship: ChampionshipConfig = { + id: 'driver-champ-club-default', + name: 'Driver Championship', + type: 'driver' as ChampionshipType, + sessionTypes, + pointsTableBySessionType, + dropScorePolicy, + }; + + return LeagueScoringConfig.create({ + id: `lsc-${seasonId}-club-default`, + seasonId, + scoringPresetId: 'club-default', + championships: [championship], + }); + }, + }, + { + id: 'endurance-main-double', + name: 'Endurance weekend', + description: + 'Single main endurance race with double points and a simple drop policy.', + primaryChampionshipType: 'driver', + dropPolicySummary: 'Best 4 results of 6 count towards the championship.', + sessionSummary: 'Main race only', + bonusSummary: 'No bonus points.', + createConfig: ({ seasonId }) => { + const sessionTypes: SessionType[] = ['main']; + + const pointsTableBySessionType: Record = { + sprint: new PointsTable({}), + main: enduranceMainPoints, + practice: new PointsTable({}), + qualifying: new PointsTable({}), + q1: new PointsTable({}), + q2: new PointsTable({}), + q3: new PointsTable({}), + timeTrial: new PointsTable({}), + }; + + const dropScorePolicy: DropScorePolicy = { + strategy: 'bestNResults', + count: 4, + }; + + const championship: ChampionshipConfig = { + id: 'driver-champ-endurance-main-double', + name: 'Driver Championship', + type: 'driver' as ChampionshipType, + sessionTypes, + pointsTableBySessionType, + dropScorePolicy, + }; + + return { + id: `lsc-${seasonId}-endurance-main-double`, + seasonId, + scoringPresetId: 'endurance-main-double', + championships: [championship], + }; + }, + }, +]; + +export function listLeagueScoringPresets(): LeagueScoringPreset[] { + return [...leagueScoringPresets]; +} + +export function getLeagueScoringPresetById( + id: string, +): LeagueScoringPreset | undefined { + return leagueScoringPresets.find((preset) => preset.id === id); +} \ No newline at end of file diff --git a/adapters/bootstrap/ScoringDemoSetup.ts b/adapters/bootstrap/ScoringDemoSetup.ts new file mode 100644 index 000000000..7755d470e --- /dev/null +++ b/adapters/bootstrap/ScoringDemoSetup.ts @@ -0,0 +1,82 @@ +import { Game } from '@core/racing/domain/entities/Game'; +import { Season } from '@core/racing/domain/entities/season/Season'; +import type { LeagueScoringConfig } from '@core/racing/domain/entities/LeagueScoringConfig'; +import { InMemoryGameRepository } from '../racing/persistence/inmemory/InMemoryScoringRepositories'; +import { InMemorySeasonRepository } from '../racing/persistence/inmemory/InMemoryScoringRepositories'; +import { InMemoryLeagueScoringConfigRepository } from '../racing/persistence/inmemory/InMemoryScoringRepositories'; +import { InMemoryChampionshipStandingRepository } from '../racing/persistence/inmemory/InMemoryScoringRepositories'; +import type { Logger } from '@core/shared/application'; +import { getLeagueScoringPresetById } from './LeagueScoringPresets'; + +/* eslint-disable @typescript-eslint/no-unused-vars */ +class SilentLogger implements Logger { + debug(...args: unknown[]): void { + // console.debug(...args); + } + info(...args: unknown[]): void { + // console.info(...args); + } + warn(...args: unknown[]): void { + // console.warn(...args); + } + error(...args: unknown[]): void { + // console.error(...args); + } +} + +export function createSprintMainDemoScoringSetup(params: { + leagueId: string; + seasonId?: string; +}): { + gameRepo: InMemoryGameRepository; + seasonRepo: InMemorySeasonRepository; + scoringConfigRepo: InMemoryLeagueScoringConfigRepository; + championshipStandingRepo: InMemoryChampionshipStandingRepository; + seasonId: string; + championshipId: string; +} { + const { leagueId } = params; + const seasonId = params.seasonId ?? 'season-sprint-main-demo'; + const championshipId = 'driver-champ'; + + const logger = new SilentLogger(); + + const game = Game.create({ id: 'iracing', name: 'iRacing' }); + + const season = Season.create({ + id: seasonId, + leagueId, + gameId: game.id.toString(), + name: 'Sprint + Main Demo Season', + year: 2025, + order: 1, + status: 'active', + startDate: new Date('2025-01-01'), + endDate: new Date('2025-12-31'), + }); + + const preset = getLeagueScoringPresetById('sprint-main-driver'); + if (!preset) { + throw new Error('Missing sprint-main-driver scoring preset'); + } + + const leagueScoringConfig: LeagueScoringConfig = preset.createConfig({ + seasonId: season.id, + }); + + const gameRepo = new InMemoryGameRepository(logger, [game]); + const seasonRepo = new InMemorySeasonRepository(logger, [season]); + const scoringConfigRepo = new InMemoryLeagueScoringConfigRepository(logger, [ + leagueScoringConfig, + ]); + const championshipStandingRepo = new InMemoryChampionshipStandingRepository(logger); + + return { + gameRepo, + seasonRepo, + scoringConfigRepo, + championshipStandingRepo, + seasonId: season.id, + championshipId, + }; +} \ No newline at end of file diff --git a/adapters/racing/persistence/inmemory/InMemoryCarRepository.test.ts b/adapters/racing/persistence/inmemory/InMemoryCarRepository.test.ts new file mode 100644 index 000000000..704259a52 --- /dev/null +++ b/adapters/racing/persistence/inmemory/InMemoryCarRepository.test.ts @@ -0,0 +1,235 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import { InMemoryCarRepository } from './InMemoryCarRepository'; +import { Car } from '@core/racing/domain/entities/Car'; +import { CarClass } from '@core/racing/domain/entities/CarClass'; +import { CarLicense } from '@core/racing/domain/entities/CarLicense'; +import type { Logger } from '@core/shared/application'; + +describe('InMemoryCarRepository', () => { + let repository: InMemoryCarRepository; + let mockLogger: Logger; + + beforeEach(() => { + mockLogger = { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; + repository = new InMemoryCarRepository(mockLogger); + }); + + const createTestCar = (id: string, name: string, manufacturer: string, gameId: string) => { + return Car.create({ + id, + name, + manufacturer, + gameId, + }); + }; + + describe('constructor', () => { + it('should initialize with a logger', () => { + expect(repository).toBeDefined(); + expect(mockLogger.info).toHaveBeenCalledWith('InMemoryCarRepository initialized'); + }); + }); + + describe('findById', () => { + it('should return null if car not found', async () => { + const result = await repository.findById('nonexistent'); + expect(result).toBeNull(); + expect(mockLogger.debug).toHaveBeenCalledWith('Attempting to find car with ID: nonexistent.'); + expect(mockLogger.warn).toHaveBeenCalledWith('Car with ID: nonexistent not found.'); + }); + + it('should return the car if found', async () => { + const car = createTestCar('1', 'Test Car', 'Test Manufacturer', 'iracing'); + await repository.create(car); + + const result = await repository.findById('1'); + expect(result).toEqual(car); + expect(mockLogger.info).toHaveBeenCalledWith('Successfully found car with ID: 1.'); + }); + }); + + describe('findAll', () => { + it('should return all cars', async () => { + const car1 = createTestCar('1', 'Car 1', 'Manufacturer 1', 'iracing'); + const car2 = createTestCar('2', 'Car 2', 'Manufacturer 2', 'iracing'); + await repository.create(car1); + await repository.create(car2); + + const result = await repository.findAll(); + expect(result).toHaveLength(2); + expect(result).toContain(car1); + expect(result).toContain(car2); + }); + }); + + describe('findByGameId', () => { + it('should return cars filtered by game ID', async () => { + const car1 = createTestCar('1', 'Car 1', 'Manufacturer 1', 'iracing'); + const car2 = createTestCar('2', 'Car 2', 'Manufacturer 2', 'assetto'); + await repository.create(car1); + await repository.create(car2); + + const result = await repository.findByGameId('iracing'); + expect(result).toHaveLength(1); + expect(result[0]).toEqual(car1); + }); + }); + + describe('findByClass', () => { + it('should return cars filtered by class', async () => { + const car1 = Car.create({ + id: '1', + name: 'Car 1', + manufacturer: 'Manufacturer 1', + gameId: 'iracing', + carClass: 'gt', + }); + const car2 = Car.create({ + id: '2', + name: 'Car 2', + manufacturer: 'Manufacturer 2', + gameId: 'iracing', + carClass: 'formula', + }); + await repository.create(car1); + await repository.create(car2); + + const result = await repository.findByClass(CarClass.create('gt')); + expect(result).toHaveLength(1); + expect(result[0]).toEqual(car1); + }); + }); + + describe('findByLicense', () => { + it('should return cars filtered by license', async () => { + const car1 = Car.create({ + id: '1', + name: 'Car 1', + manufacturer: 'Manufacturer 1', + gameId: 'iracing', + license: 'D', + }); + const car2 = Car.create({ + id: '2', + name: 'Car 2', + manufacturer: 'Manufacturer 2', + gameId: 'iracing', + license: 'C', + }); + await repository.create(car1); + await repository.create(car2); + + const result = await repository.findByLicense(CarLicense.create('D')); + expect(result).toHaveLength(1); + expect(result[0]).toEqual(car1); + }); + }); + + describe('findByManufacturer', () => { + it('should return cars filtered by manufacturer (case insensitive)', async () => { + const car1 = createTestCar('1', 'Car 1', 'Ferrari', 'iracing'); + const car2 = createTestCar('2', 'Car 2', 'ferrari', 'iracing'); + const car3 = createTestCar('3', 'Car 3', 'BMW', 'iracing'); + await repository.create(car1); + await repository.create(car2); + await repository.create(car3); + + const result = await repository.findByManufacturer('FERRARI'); + expect(result).toHaveLength(2); + expect(result).toContain(car1); + expect(result).toContain(car2); + }); + }); + + describe('searchByName', () => { + it('should return cars matching the query in name, shortName, or manufacturer', async () => { + const car1 = Car.create({ + id: '1', + name: 'Ferrari 488', + shortName: '488', + manufacturer: 'Ferrari', + gameId: 'iracing', + }); + const car2 = createTestCar('2', 'BMW M3', 'BMW', 'iracing'); + await repository.create(car1); + await repository.create(car2); + + const result = await repository.searchByName('ferrari'); + expect(result).toHaveLength(1); + expect(result[0]).toEqual(car1); + }); + }); + + describe('create', () => { + it('should create a new car', async () => { + const car = createTestCar('1', 'Test Car', 'Test Manufacturer', 'iracing'); + const result = await repository.create(car); + expect(result).toEqual(car); + expect(mockLogger.info).toHaveBeenCalledWith('Car 1 created successfully.'); + }); + + it('should throw error if car already exists', async () => { + const car = createTestCar('1', 'Test Car', 'Test Manufacturer', 'iracing'); + await repository.create(car); + await expect(repository.create(car)).rejects.toThrow('Car with ID 1 already exists'); + }); + }); + + describe('update', () => { + it('should update an existing car', async () => { + const car = createTestCar('1', 'Test Car', 'Test Manufacturer', 'iracing'); + await repository.create(car); + + const updatedCar = Car.create({ + id: '1', + name: 'Updated Car', + manufacturer: 'Test Manufacturer', + gameId: 'iracing', + }); + const result = await repository.update(updatedCar); + expect(result).toEqual(updatedCar); + expect(mockLogger.info).toHaveBeenCalledWith('Car 1 updated successfully.'); + }); + + it('should throw error if car does not exist', async () => { + const car = createTestCar('1', 'Test Car', 'Test Manufacturer', 'iracing'); + await expect(repository.update(car)).rejects.toThrow('Car with ID 1 not found'); + }); + }); + + describe('delete', () => { + it('should delete an existing car', async () => { + const car = createTestCar('1', 'Test Car', 'Test Manufacturer', 'iracing'); + await repository.create(car); + + await repository.delete('1'); + expect(mockLogger.info).toHaveBeenCalledWith('Car 1 deleted successfully.'); + const found = await repository.findById('1'); + expect(found).toBeNull(); + }); + + it('should throw error if car does not exist', async () => { + await expect(repository.delete('nonexistent')).rejects.toThrow('Car with ID nonexistent not found'); + }); + }); + + describe('exists', () => { + it('should return true if car exists', async () => { + const car = createTestCar('1', 'Test Car', 'Test Manufacturer', 'iracing'); + await repository.create(car); + + const result = await repository.exists('1'); + expect(result).toBe(true); + }); + + it('should return false if car does not exist', async () => { + const result = await repository.exists('nonexistent'); + expect(result).toBe(false); + }); + }); +}); \ No newline at end of file diff --git a/adapters/racing/persistence/inmemory/InMemoryCarRepository.ts b/adapters/racing/persistence/inmemory/InMemoryCarRepository.ts index b858018aa..1d3bdd352 100644 --- a/adapters/racing/persistence/inmemory/InMemoryCarRepository.ts +++ b/adapters/racing/persistence/inmemory/InMemoryCarRepository.ts @@ -6,7 +6,9 @@ */ import { v4 as uuidv4 } from 'uuid'; -import { Car, CarClass, CarLicense } from '@core/racing/domain/entities/Car'; +import { Car } from '@core/racing/domain/entities/Car'; +import { CarClass } from '@core/racing/domain/entities/CarClass'; +import { CarLicense } from '@core/racing/domain/entities/CarLicense'; import type { ICarRepository } from '@core/racing/domain/repositories/ICarRepository'; import type { Logger } from '@core/shared/application'; @@ -14,19 +16,11 @@ export class InMemoryCarRepository implements ICarRepository { private cars: Map; private readonly logger: Logger; - constructor(logger: Logger, seedData?: Car[]) { + constructor(logger: Logger) { this.logger = logger; this.cars = new Map(); - - this.logger.info('InMemoryCarRepository initialized'); - if (seedData) { - this.logger.debug(`Seeding ${seedData.length} cars.`); - seedData.forEach(car => { - this.cars.set(car.id, car); - this.logger.debug(`Car ${car.id} seeded.`); - }); - } + this.logger.info('InMemoryCarRepository initialized'); } async findById(id: string): Promise { @@ -61,8 +55,8 @@ export class InMemoryCarRepository implements ICarRepository { this.logger.debug(`Attempting to find cars by game ID: ${gameId}.`); try { const cars = Array.from(this.cars.values()) - .filter(car => car.gameId === gameId) - .sort((a, b) => a.name.localeCompare(b.name)); + .filter(car => car.gameId.toString() === gameId) + .sort((a, b) => a.name.toString().localeCompare(b.name.toString())); this.logger.info(`Successfully found ${cars.length} cars for game ID: ${gameId}.`); return cars; } catch (error) { @@ -75,8 +69,8 @@ export class InMemoryCarRepository implements ICarRepository { this.logger.debug(`Attempting to find cars by class: ${carClass}.`); try { const cars = Array.from(this.cars.values()) - .filter(car => car.carClass === carClass) - .sort((a, b) => a.name.localeCompare(b.name)); + .filter(car => car.carClass.equals(carClass)) + .sort((a, b) => a.name.toString().localeCompare(b.name.toString())); this.logger.info(`Successfully found ${cars.length} cars for class: ${carClass}.`); return cars; } catch (error) { @@ -89,23 +83,23 @@ export class InMemoryCarRepository implements ICarRepository { this.logger.debug(`Attempting to find cars by license: ${license}.`); try { const cars = Array.from(this.cars.values()) - .filter(car => car.license === license) - .sort((a, b) => a.name.localeCompare(b.name)); + .filter(car => car.license.equals(license)) + .sort((a, b) => a.name.toString().localeCompare(b.name.toString())); this.logger.info(`Successfully found ${cars.length} cars for license: ${license}.`); return cars; } catch (error) { this.logger.error(`Error finding cars by license ${license}:`, error instanceof Error ? error : new Error(String(error))); throw error; } - } + } async findByManufacturer(manufacturer: string): Promise { this.logger.debug(`Attempting to find cars by manufacturer: ${manufacturer}.`); try { const lowerManufacturer = manufacturer.toLowerCase(); const cars = Array.from(this.cars.values()) - .filter(car => car.manufacturer.toLowerCase() === lowerManufacturer) - .sort((a, b) => a.name.localeCompare(b.name)); + .filter(car => car.manufacturer.toString().toLowerCase() === lowerManufacturer) + .sort((a, b) => a.name.toString().localeCompare(b.name.toString())); this.logger.info(`Successfully found ${cars.length} cars for manufacturer: ${manufacturer}.`); return cars; } catch (error) { @@ -120,11 +114,11 @@ export class InMemoryCarRepository implements ICarRepository { const lowerQuery = query.toLowerCase(); const cars = Array.from(this.cars.values()) .filter(car => - car.name.toLowerCase().includes(lowerQuery) || + car.name.toString().toLowerCase().includes(lowerQuery) || car.shortName.toLowerCase().includes(lowerQuery) || - car.manufacturer.toLowerCase().includes(lowerQuery) + car.manufacturer.toString().toLowerCase().includes(lowerQuery) ) - .sort((a, b) => a.name.localeCompare(b.name)); + .sort((a, b) => a.name.toString().localeCompare(b.name.toString())); this.logger.info(`Successfully found ${cars.length} cars matching search query: ${query}.`); return cars; } catch (error) { @@ -136,12 +130,12 @@ export class InMemoryCarRepository implements ICarRepository { async create(car: Car): Promise { this.logger.debug(`Attempting to create car: ${car.id}.`); try { - if (await this.exists(car.id)) { + if (await this.exists(car.id.toString())) { this.logger.warn(`Car with ID ${car.id} already exists; creation aborted.`); throw new Error(`Car with ID ${car.id} already exists`); } - this.cars.set(car.id, car); + this.cars.set(car.id.toString(), car); this.logger.info(`Car ${car.id} created successfully.`); return car; } catch (error) { @@ -153,12 +147,12 @@ export class InMemoryCarRepository implements ICarRepository { async update(car: Car): Promise { this.logger.debug(`Attempting to update car with ID: ${car.id}.`); try { - if (!await this.exists(car.id)) { + if (!await this.exists(car.id.toString())) { this.logger.warn(`Car with ID ${car.id} not found for update; update aborted.`); throw new Error(`Car with ID ${car.id} not found`); } - this.cars.set(car.id, car); + this.cars.set(car.id.toString(), car); this.logger.info(`Car ${car.id} updated successfully.`); return car; } catch (error) { diff --git a/adapters/racing/persistence/inmemory/InMemoryDriverRepository.test.ts b/adapters/racing/persistence/inmemory/InMemoryDriverRepository.test.ts new file mode 100644 index 000000000..066647f0c --- /dev/null +++ b/adapters/racing/persistence/inmemory/InMemoryDriverRepository.test.ts @@ -0,0 +1,191 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import { InMemoryDriverRepository } from './InMemoryDriverRepository'; +import { Driver } from '@core/racing/domain/entities/Driver'; +import type { Logger } from '@core/shared/application'; + +describe('InMemoryDriverRepository', () => { + let repository: InMemoryDriverRepository; + let mockLogger: Logger; + + beforeEach(() => { + mockLogger = { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; + repository = new InMemoryDriverRepository(mockLogger); + }); + + const createTestDriver = (id: string, iracingId: string, name: string, country: string) => { + return Driver.create({ + id, + iracingId, + name, + country, + }); + }; + + describe('constructor', () => { + it('should initialize with a logger', () => { + expect(repository).toBeDefined(); + expect(mockLogger.info).toHaveBeenCalledWith('InMemoryDriverRepository initialized.'); + }); + }); + + describe('findById', () => { + it('should return null if driver not found', async () => { + const result = await repository.findById('nonexistent'); + expect(result).toBeNull(); + expect(mockLogger.debug).toHaveBeenCalledWith('[InMemoryDriverRepository] Finding driver by ID: nonexistent'); + expect(mockLogger.warn).toHaveBeenCalledWith('Driver with ID nonexistent not found.'); + }); + + it('should return the driver if found', async () => { + const driver = createTestDriver('1', '12345', 'Test Driver', 'US'); + await repository.create(driver); + + const result = await repository.findById('1'); + expect(result).toEqual(driver); + expect(mockLogger.info).toHaveBeenCalledWith('Found driver by ID: 1.'); + }); + }); + + describe('findByIRacingId', () => { + it('should return null if driver not found', async () => { + const result = await repository.findByIRacingId('nonexistent'); + expect(result).toBeNull(); + expect(mockLogger.debug).toHaveBeenCalledWith('[InMemoryDriverRepository] Finding driver by iRacing ID: nonexistent'); + expect(mockLogger.warn).toHaveBeenCalledWith('Driver with iRacing ID nonexistent not found.'); + }); + + it('should return the driver if found', async () => { + const driver = createTestDriver('1', '12345', 'Test Driver', 'US'); + await repository.create(driver); + + const result = await repository.findByIRacingId('12345'); + expect(result).toEqual(driver); + }); + }); + + describe('findAll', () => { + it('should return all drivers', async () => { + const driver1 = createTestDriver('1', '12345', 'Driver 1', 'US'); + const driver2 = createTestDriver('2', '67890', 'Driver 2', 'UK'); + await repository.create(driver1); + await repository.create(driver2); + + const result = await repository.findAll(); + expect(result).toHaveLength(2); + expect(result).toContain(driver1); + expect(result).toContain(driver2); + }); + }); + + describe('create', () => { + it('should create a new driver', async () => { + const driver = createTestDriver('1', '12345', 'Test Driver', 'US'); + const result = await repository.create(driver); + expect(result).toEqual(driver); + expect(mockLogger.info).toHaveBeenCalledWith('Driver 1 created successfully.'); + }); + + it('should throw error if driver already exists', async () => { + const driver = createTestDriver('1', '12345', 'Test Driver', 'US'); + await repository.create(driver); + await expect(repository.create(driver)).rejects.toThrow('Driver already exists'); + }); + + it('should throw error if iRacing ID already registered', async () => { + const driver1 = createTestDriver('1', '12345', 'Driver 1', 'US'); + const driver2 = createTestDriver('2', '12345', 'Driver 2', 'UK'); + await repository.create(driver1); + await expect(repository.create(driver2)).rejects.toThrow('iRacing ID already registered'); + }); + }); + + describe('update', () => { + it('should update an existing driver', async () => { + const driver = createTestDriver('1', '12345', 'Test Driver', 'US'); + await repository.create(driver); + + const updatedDriver = driver.update({ name: 'Updated Driver' }); + const result = await repository.update(updatedDriver); + expect(result).toEqual(updatedDriver); + expect(mockLogger.info).toHaveBeenCalledWith('Driver 1 updated successfully.'); + }); + + it('should throw error if driver does not exist', async () => { + const driver = createTestDriver('1', '12345', 'Test Driver', 'US'); + await expect(repository.update(driver)).rejects.toThrow('Driver not found'); + }); + + it('should re-index iRacing ID if changed', async () => { + const driver = createTestDriver('1', '12345', 'Test Driver', 'US'); + await repository.create(driver); + + const updatedDriver = Driver.create({ + id: '1', + iracingId: '67890', + name: 'Test Driver', + country: 'US', + }); + await repository.update(updatedDriver); + + const foundByOldId = await repository.findByIRacingId('12345'); + expect(foundByOldId).toBeNull(); + + const foundByNewId = await repository.findByIRacingId('67890'); + expect(foundByNewId).toEqual(updatedDriver); + }); + }); + + describe('delete', () => { + it('should delete an existing driver', async () => { + const driver = createTestDriver('1', '12345', 'Test Driver', 'US'); + await repository.create(driver); + + await repository.delete('1'); + expect(mockLogger.info).toHaveBeenCalledWith('Driver 1 deleted successfully.'); + const found = await repository.findById('1'); + expect(found).toBeNull(); + const foundByIRacingId = await repository.findByIRacingId('12345'); + expect(foundByIRacingId).toBeNull(); + }); + + it('should not throw error if driver does not exist', async () => { + await repository.delete('nonexistent'); + expect(mockLogger.warn).toHaveBeenCalledWith('Driver with ID nonexistent not found for deletion.'); + }); + }); + + describe('exists', () => { + it('should return true if driver exists', async () => { + const driver = createTestDriver('1', '12345', 'Test Driver', 'US'); + await repository.create(driver); + + const result = await repository.exists('1'); + expect(result).toBe(true); + }); + + it('should return false if driver does not exist', async () => { + const result = await repository.exists('nonexistent'); + expect(result).toBe(false); + }); + }); + + describe('existsByIRacingId', () => { + it('should return true if iRacing ID exists', async () => { + const driver = createTestDriver('1', '12345', 'Test Driver', 'US'); + await repository.create(driver); + + const result = await repository.existsByIRacingId('12345'); + expect(result).toBe(true); + }); + + it('should return false if iRacing ID does not exist', async () => { + const result = await repository.existsByIRacingId('nonexistent'); + expect(result).toBe(false); + }); + }); +}); \ No newline at end of file diff --git a/adapters/racing/persistence/inmemory/InMemoryDriverRepository.ts b/adapters/racing/persistence/inmemory/InMemoryDriverRepository.ts index 0cf1754ea..5fec1fe1a 100644 --- a/adapters/racing/persistence/inmemory/InMemoryDriverRepository.ts +++ b/adapters/racing/persistence/inmemory/InMemoryDriverRepository.ts @@ -6,13 +6,8 @@ export class InMemoryDriverRepository implements IDriverRepository { private drivers: Map = new Map(); private iracingIdIndex: Map = new Map(); // iracingId -> driverId - constructor(private readonly logger: Logger, initialDrivers: Driver[] = []) { + constructor(private readonly logger: Logger) { this.logger.info('InMemoryDriverRepository initialized.'); - for (const driver of initialDrivers) { - this.drivers.set(driver.id, driver); - this.iracingIdIndex.set(driver.iracingId, driver.id); - this.logger.debug(`Seeded driver: ${driver.id} (iRacing ID: ${driver.iracingId}).`); - } } async findById(id: string): Promise { @@ -42,33 +37,33 @@ export class InMemoryDriverRepository implements IDriverRepository { } async create(driver: Driver): Promise { - this.logger.debug(`[InMemoryDriverRepository] Creating driver: ${driver.id} (iRacing ID: ${driver.iracingId})`); + this.logger.debug(`[InMemoryDriverRepository] Creating driver: ${driver.id} (iRacing ID: ${driver.iracingId.toString()})`); if (this.drivers.has(driver.id)) { this.logger.warn(`Driver with ID ${driver.id} already exists.`); throw new Error('Driver already exists'); } - if (this.iracingIdIndex.has(driver.iracingId)) { - this.logger.warn(`Driver with iRacing ID ${driver.iracingId} already exists.`); + if (this.iracingIdIndex.has(driver.iracingId.toString())) { + this.logger.warn(`Driver with iRacing ID ${driver.iracingId.toString()} already exists.`); throw new Error('iRacing ID already registered'); } this.drivers.set(driver.id, driver); - this.iracingIdIndex.set(driver.iracingId, driver.id); + this.iracingIdIndex.set(driver.iracingId.toString(), driver.id); this.logger.info(`Driver ${driver.id} created successfully.`); return Promise.resolve(driver); } async update(driver: Driver): Promise { - this.logger.debug(`[InMemoryDriverRepository] Updating driver: ${driver.id} (iRacing ID: ${driver.iracingId})`); + this.logger.debug(`[InMemoryDriverRepository] Updating driver: ${driver.id} (iRacing ID: ${driver.iracingId.toString()})`); if (!this.drivers.has(driver.id)) { this.logger.warn(`Driver with ID ${driver.id} not found for update.`); throw new Error('Driver not found'); } + const existingDriver = this.drivers.get(driver.id); this.drivers.set(driver.id, driver); // Re-index iRacing ID if it changed - const existingDriver = this.drivers.get(driver.id); - if (existingDriver && existingDriver.iracingId !== driver.iracingId) { - this.iracingIdIndex.delete(existingDriver.iracingId); - this.iracingIdIndex.set(driver.iracingId, driver.id); + if (existingDriver && existingDriver.iracingId.toString() !== driver.iracingId.toString()) { + this.iracingIdIndex.delete(existingDriver.iracingId.toString()); + this.iracingIdIndex.set(driver.iracingId.toString(), driver.id); } this.logger.info(`Driver ${driver.id} updated successfully.`); return Promise.resolve(driver); @@ -79,7 +74,7 @@ export class InMemoryDriverRepository implements IDriverRepository { const driver = this.drivers.get(id); if (driver) { this.drivers.delete(id); - this.iracingIdIndex.delete(driver.iracingId); + this.iracingIdIndex.delete(driver.iracingId.toString()); this.logger.info(`Driver ${id} deleted successfully.`); } else { this.logger.warn(`Driver with ID ${id} not found for deletion.`); diff --git a/adapters/racing/persistence/inmemory/InMemoryGameRepository.test.ts b/adapters/racing/persistence/inmemory/InMemoryGameRepository.test.ts new file mode 100644 index 000000000..0655b0581 --- /dev/null +++ b/adapters/racing/persistence/inmemory/InMemoryGameRepository.test.ts @@ -0,0 +1,42 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import { InMemoryGameRepository } from './InMemoryGameRepository'; +import type { Logger } from '@core/shared/application'; + +describe('InMemoryGameRepository', () => { + let repository: InMemoryGameRepository; + let mockLogger: Logger; + + beforeEach(() => { + mockLogger = { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; + repository = new InMemoryGameRepository(mockLogger); + }); + + describe('constructor', () => { + it('should initialize with a logger', () => { + expect(repository).toBeDefined(); + expect(mockLogger.info).toHaveBeenCalledWith('InMemoryGameRepository initialized.'); + }); + }); + + describe('findById', () => { + it('should return null if game not found', async () => { + const result = await repository.findById('nonexistent'); + expect(result).toBeNull(); + expect(mockLogger.debug).toHaveBeenCalledWith('[InMemoryGameRepository] Finding game by ID: nonexistent'); + expect(mockLogger.warn).toHaveBeenCalledWith('Game with ID nonexistent not found.'); + }); + }); + + describe('findAll', () => { + it('should return an empty array', async () => { + const result = await repository.findAll(); + expect(result).toEqual([]); + expect(mockLogger.debug).toHaveBeenCalledWith('[InMemoryGameRepository] Finding all games.'); + }); + }); +}); \ No newline at end of file diff --git a/adapters/racing/persistence/inmemory/InMemoryGameRepository.ts b/adapters/racing/persistence/inmemory/InMemoryGameRepository.ts index 0062b6bfe..b5a012c37 100644 --- a/adapters/racing/persistence/inmemory/InMemoryGameRepository.ts +++ b/adapters/racing/persistence/inmemory/InMemoryGameRepository.ts @@ -5,12 +5,8 @@ import { Logger } from '@core/shared/application'; export class InMemoryGameRepository implements IGameRepository { private games: Map = new Map(); - constructor(private readonly logger: Logger, initialGames: Game[] = []) { + constructor(private readonly logger: Logger) { this.logger.info('InMemoryGameRepository initialized.'); - for (const game of initialGames) { - this.games.set(game.id, game); - this.logger.debug(`Seeded game: ${game.id} (${game.name}).`); - } } async findById(id: string): Promise { diff --git a/adapters/racing/persistence/inmemory/InMemoryLeagueMembershipRepository.ts b/adapters/racing/persistence/inmemory/InMemoryLeagueMembershipRepository.ts index 54dbbd464..6bdbac7ee 100644 --- a/adapters/racing/persistence/inmemory/InMemoryLeagueMembershipRepository.ts +++ b/adapters/racing/persistence/inmemory/InMemoryLeagueMembershipRepository.ts @@ -6,16 +6,8 @@ export class InMemoryLeagueMembershipRepository implements ILeagueMembershipRepo private memberships: Map = new Map(); // Key: `${leagueId}:${driverId}` private joinRequests: Map = new Map(); // Key: requestId - constructor(private readonly logger: Logger, initialMemberships: LeagueMembership[] = [], initialJoinRequests: JoinRequest[] = []) { + constructor(private readonly logger: Logger) { this.logger.info('InMemoryLeagueMembershipRepository initialized.'); - for (const membership of initialMemberships) { - this.memberships.set(`${membership.leagueId}:${membership.driverId}`, membership); - this.logger.debug(`Seeded membership: ${membership.id}.`); - } - for (const req of initialJoinRequests) { - this.joinRequests.set(req.id, req); - this.logger.debug(`Seeded join request: ${req.id}.`); - } } async getMembership(leagueId: string, driverId: string): Promise { diff --git a/adapters/racing/persistence/inmemory/InMemoryLeagueRepository.test.ts b/adapters/racing/persistence/inmemory/InMemoryLeagueRepository.test.ts new file mode 100644 index 000000000..f7be572ca --- /dev/null +++ b/adapters/racing/persistence/inmemory/InMemoryLeagueRepository.test.ts @@ -0,0 +1,159 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import { InMemoryLeagueRepository } from './InMemoryLeagueRepository'; +import { League } from '@core/racing/domain/entities/League'; +import type { Logger } from '@core/shared/application'; + +describe('InMemoryLeagueRepository', () => { + let repository: InMemoryLeagueRepository; + let mockLogger: Logger; + + beforeEach(() => { + mockLogger = { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; + repository = new InMemoryLeagueRepository(mockLogger); + }); + + const createTestLeague = (id: string, name: string, ownerId: string) => { + return League.create({ + id, + name, + description: 'Test description', + ownerId, + }); + }; + + describe('constructor', () => { + it('should initialize with a logger', () => { + expect(repository).toBeDefined(); + expect(mockLogger.info).toHaveBeenCalledWith('InMemoryLeagueRepository initialized'); + }); + }); + + describe('findById', () => { + it('should return null if league not found', async () => { + const result = await repository.findById('nonexistent'); + expect(result).toBeNull(); + expect(mockLogger.debug).toHaveBeenCalledWith('Attempting to find league with ID: nonexistent.'); + expect(mockLogger.warn).toHaveBeenCalledWith('League with ID: nonexistent not found.'); + }); + + it('should return the league if found', async () => { + const league = createTestLeague('1', 'Test League', 'owner1'); + await repository.create(league); + + const result = await repository.findById('1'); + expect(result).toEqual(league); + expect(mockLogger.info).toHaveBeenCalledWith('Successfully found league with ID: 1.'); + }); + }); + + describe('findByOwnerId', () => { + it('should return leagues filtered by owner ID', async () => { + const league1 = createTestLeague('1', 'League 1', 'owner1'); + const league2 = createTestLeague('2', 'League 2', 'owner2'); + const league3 = createTestLeague('3', 'League 3', 'owner1'); + await repository.create(league1); + await repository.create(league2); + await repository.create(league3); + + const result = await repository.findByOwnerId('owner1'); + expect(result).toHaveLength(2); + expect(result).toContain(league1); + expect(result).toContain(league3); + }); + }); + + describe('searchByName', () => { + it('should return leagues matching the query in name (case insensitive)', async () => { + const league1 = createTestLeague('1', 'Ferrari League', 'owner1'); + const league2 = createTestLeague('2', 'BMW League', 'owner2'); + await repository.create(league1); + await repository.create(league2); + + const result = await repository.searchByName('ferrari'); + expect(result).toHaveLength(1); + expect(result[0]).toEqual(league1); + }); + }); + + describe('findAll', () => { + it('should return all leagues', async () => { + const league1 = createTestLeague('1', 'League 1', 'owner1'); + const league2 = createTestLeague('2', 'League 2', 'owner2'); + await repository.create(league1); + await repository.create(league2); + + const result = await repository.findAll(); + expect(result).toHaveLength(2); + expect(result).toContain(league1); + expect(result).toContain(league2); + }); + }); + + describe('create', () => { + it('should create a new league', async () => { + const league = createTestLeague('1', 'Test League', 'owner1'); + const result = await repository.create(league); + expect(result).toEqual(league); + expect(mockLogger.info).toHaveBeenCalledWith('League 1 created successfully.'); + }); + + it('should throw error if league already exists', async () => { + const league = createTestLeague('1', 'Test League', 'owner1'); + await repository.create(league); + await expect(repository.create(league)).rejects.toThrow('League with ID 1 already exists'); + }); + }); + + describe('update', () => { + it('should update an existing league', async () => { + const league = createTestLeague('1', 'Test League', 'owner1'); + await repository.create(league); + + const updatedLeague = league.update({ name: 'Updated League' }); + const result = await repository.update(updatedLeague); + expect(result).toEqual(updatedLeague); + expect(mockLogger.info).toHaveBeenCalledWith('League 1 updated successfully.'); + }); + + it('should throw error if league does not exist', async () => { + const league = createTestLeague('1', 'Test League', 'owner1'); + await expect(repository.update(league)).rejects.toThrow('League with ID 1 not found'); + }); + }); + + describe('delete', () => { + it('should delete an existing league', async () => { + const league = createTestLeague('1', 'Test League', 'owner1'); + await repository.create(league); + + await repository.delete('1'); + expect(mockLogger.info).toHaveBeenCalledWith('League 1 deleted successfully.'); + const found = await repository.findById('1'); + expect(found).toBeNull(); + }); + + it('should throw error if league does not exist', async () => { + await expect(repository.delete('nonexistent')).rejects.toThrow('League with ID nonexistent not found'); + }); + }); + + describe('exists', () => { + it('should return true if league exists', async () => { + const league = createTestLeague('1', 'Test League', 'owner1'); + await repository.create(league); + + const result = await repository.exists('1'); + expect(result).toBe(true); + }); + + it('should return false if league does not exist', async () => { + const result = await repository.exists('nonexistent'); + expect(result).toBe(false); + }); + }); +}); \ No newline at end of file diff --git a/adapters/racing/persistence/inmemory/InMemoryLeagueRepository.ts b/adapters/racing/persistence/inmemory/InMemoryLeagueRepository.ts index d98fe192f..768dcecc3 100644 --- a/adapters/racing/persistence/inmemory/InMemoryLeagueRepository.ts +++ b/adapters/racing/persistence/inmemory/InMemoryLeagueRepository.ts @@ -4,81 +4,128 @@ import { Logger } from '@core/shared/application'; export class InMemoryLeagueRepository implements ILeagueRepository { private leagues: Map = new Map(); + private readonly logger: Logger; - constructor(private readonly logger: Logger, initialLeagues: League[] = []) { - this.logger.info('InMemoryLeagueRepository initialized.'); - for (const league of initialLeagues) { - this.leagues.set(league.id, league); - this.logger.debug(`Seeded league: ${league.id} (${league.name}).`); - } + constructor(logger: Logger) { + this.logger = logger; + this.leagues = new Map(); + + this.logger.info('InMemoryLeagueRepository initialized'); } async findById(id: string): Promise { - this.logger.debug(`[InMemoryLeagueRepository] Finding league by ID: ${id}`); - const league = this.leagues.get(id) ?? null; - if (league) { - this.logger.info(`Found league by ID: ${id}.`); - } else { - this.logger.warn(`League with ID ${id} not found.`); + this.logger.debug(`Attempting to find league with ID: ${id}.`); + try { + const league = this.leagues.get(id) ?? null; + if (league) { + this.logger.info(`Successfully found league with ID: ${id}.`); + } else { + this.logger.warn(`League with ID: ${id} not found.`); + } + return league; + } catch (error) { + this.logger.error(`Error finding league by ID ${id}:`, error instanceof Error ? error : new Error(String(error))); + throw error; } - return Promise.resolve(league); } async findByOwnerId(ownerId: string): Promise { - this.logger.debug(`[InMemoryLeagueRepository] Finding leagues by owner ID: ${ownerId}`); - const ownedLeagues = Array.from(this.leagues.values()).filter(league => league.ownerId === ownerId); - this.logger.info(`Found ${ownedLeagues.length} leagues for owner ID: ${ownerId}.`); - return Promise.resolve(ownedLeagues); + this.logger.debug(`Attempting to find leagues by owner ID: ${ownerId}.`); + try { + const ownedLeagues = Array.from(this.leagues.values()).filter(league => league.ownerId.toString() === ownerId); + this.logger.info(`Successfully found ${ownedLeagues.length} leagues for owner ID: ${ownerId}.`); + return ownedLeagues; + } catch (error) { + this.logger.error(`Error finding leagues by owner ID ${ownerId}:`, error instanceof Error ? error : new Error(String(error))); + throw error; + } } async searchByName(name: string): Promise { - this.logger.debug(`[InMemoryLeagueRepository] Searching leagues by name: ${name}`); - const matchingLeagues = Array.from(this.leagues.values()).filter(league => - league.name.toLowerCase().includes(name.toLowerCase()), - ); - this.logger.info(`Found ${matchingLeagues.length} matching leagues for name search: ${name}.`); - return Promise.resolve(matchingLeagues); + this.logger.debug(`Attempting to search leagues by name query: ${name}.`); + try { + const matchingLeagues = Array.from(this.leagues.values()).filter(league => + league.name.toString().toLowerCase().includes(name.toLowerCase()), + ); + this.logger.info(`Successfully found ${matchingLeagues.length} leagues matching search query: ${name}.`); + return matchingLeagues; + } catch (error) { + this.logger.error(`Error searching leagues by name query ${name}:`, error instanceof Error ? error : new Error(String(error))); + throw error; + } } async findAll(): Promise { - this.logger.debug('[InMemoryLeagueRepository] Finding all leagues.'); - return Promise.resolve(Array.from(this.leagues.values())); + this.logger.debug('Attempting to find all leagues.'); + try { + const leagues = Array.from(this.leagues.values()); + this.logger.info(`Successfully found ${leagues.length} leagues.`); + return leagues; + } catch (error) { + this.logger.error('Error finding all leagues:', error instanceof Error ? error : new Error(String(error))); + throw error; + } } async create(league: League): Promise { - this.logger.debug(`[InMemoryLeagueRepository] Creating league: ${league.id} (${league.name})`); - if (this.leagues.has(league.id)) { - this.logger.warn(`League with ID ${league.id} already exists.`); - throw new Error('League already exists'); + this.logger.debug(`Attempting to create league: ${league.id}.`); + try { + if (await this.exists(league.id.toString())) { + this.logger.warn(`League with ID ${league.id} already exists; creation aborted.`); + throw new Error(`League with ID ${league.id} already exists`); + } + + this.leagues.set(league.id.toString(), league); + this.logger.info(`League ${league.id} created successfully.`); + return league; + } catch (error) { + this.logger.error(`Error creating league ${league.id}:`, error instanceof Error ? error : new Error(String(error))); + throw error; } - this.leagues.set(league.id, league); - this.logger.info(`League ${league.id} created successfully.`); - return Promise.resolve(league); } async update(league: League): Promise { - this.logger.debug(`[InMemoryLeagueRepository] Updating league: ${league.id} (${league.name})`); - if (!this.leagues.has(league.id)) { - this.logger.warn(`League with ID ${league.id} not found for update.`); - throw new Error('League not found'); + this.logger.debug(`Attempting to update league with ID: ${league.id}.`); + try { + if (!(await this.exists(league.id.toString()))) { + this.logger.warn(`League with ID ${league.id} not found for update; update aborted.`); + throw new Error(`League with ID ${league.id} not found`); + } + + this.leagues.set(league.id.toString(), league); + this.logger.info(`League ${league.id} updated successfully.`); + return league; + } catch (error) { + this.logger.error(`Error updating league ${league.id}:`, error instanceof Error ? error : new Error(String(error))); + throw error; } - this.leagues.set(league.id, league); - this.logger.info(`League ${league.id} updated successfully.`); - return Promise.resolve(league); } async delete(id: string): Promise { - this.logger.debug(`[InMemoryLeagueRepository] Deleting league with ID: ${id}`); - if (this.leagues.delete(id)) { + this.logger.debug(`Attempting to delete league with ID: ${id}.`); + try { + if (!(await this.exists(id))) { + this.logger.warn(`League with ID ${id} not found for deletion; deletion aborted.`); + throw new Error(`League with ID ${id} not found`); + } + + this.leagues.delete(id); this.logger.info(`League ${id} deleted successfully.`); - } else { - this.logger.warn(`League with ID ${id} not found for deletion.`); + } catch (error) { + this.logger.error(`Error deleting league ${id}:`, error instanceof Error ? error : new Error(String(error))); + throw error; } - return Promise.resolve(); } async exists(id: string): Promise { - this.logger.debug(`[InMemoryLeagueRepository] Checking existence of league with ID: ${id}`); - return Promise.resolve(this.leagues.has(id)); + this.logger.debug(`Checking existence of league with ID: ${id}.`); + try { + const exists = this.leagues.has(id); + this.logger.info(`League with ID ${id} existence check: ${exists}.`); + return exists; + } catch (error) { + this.logger.error(`Error checking existence of league with ID ${id}:`, error instanceof Error ? error : new Error(String(error))); + throw error; + } } } diff --git a/adapters/racing/persistence/inmemory/InMemoryLeagueScoringConfigRepository.test.ts b/adapters/racing/persistence/inmemory/InMemoryLeagueScoringConfigRepository.test.ts new file mode 100644 index 000000000..e4af3fadf --- /dev/null +++ b/adapters/racing/persistence/inmemory/InMemoryLeagueScoringConfigRepository.test.ts @@ -0,0 +1,99 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import { InMemoryLeagueScoringConfigRepository } from './InMemoryLeagueScoringConfigRepository'; +import { LeagueScoringConfig, type LeagueScoringConfigProps } from '@core/racing/domain/entities/LeagueScoringConfig'; +import type { ChampionshipConfig } from '@core/racing/domain/types/ChampionshipConfig'; +import { PointsTable } from '@core/racing/domain/value-objects/PointsTable'; +import type { Logger } from '@core/shared/application'; + +const mockPointsTable = new PointsTable({ 1: 25, 2: 18, 3: 15 }); + +const mockChampionshipConfig: ChampionshipConfig = { + id: 'champ1', + name: 'Championship 1', + type: 'driver', + sessionTypes: ['main'], + pointsTableBySessionType: { + practice: mockPointsTable, + qualifying: mockPointsTable, + q1: mockPointsTable, + q2: mockPointsTable, + q3: mockPointsTable, + sprint: mockPointsTable, + main: mockPointsTable, + timeTrial: mockPointsTable, + }, + dropScorePolicy: { strategy: 'none' }, +}; + +describe('InMemoryLeagueScoringConfigRepository', () => { + let repository: InMemoryLeagueScoringConfigRepository; + let mockLogger: Logger; + + beforeEach(() => { + mockLogger = { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; + repository = new InMemoryLeagueScoringConfigRepository(mockLogger); + }); + + const createTestConfig = (seasonId: string, id?: string): LeagueScoringConfig => { + const props: LeagueScoringConfigProps = { + seasonId, + championships: [mockChampionshipConfig], + }; + if (id) { + props.id = id; + } + return LeagueScoringConfig.create(props); + }; + + describe('constructor', () => { + it('should initialize with a logger', () => { + expect(repository).toBeDefined(); + expect(mockLogger.info).toHaveBeenCalledWith('InMemoryLeagueScoringConfigRepository initialized.'); + }); + }); + + describe('findBySeasonId', () => { + it('should return null if config not found', async () => { + const result = await repository.findBySeasonId('nonexistent'); + expect(result).toBeNull(); + expect(mockLogger.debug).toHaveBeenCalledWith('[InMemoryLeagueScoringConfigRepository] Finding config for season: nonexistent.'); + expect(mockLogger.warn).toHaveBeenCalledWith('No config found for season: nonexistent.'); + }); + + it('should return the config if found', async () => { + const config = createTestConfig('season1'); + await repository.save(config); + + const result = await repository.findBySeasonId('season1'); + expect(result).toEqual(config); + expect(mockLogger.info).toHaveBeenCalledWith('Found config for season: season1.'); + }); + }); + + describe('save', () => { + it('should save a new config', async () => { + const config = createTestConfig('season1'); + const result = await repository.save(config); + expect(result).toEqual(config); + expect(mockLogger.debug).toHaveBeenCalledWith('[InMemoryLeagueScoringConfigRepository] Saving config for season: season1.'); + expect(mockLogger.info).toHaveBeenCalledWith('Config for season season1 saved successfully.'); + }); + + it('should update existing config', async () => { + const config1 = createTestConfig('season1'); + await repository.save(config1); + + const config2 = createTestConfig('season1', 'updated-id'); + const result = await repository.save(config2); + expect(result).toEqual(config2); + + const found = await repository.findBySeasonId('season1'); + expect(found).toEqual(config2); + }); + }); +}); \ No newline at end of file diff --git a/adapters/racing/persistence/inmemory/InMemoryLeagueScoringConfigRepository.ts b/adapters/racing/persistence/inmemory/InMemoryLeagueScoringConfigRepository.ts index f7faea8ab..b560a2a76 100644 --- a/adapters/racing/persistence/inmemory/InMemoryLeagueScoringConfigRepository.ts +++ b/adapters/racing/persistence/inmemory/InMemoryLeagueScoringConfigRepository.ts @@ -5,12 +5,8 @@ import { Logger } from '@core/shared/application'; export class InMemoryLeagueScoringConfigRepository implements ILeagueScoringConfigRepository { private configs: Map = new Map(); // Key: seasonId - constructor(private readonly logger: Logger, initialConfigs: LeagueScoringConfig[] = []) { + constructor(private readonly logger: Logger) { this.logger.info('InMemoryLeagueScoringConfigRepository initialized.'); - for (const config of initialConfigs) { - this.configs.set(config.seasonId, config); - this.logger.debug(`Seeded league scoring config for season: ${config.seasonId}.`); - } } async findBySeasonId(seasonId: string): Promise { @@ -25,9 +21,9 @@ export class InMemoryLeagueScoringConfigRepository implements ILeagueScoringConf } async save(config: LeagueScoringConfig): Promise { - this.logger.debug(`[InMemoryLeagueScoringConfigRepository] Saving config for season: ${config.seasonId}.`); - this.configs.set(config.seasonId, config); - this.logger.info(`Config for season ${config.seasonId} saved successfully.`); + this.logger.debug(`[InMemoryLeagueScoringConfigRepository] Saving config for season: ${config.seasonId.toString()}.`); + this.configs.set(config.seasonId.toString(), config); + this.logger.info(`Config for season ${config.seasonId.toString()} saved successfully.`); return Promise.resolve(config); } } diff --git a/adapters/racing/persistence/inmemory/InMemoryLeagueScoringPresetProvider.test.ts b/adapters/racing/persistence/inmemory/InMemoryLeagueScoringPresetProvider.test.ts new file mode 100644 index 000000000..953c140c2 --- /dev/null +++ b/adapters/racing/persistence/inmemory/InMemoryLeagueScoringPresetProvider.test.ts @@ -0,0 +1,71 @@ +import { describe, it, expect } from 'vitest'; +import { InMemoryLeagueScoringPresetProvider } from './InMemoryLeagueScoringPresetProvider'; + +describe('InMemoryLeagueScoringPresetProvider', () => { + const provider = new InMemoryLeagueScoringPresetProvider(); + + describe('listPresets', () => { + it('should return all available presets', () => { + const presets = provider.listPresets(); + + expect(presets).toHaveLength(3); + expect(presets).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: 'sprint-main-driver', + name: 'Sprint + Main', + primaryChampionshipType: 'driver', + }), + expect.objectContaining({ + id: 'club-default', + name: 'Club ladder', + primaryChampionshipType: 'driver', + }), + expect.objectContaining({ + id: 'endurance-main-double', + name: 'Endurance weekend', + primaryChampionshipType: 'driver', + }), + ]), + ); + }); + }); + + describe('getPresetById', () => { + it('should return the preset for a valid id', () => { + const preset = provider.getPresetById('sprint-main-driver'); + + expect(preset).toEqual({ + id: 'sprint-main-driver', + name: 'Sprint + Main', + description: 'Short sprint race plus main race; sprint gives fewer points.', + primaryChampionshipType: 'driver', + sessionSummary: 'Sprint + Main', + bonusSummary: 'Fastest lap +1 point in main race if finishing P10 or better.', + dropPolicySummary: 'Best 6 results of 8 count towards the championship.', + }); + }); + + it('should return undefined for an invalid id', () => { + const preset = provider.getPresetById('invalid-id'); + + expect(preset).toBeUndefined(); + }); + }); + + describe('createScoringConfigFromPreset', () => { + it('should create a scoring config from a valid preset', () => { + const config = provider.createScoringConfigFromPreset('sprint-main-driver', 'season-123'); + + expect(config).toBeDefined(); + expect(config.championships).toHaveLength(1); + expect(config.championships[0]?.name).toBe('Driver Championship'); + }); + + it('should throw an error for an invalid preset id', () => { + expect(() => { + provider.createScoringConfigFromPreset('invalid-id', 'season-123'); + }).toThrow('Scoring preset with id invalid-id not found'); + }); + }); +}); \ No newline at end of file diff --git a/adapters/racing/persistence/inmemory/InMemoryLeagueScoringPresetProvider.ts b/adapters/racing/persistence/inmemory/InMemoryLeagueScoringPresetProvider.ts index b8eefd4fe..a716f195d 100644 --- a/adapters/racing/persistence/inmemory/InMemoryLeagueScoringPresetProvider.ts +++ b/adapters/racing/persistence/inmemory/InMemoryLeagueScoringPresetProvider.ts @@ -1,7 +1,7 @@ import { getLeagueScoringPresetById, listLeagueScoringPresets, -} from './InMemoryScoringRepositories'; +} from '../../../bootstrap/LeagueScoringPresets'; import type { LeagueScoringPresetDTO, LeagueScoringPresetProvider, @@ -42,4 +42,12 @@ export class InMemoryLeagueScoringPresetProvider dropPolicySummary: preset.dropPolicySummary, }; } + + createScoringConfigFromPreset(presetId: string, seasonId: string) { + const preset = getLeagueScoringPresetById(presetId); + if (!preset) { + throw new Error(`Scoring preset with id ${presetId} not found`); + } + return preset.createConfig({ seasonId }); + } } \ No newline at end of file diff --git a/adapters/racing/persistence/inmemory/InMemoryLeagueStandingsRepository.test.ts b/adapters/racing/persistence/inmemory/InMemoryLeagueStandingsRepository.test.ts new file mode 100644 index 000000000..b2f986188 --- /dev/null +++ b/adapters/racing/persistence/inmemory/InMemoryLeagueStandingsRepository.test.ts @@ -0,0 +1,80 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import { InMemoryLeagueStandingsRepository } from './InMemoryLeagueStandingsRepository'; +import type { Logger } from '@core/shared/application'; +import type { RawStanding } from '@core/league/application/ports/ILeagueStandingsRepository'; + +describe('InMemoryLeagueStandingsRepository', () => { + let repository: InMemoryLeagueStandingsRepository; + let mockLogger: Logger; + + beforeEach(() => { + mockLogger = { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; + repository = new InMemoryLeagueStandingsRepository(mockLogger); + }); + + const createTestStanding = (id: string, leagueId: string, driverId: string, position: number, points: number): RawStanding => ({ + id, + leagueId, + driverId, + position, + points, + wins: 0, + racesCompleted: 0, + }); + + describe('constructor', () => { + it('should initialize with a logger', () => { + expect(repository).toBeDefined(); + expect(mockLogger.info).toHaveBeenCalledWith('InMemoryLeagueStandingsRepository initialized.'); + }); + }); + + describe('getLeagueStandings', () => { + it('should return empty array if league not found', async () => { + const result = await repository.getLeagueStandings('nonexistent'); + expect(result).toEqual([]); + expect(mockLogger.debug).toHaveBeenCalledWith('[InMemoryLeagueStandingsRepository] Getting standings for league: nonexistent.'); + expect(mockLogger.info).toHaveBeenCalledWith('Found 0 standings for league: nonexistent.'); + }); + + it('should return the standings if league exists', async () => { + const standings: RawStanding[] = [ + createTestStanding('1', 'league1', 'driver1', 1, 25), + createTestStanding('2', 'league1', 'driver2', 2, 20), + ]; + (repository as InMemoryLeagueStandingsRepository).setLeagueStandings('league1', standings); + + const result = await repository.getLeagueStandings('league1'); + expect(result).toEqual(standings); + expect(mockLogger.debug).toHaveBeenCalledWith('[InMemoryLeagueStandingsRepository] Getting standings for league: league1.'); + expect(mockLogger.info).toHaveBeenCalledWith('Found 2 standings for league: league1.'); + }); + + it('should return empty array if league exists but has no standings', async () => { + (repository as InMemoryLeagueStandingsRepository).setLeagueStandings('league1', []); + + const result = await repository.getLeagueStandings('league1'); + expect(result).toEqual([]); + expect(mockLogger.info).toHaveBeenCalledWith('Found 0 standings for league: league1.'); + }); + }); + + describe('setLeagueStandings', () => { + it('should set standings for a league', async () => { + const standings: RawStanding[] = [ + createTestStanding('1', 'league1', 'driver1', 1, 25), + ]; + + (repository as InMemoryLeagueStandingsRepository).setLeagueStandings('league1', standings); + + const result = await repository.getLeagueStandings('league1'); + expect(result).toEqual(standings); + expect(mockLogger.debug).toHaveBeenCalledWith('[InMemoryLeagueStandingsRepository] Set standings for league: league1.'); + }); + }); +}); \ No newline at end of file diff --git a/adapters/racing/persistence/inmemory/InMemoryLeagueStandingsRepository.ts b/adapters/racing/persistence/inmemory/InMemoryLeagueStandingsRepository.ts index 3398dd21c..92740e2f5 100644 --- a/adapters/racing/persistence/inmemory/InMemoryLeagueStandingsRepository.ts +++ b/adapters/racing/persistence/inmemory/InMemoryLeagueStandingsRepository.ts @@ -4,15 +4,8 @@ import { Logger } from '@core/shared/application'; export class InMemoryLeagueStandingsRepository implements ILeagueStandingsRepository { private standings: Map = new Map(); // Key: leagueId - constructor(private readonly logger: Logger, initialStandings: Record = {}) { + constructor(private readonly logger: Logger) { this.logger.info('InMemoryLeagueStandingsRepository initialized.'); - for (const leagueId in initialStandings) { - // Ensure initialStandings[leagueId] is not undefined before setting - if (initialStandings[leagueId] !== undefined) { - this.standings.set(leagueId, initialStandings[leagueId]); - this.logger.debug(`Seeded standings for league: ${leagueId}.`); - } - } } async getLeagueStandings(leagueId: string): Promise { diff --git a/adapters/racing/persistence/inmemory/InMemoryLeagueWalletRepository.test.ts b/adapters/racing/persistence/inmemory/InMemoryLeagueWalletRepository.test.ts new file mode 100644 index 000000000..cf464b357 --- /dev/null +++ b/adapters/racing/persistence/inmemory/InMemoryLeagueWalletRepository.test.ts @@ -0,0 +1,141 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import { InMemoryLeagueWalletRepository } from './InMemoryLeagueWalletRepository'; +import { LeagueWallet } from '@core/racing/domain/entities/league-wallet/LeagueWallet'; +import { Money } from '@core/racing/domain/value-objects/Money'; +import type { Logger } from '@core/shared/application'; + +describe('InMemoryLeagueWalletRepository', () => { + let repository: InMemoryLeagueWalletRepository; + let mockLogger: Logger; + + beforeEach(() => { + mockLogger = { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; + repository = new InMemoryLeagueWalletRepository(mockLogger); + }); + + const createTestWallet = (id: string, leagueId: string, balanceAmount: number = 10000) => { + const balance = Money.create(balanceAmount, 'USD'); + return LeagueWallet.create({ + id, + leagueId, + balance, + transactionIds: [], + }); + }; + + describe('constructor', () => { + it('should initialize with a logger', () => { + expect(repository).toBeDefined(); + expect(mockLogger.info).toHaveBeenCalledWith('InMemoryLeagueWalletRepository initialized.'); + }); + }); + + describe('findById', () => { + it('should return null if wallet not found', async () => { + const result = await repository.findById('nonexistent'); + expect(result).toBeNull(); + expect(mockLogger.debug).toHaveBeenCalledWith('Finding league wallet by id: nonexistent'); + expect(mockLogger.warn).toHaveBeenCalledWith('League wallet with id nonexistent not found.'); + }); + + it('should return the wallet if found', async () => { + const wallet = createTestWallet('1', 'league1'); + await repository.create(wallet); + + const result = await repository.findById('1'); + expect(result).toEqual(wallet); + expect(mockLogger.info).toHaveBeenCalledWith('Found league wallet: 1.'); + }); + }); + + describe('findByLeagueId', () => { + it('should return null if no wallet found for league id', async () => { + const result = await repository.findByLeagueId('nonexistent'); + expect(result).toBeNull(); + expect(mockLogger.warn).toHaveBeenCalledWith('No league wallet found for league id: nonexistent.'); + }); + + it('should return the wallet if found by league id', async () => { + const wallet = createTestWallet('1', 'league1'); + await repository.create(wallet); + + const result = await repository.findByLeagueId('league1'); + expect(result).toEqual(wallet); + expect(mockLogger.info).toHaveBeenCalledWith('Found league wallet for league id: league1.'); + }); + }); + + describe('create', () => { + it('should create a new wallet', async () => { + const wallet = createTestWallet('1', 'league1'); + const result = await repository.create(wallet); + expect(result).toEqual(wallet); + expect(mockLogger.info).toHaveBeenCalledWith('LeagueWallet 1 created successfully.'); + }); + + it('should throw error if wallet already exists', async () => { + const wallet = createTestWallet('1', 'league1'); + await repository.create(wallet); + await expect(repository.create(wallet)).rejects.toThrow('LeagueWallet with this ID already exists'); + }); + }); + + describe('update', () => { + it('should update an existing wallet', async () => { + const wallet = createTestWallet('1', 'league1'); + await repository.create(wallet); + + const updatedWallet = LeagueWallet.create({ + id: '1', + leagueId: 'league1', + balance: Money.create(20000, 'USD'), + transactionIds: [], + }); + const result = await repository.update(updatedWallet); + expect(result).toEqual(updatedWallet); + expect(mockLogger.info).toHaveBeenCalledWith('LeagueWallet 1 updated successfully.'); + }); + + it('should throw error if wallet does not exist', async () => { + const wallet = createTestWallet('1', 'league1'); + await expect(repository.update(wallet)).rejects.toThrow('LeagueWallet not found'); + }); + }); + + describe('delete', () => { + it('should delete an existing wallet', async () => { + const wallet = createTestWallet('1', 'league1'); + await repository.create(wallet); + + await repository.delete('1'); + expect(mockLogger.info).toHaveBeenCalledWith('LeagueWallet 1 deleted successfully.'); + const found = await repository.findById('1'); + expect(found).toBeNull(); + }); + + it('should not throw if wallet does not exist', async () => { + await repository.delete('nonexistent'); + expect(mockLogger.warn).toHaveBeenCalledWith('LeagueWallet with id nonexistent not found for deletion.'); + }); + }); + + describe('exists', () => { + it('should return true if wallet exists', async () => { + const wallet = createTestWallet('1', 'league1'); + await repository.create(wallet); + + const result = await repository.exists('1'); + expect(result).toBe(true); + }); + + it('should return false if wallet does not exist', async () => { + const result = await repository.exists('nonexistent'); + expect(result).toBe(false); + }); + }); +}); \ No newline at end of file diff --git a/adapters/racing/persistence/inmemory/InMemoryLeagueWalletRepository.ts b/adapters/racing/persistence/inmemory/InMemoryLeagueWalletRepository.ts index d9e56c255..507a6c2b4 100644 --- a/adapters/racing/persistence/inmemory/InMemoryLeagueWalletRepository.ts +++ b/adapters/racing/persistence/inmemory/InMemoryLeagueWalletRepository.ts @@ -1,24 +1,20 @@ /** * In-Memory Implementation: ILeagueWalletRepository - * + * * Mock repository for testing and development */ -import type { LeagueWallet } from '../../domain/entities/LeagueWallet'; -import type { ILeagueWalletRepository } from '../../domain/repositories/ILeagueWalletRepository'; +import { LeagueWallet } from '@core/racing/domain/entities/league-wallet/LeagueWallet'; +import type { ILeagueWalletRepository } from '@core/racing/domain/repositories/ILeagueWalletRepository'; import type { Logger } from '@core/shared/application'; export class InMemoryLeagueWalletRepository implements ILeagueWalletRepository { private wallets: Map = new Map(); private readonly logger: Logger; - constructor(logger: Logger, seedData?: LeagueWallet[]) { + constructor(logger: Logger) { this.logger = logger; this.logger.info('InMemoryLeagueWalletRepository initialized.'); - if (seedData) { - seedData.forEach(wallet => this.wallets.set(wallet.id, wallet)); - this.logger.debug(`Seeded ${seedData.length} league wallets.`); - } } async findById(id: string): Promise { @@ -41,7 +37,7 @@ export class InMemoryLeagueWalletRepository implements ILeagueWalletRepository { this.logger.debug(`Finding league wallet by league id: ${leagueId}`); try { for (const wallet of this.wallets.values()) { - if (wallet.leagueId === leagueId) { + if (wallet.leagueId.toString() === leagueId) { this.logger.info(`Found league wallet for league id: ${leagueId}.`); return wallet; } @@ -57,11 +53,11 @@ export class InMemoryLeagueWalletRepository implements ILeagueWalletRepository { async create(wallet: LeagueWallet): Promise { this.logger.debug(`Creating league wallet: ${wallet.id}`); try { - if (this.wallets.has(wallet.id)) { + if (this.wallets.has(wallet.id.toString())) { this.logger.warn(`LeagueWallet with ID ${wallet.id} already exists.`); throw new Error('LeagueWallet with this ID already exists'); } - this.wallets.set(wallet.id, wallet); + this.wallets.set(wallet.id.toString(), wallet); this.logger.info(`LeagueWallet ${wallet.id} created successfully.`); return wallet; } catch (error) { @@ -73,11 +69,11 @@ export class InMemoryLeagueWalletRepository implements ILeagueWalletRepository { async update(wallet: LeagueWallet): Promise { this.logger.debug(`Updating league wallet: ${wallet.id}`); try { - if (!this.wallets.has(wallet.id)) { + if (!this.wallets.has(wallet.id.toString())) { this.logger.warn(`LeagueWallet with ID ${wallet.id} not found for update.`); throw new Error('LeagueWallet not found'); } - this.wallets.set(wallet.id, wallet); + this.wallets.set(wallet.id.toString(), wallet); this.logger.info(`LeagueWallet ${wallet.id} updated successfully.`); return wallet; } catch (error) { diff --git a/adapters/racing/persistence/inmemory/InMemoryLiveryRepository.test.ts b/adapters/racing/persistence/inmemory/InMemoryLiveryRepository.test.ts new file mode 100644 index 000000000..a76bfe4fb --- /dev/null +++ b/adapters/racing/persistence/inmemory/InMemoryLiveryRepository.test.ts @@ -0,0 +1,294 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import { InMemoryLiveryRepository } from './InMemoryLiveryRepository'; +import { DriverLivery } from '@core/racing/domain/entities/DriverLivery'; +import { LiveryTemplate } from '@core/racing/domain/entities/LiveryTemplate'; +import { LiveryDecal } from '@core/racing/domain/value-objects/LiveryDecal'; +import type { Logger } from '@core/shared/application'; + +describe('InMemoryLiveryRepository', () => { + let repository: InMemoryLiveryRepository; + let mockLogger: Logger; + + beforeEach(() => { + mockLogger = { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; + repository = new InMemoryLiveryRepository(mockLogger); + }); + + const createTestDriverLivery = (id: string, driverId: string, gameId: string, carId: string, uploadedImageUrl: string) => { + return DriverLivery.create({ + id, + driverId, + gameId, + carId, + uploadedImageUrl, + }); + }; + + const createTestLiveryTemplate = (id: string, leagueId: string, seasonId: string, carId: string, baseImageUrl: string) => { + return LiveryTemplate.create({ + id, + leagueId, + seasonId, + carId, + baseImageUrl, + }); + }; + + const createTestDecal = (id: string, type: 'user' | 'sponsor', imageUrl: string) => { + return LiveryDecal.create({ + id, + type, + imageUrl, + x: 0.5, + y: 0.5, + width: 0.1, + height: 0.1, + zIndex: 1, + }); + }; + + describe('constructor', () => { + it('should initialize with a logger', () => { + expect(repository).toBeDefined(); + expect(mockLogger.info).toHaveBeenCalledWith('InMemoryLiveryRepository initialized.'); + }); + }); + + describe('DriverLivery operations', () => { + describe('findDriverLiveryById', () => { + it('should return null if driver livery not found', async () => { + const result = await repository.findDriverLiveryById('nonexistent'); + expect(result).toBeNull(); + expect(mockLogger.debug).toHaveBeenCalledWith('Finding driver livery by id: nonexistent'); + expect(mockLogger.warn).toHaveBeenCalledWith('Driver livery with id nonexistent not found.'); + }); + + it('should return the driver livery if found', async () => { + const livery = createTestDriverLivery('1', 'driver1', 'game1', 'car1', 'http://example.com/image.jpg'); + await repository.createDriverLivery(livery); + + const result = await repository.findDriverLiveryById('1'); + expect(result).toEqual(livery); + expect(mockLogger.info).toHaveBeenCalledWith('Found driver livery: 1.'); + }); + }); + + describe('findDriverLiveriesByDriverId', () => { + it('should return driver liveries for the given driver', async () => { + const livery1 = createTestDriverLivery('1', 'driver1', 'game1', 'car1', 'http://example.com/image1.jpg'); + const livery2 = createTestDriverLivery('2', 'driver1', 'game2', 'car2', 'http://example.com/image2.jpg'); + const livery3 = createTestDriverLivery('3', 'driver2', 'game1', 'car1', 'http://example.com/image3.jpg'); + await repository.createDriverLivery(livery1); + await repository.createDriverLivery(livery2); + await repository.createDriverLivery(livery3); + + const result = await repository.findDriverLiveriesByDriverId('driver1'); + expect(result).toHaveLength(2); + expect(result).toContain(livery1); + expect(result).toContain(livery2); + }); + }); + + describe('findDriverLiveryByDriverAndCar', () => { + it('should return null if no livery found', async () => { + const result = await repository.findDriverLiveryByDriverAndCar('driver1', 'car1'); + expect(result).toBeNull(); + }); + + it('should return the livery if found', async () => { + const livery = createTestDriverLivery('1', 'driver1', 'game1', 'car1', 'http://example.com/image.jpg'); + await repository.createDriverLivery(livery); + + const result = await repository.findDriverLiveryByDriverAndCar('driver1', 'car1'); + expect(result).toEqual(livery); + }); + }); + + describe('findDriverLiveriesByGameId', () => { + it('should return driver liveries for the given game', async () => { + const livery1 = createTestDriverLivery('1', 'driver1', 'game1', 'car1', 'http://example.com/image1.jpg'); + const livery2 = createTestDriverLivery('2', 'driver2', 'game1', 'car2', 'http://example.com/image2.jpg'); + const livery3 = createTestDriverLivery('3', 'driver1', 'game2', 'car1', 'http://example.com/image3.jpg'); + await repository.createDriverLivery(livery1); + await repository.createDriverLivery(livery2); + await repository.createDriverLivery(livery3); + + const result = await repository.findDriverLiveriesByGameId('game1'); + expect(result).toHaveLength(2); + expect(result).toContain(livery1); + expect(result).toContain(livery2); + }); + }); + + describe('findDriverLiveryByDriverAndGame', () => { + it('should return driver liveries for the given driver and game', async () => { + const livery1 = createTestDriverLivery('1', 'driver1', 'game1', 'car1', 'http://example.com/image1.jpg'); + const livery2 = createTestDriverLivery('2', 'driver1', 'game1', 'car2', 'http://example.com/image2.jpg'); + const livery3 = createTestDriverLivery('3', 'driver1', 'game2', 'car1', 'http://example.com/image3.jpg'); + await repository.createDriverLivery(livery1); + await repository.createDriverLivery(livery2); + await repository.createDriverLivery(livery3); + + const result = await repository.findDriverLiveryByDriverAndGame('driver1', 'game1'); + expect(result).toHaveLength(2); + expect(result).toContain(livery1); + expect(result).toContain(livery2); + }); + }); + + describe('createDriverLivery', () => { + it('should create a new driver livery', async () => { + const livery = createTestDriverLivery('1', 'driver1', 'game1', 'car1', 'http://example.com/image.jpg'); + const result = await repository.createDriverLivery(livery); + expect(result).toEqual(livery); + expect(mockLogger.info).toHaveBeenCalledWith('DriverLivery 1 created successfully.'); + }); + + it('should throw error if driver livery already exists', async () => { + const livery = createTestDriverLivery('1', 'driver1', 'game1', 'car1', 'http://example.com/image.jpg'); + await repository.createDriverLivery(livery); + await expect(repository.createDriverLivery(livery)).rejects.toThrow('DriverLivery with this ID already exists'); + }); + }); + + describe('updateDriverLivery', () => { + it('should update an existing driver livery', async () => { + const livery = createTestDriverLivery('1', 'driver1', 'game1', 'car1', 'http://example.com/image.jpg'); + await repository.createDriverLivery(livery); + + const decal = createTestDecal('decal1', 'user', 'http://example.com/decal.jpg'); + const updatedLivery = livery.addDecal(decal); + const result = await repository.updateDriverLivery(updatedLivery); + expect(result).toEqual(updatedLivery); + expect(mockLogger.info).toHaveBeenCalledWith('DriverLivery 1 updated successfully.'); + }); + + it('should throw error if driver livery does not exist', async () => { + const livery = createTestDriverLivery('1', 'driver1', 'game1', 'car1', 'http://example.com/image.jpg'); + await expect(repository.updateDriverLivery(livery)).rejects.toThrow('DriverLivery not found'); + }); + }); + + describe('deleteDriverLivery', () => { + it('should delete an existing driver livery', async () => { + const livery = createTestDriverLivery('1', 'driver1', 'game1', 'car1', 'http://example.com/image.jpg'); + await repository.createDriverLivery(livery); + + await repository.deleteDriverLivery('1'); + expect(mockLogger.info).toHaveBeenCalledWith('DriverLivery 1 deleted successfully.'); + const found = await repository.findDriverLiveryById('1'); + expect(found).toBeNull(); + }); + + it('should not throw error if driver livery does not exist', async () => { + await repository.deleteDriverLivery('nonexistent'); + expect(mockLogger.warn).toHaveBeenCalledWith('DriverLivery with id nonexistent not found for deletion.'); + }); + }); + }); + + describe('LiveryTemplate operations', () => { + describe('findTemplateById', () => { + it('should return null if template not found', async () => { + const result = await repository.findTemplateById('nonexistent'); + expect(result).toBeNull(); + expect(mockLogger.warn).toHaveBeenCalledWith('Livery template with id nonexistent not found.'); + }); + + it('should return the template if found', async () => { + const template = createTestLiveryTemplate('1', 'league1', 'season1', 'car1', 'http://example.com/base.jpg'); + await repository.createTemplate(template); + + const result = await repository.findTemplateById('1'); + expect(result).toEqual(template); + expect(mockLogger.info).toHaveBeenCalledWith('Found livery template: 1.'); + }); + }); + + describe('findTemplatesBySeasonId', () => { + it('should return templates for the given season', async () => { + const template1 = createTestLiveryTemplate('1', 'league1', 'season1', 'car1', 'http://example.com/base1.jpg'); + const template2 = createTestLiveryTemplate('2', 'league2', 'season1', 'car2', 'http://example.com/base2.jpg'); + const template3 = createTestLiveryTemplate('3', 'league1', 'season2', 'car1', 'http://example.com/base3.jpg'); + await repository.createTemplate(template1); + await repository.createTemplate(template2); + await repository.createTemplate(template3); + + const result = await repository.findTemplatesBySeasonId('season1'); + expect(result).toHaveLength(2); + expect(result).toContain(template1); + expect(result).toContain(template2); + }); + }); + + describe('findTemplateBySeasonAndCar', () => { + it('should return null if no template found', async () => { + const result = await repository.findTemplateBySeasonAndCar('season1', 'car1'); + expect(result).toBeNull(); + }); + + it('should return the template if found', async () => { + const template = createTestLiveryTemplate('1', 'league1', 'season1', 'car1', 'http://example.com/base.jpg'); + await repository.createTemplate(template); + + const result = await repository.findTemplateBySeasonAndCar('season1', 'car1'); + expect(result).toEqual(template); + }); + }); + + describe('createTemplate', () => { + it('should create a new template', async () => { + const template = createTestLiveryTemplate('1', 'league1', 'season1', 'car1', 'http://example.com/base.jpg'); + const result = await repository.createTemplate(template); + expect(result).toEqual(template); + expect(mockLogger.info).toHaveBeenCalledWith('LiveryTemplate 1 created successfully.'); + }); + + it('should throw error if template already exists', async () => { + const template = createTestLiveryTemplate('1', 'league1', 'season1', 'car1', 'http://example.com/base.jpg'); + await repository.createTemplate(template); + await expect(repository.createTemplate(template)).rejects.toThrow('LiveryTemplate with this ID already exists'); + }); + }); + + describe('updateTemplate', () => { + it('should update an existing template', async () => { + const template = createTestLiveryTemplate('1', 'league1', 'season1', 'car1', 'http://example.com/base.jpg'); + await repository.createTemplate(template); + + const decal = createTestDecal('decal1', 'sponsor', 'http://example.com/decal.jpg'); + const updatedTemplate = template.addDecal(decal); + const result = await repository.updateTemplate(updatedTemplate); + expect(result).toEqual(updatedTemplate); + expect(mockLogger.info).toHaveBeenCalledWith('LiveryTemplate 1 updated successfully.'); + }); + + it('should throw error if template does not exist', async () => { + const template = createTestLiveryTemplate('1', 'league1', 'season1', 'car1', 'http://example.com/base.jpg'); + await expect(repository.updateTemplate(template)).rejects.toThrow('LiveryTemplate not found'); + }); + }); + + describe('deleteTemplate', () => { + it('should delete an existing template', async () => { + const template = createTestLiveryTemplate('1', 'league1', 'season1', 'car1', 'http://example.com/base.jpg'); + await repository.createTemplate(template); + + await repository.deleteTemplate('1'); + expect(mockLogger.info).toHaveBeenCalledWith('LiveryTemplate 1 deleted successfully.'); + const found = await repository.findTemplateById('1'); + expect(found).toBeNull(); + }); + + it('should not throw error if template does not exist', async () => { + await repository.deleteTemplate('nonexistent'); + expect(mockLogger.warn).toHaveBeenCalledWith('LiveryTemplate with id nonexistent not found for deletion.'); + }); + }); + }); +}); \ No newline at end of file diff --git a/adapters/racing/persistence/inmemory/InMemoryLiveryRepository.ts b/adapters/racing/persistence/inmemory/InMemoryLiveryRepository.ts index 93e8dc53c..486290f1f 100644 --- a/adapters/racing/persistence/inmemory/InMemoryLiveryRepository.ts +++ b/adapters/racing/persistence/inmemory/InMemoryLiveryRepository.ts @@ -4,9 +4,9 @@ * Mock repository for testing and development */ -import type { DriverLivery } from '../../domain/entities/DriverLivery'; -import type { LiveryTemplate } from '../../domain/entities/LiveryTemplate'; -import type { ILiveryRepository } from '../../domain/repositories/ILiveryRepository'; +import type { DriverLivery } from '../../../../core/racing/domain/entities/DriverLivery'; +import type { LiveryTemplate } from '../../../../core/racing/domain/entities/LiveryTemplate'; +import type { ILiveryRepository } from '../../../../core/racing/domain/repositories/ILiveryRepository'; import type { Logger } from '@core/shared/application'; export class InMemoryLiveryRepository implements ILiveryRepository { @@ -22,7 +22,7 @@ export class InMemoryLiveryRepository implements ILiveryRepository { this.logger.debug(`Seeded ${seedDriverLiveries.length} driver liveries.`); } if (seedTemplates) { - seedTemplates.forEach(template => this.templates.set(template.id, template)); + seedTemplates.forEach(template => this.templates.set(template.id.toString(), template)); this.logger.debug(`Seeded ${seedTemplates.length} livery templates.`); } } @@ -47,7 +47,7 @@ export class InMemoryLiveryRepository implements ILiveryRepository { async findDriverLiveriesByDriverId(driverId: string): Promise { this.logger.debug(`Finding driver liveries by driver id: ${driverId}`); try { - const liveries = Array.from(this.driverLiveries.values()).filter(l => l.driverId === driverId); + const liveries = Array.from(this.driverLiveries.values()).filter(l => l.driverId.toString() === driverId); this.logger.info(`Found ${liveries.length} driver liveries for driver id: ${driverId}.`); return liveries; } catch (error) { @@ -60,7 +60,7 @@ export class InMemoryLiveryRepository implements ILiveryRepository { this.logger.debug(`Finding driver livery by driver: ${driverId} and car: ${carId}`); try { for (const livery of this.driverLiveries.values()) { - if (livery.driverId === driverId && livery.carId === carId) { + if (livery.driverId.toString() === driverId && livery.carId.toString() === carId) { this.logger.info(`Found driver livery for driver: ${driverId}, car: ${carId}.`); return livery; } @@ -68,7 +68,7 @@ export class InMemoryLiveryRepository implements ILiveryRepository { this.logger.warn(`Driver livery for driver ${driverId} and car ${carId} not found.`); return null; } catch (error) { - this.logger.error(`Error finding driver livery by driver ${driverId}, car ${carId}:`, error); + this.logger.error(`Error finding driver livery by driver ${driverId}, car ${carId}:`, error instanceof Error ? error : new Error(String(error))); throw error; } } @@ -76,7 +76,7 @@ export class InMemoryLiveryRepository implements ILiveryRepository { async findDriverLiveriesByGameId(gameId: string): Promise { this.logger.debug(`Finding driver liveries by game id: ${gameId}`); try { - const liveries = Array.from(this.driverLiveries.values()).filter(l => l.gameId === gameId); + const liveries = Array.from(this.driverLiveries.values()).filter(l => l.gameId.toString() === gameId); this.logger.info(`Found ${liveries.length} driver liveries for game id: ${gameId}.`); return liveries; } catch (error) { @@ -89,12 +89,12 @@ export class InMemoryLiveryRepository implements ILiveryRepository { this.logger.debug(`Finding driver liveries by driver: ${driverId} and game: ${gameId}`); try { const liveries = Array.from(this.driverLiveries.values()).filter( - l => l.driverId === driverId && l.gameId === gameId + l => l.driverId.toString() === driverId && l.gameId.toString() === gameId ); this.logger.info(`Found ${liveries.length} driver liveries for driver: ${driverId}, game: ${gameId}.`); return liveries; } catch (error) { - this.logger.error(`Error finding driver liveries by driver ${driverId}, game ${gameId}:`, error); + this.logger.error(`Error finding driver liveries by driver ${driverId}, game ${gameId}:`, error instanceof Error ? error : new Error(String(error))); throw error; } } @@ -165,7 +165,7 @@ export class InMemoryLiveryRepository implements ILiveryRepository { async findTemplatesBySeasonId(seasonId: string): Promise { this.logger.debug(`Finding livery templates by season id: ${seasonId}`); try { - const templates = Array.from(this.templates.values()).filter(t => t.seasonId === seasonId); + const templates = Array.from(this.templates.values()).filter(t => t.seasonId.toString() === seasonId); this.logger.info(`Found ${templates.length} livery templates for season id: ${seasonId}.`); return templates; } catch (error) { @@ -178,7 +178,7 @@ export class InMemoryLiveryRepository implements ILiveryRepository { this.logger.debug(`Finding livery template by season: ${seasonId} and car: ${carId}`); try { for (const template of this.templates.values()) { - if (template.seasonId === seasonId && template.carId === carId) { + if (template.seasonId.toString() === seasonId && template.carId.toString() === carId) { this.logger.info(`Found livery template for season: ${seasonId}, car: ${carId}.`); return template; } @@ -186,39 +186,39 @@ export class InMemoryLiveryRepository implements ILiveryRepository { this.logger.warn(`Livery template for season ${seasonId} and car ${carId} not found.`); return null; } catch (error) { - this.logger.error(`Error finding livery template by season ${seasonId}, car ${carId}:`, error); + this.logger.error(`Error finding livery template by season ${seasonId}, car ${carId}:`, error instanceof Error ? error : new Error(String(error))); throw error; } } async createTemplate(template: LiveryTemplate): Promise { - this.logger.debug(`Creating livery template: ${template.id}`); + this.logger.debug(`Creating livery template: ${template.id.toString()}`); try { - if (this.templates.has(template.id)) { - this.logger.warn(`LiveryTemplate with ID ${template.id} already exists.`); + if (this.templates.has(template.id.toString())) { + this.logger.warn(`LiveryTemplate with ID ${template.id.toString()} already exists.`); throw new Error('LiveryTemplate with this ID already exists'); } - this.templates.set(template.id, template); - this.logger.info(`LiveryTemplate ${template.id} created successfully.`); + this.templates.set(template.id.toString(), template); + this.logger.info(`LiveryTemplate ${template.id.toString()} created successfully.`); return template; } catch (error) { - this.logger.error(`Error creating livery template ${template.id}:`, error instanceof Error ? error : new Error(String(error))); + this.logger.error(`Error creating livery template ${template.id.toString()}:`, error instanceof Error ? error : new Error(String(error))); throw error; } } async updateTemplate(template: LiveryTemplate): Promise { - this.logger.debug(`Updating livery template: ${template.id}`); + this.logger.debug(`Updating livery template: ${template.id.toString()}`); try { - if (!this.templates.has(template.id)) { - this.logger.warn(`LiveryTemplate with ID ${template.id} not found for update.`); + if (!this.templates.has(template.id.toString())) { + this.logger.warn(`LiveryTemplate with ID ${template.id.toString()} not found for update.`); throw new Error('LiveryTemplate not found'); } - this.templates.set(template.id, template); - this.logger.info(`LiveryTemplate ${template.id} updated successfully.`); + this.templates.set(template.id.toString(), template); + this.logger.info(`LiveryTemplate ${template.id.toString()} updated successfully.`); return template; } catch (error) { - this.logger.error(`Error updating livery template ${template.id}:`, error instanceof Error ? error : new Error(String(error))); + this.logger.error(`Error updating livery template ${template.id.toString()}:`, error instanceof Error ? error : new Error(String(error))); throw error; } } diff --git a/adapters/racing/persistence/inmemory/InMemoryPenaltyRepository.test.ts b/adapters/racing/persistence/inmemory/InMemoryPenaltyRepository.test.ts new file mode 100644 index 000000000..9a5c83635 --- /dev/null +++ b/adapters/racing/persistence/inmemory/InMemoryPenaltyRepository.test.ts @@ -0,0 +1,187 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import { InMemoryPenaltyRepository } from './InMemoryPenaltyRepository'; +import { Penalty } from '@core/racing/domain/entities/penalty/Penalty'; +import type { Logger } from '@core/shared/application'; + +describe('InMemoryPenaltyRepository', () => { + let repository: InMemoryPenaltyRepository; + let mockLogger: Logger; + + beforeEach(() => { + mockLogger = { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; + repository = new InMemoryPenaltyRepository(mockLogger); + }); + + const createTestPenalty = (id: string, raceId: string, driverId: string, issuedBy: string) => { + return Penalty.create({ + id, + leagueId: 'league1', + raceId, + driverId, + type: 'time_penalty', + value: 5, + reason: 'Test reason', + issuedBy, + }); + }; + + describe('constructor', () => { + it('should initialize with a logger', () => { + expect(repository).toBeDefined(); + expect(mockLogger.info).toHaveBeenCalledWith('InMemoryPenaltyRepository initialized.'); + }); + }); + + describe('findById', () => { + it('should return null if penalty not found', async () => { + const result = await repository.findById('nonexistent'); + expect(result).toBeNull(); + expect(mockLogger.debug).toHaveBeenCalledWith('Finding penalty by id: nonexistent'); + expect(mockLogger.warn).toHaveBeenCalledWith('Penalty with id nonexistent not found.'); + }); + + it('should return the penalty if found', async () => { + const penalty = createTestPenalty('1', 'race1', 'driver1', 'steward1'); + await repository.create(penalty); + + const result = await repository.findById('1'); + expect(result).toEqual(penalty); + expect(mockLogger.info).toHaveBeenCalledWith('Found penalty with id: 1.'); + }); + }); + + describe('findByRaceId', () => { + it('should return penalties filtered by race ID', async () => { + const penalty1 = createTestPenalty('1', 'race1', 'driver1', 'steward1'); + const penalty2 = createTestPenalty('2', 'race2', 'driver2', 'steward1'); + await repository.create(penalty1); + await repository.create(penalty2); + + const result = await repository.findByRaceId('race1'); + expect(result).toHaveLength(1); + expect(result[0]).toEqual(penalty1); + }); + }); + + describe('findByDriverId', () => { + it('should return penalties filtered by driver ID', async () => { + const penalty1 = createTestPenalty('1', 'race1', 'driver1', 'steward1'); + const penalty2 = createTestPenalty('2', 'race1', 'driver2', 'steward1'); + await repository.create(penalty1); + await repository.create(penalty2); + + const result = await repository.findByDriverId('driver1'); + expect(result).toHaveLength(1); + expect(result[0]).toEqual(penalty1); + }); + }); + + describe('findByProtestId', () => { + it('should return penalties filtered by protest ID', async () => { + const penalty1 = Penalty.create({ + id: '1', + leagueId: 'league1', + raceId: 'race1', + driverId: 'driver1', + type: 'time_penalty', + value: 5, + reason: 'Test reason', + protestId: 'protest1', + issuedBy: 'steward1', + }); + const penalty2 = createTestPenalty('2', 'race1', 'driver2', 'steward1'); + await repository.create(penalty1); + await repository.create(penalty2); + + const result = await repository.findByProtestId('protest1'); + expect(result).toHaveLength(1); + expect(result[0]).toEqual(penalty1); + }); + }); + + describe('findPending', () => { + it('should return only pending penalties', async () => { + const penalty1 = createTestPenalty('1', 'race1', 'driver1', 'steward1'); + const penalty2 = Penalty.create({ + id: '2', + leagueId: 'league1', + raceId: 'race1', + driverId: 'driver2', + type: 'time_penalty', + value: 5, + reason: 'Test reason', + issuedBy: 'steward1', + status: 'applied', + }); + await repository.create(penalty1); + await repository.create(penalty2); + + const result = await repository.findPending(); + expect(result).toHaveLength(1); + expect(result[0]).toEqual(penalty1); + }); + }); + + describe('findIssuedBy', () => { + it('should return penalties issued by a specific steward', async () => { + const penalty1 = createTestPenalty('1', 'race1', 'driver1', 'steward1'); + const penalty2 = createTestPenalty('2', 'race1', 'driver2', 'steward2'); + await repository.create(penalty1); + await repository.create(penalty2); + + const result = await repository.findIssuedBy('steward1'); + expect(result).toHaveLength(1); + expect(result[0]).toEqual(penalty1); + }); + }); + + describe('create', () => { + it('should create a new penalty', async () => { + const penalty = createTestPenalty('1', 'race1', 'driver1', 'steward1'); + await repository.create(penalty); + expect(mockLogger.info).toHaveBeenCalledWith('Penalty 1 created successfully.'); + }); + + it('should throw error if penalty already exists', async () => { + const penalty = createTestPenalty('1', 'race1', 'driver1', 'steward1'); + await repository.create(penalty); + await expect(repository.create(penalty)).rejects.toThrow('Penalty with ID 1 already exists'); + }); + }); + + describe('update', () => { + it('should update an existing penalty', async () => { + const penalty = createTestPenalty('1', 'race1', 'driver1', 'steward1'); + await repository.create(penalty); + + const updatedPenalty = penalty.markAsApplied(); + await repository.update(updatedPenalty); + expect(mockLogger.info).toHaveBeenCalledWith('Penalty 1 updated successfully.'); + }); + + it('should throw error if penalty does not exist', async () => { + const penalty = createTestPenalty('1', 'race1', 'driver1', 'steward1'); + await expect(repository.update(penalty)).rejects.toThrow('Penalty with ID 1 not found'); + }); + }); + + describe('exists', () => { + it('should return true if penalty exists', async () => { + const penalty = createTestPenalty('1', 'race1', 'driver1', 'steward1'); + await repository.create(penalty); + + const result = await repository.exists('1'); + expect(result).toBe(true); + }); + + it('should return false if penalty does not exist', async () => { + const result = await repository.exists('nonexistent'); + expect(result).toBe(false); + }); + }); +}); \ No newline at end of file diff --git a/adapters/racing/persistence/inmemory/InMemoryPenaltyRepository.ts b/adapters/racing/persistence/inmemory/InMemoryPenaltyRepository.ts index 2ac7fcf85..71542a09e 100644 --- a/adapters/racing/persistence/inmemory/InMemoryPenaltyRepository.ts +++ b/adapters/racing/persistence/inmemory/InMemoryPenaltyRepository.ts @@ -1,11 +1,11 @@ /** * In-Memory Implementation: InMemoryPenaltyRepository - * + * * Provides an in-memory storage implementation for penalties. */ -import type { Penalty } from '../../domain/entities/Penalty'; -import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository'; +import type { Penalty } from '@core/racing/domain/entities/Penalty'; +import type { IPenaltyRepository } from '@core/racing/domain/repositories/IPenaltyRepository'; import type { Logger } from '@core/shared/application'; export class InMemoryPenaltyRepository implements IPenaltyRepository { diff --git a/adapters/racing/persistence/inmemory/InMemoryProtestRepository.test.ts b/adapters/racing/persistence/inmemory/InMemoryProtestRepository.test.ts new file mode 100644 index 000000000..66c813940 --- /dev/null +++ b/adapters/racing/persistence/inmemory/InMemoryProtestRepository.test.ts @@ -0,0 +1,176 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import { InMemoryProtestRepository } from './InMemoryProtestRepository'; +import { Protest } from '@core/racing/domain/entities/Protest'; +import type { Logger } from '@core/shared/application'; + +describe('InMemoryProtestRepository', () => { + let repository: InMemoryProtestRepository; + let mockLogger: Logger; + + beforeEach(() => { + mockLogger = { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; + repository = new InMemoryProtestRepository(mockLogger); + }); + + const createTestProtest = (id: string, raceId: string, protestingDriverId: string, accusedDriverId: string, status?: string) => { + const baseProps = { + id, + raceId, + protestingDriverId, + accusedDriverId, + incident: { lap: 1, description: 'Test incident' }, + }; + if (status) { + return Protest.create({ ...baseProps, status }); + } + return Protest.create(baseProps); + }; + + describe('constructor', () => { + it('should initialize with a logger', () => { + expect(repository).toBeDefined(); + expect(mockLogger.info).toHaveBeenCalledWith('InMemoryProtestRepository initialized.'); + }); + }); + + describe('findById', () => { + it('should return null if protest not found', async () => { + const result = await repository.findById('nonexistent'); + expect(result).toBeNull(); + expect(mockLogger.debug).toHaveBeenCalledWith('[InMemoryProtestRepository] Finding protest by ID: nonexistent'); + expect(mockLogger.warn).toHaveBeenCalledWith('Protest with ID nonexistent not found.'); + }); + + it('should return the protest if found', async () => { + const protest = createTestProtest('1', 'race1', 'driver1', 'driver2'); + await repository.create(protest); + + const result = await repository.findById('1'); + expect(result).toEqual(protest); + expect(mockLogger.info).toHaveBeenCalledWith('Found protest by ID: 1.'); + }); + }); + + describe('findByRaceId', () => { + it('should return protests filtered by race ID', async () => { + const protest1 = createTestProtest('1', 'race1', 'driver1', 'driver2'); + const protest2 = createTestProtest('2', 'race2', 'driver3', 'driver4'); + await repository.create(protest1); + await repository.create(protest2); + + const result = await repository.findByRaceId('race1'); + expect(result).toHaveLength(1); + expect(result[0]).toEqual(protest1); + }); + }); + + describe('findByProtestingDriverId', () => { + it('should return protests filtered by protesting driver ID', async () => { + const protest1 = createTestProtest('1', 'race1', 'driver1', 'driver2'); + const protest2 = createTestProtest('2', 'race1', 'driver3', 'driver1'); + await repository.create(protest1); + await repository.create(protest2); + + const result = await repository.findByProtestingDriverId('driver1'); + expect(result).toHaveLength(1); + expect(result[0]).toEqual(protest1); + }); + }); + + describe('findByAccusedDriverId', () => { + it('should return protests filtered by accused driver ID', async () => { + const protest1 = createTestProtest('1', 'race1', 'driver1', 'driver2'); + const protest2 = createTestProtest('2', 'race1', 'driver2', 'driver3'); + await repository.create(protest1); + await repository.create(protest2); + + const result = await repository.findByAccusedDriverId('driver2'); + expect(result).toHaveLength(1); + expect(result[0]).toEqual(protest1); + }); + }); + + describe('findPending', () => { + it('should return only pending protests', async () => { + const protest1 = createTestProtest('1', 'race1', 'driver1', 'driver2'); + const protest2 = createTestProtest('2', 'race1', 'driver3', 'driver4', 'dismissed'); + await repository.create(protest1); + await repository.create(protest2); + + const result = await repository.findPending(); + expect(result).toHaveLength(1); + expect(result[0]).toEqual(protest1); + }); + }); + + describe('findUnderReviewBy', () => { + it('should return protests under review by a specific steward', async () => { + const protest1 = Protest.create({ + id: '1', + raceId: 'race1', + protestingDriverId: 'driver1', + accusedDriverId: 'driver2', + incident: { lap: 1, description: 'Test incident' }, + status: 'under_review', + reviewedBy: 'steward1', + }); + const protest2 = createTestProtest('2', 'race1', 'driver3', 'driver4'); + await repository.create(protest1); + await repository.create(protest2); + + const result = await repository.findUnderReviewBy('steward1'); + expect(result).toHaveLength(1); + expect(result[0]).toEqual(protest1); + }); + }); + + describe('create', () => { + it('should create a new protest', async () => { + const protest = createTestProtest('1', 'race1', 'driver1', 'driver2'); + await repository.create(protest); + expect(mockLogger.info).toHaveBeenCalledWith('Protest 1 created successfully.'); + }); + + it('should throw error if protest already exists', async () => { + const protest = createTestProtest('1', 'race1', 'driver1', 'driver2'); + await repository.create(protest); + await expect(repository.create(protest)).rejects.toThrow('Protest already exists'); + }); + }); + + describe('update', () => { + it('should update an existing protest', async () => { + const protest = createTestProtest('1', 'race1', 'driver1', 'driver2'); + await repository.create(protest); + + const updatedProtest = protest.startReview('steward1'); + await repository.update(updatedProtest); + expect(mockLogger.info).toHaveBeenCalledWith('Protest 1 updated successfully.'); + }); + + it('should throw error if protest does not exist', async () => { + const protest = createTestProtest('1', 'race1', 'driver1', 'driver2'); + await expect(repository.update(protest)).rejects.toThrow('Protest not found'); + }); + }); + + describe('exists', () => { + it('should return true if protest exists', async () => { + const protest = createTestProtest('1', 'race1', 'driver1', 'driver2'); + await repository.create(protest); + + const result = await repository.exists('1'); + expect(result).toBe(true); + }); + + it('should return false if protest does not exist', async () => { + const result = await repository.exists('nonexistent'); + expect(result).toBe(false); + }); + }); +}); \ No newline at end of file diff --git a/adapters/racing/persistence/inmemory/InMemoryProtestRepository.ts b/adapters/racing/persistence/inmemory/InMemoryProtestRepository.ts index 6f833c599..fb8432bb3 100644 --- a/adapters/racing/persistence/inmemory/InMemoryProtestRepository.ts +++ b/adapters/racing/persistence/inmemory/InMemoryProtestRepository.ts @@ -1,16 +1,12 @@ import { IProtestRepository } from '@core/racing/domain/repositories/IProtestRepository'; -import { Protest, ProtestStatus } from '@core/racing/domain/entities/Protest'; +import { Protest } from '@core/racing/domain/entities/Protest'; import { Logger } from '@core/shared/application'; export class InMemoryProtestRepository implements IProtestRepository { private protests: Map = new Map(); - constructor(private readonly logger: Logger, initialProtests: Protest[] = []) { + constructor(private readonly logger: Logger) { this.logger.info('InMemoryProtestRepository initialized.'); - for (const protest of initialProtests) { - this.protests.set(protest.id, protest); - this.logger.debug(`Seeded protest: ${protest.id}.`); - } } async findById(id: string): Promise { @@ -47,14 +43,14 @@ export class InMemoryProtestRepository implements IProtestRepository { async findPending(): Promise { this.logger.debug('[InMemoryProtestRepository] Finding all pending protests.'); - const pendingProtests = Array.from(this.protests.values()).filter(p => p.status === 'pending'); + const pendingProtests = Array.from(this.protests.values()).filter(p => p.status.toString() === 'pending'); this.logger.info(`Found ${pendingProtests.length} pending protests.`); return Promise.resolve(pendingProtests); } async findUnderReviewBy(stewardId: string): Promise { this.logger.debug(`[InMemoryProtestRepository] Finding protests under review by steward: ${stewardId}.`); - const underReviewProtests = Array.from(this.protests.values()).filter(p => p.reviewedBy === stewardId && p.status === 'under_review'); + const underReviewProtests = Array.from(this.protests.values()).filter(p => p.reviewedBy === stewardId && p.status.toString() === 'under_review'); this.logger.info(`Found ${underReviewProtests.length} protests under review by steward ${stewardId}.`); return Promise.resolve(underReviewProtests); } diff --git a/adapters/racing/persistence/inmemory/InMemoryRaceEventRepository.test.ts b/adapters/racing/persistence/inmemory/InMemoryRaceEventRepository.test.ts new file mode 100644 index 000000000..2393d4277 --- /dev/null +++ b/adapters/racing/persistence/inmemory/InMemoryRaceEventRepository.test.ts @@ -0,0 +1,236 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import { InMemoryRaceEventRepository } from './InMemoryRaceEventRepository'; +import { RaceEvent } from '@core/racing/domain/entities/RaceEvent'; +import { Session } from '@core/racing/domain/entities/Session'; +import { SessionType } from '@core/racing/domain/value-objects/SessionType'; +import type { Logger } from '@core/shared/application'; + +describe('InMemoryRaceEventRepository', () => { + let repository: InMemoryRaceEventRepository; + let mockLogger: Logger; + + const createMockSession = (overrides: Partial<{ + id: string; + raceEventId: string; + sessionType: SessionType; + status: 'scheduled' | 'running' | 'completed' | 'cancelled'; + scheduledAt: Date; + }> = {}): Session => { + return Session.create({ + id: overrides.id ?? 'session-1', + raceEventId: overrides.raceEventId ?? 'race-event-1', + scheduledAt: overrides.scheduledAt ?? new Date('2023-01-01T10:00:00Z'), + track: 'Monza', + car: 'Ferrari SF21', + sessionType: overrides.sessionType ?? SessionType.main(), + status: overrides.status ?? 'scheduled', + }); + }; + + const createTestRaceEvent = (id: string, seasonId: string, leagueId: string, name?: string, status?: 'scheduled' | 'in_progress' | 'awaiting_stewarding' | 'closed' | 'cancelled') => { + const sessions = [createMockSession({ raceEventId: id, sessionType: SessionType.main() })]; + const baseProps = { + id, + seasonId, + leagueId, + name: name ?? 'Test Race Event', + sessions, + }; + if (status) { + return RaceEvent.create({ ...baseProps, status }); + } + return RaceEvent.create(baseProps); + }; + + beforeEach(() => { + mockLogger = { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; + repository = new InMemoryRaceEventRepository(mockLogger); + }); + + describe('constructor', () => { + it('should initialize with a logger', () => { + expect(repository).toBeDefined(); + expect(mockLogger.info).toHaveBeenCalledWith('InMemoryRaceEventRepository initialized.'); + }); + }); + + describe('findById', () => { + it('should return null if race event not found', async () => { + const result = await repository.findById('nonexistent'); + expect(result).toBeNull(); + expect(mockLogger.debug).toHaveBeenCalledWith('Finding race event by id: nonexistent'); + expect(mockLogger.warn).toHaveBeenCalledWith('Race event with id nonexistent not found.'); + }); + + it('should return the race event if found', async () => { + const raceEvent = createTestRaceEvent('1', 'season1', 'league1'); + await repository.create(raceEvent); + + const result = await repository.findById('1'); + expect(result).toEqual(raceEvent); + expect(mockLogger.info).toHaveBeenCalledWith('Found race event: 1'); + }); + }); + + describe('findAll', () => { + it('should return all race events', async () => { + const raceEvent1 = createTestRaceEvent('1', 'season1', 'league1'); + const raceEvent2 = createTestRaceEvent('2', 'season2', 'league2'); + await repository.create(raceEvent1); + await repository.create(raceEvent2); + + const result = await repository.findAll(); + expect(result).toHaveLength(2); + expect(result).toContain(raceEvent1); + expect(result).toContain(raceEvent2); + expect(mockLogger.info).toHaveBeenCalledWith('Found 2 race events.'); + }); + }); + + describe('findBySeasonId', () => { + it('should return race events filtered by season ID', async () => { + const raceEvent1 = createTestRaceEvent('1', 'season1', 'league1'); + const raceEvent2 = createTestRaceEvent('2', 'season2', 'league1'); + await repository.create(raceEvent1); + await repository.create(raceEvent2); + + const result = await repository.findBySeasonId('season1'); + expect(result).toHaveLength(1); + expect(result[0]).toEqual(raceEvent1); + }); + }); + + describe('findByLeagueId', () => { + it('should return race events filtered by league ID', async () => { + const raceEvent1 = createTestRaceEvent('1', 'season1', 'league1'); + const raceEvent2 = createTestRaceEvent('2', 'season1', 'league2'); + await repository.create(raceEvent1); + await repository.create(raceEvent2); + + const result = await repository.findByLeagueId('league1'); + expect(result).toHaveLength(1); + expect(result[0]).toEqual(raceEvent1); + }); + }); + + describe('findByStatus', () => { + it('should return race events filtered by status', async () => { + const raceEvent1 = createTestRaceEvent('1', 'season1', 'league1'); + const raceEvent2 = createTestRaceEvent('2', 'season1', 'league1', 'Test', 'in_progress'); + await repository.create(raceEvent1); + await repository.create(raceEvent2); + + const result = await repository.findByStatus('scheduled'); + expect(result).toHaveLength(1); + expect(result[0]).toEqual(raceEvent1); + }); + }); + + describe('findAwaitingStewardingClose', () => { + it('should return race events awaiting stewarding close', async () => { + const pastDate = new Date(Date.now() - 3600000); + const sessions = [createMockSession({ raceEventId: '1', sessionType: SessionType.main() })]; + const raceEvent = RaceEvent.create({ + id: '1', + seasonId: 'season1', + leagueId: 'league1', + name: 'Test', + sessions, + status: 'awaiting_stewarding', + stewardingClosesAt: pastDate, + }); + await repository.create(raceEvent); + + const result = await repository.findAwaitingStewardingClose(); + expect(result).toHaveLength(1); + expect(result[0]).toEqual(raceEvent); + }); + + it('should not return race events not awaiting stewarding', async () => { + const raceEvent = createTestRaceEvent('1', 'season1', 'league1'); + await repository.create(raceEvent); + + const result = await repository.findAwaitingStewardingClose(); + expect(result).toHaveLength(0); + }); + }); + + describe('create', () => { + it('should create a new race event', async () => { + const raceEvent = createTestRaceEvent('1', 'season1', 'league1'); + await repository.create(raceEvent); + expect(mockLogger.info).toHaveBeenCalledWith('Race event 1 created successfully.'); + }); + }); + + describe('update', () => { + it('should update an existing race event', async () => { + const raceEvent = createTestRaceEvent('1', 'season1', 'league1'); + await repository.create(raceEvent); + + const updatedRaceEvent = raceEvent.start(); + await repository.update(updatedRaceEvent); + expect(mockLogger.info).toHaveBeenCalledWith('Race event 1 updated successfully.'); + }); + + it('should create new if not exists', async () => { + const raceEvent = createTestRaceEvent('1', 'season1', 'league1'); + await repository.update(raceEvent); + expect(mockLogger.warn).toHaveBeenCalledWith('Race event with id 1 not found for update. Creating new.'); + }); + }); + + describe('delete', () => { + it('should delete an existing race event', async () => { + const raceEvent = createTestRaceEvent('1', 'season1', 'league1'); + await repository.create(raceEvent); + + await repository.delete('1'); + expect(mockLogger.info).toHaveBeenCalledWith('Race event 1 deleted successfully.'); + }); + + it('should warn if race event not found', async () => { + await repository.delete('nonexistent'); + expect(mockLogger.warn).toHaveBeenCalledWith('Race event with id nonexistent not found for deletion.'); + }); + }); + + describe('exists', () => { + it('should return true if race event exists', async () => { + const raceEvent = createTestRaceEvent('1', 'season1', 'league1'); + await repository.create(raceEvent); + + const result = await repository.exists('1'); + expect(result).toBe(true); + }); + + it('should return false if race event does not exist', async () => { + const result = await repository.exists('nonexistent'); + expect(result).toBe(false); + }); + }); + + describe('clear', () => { + it('should clear all race events', () => { + repository.clear(); + expect(mockLogger.debug).toHaveBeenCalledWith('Clearing all race events.'); + expect(mockLogger.info).toHaveBeenCalledWith('All race events cleared.'); + }); + }); + + describe('getAll', () => { + it('should return all race events', () => { + const raceEvent = createTestRaceEvent('1', 'season1', 'league1'); + repository.create(raceEvent); + + const result = repository.getAll(); + expect(result).toHaveLength(1); + expect(result[0]).toEqual(raceEvent); + }); + }); +}); \ No newline at end of file diff --git a/adapters/racing/persistence/inmemory/InMemoryRaceEventRepository.ts b/adapters/racing/persistence/inmemory/InMemoryRaceEventRepository.ts index c165c549a..aa8a9294c 100644 --- a/adapters/racing/persistence/inmemory/InMemoryRaceEventRepository.ts +++ b/adapters/racing/persistence/inmemory/InMemoryRaceEventRepository.ts @@ -1,21 +1,17 @@ /** * In-memory implementation of IRaceEventRepository for development/testing. */ -import type { IRaceEventRepository } from '../../domain/repositories/IRaceEventRepository'; -import type { RaceEvent } from '../../domain/entities/RaceEvent'; +import type { IRaceEventRepository } from '@core/racing/domain/repositories/IRaceEventRepository'; +import type { RaceEvent } from '@core/racing/domain/entities/RaceEvent'; import type { Logger } from '@core/shared/application'; export class InMemoryRaceEventRepository implements IRaceEventRepository { private raceEvents: Map = new Map(); private readonly logger: Logger; - constructor(logger: Logger, seedData?: RaceEvent[]) { + constructor(logger: Logger) { this.logger = logger; this.logger.info('InMemoryRaceEventRepository initialized.'); - if (seedData) { - seedData.forEach(event => this.raceEvents.set(event.id, event)); - this.logger.debug(`Seeded ${seedData.length} race events.`); - } } async findById(id: string): Promise { diff --git a/adapters/racing/persistence/inmemory/InMemoryRaceRegistrationRepository.test.ts b/adapters/racing/persistence/inmemory/InMemoryRaceRegistrationRepository.test.ts new file mode 100644 index 000000000..653973531 --- /dev/null +++ b/adapters/racing/persistence/inmemory/InMemoryRaceRegistrationRepository.test.ts @@ -0,0 +1,178 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import { InMemoryRaceRegistrationRepository } from './InMemoryRaceRegistrationRepository'; +import { RaceRegistration } from '@core/racing/domain/entities/RaceRegistration'; +import type { Logger } from '@core/shared/application'; + +describe('InMemoryRaceRegistrationRepository', () => { + let repository: InMemoryRaceRegistrationRepository; + let mockLogger: Logger; + + const createTestRegistration = (raceId: string, driverId: string, id?: string) => { + const props: { raceId: string; driverId: string; id?: string } = { + raceId, + driverId, + }; + if (id) { + props.id = id; + } + return RaceRegistration.create(props); + }; + + beforeEach(() => { + mockLogger = { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; + repository = new InMemoryRaceRegistrationRepository(mockLogger); + }); + + describe('constructor', () => { + it('should initialize with a logger', () => { + expect(repository).toBeDefined(); + expect(mockLogger.info).toHaveBeenCalledWith('InMemoryRaceRegistrationRepository initialized.'); + }); + }); + + describe('isRegistered', () => { + it('should return false if driver is not registered for race', async () => { + const result = await repository.isRegistered('race1', 'driver1'); + expect(result).toBe(false); + expect(mockLogger.debug).toHaveBeenCalledWith('[InMemoryRaceRegistrationRepository] Checking if driver driver1 is registered for race race1.'); + }); + + it('should return true if driver is registered for race', async () => { + const registration = createTestRegistration('race1', 'driver1'); + await repository.register(registration); + + const result = await repository.isRegistered('race1', 'driver1'); + expect(result).toBe(true); + }); + }); + + describe('getRegisteredDrivers', () => { + it('should return empty array if no drivers registered for race', async () => { + const result = await repository.getRegisteredDrivers('race1'); + expect(result).toEqual([]); + expect(mockLogger.debug).toHaveBeenCalledWith('[InMemoryRaceRegistrationRepository] Getting registered drivers for race race1.'); + expect(mockLogger.info).toHaveBeenCalledWith('Found 0 registered drivers for race race1.'); + }); + + it('should return registered drivers for race', async () => { + const registration1 = createTestRegistration('race1', 'driver1'); + const registration2 = createTestRegistration('race1', 'driver2'); + const registration3 = createTestRegistration('race2', 'driver1'); + await repository.register(registration1); + await repository.register(registration2); + await repository.register(registration3); + + const result = await repository.getRegisteredDrivers('race1'); + expect(result).toHaveLength(2); + expect(result).toContain('driver1'); + expect(result).toContain('driver2'); + }); + }); + + describe('getRegistrationCount', () => { + it('should return 0 if no registrations for race', async () => { + const result = await repository.getRegistrationCount('race1'); + expect(result).toBe(0); + expect(mockLogger.debug).toHaveBeenCalledWith('[InMemoryRaceRegistrationRepository] Getting registration count for race race1.'); + expect(mockLogger.info).toHaveBeenCalledWith('Registration count for race race1: 0.'); + }); + + it('should return correct count of registrations for race', async () => { + const registration1 = createTestRegistration('race1', 'driver1'); + const registration2 = createTestRegistration('race1', 'driver2'); + await repository.register(registration1); + await repository.register(registration2); + + const result = await repository.getRegistrationCount('race1'); + expect(result).toBe(2); + }); + }); + + describe('register', () => { + it('should register a driver for a race', async () => { + const registration = createTestRegistration('race1', 'driver1'); + + await repository.register(registration); + + const isRegistered = await repository.isRegistered('race1', 'driver1'); + expect(isRegistered).toBe(true); + expect(mockLogger.debug).toHaveBeenCalledWith('[InMemoryRaceRegistrationRepository] Registering driver driver1 for race race1.'); + expect(mockLogger.info).toHaveBeenCalledWith('Driver driver1 registered for race race1.'); + }); + + it('should throw error if driver already registered', async () => { + const registration = createTestRegistration('race1', 'driver1'); + await repository.register(registration); + + await expect(repository.register(registration)).rejects.toThrow('Driver already registered for this race'); + expect(mockLogger.warn).toHaveBeenCalledWith('Driver driver1 already registered for race race1.'); + }); + }); + + describe('withdraw', () => { + it('should withdraw a driver from a race', async () => { + const registration = createTestRegistration('race1', 'driver1'); + await repository.register(registration); + + await repository.withdraw('race1', 'driver1'); + + const isRegistered = await repository.isRegistered('race1', 'driver1'); + expect(isRegistered).toBe(false); + expect(mockLogger.debug).toHaveBeenCalledWith('[InMemoryRaceRegistrationRepository] Withdrawing driver driver1 from race race1.'); + expect(mockLogger.info).toHaveBeenCalledWith('Driver driver1 withdrawn from race race1.'); + }); + + it('should throw error if driver not registered', async () => { + await expect(repository.withdraw('race1', 'driver1')).rejects.toThrow('Driver not registered for this race'); + expect(mockLogger.warn).toHaveBeenCalledWith('Driver driver1 not registered for race race1. No withdrawal needed.'); + }); + }); + + describe('getDriverRegistrations', () => { + it('should return empty array if driver has no registrations', async () => { + const result = await repository.getDriverRegistrations('driver1'); + expect(result).toEqual([]); + expect(mockLogger.debug).toHaveBeenCalledWith('[InMemoryRaceRegistrationRepository] Getting registrations for driver: driver1.'); + expect(mockLogger.info).toHaveBeenCalledWith('Found 0 registrations for driver driver1.'); + }); + + it('should return race IDs for driver registrations', async () => { + const registration1 = createTestRegistration('race1', 'driver1'); + const registration2 = createTestRegistration('race2', 'driver1'); + const registration3 = createTestRegistration('race1', 'driver2'); + await repository.register(registration1); + await repository.register(registration2); + await repository.register(registration3); + + const result = await repository.getDriverRegistrations('driver1'); + expect(result).toHaveLength(2); + expect(result).toContain('race1'); + expect(result).toContain('race2'); + }); + }); + + describe('clearRaceRegistrations', () => { + it('should clear all registrations for a race', async () => { + const registration1 = createTestRegistration('race1', 'driver1'); + const registration2 = createTestRegistration('race1', 'driver2'); + const registration3 = createTestRegistration('race2', 'driver1'); + await repository.register(registration1); + await repository.register(registration2); + await repository.register(registration3); + + await repository.clearRaceRegistrations('race1'); + + const count = await repository.getRegistrationCount('race1'); + expect(count).toBe(0); + const race2Count = await repository.getRegistrationCount('race2'); + expect(race2Count).toBe(1); + expect(mockLogger.debug).toHaveBeenCalledWith('[InMemoryRaceRegistrationRepository] Clearing all registrations for race: race1.'); + expect(mockLogger.info).toHaveBeenCalledWith('Cleared 2 registrations for race race1.'); + }); + }); +}); \ No newline at end of file diff --git a/adapters/racing/persistence/inmemory/InMemoryRaceRegistrationRepository.ts b/adapters/racing/persistence/inmemory/InMemoryRaceRegistrationRepository.ts index c85969d92..ce1437bb3 100644 --- a/adapters/racing/persistence/inmemory/InMemoryRaceRegistrationRepository.ts +++ b/adapters/racing/persistence/inmemory/InMemoryRaceRegistrationRepository.ts @@ -5,12 +5,8 @@ import { Logger } from '@core/shared/application'; export class InMemoryRaceRegistrationRepository implements IRaceRegistrationRepository { private registrations: Map = new Map(); // Key: `${raceId}:${driverId}` - constructor(private readonly logger: Logger, initialRegistrations: RaceRegistration[] = []) { + constructor(private readonly logger: Logger) { this.logger.info('InMemoryRaceRegistrationRepository initialized.'); - for (const reg of initialRegistrations) { - this.registrations.set(reg.id, reg); - this.logger.debug(`Seeded registration: ${reg.id}.`); - } } async isRegistered(raceId: string, driverId: string): Promise { @@ -23,8 +19,8 @@ export class InMemoryRaceRegistrationRepository implements IRaceRegistrationRepo this.logger.debug(`[InMemoryRaceRegistrationRepository] Getting registered drivers for race ${raceId}.`); const driverIds: string[] = []; for (const registration of this.registrations.values()) { - if (registration.raceId === raceId) { - driverIds.push(registration.driverId); + if (registration.raceId.toString() === raceId) { + driverIds.push(registration.driverId.toString()); } } this.logger.info(`Found ${driverIds.length} registered drivers for race ${raceId}.`); @@ -33,19 +29,19 @@ export class InMemoryRaceRegistrationRepository implements IRaceRegistrationRepo async getRegistrationCount(raceId: string): Promise { this.logger.debug(`[InMemoryRaceRegistrationRepository] Getting registration count for race ${raceId}.`); - const count = Array.from(this.registrations.values()).filter(reg => reg.raceId === raceId).length; + const count = Array.from(this.registrations.values()).filter(reg => reg.raceId.toString() === raceId).length; this.logger.info(`Registration count for race ${raceId}: ${count}.`); return Promise.resolve(count); } async register(registration: RaceRegistration): Promise { - this.logger.debug(`[InMemoryRaceRegistrationRepository] Registering driver ${registration.driverId} for race ${registration.raceId}.`); - if (await this.isRegistered(registration.raceId, registration.driverId)) { - this.logger.warn(`Driver ${registration.driverId} already registered for race ${registration.raceId}.`); + this.logger.debug(`[InMemoryRaceRegistrationRepository] Registering driver ${registration.driverId.toString()} for race ${registration.raceId.toString()}.`); + if (await this.isRegistered(registration.raceId.toString(), registration.driverId.toString())) { + this.logger.warn(`Driver ${registration.driverId.toString()} already registered for race ${registration.raceId.toString()}.`); throw new Error('Driver already registered for this race'); } this.registrations.set(registration.id, registration); - this.logger.info(`Driver ${registration.driverId} registered for race ${registration.raceId}.`); + this.logger.info(`Driver ${registration.driverId.toString()} registered for race ${registration.raceId.toString()}.`); return Promise.resolve(); } @@ -65,8 +61,8 @@ export class InMemoryRaceRegistrationRepository implements IRaceRegistrationRepo this.logger.debug(`[InMemoryRaceRegistrationRepository] Getting registrations for driver: ${driverId}.`); const raceIds: string[] = []; for (const registration of this.registrations.values()) { - if (registration.driverId === driverId) { - raceIds.push(registration.raceId); + if (registration.driverId.toString() === driverId) { + raceIds.push(registration.raceId.toString()); } } this.logger.info(`Found ${raceIds.length} registrations for driver ${driverId}.`); @@ -77,7 +73,7 @@ export class InMemoryRaceRegistrationRepository implements IRaceRegistrationRepo this.logger.debug(`[InMemoryRaceRegistrationRepository] Clearing all registrations for race: ${raceId}.`); const registrationsToDelete: string[] = []; for (const registration of this.registrations.values()) { - if (registration.raceId === raceId) { + if (registration.raceId.toString() === raceId) { registrationsToDelete.push(registration.id); } } diff --git a/adapters/racing/persistence/inmemory/InMemoryRaceRepository.test.ts b/adapters/racing/persistence/inmemory/InMemoryRaceRepository.test.ts new file mode 100644 index 000000000..f218f732b --- /dev/null +++ b/adapters/racing/persistence/inmemory/InMemoryRaceRepository.test.ts @@ -0,0 +1,224 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import { InMemoryRaceRepository } from './InMemoryRaceRepository'; +import { Race, RaceStatus } from '@core/racing/domain/entities/Race'; +import type { Logger } from '@core/shared/application'; + +describe('InMemoryRaceRepository', () => { + let repository: InMemoryRaceRepository; + let mockLogger: Logger; + + beforeEach(() => { + mockLogger = { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; + repository = new InMemoryRaceRepository(mockLogger); + }); + + const createTestRace = ( + id: string, + leagueId: string, + track: string, + car: string, + scheduledAt: Date, + status: RaceStatus = 'scheduled' + ) => { + return Race.create({ + id, + leagueId, + scheduledAt, + track, + car, + status, + }); + }; + + describe('constructor', () => { + it('should initialize with a logger', () => { + expect(repository).toBeDefined(); + expect(mockLogger.info).toHaveBeenCalledWith('InMemoryRaceRepository initialized.'); + }); + }); + + describe('findById', () => { + it('should return null if race not found', async () => { + const result = await repository.findById('nonexistent'); + expect(result).toBeNull(); + expect(mockLogger.debug).toHaveBeenCalledWith('[InMemoryRaceRepository] Finding race by ID: nonexistent'); + expect(mockLogger.warn).toHaveBeenCalledWith('Race with ID nonexistent not found.'); + }); + + it('should return the race if found', async () => { + const race = createTestRace('1', 'league1', 'Track1', 'Car1', new Date()); + await repository.create(race); + + const result = await repository.findById('1'); + expect(result).toEqual(race); + expect(mockLogger.info).toHaveBeenCalledWith('Found race by ID: 1.'); + }); + }); + + describe('findAll', () => { + it('should return all races', async () => { + const race1 = createTestRace('1', 'league1', 'Track1', 'Car1', new Date()); + const race2 = createTestRace('2', 'league2', 'Track2', 'Car2', new Date()); + await repository.create(race1); + await repository.create(race2); + + const result = await repository.findAll(); + expect(result).toHaveLength(2); + expect(result).toContain(race1); + expect(result).toContain(race2); + }); + }); + + describe('findByLeagueId', () => { + it('should return races filtered by league ID', async () => { + const race1 = createTestRace('1', 'league1', 'Track1', 'Car1', new Date()); + const race2 = createTestRace('2', 'league2', 'Track2', 'Car2', new Date()); + const race3 = createTestRace('3', 'league1', 'Track3', 'Car3', new Date()); + await repository.create(race1); + await repository.create(race2); + await repository.create(race3); + + const result = await repository.findByLeagueId('league1'); + expect(result).toHaveLength(2); + expect(result).toContain(race1); + expect(result).toContain(race3); + }); + }); + + describe('findUpcomingByLeagueId', () => { + it('should return upcoming races for league ID', async () => { + const pastDate = new Date(Date.now() - 1000 * 60 * 60); // 1 hour ago + const futureDate = new Date(Date.now() + 1000 * 60 * 60); // 1 hour from now + const race1 = createTestRace('1', 'league1', 'Track1', 'Car1', pastDate, 'scheduled'); + const race2 = createTestRace('2', 'league1', 'Track2', 'Car2', futureDate, 'scheduled'); + const race3 = createTestRace('3', 'league1', 'Track3', 'Car3', futureDate, 'completed'); + await repository.create(race1); + await repository.create(race2); + await repository.create(race3); + + const result = await repository.findUpcomingByLeagueId('league1'); + expect(result).toHaveLength(1); + expect(result).toContain(race2); + }); + }); + + describe('findCompletedByLeagueId', () => { + it('should return completed races for league ID', async () => { + const race1 = createTestRace('1', 'league1', 'Track1', 'Car1', new Date(), 'scheduled'); + const race2 = createTestRace('2', 'league1', 'Track2', 'Car2', new Date(), 'completed'); + const race3 = createTestRace('3', 'league1', 'Track3', 'Car3', new Date(), 'completed'); + await repository.create(race1); + await repository.create(race2); + await repository.create(race3); + + const result = await repository.findCompletedByLeagueId('league1'); + expect(result).toHaveLength(2); + expect(result).toContain(race2); + expect(result).toContain(race3); + }); + }); + + describe('findByStatus', () => { + it('should return races filtered by status', async () => { + const race1 = createTestRace('1', 'league1', 'Track1', 'Car1', new Date(), 'scheduled'); + const race2 = createTestRace('2', 'league1', 'Track2', 'Car2', new Date(), 'completed'); + const race3 = createTestRace('3', 'league1', 'Track3', 'Car3', new Date(), 'scheduled'); + await repository.create(race1); + await repository.create(race2); + await repository.create(race3); + + const result = await repository.findByStatus('scheduled'); + expect(result).toHaveLength(2); + expect(result).toContain(race1); + expect(result).toContain(race3); + }); + }); + + describe('findByDateRange', () => { + it('should return races within date range', async () => { + const date1 = new Date('2023-01-01'); + const date2 = new Date('2023-01-02'); + const date3 = new Date('2023-01-03'); + const race1 = createTestRace('1', 'league1', 'Track1', 'Car1', date1); + const race2 = createTestRace('2', 'league1', 'Track2', 'Car2', date2); + const race3 = createTestRace('3', 'league1', 'Track3', 'Car3', date3); + await repository.create(race1); + await repository.create(race2); + await repository.create(race3); + + const result = await repository.findByDateRange(date1, date2); + expect(result).toHaveLength(2); + expect(result).toContain(race1); + expect(result).toContain(race2); + }); + }); + + describe('create', () => { + it('should create a new race', async () => { + const race = createTestRace('1', 'league1', 'Track1', 'Car1', new Date()); + const result = await repository.create(race); + expect(result).toEqual(race); + expect(mockLogger.info).toHaveBeenCalledWith('Race 1 created successfully.'); + }); + + it('should throw error if race already exists', async () => { + const race = createTestRace('1', 'league1', 'Track1', 'Car1', new Date()); + await repository.create(race); + await expect(repository.create(race)).rejects.toThrow('Race already exists'); + }); + }); + + describe('update', () => { + it('should update an existing race', async () => { + const race = createTestRace('1', 'league1', 'Track1', 'Car1', new Date()); + await repository.create(race); + + const updatedRace = race.complete(); + const result = await repository.update(updatedRace); + expect(result).toEqual(updatedRace); + expect(mockLogger.info).toHaveBeenCalledWith('Race 1 updated successfully.'); + }); + + it('should throw error if race does not exist', async () => { + const race = createTestRace('1', 'league1', 'Track1', 'Car1', new Date()); + await expect(repository.update(race)).rejects.toThrow('Race not found'); + }); + }); + + describe('delete', () => { + it('should delete an existing race', async () => { + const race = createTestRace('1', 'league1', 'Track1', 'Car1', new Date()); + await repository.create(race); + + await repository.delete('1'); + expect(mockLogger.info).toHaveBeenCalledWith('Race 1 deleted successfully.'); + const found = await repository.findById('1'); + expect(found).toBeNull(); + }); + + it('should not throw if race does not exist', async () => { + await repository.delete('nonexistent'); + expect(mockLogger.warn).toHaveBeenCalledWith('Race with ID nonexistent not found for deletion.'); + }); + }); + + describe('exists', () => { + it('should return true if race exists', async () => { + const race = createTestRace('1', 'league1', 'Track1', 'Car1', new Date()); + await repository.create(race); + + const result = await repository.exists('1'); + expect(result).toBe(true); + }); + + it('should return false if race does not exist', async () => { + const result = await repository.exists('nonexistent'); + expect(result).toBe(false); + }); + }); +}); \ No newline at end of file diff --git a/adapters/racing/persistence/inmemory/InMemoryRaceRepository.ts b/adapters/racing/persistence/inmemory/InMemoryRaceRepository.ts index 397af855a..df22b9273 100644 --- a/adapters/racing/persistence/inmemory/InMemoryRaceRepository.ts +++ b/adapters/racing/persistence/inmemory/InMemoryRaceRepository.ts @@ -5,12 +5,8 @@ import { Logger } from '@core/shared/application'; export class InMemoryRaceRepository implements IRaceRepository { private races: Map = new Map(); - constructor(private readonly logger: Logger, initialRaces: Race[] = []) { + constructor(private readonly logger: Logger) { this.logger.info('InMemoryRaceRepository initialized.'); - for (const race of initialRaces) { - this.races.set(race.id, race); - this.logger.debug(`Seeded race: ${race.id} (${race.track}).`); - } } async findById(id: string): Promise { diff --git a/adapters/racing/persistence/inmemory/InMemoryResultRepository.test.ts b/adapters/racing/persistence/inmemory/InMemoryResultRepository.test.ts new file mode 100644 index 000000000..74596edc8 --- /dev/null +++ b/adapters/racing/persistence/inmemory/InMemoryResultRepository.test.ts @@ -0,0 +1,257 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import { InMemoryResultRepository } from './InMemoryResultRepository'; +import { Result } from '@core/racing/domain/entities/result/Result'; +import type { Logger } from '@core/shared/application'; +import type { IRaceRepository } from '@core/racing/domain/repositories/IRaceRepository'; + +describe('InMemoryResultRepository', () => { + let repository: InMemoryResultRepository; + let mockLogger: Logger; + let mockRaceRepository: IRaceRepository; + + beforeEach(() => { + mockLogger = { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; + mockRaceRepository = { + findById: vi.fn(), + findAll: vi.fn(), + findByLeagueId: vi.fn(), + findUpcomingByLeagueId: vi.fn(), + findCompletedByLeagueId: vi.fn(), + findByStatus: vi.fn(), + findByDateRange: vi.fn(), + create: vi.fn(), + update: vi.fn(), + delete: vi.fn(), + exists: vi.fn(), + } as any; // eslint-disable-line @typescript-eslint/no-explicit-any + repository = new InMemoryResultRepository(mockLogger, mockRaceRepository); + }); + + const createTestResult = ( + id: string, + raceId: string, + driverId: string, + position: number, + fastestLap: number = 120.5, + incidents: number = 0, + startPosition: number = position + ) => { + return Result.create({ + id, + raceId, + driverId, + position, + fastestLap, + incidents, + startPosition, + }); + }; + + describe('constructor', () => { + it('should initialize with a logger and race repository', () => { + expect(repository).toBeDefined(); + expect(mockLogger.info).toHaveBeenCalledWith('[InMemoryResultRepository] Initialized.'); + }); + }); + + describe('findById', () => { + it('should return null if result not found', async () => { + const result = await repository.findById('nonexistent'); + expect(result).toBeNull(); + expect(mockLogger.debug).toHaveBeenCalledWith('[InMemoryResultRepository] Finding result by id: nonexistent'); + expect(mockLogger.warn).toHaveBeenCalledWith('[InMemoryResultRepository] Result with id nonexistent not found.'); + }); + + it('should return the result if found', async () => { + const result = createTestResult('1', 'race1', 'driver1', 1); + await repository.create(result); + + const found = await repository.findById('1'); + expect(found).toEqual(result); + expect(mockLogger.info).toHaveBeenCalledWith('[InMemoryResultRepository] Found result with id: 1.'); + }); + }); + + describe('findAll', () => { + it('should return all results', async () => { + const result1 = createTestResult('1', 'race1', 'driver1', 1); + const result2 = createTestResult('2', 'race1', 'driver2', 2); + await repository.create(result1); + await repository.create(result2); + + const results = await repository.findAll(); + expect(results).toHaveLength(2); + expect(results).toContain(result1); + expect(results).toContain(result2); + }); + }); + + describe('findByRaceId', () => { + it('should return results filtered by race ID', async () => { + const result1 = createTestResult('1', 'race1', 'driver1', 1); + const result2 = createTestResult('2', 'race2', 'driver2', 1); + const result3 = createTestResult('3', 'race1', 'driver3', 2); + await repository.create(result1); + await repository.create(result2); + await repository.create(result3); + + const results = await repository.findByRaceId('race1'); + expect(results).toHaveLength(2); + expect(results).toContain(result1); + expect(results).toContain(result3); + expect(results[0]?.id).toBe('1'); // sorted by position + expect(results[1]?.id).toBe('3'); + }); + }); + + describe('findByDriverId', () => { + it('should return results filtered by driver ID', async () => { + const result1 = createTestResult('1', 'race1', 'driver1', 1); + const result2 = createTestResult('2', 'race2', 'driver1', 2); + const result3 = createTestResult('3', 'race1', 'driver2', 1); + await repository.create(result1); + await repository.create(result2); + await repository.create(result3); + + const results = await repository.findByDriverId('driver1'); + expect(results).toHaveLength(2); + expect(results).toContain(result1); + expect(results).toContain(result2); + }); + }); + + describe('findByDriverIdAndLeagueId', () => { + it('should return results for driver in league', async () => { + const result1 = createTestResult('1', 'race1', 'driver1', 1); + const result2 = createTestResult('2', 'race2', 'driver1', 2); + await repository.create(result1); + await repository.create(result2); + + (mockRaceRepository.findByLeagueId as any).mockResolvedValue([ // eslint-disable-line @typescript-eslint/no-explicit-any + { id: 'race1' } as { id: string }, + ]); + + const results = await repository.findByDriverIdAndLeagueId('driver1', 'league1'); + expect(results).toHaveLength(1); + expect(results).toContain(result1); + }); + + it('should return empty if no race repository', async () => { + repository = new InMemoryResultRepository(mockLogger); // no race repo + const results = await repository.findByDriverIdAndLeagueId('driver1', 'league1'); + expect(results).toHaveLength(0); + }); + }); + + describe('create', () => { + it('should create a new result', async () => { + const result = createTestResult('1', 'race1', 'driver1', 1); + const created = await repository.create(result); + expect(created).toEqual(result); + expect(mockLogger.info).toHaveBeenCalledWith('[InMemoryResultRepository] Result 1 created successfully.'); + }); + + it('should throw error if result already exists', async () => { + const result = createTestResult('1', 'race1', 'driver1', 1); + await repository.create(result); + await expect(repository.create(result)).rejects.toThrow('Result with ID 1 already exists'); + }); + }); + + describe('createMany', () => { + it('should create multiple results', async () => { + const results = [ + createTestResult('1', 'race1', 'driver1', 1), + createTestResult('2', 'race1', 'driver2', 2), + ]; + const created = await repository.createMany(results); + expect(created).toHaveLength(2); + expect(created).toContain(results[0]); + expect(created).toContain(results[1]); + }); + }); + + describe('update', () => { + it('should update an existing result', async () => { + const result = createTestResult('1', 'race1', 'driver1', 1); + await repository.create(result); + + const updated = createTestResult('1', 'race1', 'driver1', 2); + const result2 = await repository.update(updated); + expect(result2).toEqual(updated); + expect(mockLogger.info).toHaveBeenCalledWith('[InMemoryResultRepository] Result 1 updated successfully.'); + }); + + it('should throw error if result does not exist', async () => { + const result = createTestResult('1', 'race1', 'driver1', 1); + await expect(repository.update(result)).rejects.toThrow('Result with ID 1 not found'); + }); + }); + + describe('delete', () => { + it('should delete an existing result', async () => { + const result = createTestResult('1', 'race1', 'driver1', 1); + await repository.create(result); + + await repository.delete('1'); + expect(mockLogger.info).toHaveBeenCalledWith('[InMemoryResultRepository] Result 1 deleted successfully.'); + const found = await repository.findById('1'); + expect(found).toBeNull(); + }); + + it('should throw error if result does not exist', async () => { + await expect(repository.delete('nonexistent')).rejects.toThrow('Result with ID nonexistent not found'); + }); + }); + + describe('deleteByRaceId', () => { + it('should delete results for a race', async () => { + const result1 = createTestResult('1', 'race1', 'driver1', 1); + const result2 = createTestResult('2', 'race2', 'driver2', 1); + await repository.create(result1); + await repository.create(result2); + + await repository.deleteByRaceId('race1'); + expect(mockLogger.info).toHaveBeenCalledWith('[InMemoryResultRepository] Deleted 1 results for race id: race1.'); + const found = await repository.findById('1'); + expect(found).toBeNull(); + const found2 = await repository.findById('2'); + expect(found2).toEqual(result2); + }); + }); + + describe('exists', () => { + it('should return true if result exists', async () => { + const result = createTestResult('1', 'race1', 'driver1', 1); + await repository.create(result); + + const exists = await repository.exists('1'); + expect(exists).toBe(true); + }); + + it('should return false if result does not exist', async () => { + const exists = await repository.exists('nonexistent'); + expect(exists).toBe(false); + }); + }); + + describe('existsByRaceId', () => { + it('should return true if results exist for race', async () => { + const result = createTestResult('1', 'race1', 'driver1', 1); + await repository.create(result); + + const exists = await repository.existsByRaceId('race1'); + expect(exists).toBe(true); + }); + + it('should return false if no results for race', async () => { + const exists = await repository.existsByRaceId('nonexistent'); + expect(exists).toBe(false); + }); + }); +}); \ No newline at end of file diff --git a/adapters/racing/persistence/inmemory/InMemoryResultRepository.ts b/adapters/racing/persistence/inmemory/InMemoryResultRepository.ts index fa53f9f25..437d9178c 100644 --- a/adapters/racing/persistence/inmemory/InMemoryResultRepository.ts +++ b/adapters/racing/persistence/inmemory/InMemoryResultRepository.ts @@ -6,7 +6,7 @@ */ import { v4 as uuidv4 } from 'uuid'; -import { Result } from '@core/racing/domain/entities/Result'; +import { Result } from '@core/racing/domain/entities/result/Result'; import type { IResultRepository } from '@core/racing/domain/repositories/IResultRepository'; import type { IRaceRepository } from '@core/racing/domain/repositories/IRaceRepository'; import type { Logger } from '@core/shared/application'; @@ -16,214 +16,207 @@ export class InMemoryResultRepository implements IResultRepository { private raceRepository: IRaceRepository | null; private readonly logger: Logger; - constructor(logger: Logger, seedData?: Result[], raceRepository?: IRaceRepository | null) { + constructor(logger: Logger, raceRepository?: IRaceRepository | null) { this.logger = logger; - this.logger.info('InMemoryResultRepository initialized.'); + this.logger.info('[InMemoryResultRepository] Initialized.'); this.results = new Map(); this.raceRepository = raceRepository ?? null; - - if (seedData) { - seedData.forEach(result => { - this.results.set(result.id, result); - this.logger.debug(`Seeded result: ${result.id}`); - }); - } } async findById(id: string): Promise { - this.logger.debug(`Finding result by id: ${id}`); + this.logger.debug(`[InMemoryResultRepository] Finding result by id: ${id}`); try { const result = this.results.get(id) ?? null; if (result) { - this.logger.info(`Found result with id: ${id}.`); + this.logger.info(`[InMemoryResultRepository] Found result with id: ${id}.`); } else { - this.logger.warn(`Result with id ${id} not found.`); + this.logger.warn(`[InMemoryResultRepository] Result with id ${id} not found.`); } return result; } catch (error) { - this.logger.error(`Error finding result by id ${id}:`, error instanceof Error ? error : new Error(String(error))); + this.logger.error(`[InMemoryResultRepository] Error finding result by id ${id}:`, error instanceof Error ? error : new Error(String(error))); throw error; } } async findAll(): Promise { - this.logger.debug('Finding all results.'); + this.logger.debug('[InMemoryResultRepository] Finding all results.'); try { const results = Array.from(this.results.values()); - this.logger.info(`Found ${results.length} results.`); + this.logger.info(`[InMemoryResultRepository] Found ${results.length} results.`); return results; } catch (error) { - this.logger.error('Error finding all results:', error instanceof Error ? error : new Error(String(error))); + this.logger.error('[InMemoryResultRepository] Error finding all results:', error instanceof Error ? error : new Error(String(error))); throw error; } } async findByRaceId(raceId: string): Promise { - this.logger.debug(`Finding results for race id: ${raceId}`); + this.logger.debug(`[InMemoryResultRepository] Finding results for race id: ${raceId}`); try { const results = Array.from(this.results.values()) - .filter(result => result.raceId === raceId) - .sort((a, b) => a.position - b.position); - this.logger.info(`Found ${results.length} results for race id: ${raceId}.`); + .filter(result => result.raceId.toString() === raceId) + .sort((a, b) => a.position.toNumber() - b.position.toNumber()); + this.logger.info(`[InMemoryResultRepository] Found ${results.length} results for race id: ${raceId}.`); return results; } catch (error) { - this.logger.error(`Error finding results for race id ${raceId}:`, error instanceof Error ? error : new Error(String(error))); + this.logger.error(`[InMemoryResultRepository] Error finding results for race id ${raceId}:`, error instanceof Error ? error : new Error(String(error))); throw error; } } async findByDriverId(driverId: string): Promise { - this.logger.debug(`Finding results for driver id: ${driverId}`); + this.logger.debug(`[InMemoryResultRepository] Finding results for driver id: ${driverId}`); try { const results = Array.from(this.results.values()) - .filter(result => result.driverId === driverId); - this.logger.info(`Found ${results.length} results for driver id: ${driverId}.`); + .filter(result => result.driverId.toString() === driverId); + this.logger.info(`[InMemoryResultRepository] Found ${results.length} results for driver id: ${driverId}.`); return results; } catch (error) { - this.logger.error(`Error finding results for driver id ${driverId}:`, error instanceof Error ? error : new Error(String(error))); + this.logger.error(`[InMemoryResultRepository] Error finding results for driver id ${driverId}:`, error instanceof Error ? error : new Error(String(error))); throw error; } } async findByDriverIdAndLeagueId(driverId: string, leagueId: string): Promise { - this.logger.debug(`Finding results for driver id: ${driverId} and league id: ${leagueId}`); + this.logger.debug(`[InMemoryResultRepository] Finding results for driver id: ${driverId} and league id: ${leagueId}`); try { if (!this.raceRepository) { - this.logger.warn('Race repository not provided to InMemoryResultRepository. Skipping league-filtered search.'); + this.logger.warn('[InMemoryResultRepository] Race repository not provided to InMemoryResultRepository. Skipping league-filtered search.'); return []; } const leagueRaces = await this.raceRepository.findByLeagueId(leagueId); const leagueRaceIds = new Set(leagueRaces.map(race => race.id)); - this.logger.debug(`Found ${leagueRaces.length} races in league ${leagueId}.`); + this.logger.debug(`[InMemoryResultRepository] Found ${leagueRaces.length} races in league ${leagueId}.`); const results = Array.from(this.results.values()) .filter(result => - result.driverId === driverId && - leagueRaceIds.has(result.raceId) + result.driverId.toString() === driverId && + leagueRaceIds.has(result.raceId.toString()) ); - this.logger.info(`Found ${results.length} results for driver ${driverId} in league ${leagueId}.`); + this.logger.info(`[InMemoryResultRepository] Found ${results.length} results for driver ${driverId} in league ${leagueId}.`); return results; } catch (error) { - this.logger.error(`Error finding results for driver ${driverId} and league ${leagueId}:`, error instanceof Error ? error : new Error(String(error))); + this.logger.error(`[InMemoryResultRepository] Error finding results for driver ${driverId} and league ${leagueId}:`, error instanceof Error ? error : new Error(String(error))); throw error; } } async create(result: Result): Promise { - this.logger.debug(`Creating result: ${result.id}`); + this.logger.debug(`[InMemoryResultRepository] Creating result: ${result.id}`); try { if (await this.exists(result.id)) { - this.logger.warn(`Result with ID ${result.id} already exists. Throwing error.`); + this.logger.warn(`[InMemoryResultRepository] Result with ID ${result.id} already exists. Throwing error.`); throw new Error(`Result with ID ${result.id} already exists`); } this.results.set(result.id, result); - this.logger.info(`Result ${result.id} created successfully.`); + this.logger.info(`[InMemoryResultRepository] Result ${result.id} created successfully.`); return result; } catch (error) { - this.logger.error(`Error creating result ${result.id}:`, error instanceof Error ? error : new Error(String(error))); + this.logger.error(`[InMemoryResultRepository] Error creating result ${result.id}:`, error instanceof Error ? error : new Error(String(error))); throw error; } } async createMany(results: Result[]): Promise { - this.logger.debug(`Creating ${results.length} results.`); + this.logger.debug(`[InMemoryResultRepository] Creating ${results.length} results.`); try { const created: Result[] = []; - + for (const result of results) { if (await this.exists(result.id)) { - this.logger.warn(`Result with ID ${result.id} already exists. Skipping creation.`); + this.logger.warn(`[InMemoryResultRepository] Result with ID ${result.id} already exists. Skipping creation.`); // In a real system, decide if this should throw or log and skip continue; } this.results.set(result.id, result); created.push(result); } - this.logger.info(`Created ${created.length} results successfully.`); - + this.logger.info(`[InMemoryResultRepository] Created ${created.length} results successfully.`); + return created; } catch (error) { - this.logger.error(`Error creating many results:`, error instanceof Error ? error : new Error(String(error))); + this.logger.error(`[InMemoryResultRepository] Error creating many results:`, error instanceof Error ? error : new Error(String(error))); throw error; } } async update(result: Result): Promise { - this.logger.debug(`Updating result: ${result.id}`); + this.logger.debug(`[InMemoryResultRepository] Updating result: ${result.id}`); try { if (!await this.exists(result.id)) { - this.logger.warn(`Result with ID ${result.id} not found for update. Throwing error.`); + this.logger.warn(`[InMemoryResultRepository] Result with ID ${result.id} not found for update. Throwing error.`); throw new Error(`Result with ID ${result.id} not found`); } this.results.set(result.id, result); - this.logger.info(`Result ${result.id} updated successfully.`); + this.logger.info(`[InMemoryResultRepository] Result ${result.id} updated successfully.`); return result; } catch (error) { - this.logger.error(`Error updating result ${result.id}:`, error instanceof Error ? error : new Error(String(error))); + this.logger.error(`[InMemoryResultRepository] Error updating result ${result.id}:`, error instanceof Error ? error : new Error(String(error))); throw error; } } async delete(id: string): Promise { - this.logger.debug(`Deleting result: ${id}`); + this.logger.debug(`[InMemoryResultRepository] Deleting result: ${id}`); try { if (!await this.exists(id)) { - this.logger.warn(`Result with ID ${id} not found for deletion. Throwing error.`); + this.logger.warn(`[InMemoryResultRepository] Result with ID ${id} not found for deletion. Throwing error.`); throw new Error(`Result with ID ${id} not found`); } this.results.delete(id); - this.logger.info(`Result ${id} deleted successfully.`); + this.logger.info(`[InMemoryResultRepository] Result ${id} deleted successfully.`); } catch (error) { - this.logger.error(`Error deleting result ${id}:`, error instanceof Error ? error : new Error(String(error))); + this.logger.error(`[InMemoryResultRepository] Error deleting result ${id}:`, error instanceof Error ? error : new Error(String(error))); throw error; } } async deleteByRaceId(raceId: string): Promise { - this.logger.debug(`Deleting results for race id: ${raceId}`); + this.logger.debug(`[InMemoryResultRepository] Deleting results for race id: ${raceId}`); try { - const initialCount = this.results.size; const raceResults = Array.from(this.results.values()).filter( - result => result.raceId === raceId + result => result.raceId.toString() === raceId ); raceResults.forEach(result => { this.results.delete(result.id); }); - this.logger.info(`Deleted ${raceResults.length} results for race id: ${raceId}.`); + this.logger.info(`[InMemoryResultRepository] Deleted ${raceResults.length} results for race id: ${raceId}.`); } catch (error) { - this.logger.error(`Error deleting results for race id ${raceId}:`, error instanceof Error ? error : new Error(String(error))); + this.logger.error(`[InMemoryResultRepository] Error deleting results for race id ${raceId}:`, error instanceof Error ? error : new Error(String(error))); throw error; } } async exists(id: string): Promise { - this.logger.debug(`Checking existence of result with id: ${id}`); + this.logger.debug(`[InMemoryResultRepository] Checking existence of result with id: ${id}`); try { const exists = this.results.has(id); - this.logger.debug(`Result ${id} exists: ${exists}.`); + this.logger.debug(`[InMemoryResultRepository] Result ${id} exists: ${exists}.`); return exists; } catch (error) { - this.logger.error(`Error checking existence of result with id ${id}:`, error instanceof Error ? error : new Error(String(error))); + this.logger.error(`[InMemoryResultRepository] Error checking existence of result with id ${id}:`, error instanceof Error ? error : new Error(String(error))); throw error; } } async existsByRaceId(raceId: string): Promise { - this.logger.debug(`Checking existence of results for race id: ${raceId}`); + this.logger.debug(`[InMemoryResultRepository] Checking existence of results for race id: ${raceId}`); try { const exists = Array.from(this.results.values()).some( - result => result.raceId === raceId + result => result.raceId.toString() === raceId ); - this.logger.debug(`Results for race ${raceId} exist: ${exists}.`); + this.logger.debug(`[InMemoryResultRepository] Results for race ${raceId} exist: ${exists}.`); return exists; } catch (error) { - this.logger.error(`Error checking existence of results for race id ${raceId}:`, error instanceof Error ? error : new Error(String(error))); + this.logger.error(`[InMemoryResultRepository] Error checking existence of results for race id ${raceId}:`, error instanceof Error ? error : new Error(String(error))); throw error; } + } /** * Utility method to generate a new UUID diff --git a/adapters/racing/persistence/inmemory/InMemoryScoringRepositories.test.ts b/adapters/racing/persistence/inmemory/InMemoryScoringRepositories.test.ts new file mode 100644 index 000000000..51b8efb86 --- /dev/null +++ b/adapters/racing/persistence/inmemory/InMemoryScoringRepositories.test.ts @@ -0,0 +1,393 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import { + InMemoryGameRepository, + InMemorySeasonRepository, + InMemoryLeagueScoringConfigRepository, + InMemoryChampionshipStandingRepository, +} from './InMemoryScoringRepositories'; +import { Game } from '@core/racing/domain/entities/Game'; +import { Season } from '@core/racing/domain/entities/season/Season'; +import { LeagueScoringConfig } from '@core/racing/domain/entities/LeagueScoringConfig'; +import { ChampionshipStanding } from '@core/racing/domain/entities/championship/ChampionshipStanding'; +import type { Logger } from '@core/shared/application'; + +describe('InMemoryScoringRepositories', () => { + let mockLogger: Logger; + + beforeEach(() => { + mockLogger = { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; + }); + + describe('InMemoryGameRepository', () => { + let repository: InMemoryGameRepository; + + beforeEach(() => { + repository = new InMemoryGameRepository(mockLogger); + }); + + describe('constructor', () => { + it('should initialize with a logger', () => { + expect(repository).toBeDefined(); + expect(mockLogger.info).toHaveBeenCalledWith('InMemoryGameRepository initialized.'); + }); + }); + + describe('findById', () => { + it('should return null if game not found', async () => { + const result = await repository.findById('nonexistent'); + expect(result).toBeNull(); + expect(mockLogger.debug).toHaveBeenCalledWith('Finding game by id: nonexistent'); + expect(mockLogger.warn).toHaveBeenCalledWith('Game with id nonexistent not found.'); + }); + + it('should return the game if found', async () => { + const game = Game.create({ id: 'test-game', name: 'Test Game' }); + repository.seed(game); + const result = await repository.findById('test-game'); + expect(result).toEqual(game); + expect(mockLogger.info).toHaveBeenCalledWith(`Found game: ${game.id}`); + }); + }); + + describe('findAll', () => { + it('should return all games', async () => { + const game1 = Game.create({ id: 'game1', name: 'Game 1' }); + const game2 = Game.create({ id: 'game2', name: 'Game 2' }); + repository.seed(game1); + repository.seed(game2); + const result = await repository.findAll(); + expect(result).toEqual([game1, game2]); + expect(mockLogger.info).toHaveBeenCalledWith('Found 2 games.'); + }); + }); + }); + + describe('InMemorySeasonRepository', () => { + let repository: InMemorySeasonRepository; + + beforeEach(() => { + repository = new InMemorySeasonRepository(mockLogger); + }); + + describe('constructor', () => { + it('should initialize with a logger', () => { + expect(repository).toBeDefined(); + expect(mockLogger.info).toHaveBeenCalledWith('InMemorySeasonRepository initialized.'); + }); + }); + + describe('findById', () => { + it('should return null if season not found', async () => { + const result = await repository.findById('nonexistent'); + expect(result).toBeNull(); + expect(mockLogger.warn).toHaveBeenCalledWith('Season with id nonexistent not found.'); + }); + + it('should return the season if found', async () => { + const season = Season.create({ + id: 'test-season', + leagueId: 'league1', + gameId: 'game1', + name: 'Test Season', + year: 2025, + order: 1, + status: 'active', + startDate: new Date(), + endDate: new Date(), + }); + repository.seed(season); + const result = await repository.findById('test-season'); + expect(result).toEqual(season); + }); + }); + + describe('findByLeagueId', () => { + it('should return seasons for the league', async () => { + const season1 = Season.create({ + id: 'season1', + leagueId: 'league1', + gameId: 'game1', + name: 'Season 1', + year: 2025, + order: 1, + status: 'active', + startDate: new Date(), + endDate: new Date(), + }); + const season2 = Season.create({ + id: 'season2', + leagueId: 'league1', + gameId: 'game1', + name: 'Season 2', + year: 2025, + order: 2, + status: 'active', + startDate: new Date(), + endDate: new Date(), + }); + repository.seed(season1); + repository.seed(season2); + const result = await repository.findByLeagueId('league1'); + expect(result).toEqual([season1, season2]); + }); + }); + + describe('create', () => { + it('should create a season', async () => { + const season = Season.create({ + id: 'new-season', + leagueId: 'league1', + gameId: 'game1', + name: 'New Season', + year: 2025, + order: 1, + status: 'active', + startDate: new Date(), + endDate: new Date(), + }); + const result = await repository.create(season); + expect(result).toEqual(season); + const found = await repository.findById('new-season'); + expect(found).toEqual(season); + }); + }); + + describe('add', () => { + it('should add a season', async () => { + const season = Season.create({ + id: 'add-season', + leagueId: 'league1', + gameId: 'game1', + name: 'Add Season', + year: 2025, + order: 1, + status: 'active', + startDate: new Date(), + endDate: new Date(), + }); + await repository.add(season); + const found = await repository.findById('add-season'); + expect(found).toEqual(season); + }); + }); + + describe('update', () => { + it('should update an existing season', async () => { + const season = Season.create({ + id: 'update-season', + leagueId: 'league1', + gameId: 'game1', + name: 'Update Season', + year: 2025, + order: 1, + status: 'active', + startDate: new Date(), + endDate: new Date(), + }); + repository.seed(season); + const updatedSeason = { ...season, name: 'Updated Name' } as Season; + await repository.update(updatedSeason); + const found = await repository.findById('update-season'); + expect(found?.name).toBe('Updated Name'); + }); + }); + + describe('listByLeague', () => { + it('should list seasons by league', async () => { + const season = Season.create({ + id: 'list-season', + leagueId: 'league1', + gameId: 'game1', + name: 'List Season', + year: 2025, + order: 1, + status: 'active', + startDate: new Date(), + endDate: new Date(), + }); + repository.seed(season); + const result = await repository.listByLeague('league1'); + expect(result).toEqual([season]); + }); + }); + + describe('listActiveByLeague', () => { + it('should list active seasons by league', async () => { + const activeSeason = Season.create({ + id: 'active-season', + leagueId: 'league1', + gameId: 'game1', + name: 'Active Season', + year: 2025, + order: 1, + status: 'active', + startDate: new Date(), + endDate: new Date(), + }); + const inactiveSeason = Season.create({ + id: 'inactive-season', + leagueId: 'league1', + gameId: 'game1', + name: 'Inactive Season', + year: 2025, + order: 2, + status: 'completed', + startDate: new Date(), + endDate: new Date(), + }); + repository.seed(activeSeason); + repository.seed(inactiveSeason); + const result = await repository.listActiveByLeague('league1'); + expect(result).toEqual([activeSeason]); + }); + }); + }); + + describe('InMemoryLeagueScoringConfigRepository', () => { + let repository: InMemoryLeagueScoringConfigRepository; + + beforeEach(() => { + repository = new InMemoryLeagueScoringConfigRepository(mockLogger); + }); + + describe('constructor', () => { + it('should initialize with a logger', () => { + expect(repository).toBeDefined(); + expect(mockLogger.info).toHaveBeenCalledWith('InMemoryLeagueScoringConfigRepository initialized.'); + }); + }); + + describe('findBySeasonId', () => { + it('should return null if config not found', async () => { + const result = await repository.findBySeasonId('nonexistent'); + expect(result).toBeNull(); + expect(mockLogger.warn).toHaveBeenCalledWith('League scoring config for seasonId nonexistent not found.'); + }); + + it('should return the config if found', async () => { + const config = { + id: 'config1', + seasonId: 'season1', + scoringPresetId: 'preset1', + championships: [], + equals: vi.fn(), + } as unknown as LeagueScoringConfig; + repository.seed(config); + const result = await repository.findBySeasonId('season1'); + expect(result).toEqual(config); + }); + }); + + describe('save', () => { + it('should save a new config', async () => { + const config = { + id: 'new-config', + seasonId: 'season1', + scoringPresetId: 'preset1', + championships: [], + equals: vi.fn(), + } as unknown as LeagueScoringConfig; + const result = await repository.save(config); + expect(result).toEqual(config); + const found = await repository.findBySeasonId('season1'); + expect(found).toEqual(config); + }); + + it('should update an existing config', async () => { + const config = { + id: 'update-config', + seasonId: 'season1', + scoringPresetId: 'preset1', + championships: [], + equals: vi.fn(), + } as unknown as LeagueScoringConfig; + repository.seed(config); + const updatedConfig = { ...config, scoringPresetId: 'preset2' } as unknown as LeagueScoringConfig; + const result = await repository.save(updatedConfig); + expect(result).toEqual(updatedConfig); + const found = await repository.findBySeasonId('season1'); + expect(found?.scoringPresetId).toBe('preset2'); + }); + }); + }); + + describe('InMemoryChampionshipStandingRepository', () => { + let repository: InMemoryChampionshipStandingRepository; + + beforeEach(() => { + repository = new InMemoryChampionshipStandingRepository(mockLogger); + }); + + describe('constructor', () => { + it('should initialize with a logger', () => { + expect(repository).toBeDefined(); + expect(mockLogger.info).toHaveBeenCalledWith('InMemoryChampionshipStandingRepository initialized.'); + }); + }); + + describe('findBySeasonAndChampionship', () => { + it('should return standings for season and championship', async () => { + const standing = ChampionshipStanding.create({ + seasonId: 'season1', + championshipId: 'champ1', + participant: { type: 'driver', id: 'driver1' }, + totalPoints: 100, + resultsCounted: 10, + resultsDropped: 0, + position: 1, + }); + repository.seed(standing); + const result = await repository.findBySeasonAndChampionship('season1', 'champ1'); + expect(result).toEqual([standing]); + }); + }); + + describe('saveAll', () => { + it('should save all standings', async () => { + const standing1 = ChampionshipStanding.create({ + seasonId: 'season1', + championshipId: 'champ1', + participant: { type: 'driver', id: 'driver1' }, + totalPoints: 100, + resultsCounted: 10, + resultsDropped: 0, + position: 1, + }); + const standing2 = ChampionshipStanding.create({ + seasonId: 'season1', + championshipId: 'champ1', + participant: { type: 'driver', id: 'driver2' }, + totalPoints: 80, + resultsCounted: 10, + resultsDropped: 0, + position: 2, + }); + await repository.saveAll([standing1, standing2]); + const result = await repository.findBySeasonAndChampionship('season1', 'champ1'); + expect(result).toEqual([standing1, standing2]); + }); + }); + + describe('getAll', () => { + it('should return all standings', () => { + const standing = ChampionshipStanding.create({ + seasonId: 'season1', + championshipId: 'champ1', + participant: { type: 'driver', id: 'driver1' }, + totalPoints: 100, + resultsCounted: 10, + resultsDropped: 0, + position: 1, + }); + repository.seed(standing); + const result = repository.getAll(); + expect(result).toEqual([standing]); + }); + }); + }); +}); \ No newline at end of file diff --git a/adapters/racing/persistence/inmemory/InMemoryScoringRepositories.ts b/adapters/racing/persistence/inmemory/InMemoryScoringRepositories.ts index 3fc744d8d..00ca02a66 100644 --- a/adapters/racing/persistence/inmemory/InMemoryScoringRepositories.ts +++ b/adapters/racing/persistence/inmemory/InMemoryScoringRepositories.ts @@ -1,266 +1,13 @@ import { Game } from '@core/racing/domain/entities/Game'; import { Season } from '@core/racing/domain/entities/season/Season'; import type { LeagueScoringConfig } from '@core/racing/domain/entities/LeagueScoringConfig'; -import { PointsTable } from '@core/racing/domain/value-objects/PointsTable'; -import type { ChampionshipConfig } from '@core/racing/domain/types/ChampionshipConfig'; -import type { SessionType } from '@core/racing/domain/types/SessionType'; -import type { BonusRule } from '@core/racing/domain/types/BonusRule'; -import type { DropScorePolicy } from '@core/racing/domain/types/DropScorePolicy'; import type { IGameRepository } from '@core/racing/domain/repositories/IGameRepository'; import type { ISeasonRepository } from '@core/racing/domain/repositories/ISeasonRepository'; import type { ILeagueScoringConfigRepository } from '@core/racing/domain/repositories/ILeagueScoringConfigRepository'; import type { IChampionshipStandingRepository } from '@core/racing/domain/repositories/IChampionshipStandingRepository'; import { ChampionshipStanding } from '@core/racing/domain/entities/championship/ChampionshipStanding'; -import type { ChampionshipType } from '@core/racing/domain/types/ChampionshipType'; -import type { ParticipantRef } from '@core/racing/domain/types/ParticipantRef'; import type { Logger } from '@core/shared/application'; -class SilentLogger implements Logger { - debug(..._args: unknown[]): void { - // console.debug(..._args); - } - info(..._args: unknown[]): void { - // console.info(..._args); - } - warn(..._args: unknown[]): void { - // console.warn(..._args); - } - error(..._args: unknown[]): void { - // console.error(..._args); - } -} - -export type LeagueScoringPresetPrimaryChampionshipType = - | 'driver' - | 'team' - | 'nations' - | 'trophy'; - -export interface LeagueScoringPreset { - id: string; - name: string; - description: string; - primaryChampionshipType: LeagueScoringPresetPrimaryChampionshipType; - dropPolicySummary: string; - sessionSummary: string; - bonusSummary: string; - createConfig: (options: { seasonId: string }) => LeagueScoringConfig; -} - -const mainPointsSprintMain = new PointsTable({ - 1: 25, - 2: 18, - 3: 15, - 4: 12, - 5: 10, - 6: 8, - 7: 6, - 8: 4, - 9: 2, - 10: 1, -}); - -const sprintPointsSprintMain = new PointsTable({ - 1: 8, - 2: 7, - 3: 6, - 4: 5, - 5: 4, - 6: 3, - 7: 2, - 8: 1, -}); - -const clubMainPoints = new PointsTable({ - 1: 20, - 2: 15, - 3: 12, - 4: 10, - 5: 8, - 6: 6, - 7: 4, - 8: 2, - 9: 1, -}); - -const enduranceMainPoints = new PointsTable({ - 1: 50, - 2: 36, - 3: 30, - 4: 24, - 5: 20, - 6: 16, - 7: 12, - 8: 8, - 9: 4, - 10: 2, -}); - -const leagueScoringPresets: LeagueScoringPreset[] = [ - { - id: 'sprint-main-driver', - name: 'Sprint + Main', - description: - 'Short sprint race plus main race; sprint gives fewer points.', - primaryChampionshipType: 'driver', - dropPolicySummary: 'Best 6 results of 8 count towards the championship.', - sessionSummary: 'Sprint + Main', - bonusSummary: 'Fastest lap +1 point in main race if finishing P10 or better.', - createConfig: ({ seasonId }) => { - const fastestLapBonus: BonusRule = { - id: 'fastest-lap-main', - type: 'fastestLap', - points: 1, - requiresFinishInTopN: 10, - }; - - const sessionTypes: SessionType[] = ['sprint', 'main']; - - const pointsTableBySessionType: Record = { - sprint: sprintPointsSprintMain, - main: mainPointsSprintMain, - practice: new PointsTable({}), - qualifying: new PointsTable({}), - q1: new PointsTable({}), - q2: new PointsTable({}), - q3: new PointsTable({}), - timeTrial: new PointsTable({}), - }; - - const bonusRulesBySessionType: Record = { - sprint: [], - main: [fastestLapBonus], - practice: [], - qualifying: [], - q1: [], - q2: [], - q3: [], - timeTrial: [], - }; - - const dropScorePolicy: DropScorePolicy = { - strategy: 'bestNResults', - count: 6, - }; - - const championship: ChampionshipConfig = { - id: 'driver-champ-sprint-main', - name: 'Driver Championship', - type: 'driver' as ChampionshipType, - sessionTypes, - pointsTableBySessionType, - bonusRulesBySessionType, - dropScorePolicy, - }; - - return { - id: `lsc-${seasonId}-sprint-main-driver`, - seasonId, - scoringPresetId: 'sprint-main-driver', - championships: [championship], - }; - }, - }, - { - id: 'club-default', - name: 'Club ladder', - description: - 'Simple club ladder with a single main race and no bonuses or drop scores.', - primaryChampionshipType: 'driver', - dropPolicySummary: 'All race results count, no drop scores.', - sessionSummary: 'Main race only', - bonusSummary: 'No bonus points.', - createConfig: ({ seasonId }) => { - const sessionTypes: SessionType[] = ['main']; - - const pointsTableBySessionType: Record = { - sprint: new PointsTable({}), - main: clubMainPoints, - practice: new PointsTable({}), - qualifying: new PointsTable({}), - q1: new PointsTable({}), - q2: new PointsTable({}), - q3: new PointsTable({}), - timeTrial: new PointsTable({}), - }; - - const dropScorePolicy: DropScorePolicy = { - strategy: 'none', - }; - - const championship: ChampionshipConfig = { - id: 'driver-champ-club-default', - name: 'Driver Championship', - type: 'driver' as ChampionshipType, - sessionTypes, - pointsTableBySessionType, - dropScorePolicy, - }; - - return { - id: `lsc-${seasonId}-club-default`, - seasonId, - scoringPresetId: 'club-default', - championships: [championship], - }; - }, - }, - { - id: 'endurance-main-double', - name: 'Endurance weekend', - description: - 'Single main endurance race with double points and a simple drop policy.', - primaryChampionshipType: 'driver', - dropPolicySummary: 'Best 4 results of 6 count towards the championship.', - sessionSummary: 'Main race only', - bonusSummary: 'No bonus points.', - createConfig: ({ seasonId }) => { - const sessionTypes: SessionType[] = ['main']; - - const pointsTableBySessionType: Record = { - sprint: new PointsTable({}), - main: enduranceMainPoints, - practice: new PointsTable({}), - qualifying: new PointsTable({}), - q1: new PointsTable({}), - q2: new PointsTable({}), - q3: new PointsTable({}), - timeTrial: new PointsTable({}), - }; - - const dropScorePolicy: DropScorePolicy = { - strategy: 'bestNResults', - count: 4, - }; - - const championship: ChampionshipConfig = { - id: 'driver-champ-endurance-main-double', - name: 'Driver Championship', - type: 'driver' as ChampionshipType, - sessionTypes, - pointsTableBySessionType, - dropScorePolicy, - }; - - return { - id: `lsc-${seasonId}-endurance-main-double`, - seasonId, - scoringPresetId: 'endurance-main-double', - championships: [championship], - }; - }, - }, -]; - -export function listLeagueScoringPresets(): LeagueScoringPreset[] { - return [...leagueScoringPresets]; -} - -export function getLeagueScoringPresetById( - id: string, -): LeagueScoringPreset | undefined { - return leagueScoringPresets.find((preset) => preset.id === id); -} export class InMemoryGameRepository implements IGameRepository { private games: Game[]; @@ -275,7 +22,7 @@ export class InMemoryGameRepository implements IGameRepository { async findById(id: string): Promise { this.logger.debug(`Finding game by id: ${id}`); try { - const game = this.games.find((g) => g.id === id) ?? null; + const game = this.games.find((g) => g.id.toString() === id) ?? null; if (game) { this.logger.info(`Found game: ${game.id}`); } else { @@ -444,7 +191,7 @@ export class InMemoryLeagueScoringConfigRepository async findBySeasonId(seasonId: string): Promise { this.logger.debug(`Finding league scoring config by seasonId: ${seasonId}`); try { - const config = this.configs.find((c) => c.seasonId === seasonId) ?? null; + const config = this.configs.find((c) => c.seasonId.toString() === seasonId) ?? null; if (config) { this.logger.info(`Found league scoring config for seasonId: ${seasonId}.`); } else { @@ -513,7 +260,7 @@ export class InMemoryChampionshipStandingRepository this.logger.info(`Found ${standings.length} championship standings.`); return standings; } catch (error) { - this.logger.error(`Error finding championship standings for season ${seasonId}, championship ${championshipId}:`, error); + this.logger.error(`Error finding championship standings for season ${seasonId}, championship ${championshipId}:`, error instanceof Error ? error : new Error(String(error))); throw error; } } @@ -551,68 +298,4 @@ export class InMemoryChampionshipStandingRepository throw error; } } -} - -export function createSprintMainDemoScoringSetup(params: { - leagueId: string; - seasonId?: string; -}): { - gameRepo: InMemoryGameRepository; - seasonRepo: InMemorySeasonRepository; - scoringConfigRepo: InMemoryLeagueScoringConfigRepository; - championshipStandingRepo: InMemoryChampionshipStandingRepository; - seasonId: string; - championshipId: string; -} { - const { leagueId } = params; - const seasonId = params.seasonId ?? 'season-sprint-main-demo'; - const championshipId = 'driver-champ'; - - const logger = new SilentLogger(); - - const game = Game.create({ id: 'iracing', name: 'iRacing' }); - - const season = Season.create({ - id: seasonId, - leagueId, - gameId: game.id, - name: 'Sprint + Main Demo Season', - year: 2025, - order: 1, - status: 'active', - startDate: new Date('2025-01-01'), - endDate: new Date('2025-12-31'), - }); - - const preset = getLeagueScoringPresetById('sprint-main-driver'); - if (!preset) { - throw new Error('Missing sprint-main-driver scoring preset'); - } - - const leagueScoringConfig: LeagueScoringConfig = preset.createConfig({ - seasonId: season.id, - }); - - const gameRepo = new InMemoryGameRepository(logger, [game]); - const seasonRepo = new InMemorySeasonRepository(logger, [season]); - const scoringConfigRepo = new InMemoryLeagueScoringConfigRepository(logger, [ - leagueScoringConfig, - ]); - const championshipStandingRepo = new InMemoryChampionshipStandingRepository(logger); - - return { - gameRepo, - seasonRepo, - scoringConfigRepo, - championshipStandingRepo, - seasonId: season.id, - championshipId, - }; -} - -export function createParticipantRef(driverId: string): ParticipantRef { - return { - type: 'driver' as ChampionshipType, - id: driverId, - }; } \ No newline at end of file diff --git a/adapters/racing/persistence/inmemory/InMemorySeasonRepository.test.ts b/adapters/racing/persistence/inmemory/InMemorySeasonRepository.test.ts new file mode 100644 index 000000000..da0cedef5 --- /dev/null +++ b/adapters/racing/persistence/inmemory/InMemorySeasonRepository.test.ts @@ -0,0 +1,175 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import { InMemorySeasonRepository } from './InMemorySeasonRepository'; +import { Season } from '@core/racing/domain/entities/season/Season'; +import type { Logger } from '@core/shared/application'; + +describe('InMemorySeasonRepository', () => { + let repository: InMemorySeasonRepository; + let mockLogger: Logger; + + beforeEach(() => { + mockLogger = { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; + repository = new InMemorySeasonRepository(mockLogger); + }); + + const createTestSeason = ( + id: string, + leagueId: string, + name: string = 'Test Season', + status: 'planned' | 'active' | 'completed' = 'planned' + ) => { + return Season.create({ + id, + leagueId, + gameId: 'iracing', + name, + status, + }); + }; + + describe('constructor', () => { + it('should initialize with a logger', () => { + expect(repository).toBeDefined(); + expect(mockLogger.info).toHaveBeenCalledWith('InMemorySeasonRepository initialized.'); + }); + }); + + describe('findById', () => { + it('should return null if season not found', async () => { + const result = await repository.findById('nonexistent'); + expect(result).toBeNull(); + expect(mockLogger.debug).toHaveBeenCalledWith('[InMemorySeasonRepository] Finding season by ID: nonexistent'); + expect(mockLogger.warn).toHaveBeenCalledWith('Season with ID nonexistent not found.'); + }); + + it('should return the season if found', async () => { + const season = createTestSeason('1', 'league1'); + await repository.create(season); + + const result = await repository.findById('1'); + expect(result).toEqual(season); + expect(mockLogger.info).toHaveBeenCalledWith('Found season by ID: 1.'); + }); + }); + + describe('findByLeagueId', () => { + it('should return seasons filtered by league ID', async () => { + const season1 = createTestSeason('1', 'league1'); + const season2 = createTestSeason('2', 'league2'); + const season3 = createTestSeason('3', 'league1'); + await repository.create(season1); + await repository.create(season2); + await repository.create(season3); + + const result = await repository.findByLeagueId('league1'); + expect(result).toHaveLength(2); + expect(result).toContain(season1); + expect(result).toContain(season3); + }); + }); + + describe('create', () => { + it('should create a new season', async () => { + const season = createTestSeason('1', 'league1'); + const result = await repository.create(season); + expect(result).toEqual(season); + expect(mockLogger.info).toHaveBeenCalledWith('Season 1 created successfully.'); + }); + + it('should throw error if season already exists', async () => { + const season = createTestSeason('1', 'league1'); + await repository.create(season); + await expect(repository.create(season)).rejects.toThrow('Season already exists'); + }); + }); + + describe('add', () => { + it('should add a new season', async () => { + const season = createTestSeason('1', 'league1'); + await repository.add(season); + const found = await repository.findById('1'); + expect(found).toEqual(season); + expect(mockLogger.info).toHaveBeenCalledWith('Season 1 added successfully.'); + }); + + it('should throw error if season already exists', async () => { + const season = createTestSeason('1', 'league1'); + await repository.add(season); + await expect(repository.add(season)).rejects.toThrow('Season already exists'); + }); + }); + + describe('update', () => { + it('should update an existing season', async () => { + const season = createTestSeason('1', 'league1'); + await repository.create(season); + + const updatedSeason = Season.create({ + ...season, + name: 'Updated Season', + }); + await repository.update(updatedSeason); + const found = await repository.findById('1'); + expect(found?.name).toBe('Updated Season'); + expect(mockLogger.info).toHaveBeenCalledWith('Season 1 updated successfully.'); + }); + + it('should throw error if season does not exist', async () => { + const season = createTestSeason('1', 'league1'); + await expect(repository.update(season)).rejects.toThrow('Season not found'); + }); + }); + + describe('delete', () => { + it('should delete an existing season', async () => { + const season = createTestSeason('1', 'league1'); + await repository.create(season); + + await repository.delete('1'); + expect(mockLogger.info).toHaveBeenCalledWith('Season 1 deleted successfully.'); + const found = await repository.findById('1'); + expect(found).toBeNull(); + }); + + it('should not throw if season does not exist', async () => { + await repository.delete('nonexistent'); + expect(mockLogger.warn).toHaveBeenCalledWith('Season with ID nonexistent not found for deletion.'); + }); + }); + + describe('listByLeague', () => { + it('should list seasons by league ID', async () => { + const season1 = createTestSeason('1', 'league1'); + const season2 = createTestSeason('2', 'league2'); + const season3 = createTestSeason('3', 'league1'); + await repository.create(season1); + await repository.create(season2); + await repository.create(season3); + + const result = await repository.listByLeague('league1'); + expect(result).toHaveLength(2); + expect(result).toContain(season1); + expect(result).toContain(season3); + }); + }); + + describe('listActiveByLeague', () => { + it('should list active seasons by league ID', async () => { + const activeSeason = createTestSeason('1', 'league1', 'Active Season', 'active'); + const plannedSeason = createTestSeason('2', 'league1', 'Planned Season', 'planned'); + const otherLeagueSeason = createTestSeason('3', 'league2', 'Other League', 'active'); + await repository.create(activeSeason); + await repository.create(plannedSeason); + await repository.create(otherLeagueSeason); + + const result = await repository.listActiveByLeague('league1'); + expect(result).toHaveLength(1); + expect(result).toContain(activeSeason); + }); + }); +}); \ No newline at end of file diff --git a/adapters/racing/persistence/inmemory/InMemorySeasonRepository.ts b/adapters/racing/persistence/inmemory/InMemorySeasonRepository.ts index 5344dd1cd..57d6932ed 100644 --- a/adapters/racing/persistence/inmemory/InMemorySeasonRepository.ts +++ b/adapters/racing/persistence/inmemory/InMemorySeasonRepository.ts @@ -5,12 +5,8 @@ import { Logger } from '@core/shared/application'; export class InMemorySeasonRepository implements ISeasonRepository { private seasons: Map = new Map(); // Key: seasonId - constructor(private readonly logger: Logger, initialSeasons: Season[] = []) { + constructor(private readonly logger: Logger) { this.logger.info('InMemorySeasonRepository initialized.'); - for (const season of initialSeasons) { - this.seasons.set(season.id, season); - this.logger.debug(`Seeded season: ${season.id} (${season.name}).`); - } } async findById(id: string): Promise { diff --git a/adapters/racing/persistence/inmemory/InMemorySeasonSponsorshipRepository.test.ts b/adapters/racing/persistence/inmemory/InMemorySeasonSponsorshipRepository.test.ts new file mode 100644 index 000000000..32b5c4fbc --- /dev/null +++ b/adapters/racing/persistence/inmemory/InMemorySeasonSponsorshipRepository.test.ts @@ -0,0 +1,190 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import { InMemorySeasonSponsorshipRepository } from './InMemorySeasonSponsorshipRepository'; +import { SeasonSponsorship, type SponsorshipTier } from '@core/racing/domain/entities/season/SeasonSponsorship'; +import { Money } from '@core/racing/domain/value-objects/Money'; +import type { Logger } from '@core/shared/application'; + +describe('InMemorySeasonSponsorshipRepository', () => { + let repository: InMemorySeasonSponsorshipRepository; + let mockLogger: Logger; + + beforeEach(() => { + mockLogger = { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; + repository = new InMemorySeasonSponsorshipRepository(mockLogger); + }); + + const createTestSeasonSponsorship = ( + id: string, + seasonId: string, + sponsorId: string, + tier: SponsorshipTier = 'main', + leagueId?: string + ) => { + return SeasonSponsorship.create({ + id, + seasonId, + sponsorId, + tier, + pricing: Money.create(1000), + ...(leagueId ? { leagueId } : {}), + }); + }; + + describe('constructor', () => { + it('should initialize with a logger', () => { + expect(repository).toBeDefined(); + expect(mockLogger.info).toHaveBeenCalledWith('InMemorySeasonSponsorshipRepository initialized.'); + }); + }); + + describe('findById', () => { + it('should return null if sponsorship not found', async () => { + const result = await repository.findById('nonexistent'); + expect(result).toBeNull(); + expect(mockLogger.debug).toHaveBeenCalledWith('Finding season sponsorship by id: nonexistent'); + expect(mockLogger.warn).toHaveBeenCalledWith('Season sponsorship with id nonexistent not found.'); + }); + + it('should return the sponsorship if found', async () => { + const sponsorship = createTestSeasonSponsorship('1', 'season1', 'sponsor1'); + await repository.create(sponsorship); + + const result = await repository.findById('1'); + expect(result).toEqual(sponsorship); + expect(mockLogger.info).toHaveBeenCalledWith('Found season sponsorship: 1.'); + }); + }); + + describe('findBySeasonId', () => { + it('should return sponsorships filtered by season ID', async () => { + const sponsorship1 = createTestSeasonSponsorship('1', 'season1', 'sponsor1'); + const sponsorship2 = createTestSeasonSponsorship('2', 'season2', 'sponsor2'); + const sponsorship3 = createTestSeasonSponsorship('3', 'season1', 'sponsor3'); + await repository.create(sponsorship1); + await repository.create(sponsorship2); + await repository.create(sponsorship3); + + const result = await repository.findBySeasonId('season1'); + expect(result).toHaveLength(2); + expect(result).toContain(sponsorship1); + expect(result).toContain(sponsorship3); + }); + }); + + describe('findByLeagueId', () => { + it('should return sponsorships filtered by league ID', async () => { + const sponsorship1 = createTestSeasonSponsorship('1', 'season1', 'sponsor1', 'main', 'league1'); + const sponsorship2 = createTestSeasonSponsorship('2', 'season2', 'sponsor2', 'main', 'league2'); + const sponsorship3 = createTestSeasonSponsorship('3', 'season1', 'sponsor3', 'main', 'league1'); + await repository.create(sponsorship1); + await repository.create(sponsorship2); + await repository.create(sponsorship3); + + const result = await repository.findByLeagueId('league1'); + expect(result).toHaveLength(2); + expect(result).toContain(sponsorship1); + expect(result).toContain(sponsorship3); + }); + }); + + describe('findBySponsorId', () => { + it('should return sponsorships filtered by sponsor ID', async () => { + const sponsorship1 = createTestSeasonSponsorship('1', 'season1', 'sponsor1'); + const sponsorship2 = createTestSeasonSponsorship('2', 'season2', 'sponsor2'); + const sponsorship3 = createTestSeasonSponsorship('3', 'season1', 'sponsor1'); + await repository.create(sponsorship1); + await repository.create(sponsorship2); + await repository.create(sponsorship3); + + const result = await repository.findBySponsorId('sponsor1'); + expect(result).toHaveLength(2); + expect(result).toContain(sponsorship1); + expect(result).toContain(sponsorship3); + }); + }); + + describe('findBySeasonAndTier', () => { + it('should return sponsorships filtered by season ID and tier', async () => { + const sponsorship1 = createTestSeasonSponsorship('1', 'season1', 'sponsor1', 'main'); + const sponsorship2 = createTestSeasonSponsorship('2', 'season1', 'sponsor2', 'secondary'); + const sponsorship3 = createTestSeasonSponsorship('3', 'season2', 'sponsor3', 'main'); + await repository.create(sponsorship1); + await repository.create(sponsorship2); + await repository.create(sponsorship3); + + const result = await repository.findBySeasonAndTier('season1', 'main'); + expect(result).toHaveLength(1); + expect(result).toContain(sponsorship1); + }); + }); + + describe('create', () => { + it('should create a new sponsorship', async () => { + const sponsorship = createTestSeasonSponsorship('1', 'season1', 'sponsor1'); + const result = await repository.create(sponsorship); + expect(result).toEqual(sponsorship); + expect(mockLogger.info).toHaveBeenCalledWith('SeasonSponsorship 1 created successfully.'); + }); + + it('should throw error if sponsorship already exists', async () => { + const sponsorship = createTestSeasonSponsorship('1', 'season1', 'sponsor1'); + await repository.create(sponsorship); + await expect(repository.create(sponsorship)).rejects.toThrow('SeasonSponsorship with this ID already exists'); + }); + }); + + describe('update', () => { + it('should update an existing sponsorship', async () => { + const sponsorship = createTestSeasonSponsorship('1', 'season1', 'sponsor1'); + await repository.create(sponsorship); + + const updatedSponsorship = sponsorship.activate(); + await repository.update(updatedSponsorship); + const found = await repository.findById('1'); + expect(found?.status).toBe('active'); + expect(mockLogger.info).toHaveBeenCalledWith('SeasonSponsorship 1 updated successfully.'); + }); + + it('should throw error if sponsorship does not exist', async () => { + const sponsorship = createTestSeasonSponsorship('1', 'season1', 'sponsor1'); + await expect(repository.update(sponsorship)).rejects.toThrow('SeasonSponsorship not found'); + }); + }); + + describe('delete', () => { + it('should delete an existing sponsorship', async () => { + const sponsorship = createTestSeasonSponsorship('1', 'season1', 'sponsor1'); + await repository.create(sponsorship); + + await repository.delete('1'); + expect(mockLogger.info).toHaveBeenCalledWith('SeasonSponsorship 1 deleted successfully.'); + const found = await repository.findById('1'); + expect(found).toBeNull(); + }); + + it('should not throw if sponsorship does not exist', async () => { + await repository.delete('nonexistent'); + expect(mockLogger.warn).toHaveBeenCalledWith('SeasonSponsorship with id nonexistent not found for deletion.'); + }); + }); + + describe('exists', () => { + it('should return true if sponsorship exists', async () => { + const sponsorship = createTestSeasonSponsorship('1', 'season1', 'sponsor1'); + await repository.create(sponsorship); + + const result = await repository.exists('1'); + expect(result).toBe(true); + }); + + it('should return false if sponsorship does not exist', async () => { + const result = await repository.exists('nonexistent'); + expect(result).toBe(false); + }); + }); +}); \ No newline at end of file diff --git a/adapters/racing/persistence/inmemory/InMemorySeasonSponsorshipRepository.ts b/adapters/racing/persistence/inmemory/InMemorySeasonSponsorshipRepository.ts index cd8595702..91a345bbb 100644 --- a/adapters/racing/persistence/inmemory/InMemorySeasonSponsorshipRepository.ts +++ b/adapters/racing/persistence/inmemory/InMemorySeasonSponsorshipRepository.ts @@ -4,7 +4,7 @@ * Mock repository for testing and development */ -import type { SeasonSponsorship, SponsorshipTier } from '@core/racing/domain/entities/SeasonSponsorship'; +import type { SeasonSponsorship, SponsorshipTier } from '@core/racing/domain/entities/season/SeasonSponsorship'; import type { ISeasonSponsorshipRepository } from '@core/racing/domain/repositories/ISeasonSponsorshipRepository'; import type { Logger } from '@core/shared/application'; @@ -12,12 +12,9 @@ export class InMemorySeasonSponsorshipRepository implements ISeasonSponsorshipRe private sponsorships: Map = new Map(); private readonly logger: Logger; - constructor(logger: Logger, seedData?: SeasonSponsorship[]) { + constructor(logger: Logger) { this.logger = logger; this.logger.info('InMemorySeasonSponsorshipRepository initialized.'); - if (seedData) { - this.seed(seedData); - } } async findById(id: string): Promise { @@ -81,7 +78,7 @@ export class InMemorySeasonSponsorshipRepository implements ISeasonSponsorshipRe this.logger.info(`Found ${sponsorships.length} season sponsorships for season id: ${seasonId}, tier: ${tier}.`); return sponsorships; } catch (error) { - this.logger.error(`Error finding season sponsorships by season id ${seasonId}, tier ${tier}:`, error); + this.logger.error(`Error finding season sponsorships by season id ${seasonId}, tier ${tier}:`, error instanceof Error ? error : new Error(String(error))); throw error; } } @@ -144,22 +141,6 @@ export class InMemorySeasonSponsorshipRepository implements ISeasonSponsorshipRe } } - /** - * Seed initial data - */ - seed(sponsorships: SeasonSponsorship[]): void { - this.logger.debug(`Seeding ${sponsorships.length} season sponsorships.`); - try { - for (const sponsorship of sponsorships) { - this.sponsorships.set(sponsorship.id, sponsorship); - this.logger.debug(`Seeded season sponsorship: ${sponsorship.id}.`); - } - this.logger.info(`Successfully seeded ${sponsorships.length} season sponsorships.`); - } catch (error) { - this.logger.error(`Error seeding season sponsorships:`, error instanceof Error ? error : new Error(String(error))); - throw error; - } - } // Test helper clear(): void { diff --git a/adapters/racing/persistence/inmemory/InMemorySessionRepository.test.ts b/adapters/racing/persistence/inmemory/InMemorySessionRepository.test.ts new file mode 100644 index 000000000..c81a5b4a8 --- /dev/null +++ b/adapters/racing/persistence/inmemory/InMemorySessionRepository.test.ts @@ -0,0 +1,181 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import { InMemorySessionRepository } from './InMemorySessionRepository'; +import { Session, SessionStatus } from '@core/racing/domain/entities/Session'; +import { SessionType } from '@core/racing/domain/value-objects/SessionType'; +import type { Logger } from '@core/shared/application'; + +describe('InMemorySessionRepository', () => { + let repository: InMemorySessionRepository; + let mockLogger: Logger; + + beforeEach(() => { + mockLogger = { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; + repository = new InMemorySessionRepository(mockLogger); + }); + + const createTestSession = (id: string, raceEventId: string, scheduledAt: Date, track: string, car: string, sessionType: SessionType, status?: SessionStatus) => { + return Session.create({ + id, + raceEventId, + scheduledAt, + track, + car, + sessionType, + status: status || 'scheduled', + }); + }; + + describe('constructor', () => { + it('should initialize with a logger', () => { + expect(repository).toBeDefined(); + expect(mockLogger.info).toHaveBeenCalledWith('InMemorySessionRepository initialized.'); + }); + }); + + describe('findById', () => { + it('should return null if session not found', async () => { + const result = await repository.findById('nonexistent'); + expect(result).toBeNull(); + expect(mockLogger.debug).toHaveBeenCalledWith('Finding session by id: nonexistent'); + expect(mockLogger.warn).toHaveBeenCalledWith('Session with id nonexistent not found.'); + }); + + it('should return the session if found', async () => { + const session = createTestSession('1', 'race1', new Date(), 'track1', 'car1', SessionType.practice()); + await repository.create(session); + + const result = await repository.findById('1'); + expect(result).toEqual(session); + expect(mockLogger.info).toHaveBeenCalledWith('Found session: 1.'); + }); + }); + + describe('findAll', () => { + it('should return all sessions', async () => { + const session1 = createTestSession('1', 'race1', new Date(), 'track1', 'car1', SessionType.practice()); + const session2 = createTestSession('2', 'race2', new Date(), 'track2', 'car2', SessionType.qualifying()); + await repository.create(session1); + await repository.create(session2); + + const result = await repository.findAll(); + expect(result).toHaveLength(2); + expect(result).toContain(session1); + expect(result).toContain(session2); + }); + }); + + describe('findByRaceEventId', () => { + it('should return sessions for the given race event id', async () => { + const session1 = createTestSession('1', 'race1', new Date(), 'track1', 'car1', SessionType.practice()); + const session2 = createTestSession('2', 'race1', new Date(), 'track2', 'car2', SessionType.qualifying()); + const session3 = createTestSession('3', 'race2', new Date(), 'track3', 'car3', SessionType.main()); + await repository.create(session1); + await repository.create(session2); + await repository.create(session3); + + const result = await repository.findByRaceEventId('race1'); + expect(result).toHaveLength(2); + expect(result).toContain(session1); + expect(result).toContain(session2); + }); + + it('should return empty array if no sessions for race event id', async () => { + const result = await repository.findByRaceEventId('nonexistent'); + expect(result).toEqual([]); + }); + }); + + describe('findByLeagueId', () => { + it('should return empty array as leagueId is not directly supported', async () => { + const result = await repository.findByLeagueId('league1'); + expect(result).toEqual([]); + }); + }); + + describe('findByStatus', () => { + it('should return sessions with the given status', async () => { + const session1 = createTestSession('1', 'race1', new Date(), 'track1', 'car1', SessionType.practice(), 'scheduled'); + const session2 = createTestSession('2', 'race2', new Date(), 'track2', 'car2', SessionType.qualifying(), 'running'); + await repository.create(session1); + await repository.create(session2); + + const result = await repository.findByStatus('scheduled'); + expect(result).toHaveLength(1); + expect(result).toContain(session1); + }); + + it('should return empty array if no sessions with status', async () => { + const result = await repository.findByStatus('completed'); + expect(result).toEqual([]); + }); + }); + + describe('create', () => { + it('should create a new session', async () => { + const session = createTestSession('1', 'race1', new Date(), 'track1', 'car1', SessionType.practice()); + const result = await repository.create(session); + expect(result).toEqual(session); + expect(mockLogger.info).toHaveBeenCalledWith('Session 1 created successfully.'); + }); + + it('should throw error if session already exists', async () => { + const session = createTestSession('1', 'race1', new Date(), 'track1', 'car1', SessionType.practice()); + await repository.create(session); + await expect(repository.create(session)).rejects.toThrow('Session with ID 1 already exists'); + }); + }); + + describe('update', () => { + it('should update an existing session', async () => { + const session = createTestSession('1', 'race1', new Date(), 'track1', 'car1', SessionType.practice()); + await repository.create(session); + + const updatedSession = session.start(); // Assuming start changes status + const result = await repository.update(updatedSession); + expect(result).toEqual(updatedSession); + expect(mockLogger.info).toHaveBeenCalledWith('Session 1 updated successfully.'); + }); + + it('should throw error if session does not exist', async () => { + const session = createTestSession('1', 'race1', new Date(), 'track1', 'car1', SessionType.practice()); + await expect(repository.update(session)).rejects.toThrow('Session with ID 1 not found'); + }); + }); + + describe('delete', () => { + it('should delete an existing session', async () => { + const session = createTestSession('1', 'race1', new Date(), 'track1', 'car1', SessionType.practice()); + await repository.create(session); + + await repository.delete('1'); + expect(mockLogger.info).toHaveBeenCalledWith('Session 1 deleted successfully.'); + const found = await repository.findById('1'); + expect(found).toBeNull(); + }); + + it('should not throw error if session does not exist', async () => { + await repository.delete('nonexistent'); + expect(mockLogger.warn).toHaveBeenCalledWith('Session with id nonexistent not found for deletion.'); + }); + }); + + describe('exists', () => { + it('should return true if session exists', async () => { + const session = createTestSession('1', 'race1', new Date(), 'track1', 'car1', SessionType.practice()); + await repository.create(session); + + const result = await repository.exists('1'); + expect(result).toBe(true); + }); + + it('should return false if session does not exist', async () => { + const result = await repository.exists('nonexistent'); + expect(result).toBe(false); + }); + }); +}); \ No newline at end of file diff --git a/adapters/racing/persistence/inmemory/InMemorySessionRepository.ts b/adapters/racing/persistence/inmemory/InMemorySessionRepository.ts index f0d221bb0..6f2fcfaf2 100644 --- a/adapters/racing/persistence/inmemory/InMemorySessionRepository.ts +++ b/adapters/racing/persistence/inmemory/InMemorySessionRepository.ts @@ -1,8 +1,8 @@ /** * In-memory implementation of ISessionRepository for development/testing. */ -import type { ISessionRepository } from '../../domain/repositories/ISessionRepository'; -import type { Session } from '../../domain/entities/Session'; +import type { ISessionRepository } from '@core/racing/domain/repositories/ISessionRepository'; +import type { Session } from '@core/racing/domain/entities/Session'; import type { Logger } from '@core/shared/application'; export class InMemorySessionRepository implements ISessionRepository { diff --git a/adapters/racing/persistence/inmemory/InMemorySponsorRepository.test.ts b/adapters/racing/persistence/inmemory/InMemorySponsorRepository.test.ts new file mode 100644 index 000000000..dc3cc8408 --- /dev/null +++ b/adapters/racing/persistence/inmemory/InMemorySponsorRepository.test.ts @@ -0,0 +1,172 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import { InMemorySponsorRepository } from './InMemorySponsorRepository'; +import { Sponsor } from '@core/racing/domain/entities/sponsor/Sponsor'; +import type { Logger } from '@core/shared/application'; + +describe('InMemorySponsorRepository', () => { + let repository: InMemorySponsorRepository; + let mockLogger: Logger; + + beforeEach(() => { + mockLogger = { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; + repository = new InMemorySponsorRepository(mockLogger); + }); + + const createTestSponsor = (id: string, name: string, contactEmail: string) => { + return Sponsor.create({ + id, + name, + contactEmail, + }); + }; + + describe('constructor', () => { + it('should initialize with a logger', () => { + expect(repository).toBeDefined(); + expect(mockLogger.info).toHaveBeenCalledWith('InMemorySponsorRepository initialized.'); + }); + + it('should seed initial sponsors', () => { + const sponsor = createTestSponsor('1', 'Test Sponsor', 'test@example.com'); + new InMemorySponsorRepository(mockLogger, [sponsor]); + expect(mockLogger.debug).toHaveBeenCalledWith(`Seeded sponsor: 1 (Test Sponsor).`); + }); + }); + + describe('findById', () => { + it('should return null if sponsor not found', async () => { + const result = await repository.findById('nonexistent'); + expect(result).toBeNull(); + expect(mockLogger.debug).toHaveBeenCalledWith('[InMemorySponsorRepository] Finding sponsor by ID: nonexistent'); + expect(mockLogger.warn).toHaveBeenCalledWith('Sponsor with ID nonexistent not found.'); + }); + + it('should return the sponsor if found', async () => { + const sponsor = createTestSponsor('1', 'Test Sponsor', 'test@example.com'); + await repository.create(sponsor); + const result = await repository.findById('1'); + expect(result).toEqual(sponsor); + expect(mockLogger.info).toHaveBeenCalledWith('Found sponsor by ID: 1.'); + }); + }); + + describe('findAll', () => { + it('should return all sponsors', async () => { + const sponsor1 = createTestSponsor('1', 'Sponsor 1', 's1@example.com'); + const sponsor2 = createTestSponsor('2', 'Sponsor 2', 's2@example.com'); + await repository.create(sponsor1); + await repository.create(sponsor2); + const result = await repository.findAll(); + expect(result).toHaveLength(2); + expect(result).toContain(sponsor1); + expect(result).toContain(sponsor2); + }); + }); + + describe('findByEmail', () => { + it('should return null if sponsor not found', async () => { + const result = await repository.findByEmail('nonexistent@example.com'); + expect(result).toBeNull(); + expect(mockLogger.debug).toHaveBeenCalledWith('[InMemorySponsorRepository] Finding sponsor by email: nonexistent@example.com'); + expect(mockLogger.warn).toHaveBeenCalledWith('Sponsor with email nonexistent@example.com not found.'); + }); + + it('should return the sponsor if found', async () => { + const sponsor = createTestSponsor('1', 'Test Sponsor', 'test@example.com'); + await repository.create(sponsor); + const result = await repository.findByEmail('TEST@EXAMPLE.COM'); + expect(result).toEqual(sponsor); + }); + }); + + describe('create', () => { + it('should create a new sponsor', async () => { + const sponsor = createTestSponsor('1', 'Test Sponsor', 'test@example.com'); + const result = await repository.create(sponsor); + expect(result).toEqual(sponsor); + expect(mockLogger.info).toHaveBeenCalledWith('Sponsor 1 (Test Sponsor) created successfully.'); + }); + + it('should throw error if sponsor already exists', async () => { + const sponsor = createTestSponsor('1', 'Test Sponsor', 'test@example.com'); + await repository.create(sponsor); + await expect(repository.create(sponsor)).rejects.toThrow('Sponsor already exists'); + }); + + it('should throw error if email already exists', async () => { + const sponsor1 = createTestSponsor('1', 'Sponsor 1', 'test@example.com'); + const sponsor2 = createTestSponsor('2', 'Sponsor 2', 'test@example.com'); + await repository.create(sponsor1); + await expect(repository.create(sponsor2)).rejects.toThrow('Sponsor with this email already exists'); + }); + }); + + describe('update', () => { + it('should update an existing sponsor', async () => { + const sponsor = createTestSponsor('1', 'Test Sponsor', 'test@example.com'); + await repository.create(sponsor); + const updatedSponsor = sponsor.update({ name: 'Updated Sponsor' }); + const result = await repository.update(updatedSponsor); + expect(result).toEqual(updatedSponsor); + expect(mockLogger.info).toHaveBeenCalledWith('Sponsor 1 (Updated Sponsor) updated successfully.'); + }); + + it('should throw error if sponsor does not exist', async () => { + const sponsor = createTestSponsor('1', 'Test Sponsor', 'test@example.com'); + await expect(repository.update(sponsor)).rejects.toThrow('Sponsor not found'); + }); + + it('should update email index when email changes', async () => { + const sponsor = createTestSponsor('1', 'Test Sponsor', 'test@example.com'); + await repository.create(sponsor); + const updatedSponsor = sponsor.update({ contactEmail: 'new@example.com' }); + await repository.update(updatedSponsor); + const found = await repository.findByEmail('new@example.com'); + expect(found).toEqual(updatedSponsor); + }); + + it('should throw error if updating to existing email', async () => { + const sponsor1 = createTestSponsor('1', 'Sponsor 1', 'test1@example.com'); + const sponsor2 = createTestSponsor('2', 'Sponsor 2', 'test2@example.com'); + await repository.create(sponsor1); + await repository.create(sponsor2); + const updatedSponsor1 = sponsor1.update({ contactEmail: 'test2@example.com' }); + await expect(repository.update(updatedSponsor1)).rejects.toThrow('Sponsor with this email already exists'); + }); + }); + + describe('delete', () => { + it('should delete an existing sponsor', async () => { + const sponsor = createTestSponsor('1', 'Test Sponsor', 'test@example.com'); + await repository.create(sponsor); + await repository.delete('1'); + expect(mockLogger.info).toHaveBeenCalledWith('Sponsor 1 deleted successfully.'); + const found = await repository.findById('1'); + expect(found).toBeNull(); + }); + + it('should not throw error if sponsor does not exist', async () => { + await repository.delete('nonexistent'); + expect(mockLogger.warn).toHaveBeenCalledWith('Sponsor with ID nonexistent not found for deletion.'); + }); + }); + + describe('exists', () => { + it('should return true if sponsor exists', async () => { + const sponsor = createTestSponsor('1', 'Test Sponsor', 'test@example.com'); + await repository.create(sponsor); + const result = await repository.exists('1'); + expect(result).toBe(true); + }); + + it('should return false if sponsor does not exist', async () => { + const result = await repository.exists('nonexistent'); + expect(result).toBe(false); + }); + }); +}); \ No newline at end of file diff --git a/adapters/racing/persistence/inmemory/InMemorySponsorRepository.ts b/adapters/racing/persistence/inmemory/InMemorySponsorRepository.ts index 24b0e9ac7..2bb66aec8 100644 --- a/adapters/racing/persistence/inmemory/InMemorySponsorRepository.ts +++ b/adapters/racing/persistence/inmemory/InMemorySponsorRepository.ts @@ -1,5 +1,5 @@ import { ISponsorRepository } from '@core/racing/domain/repositories/ISponsorRepository'; -import { Sponsor } from '@core/racing/domain/entities/Sponsor'; +import { Sponsor } from '@core/racing/domain/entities/sponsor/Sponsor'; import { Logger } from '@core/shared/application'; export class InMemorySponsorRepository implements ISponsorRepository { @@ -9,8 +9,8 @@ export class InMemorySponsorRepository implements ISponsorRepository { constructor(private readonly logger: Logger, initialSponsors: Sponsor[] = []) { this.logger.info('InMemorySponsorRepository initialized.'); for (const sponsor of initialSponsors) { - this.sponsors.set(sponsor.id, sponsor); - this.emailIndex.set(sponsor.contactEmail.toLowerCase(), sponsor.id); + this.sponsors.set(sponsor.id.toString(), sponsor); + this.emailIndex.set(sponsor.contactEmail.toString().toLowerCase(), sponsor.id.toString()); this.logger.debug(`Seeded sponsor: ${sponsor.id} (${sponsor.name}).`); } } @@ -43,37 +43,37 @@ export class InMemorySponsorRepository implements ISponsorRepository { async create(sponsor: Sponsor): Promise { this.logger.debug(`[InMemorySponsorRepository] Creating sponsor: ${sponsor.id} (${sponsor.name})`); - if (this.sponsors.has(sponsor.id)) { + if (this.sponsors.has(sponsor.id.toString())) { this.logger.warn(`Sponsor with ID ${sponsor.id} already exists.`); throw new Error('Sponsor already exists'); } - if (this.emailIndex.has(sponsor.contactEmail.toLowerCase())) { + if (this.emailIndex.has(sponsor.contactEmail.toString().toLowerCase())) { this.logger.warn(`Sponsor with email ${sponsor.contactEmail} already exists.`); throw new Error('Sponsor with this email already exists'); } - this.sponsors.set(sponsor.id, sponsor); - this.emailIndex.set(sponsor.contactEmail.toLowerCase(), sponsor.id); + this.sponsors.set(sponsor.id.toString(), sponsor); + this.emailIndex.set(sponsor.contactEmail.toString().toLowerCase(), sponsor.id.toString()); this.logger.info(`Sponsor ${sponsor.id} (${sponsor.name}) created successfully.`); return Promise.resolve(sponsor); } async update(sponsor: Sponsor): Promise { this.logger.debug(`[InMemorySponsorRepository] Updating sponsor: ${sponsor.id} (${sponsor.name})`); - if (!this.sponsors.has(sponsor.id)) { + if (!this.sponsors.has(sponsor.id.toString())) { this.logger.warn(`Sponsor with ID ${sponsor.id} not found for update.`); throw new Error('Sponsor not found'); } - const existingSponsor = this.sponsors.get(sponsor.id); + const existingSponsor = this.sponsors.get(sponsor.id.toString()); // If email changed, update index - if (existingSponsor && existingSponsor.contactEmail.toLowerCase() !== sponsor.contactEmail.toLowerCase()) { - if (this.emailIndex.has(sponsor.contactEmail.toLowerCase()) && this.emailIndex.get(sponsor.contactEmail.toLowerCase()) !== sponsor.id) { + if (existingSponsor && existingSponsor.contactEmail.toString().toLowerCase() !== sponsor.contactEmail.toString().toLowerCase()) { + if (this.emailIndex.has(sponsor.contactEmail.toString().toLowerCase()) && this.emailIndex.get(sponsor.contactEmail.toString().toLowerCase()) !== sponsor.id.toString()) { this.logger.warn(`Cannot update sponsor ${sponsor.id} to email ${sponsor.contactEmail} as it's already taken.`); throw new Error('Sponsor with this email already exists'); } - this.emailIndex.delete(existingSponsor.contactEmail.toLowerCase()); - this.emailIndex.set(sponsor.contactEmail.toLowerCase(), sponsor.id); + this.emailIndex.delete(existingSponsor.contactEmail.toString().toLowerCase()); + this.emailIndex.set(sponsor.contactEmail.toString().toLowerCase(), sponsor.id.toString()); } - this.sponsors.set(sponsor.id, sponsor); + this.sponsors.set(sponsor.id.toString(), sponsor); this.logger.info(`Sponsor ${sponsor.id} (${sponsor.name}) updated successfully.`); return Promise.resolve(sponsor); } @@ -83,7 +83,7 @@ export class InMemorySponsorRepository implements ISponsorRepository { const sponsor = this.sponsors.get(id); if (sponsor) { this.sponsors.delete(id); - this.emailIndex.delete(sponsor.contactEmail.toLowerCase()); + this.emailIndex.delete(sponsor.contactEmail.toString().toLowerCase()); this.logger.info(`Sponsor ${id} deleted successfully.`); } else { this.logger.warn(`Sponsor with ID ${id} not found for deletion.`); diff --git a/adapters/racing/persistence/inmemory/InMemorySponsorshipPricingRepository.test.ts b/adapters/racing/persistence/inmemory/InMemorySponsorshipPricingRepository.test.ts new file mode 100644 index 000000000..2129f809a --- /dev/null +++ b/adapters/racing/persistence/inmemory/InMemorySponsorshipPricingRepository.test.ts @@ -0,0 +1,117 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import { InMemorySponsorshipPricingRepository } from './InMemorySponsorshipPricingRepository'; +import { SponsorshipPricing } from '@core/racing/domain/value-objects/SponsorshipPricing'; +import type { Logger } from '@core/shared/application'; + +describe('InMemorySponsorshipPricingRepository', () => { + let repository: InMemorySponsorshipPricingRepository; + let mockLogger: Logger; + + beforeEach(() => { + mockLogger = { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; + repository = new InMemorySponsorshipPricingRepository(mockLogger); + }); + + const createTestPricing = (acceptingApplications: boolean = true) => { + return SponsorshipPricing.create({ acceptingApplications }); + }; + + describe('constructor', () => { + it('should initialize with a logger', () => { + expect(repository).toBeDefined(); + expect(mockLogger.info).toHaveBeenCalledWith('InMemorySponsorshipPricingRepository initialized.'); + }); + }); + + describe('findByEntity', () => { + it('should return null if pricing not found', async () => { + const result = await repository.findByEntity('team', 'nonexistent'); + expect(result).toBeNull(); + expect(mockLogger.debug).toHaveBeenCalledWith('Finding sponsorship pricing for entity: team, nonexistent'); + expect(mockLogger.warn).toHaveBeenCalledWith('Sponsorship pricing for entity team, nonexistent not found.'); + }); + + it('should return the pricing if found', async () => { + const pricing = createTestPricing(); + await repository.save('team', '1', pricing); + const result = await repository.findByEntity('team', '1'); + expect(result).toEqual(pricing); + expect(mockLogger.info).toHaveBeenCalledWith('Found sponsorship pricing for entity: team, 1.'); + }); + }); + + describe('save', () => { + it('should save new pricing', async () => { + const pricing = createTestPricing(); + await repository.save('team', '1', pricing); + expect(mockLogger.info).toHaveBeenCalledWith('Creating new sponsorship pricing for entity: team, 1.'); + const found = await repository.findByEntity('team', '1'); + expect(found).toEqual(pricing); + }); + + it('should update existing pricing', async () => { + const pricing1 = createTestPricing(true); + const pricing2 = createTestPricing(false); + await repository.save('team', '1', pricing1); + await repository.save('team', '1', pricing2); + expect(mockLogger.info).toHaveBeenCalledWith('Updating existing sponsorship pricing for entity: team, 1.'); + const found = await repository.findByEntity('team', '1'); + expect(found).toEqual(pricing2); + }); + }); + + describe('delete', () => { + it('should delete existing pricing', async () => { + const pricing = createTestPricing(); + await repository.save('team', '1', pricing); + await repository.delete('team', '1'); + expect(mockLogger.info).toHaveBeenCalledWith('Sponsorship pricing deleted for entity: team, 1.'); + const found = await repository.findByEntity('team', '1'); + expect(found).toBeNull(); + }); + + it('should not throw error if pricing does not exist', async () => { + await repository.delete('team', 'nonexistent'); + expect(mockLogger.warn).toHaveBeenCalledWith('Sponsorship pricing for entity team, nonexistent not found for deletion.'); + }); + }); + + describe('exists', () => { + it('should return true if pricing exists', async () => { + const pricing = createTestPricing(); + await repository.save('team', '1', pricing); + const result = await repository.exists('team', '1'); + expect(result).toBe(true); + }); + + it('should return false if pricing does not exist', async () => { + const result = await repository.exists('team', 'nonexistent'); + expect(result).toBe(false); + }); + }); + + describe('findAcceptingApplications', () => { + it('should return entities accepting applications', async () => { + const accepting = createTestPricing(true); + const notAccepting = createTestPricing(false); + await repository.save('team', '1', accepting); + await repository.save('team', '2', notAccepting); + await repository.save('driver', '3', accepting); + const result = await repository.findAcceptingApplications('team'); + expect(result).toHaveLength(1); + expect(result[0]).toEqual({ entityId: '1', pricing: accepting }); + }); + + it('should return empty array if no entities accepting', async () => { + const notAccepting = createTestPricing(false); + await repository.save('team', '1', notAccepting); + const result = await repository.findAcceptingApplications('team'); + expect(result).toEqual([]); + }); + }); +}); \ No newline at end of file diff --git a/adapters/racing/persistence/inmemory/InMemorySponsorshipPricingRepository.ts b/adapters/racing/persistence/inmemory/InMemorySponsorshipPricingRepository.ts index 5c1748ce5..3807e06f8 100644 --- a/adapters/racing/persistence/inmemory/InMemorySponsorshipPricingRepository.ts +++ b/adapters/racing/persistence/inmemory/InMemorySponsorshipPricingRepository.ts @@ -7,21 +7,13 @@ import { SponsorshipPricing } from '@core/racing/domain/value-objects/Sponsorshi import type { SponsorableEntityType } from '@core/racing/domain/entities/SponsorshipRequest'; import type { Logger } from '@core/shared/application'; -interface StorageKey { - entityType: SponsorableEntityType; - entityId: string; -} - export class InMemorySponsorshipPricingRepository implements ISponsorshipPricingRepository { private pricings: Map = new Map(); private readonly logger: Logger; - constructor(logger: Logger, seedData?: Array<{ entityType: SponsorableEntityType; entityId: string; pricing: SponsorshipPricing }>) { + constructor(logger: Logger) { this.logger = logger; this.logger.info('InMemorySponsorshipPricingRepository initialized.'); - if (seedData) { - this.seed(seedData); - } } private makeKey(entityType: SponsorableEntityType, entityId: string): string { @@ -41,7 +33,7 @@ export class InMemorySponsorshipPricingRepository implements ISponsorshipPricing } return pricing; } catch (error) { - this.logger.error(`Error finding sponsorship pricing for entity ${entityType}, ${entityId}:`, error); + this.logger.error(`Error finding sponsorship pricing for entity ${entityType}, ${entityId}:`, error instanceof Error ? error : new Error(String(error))); throw error; } } @@ -58,7 +50,7 @@ export class InMemorySponsorshipPricingRepository implements ISponsorshipPricing this.pricings.set(key, { entityType, entityId, pricing }); this.logger.info(`Sponsorship pricing saved for entity: ${entityType}, ${entityId}.`); } catch (error) { - this.logger.error(`Error saving sponsorship pricing for entity ${entityType}, ${entityId}:`, error); + this.logger.error(`Error saving sponsorship pricing for entity ${entityType}, ${entityId}:`, error instanceof Error ? error : new Error(String(error))); throw error; } } @@ -73,7 +65,7 @@ export class InMemorySponsorshipPricingRepository implements ISponsorshipPricing this.logger.warn(`Sponsorship pricing for entity ${entityType}, ${entityId} not found for deletion.`); } } catch (error) { - this.logger.error(`Error deleting sponsorship pricing for entity ${entityType}, ${entityId}:`, error); + this.logger.error(`Error deleting sponsorship pricing for entity ${entityType}, ${entityId}:`, error instanceof Error ? error : new Error(String(error))); throw error; } } @@ -86,7 +78,7 @@ export class InMemorySponsorshipPricingRepository implements ISponsorshipPricing this.logger.debug(`Sponsorship pricing for entity ${entityType}, ${entityId} exists: ${exists}.`); return exists; } catch (error) { - this.logger.error(`Error checking existence of sponsorship pricing for entity ${entityType}, ${entityId}:`, error); + this.logger.error(`Error checking existence of sponsorship pricing for entity ${entityType}, ${entityId}:`, error instanceof Error ? error : new Error(String(error))); throw error; } } @@ -107,31 +99,4 @@ export class InMemorySponsorshipPricingRepository implements ISponsorshipPricing throw error; } } - - /** - * Seed initial data - */ - seed(data: Array<{ entityType: SponsorableEntityType; entityId: string; pricing: SponsorshipPricing }>): void { - this.logger.debug(`Seeding ${data.length} sponsorship pricing entries.`); - try { - for (const entry of data) { - const key = this.makeKey(entry.entityType, entry.entityId); - this.pricings.set(key, entry); - this.logger.debug(`Seeded pricing for entity ${entry.entityType}, ${entry.entityId}.`); - } - this.logger.info(`Successfully seeded ${data.length} sponsorship pricing entries.`); - } catch (error) { - this.logger.error(`Error seeding sponsorship pricing data:`, error instanceof Error ? error : new Error(String(error))); - throw error; - } - } - - /** - * Clear all data (for testing) - */ - clear(): void { - this.logger.debug('Clearing all sponsorship pricing data.'); - this.pricings.clear(); - this.logger.info('All sponsorship pricing data cleared.'); - } } \ No newline at end of file diff --git a/adapters/racing/persistence/inmemory/InMemorySponsorshipRequestRepository.test.ts b/adapters/racing/persistence/inmemory/InMemorySponsorshipRequestRepository.test.ts new file mode 100644 index 000000000..57567a48e --- /dev/null +++ b/adapters/racing/persistence/inmemory/InMemorySponsorshipRequestRepository.test.ts @@ -0,0 +1,243 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import { InMemorySponsorshipRequestRepository } from './InMemorySponsorshipRequestRepository'; +import { SponsorshipRequest, SponsorableEntityType, SponsorshipRequestStatus } from '@core/racing/domain/entities/SponsorshipRequest'; +import { Money } from '@core/racing/domain/value-objects/Money'; +import { SponsorshipTier } from '@core/racing/domain/entities/season/SeasonSponsorship'; +import type { Logger } from '@core/shared/application'; + +describe('InMemorySponsorshipRequestRepository', () => { + let repository: InMemorySponsorshipRequestRepository; + let mockLogger: Logger; + + beforeEach(() => { + mockLogger = { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; + repository = new InMemorySponsorshipRequestRepository(mockLogger); + }); + + const createTestRequest = ( + id: string, + sponsorId: string = 'sponsor-1', + entityType: SponsorableEntityType = 'team', + entityId: string = 'entity-1', + status: SponsorshipRequestStatus = 'pending', + tier: SponsorshipTier = 'main', + offeredAmount: Money = Money.create(1000, 'USD') + ): SponsorshipRequest => { + return SponsorshipRequest.create({ + id, + sponsorId, + entityType, + entityId, + tier, + offeredAmount, + status, + createdAt: new Date(), + }); + }; + + describe('constructor', () => { + it('should initialize with a logger', () => { + expect(repository).toBeDefined(); + expect(mockLogger.info).toHaveBeenCalledWith('InMemorySponsorshipRequestRepository initialized.'); + }); + + it('should seed initial requests', () => { + const request = createTestRequest('req-1'); + const repoWithSeed = new InMemorySponsorshipRequestRepository(mockLogger, [request]); + expect(mockLogger.debug).toHaveBeenCalledWith('Seeded sponsorship request: req-1.'); + }); + }); + + describe('findById', () => { + it('should return the request if found', async () => { + const request = createTestRequest('req-1'); + await repository.create(request); + const result = await repository.findById('req-1'); + expect(result).toEqual(request); + expect(mockLogger.info).toHaveBeenCalledWith('Found request by ID: req-1.'); + }); + + it('should return null if not found', async () => { + const result = await repository.findById('nonexistent'); + expect(result).toBeNull(); + expect(mockLogger.warn).toHaveBeenCalledWith('Request with ID nonexistent not found.'); + }); + }); + + describe('findByEntity', () => { + it('should return requests for the entity', async () => { + const request1 = createTestRequest('req-1', 'sponsor-1', 'team', 'entity-1'); + const request2 = createTestRequest('req-2', 'sponsor-2', 'team', 'entity-1'); + const request3 = createTestRequest('req-3', 'sponsor-1', 'driver', 'entity-2'); + await repository.create(request1); + await repository.create(request2); + await repository.create(request3); + const result = await repository.findByEntity('team', 'entity-1'); + expect(result).toHaveLength(2); + expect(result).toContain(request1); + expect(result).toContain(request2); + expect(mockLogger.info).toHaveBeenCalledWith('Found 2 requests for entity team:entity-1.'); + }); + + it('should return empty array if no requests', async () => { + const result = await repository.findByEntity('team', 'nonexistent'); + expect(result).toEqual([]); + expect(mockLogger.info).toHaveBeenCalledWith('Found 0 requests for entity team:nonexistent.'); + }); + }); + + describe('findPendingByEntity', () => { + it('should return pending requests for the entity', async () => { + const pending = createTestRequest('req-1', 'sponsor-1', 'team', 'entity-1', 'pending'); + const accepted = createTestRequest('req-2', 'sponsor-1', 'team', 'entity-1', 'accepted'); + await repository.create(pending); + await repository.create(accepted); + const result = await repository.findPendingByEntity('team', 'entity-1'); + expect(result).toHaveLength(1); + expect(result[0]).toEqual(pending); + expect(mockLogger.info).toHaveBeenCalledWith('Found 1 pending requests for entity team:entity-1.'); + }); + }); + + describe('findBySponsorId', () => { + it('should return requests by sponsor', async () => { + const request1 = createTestRequest('req-1', 'sponsor-1'); + const request2 = createTestRequest('req-2', 'sponsor-1'); + const request3 = createTestRequest('req-3', 'sponsor-2'); + await repository.create(request1); + await repository.create(request2); + await repository.create(request3); + const result = await repository.findBySponsorId('sponsor-1'); + expect(result).toHaveLength(2); + expect(result).toContain(request1); + expect(result).toContain(request2); + expect(mockLogger.info).toHaveBeenCalledWith('Found 2 requests by sponsor ID: sponsor-1.'); + }); + }); + + describe('findByStatus', () => { + it('should return requests by status', async () => { + const pending = createTestRequest('req-1', 'sponsor-1', 'team', 'entity-1', 'pending'); + const accepted = createTestRequest('req-2', 'sponsor-1', 'team', 'entity-1', 'accepted'); + await repository.create(pending); + await repository.create(accepted); + const result = await repository.findByStatus('pending'); + expect(result).toHaveLength(1); + expect(result[0]).toEqual(pending); + expect(mockLogger.info).toHaveBeenCalledWith('Found 1 requests with status: pending.'); + }); + }); + + describe('findBySponsorIdAndStatus', () => { + it('should return requests by sponsor and status', async () => { + const pending1 = createTestRequest('req-1', 'sponsor-1', 'team', 'entity-1', 'pending'); + const accepted1 = createTestRequest('req-2', 'sponsor-1', 'team', 'entity-1', 'accepted'); + const pending2 = createTestRequest('req-3', 'sponsor-2', 'team', 'entity-1', 'pending'); + await repository.create(pending1); + await repository.create(accepted1); + await repository.create(pending2); + const result = await repository.findBySponsorIdAndStatus('sponsor-1', 'pending'); + expect(result).toHaveLength(1); + expect(result[0]).toEqual(pending1); + expect(mockLogger.info).toHaveBeenCalledWith('Found 1 requests by sponsor ID sponsor-1 and status pending.'); + }); + }); + + describe('hasPendingRequest', () => { + it('should return true if pending request exists', async () => { + const request = createTestRequest('req-1', 'sponsor-1', 'team', 'entity-1', 'pending'); + await repository.create(request); + const result = await repository.hasPendingRequest('sponsor-1', 'team', 'entity-1'); + expect(result).toBe(true); + expect(mockLogger.info).toHaveBeenCalledWith('Pending request for sponsor sponsor-1, entity team:entity-1 exists: true.'); + }); + + it('should return false if no pending request', async () => { + const result = await repository.hasPendingRequest('sponsor-1', 'team', 'entity-1'); + expect(result).toBe(false); + expect(mockLogger.info).toHaveBeenCalledWith('Pending request for sponsor sponsor-1, entity team:entity-1 exists: false.'); + }); + }); + + describe('countPendingByEntity', () => { + it('should count pending requests for entity', async () => { + const request1 = createTestRequest('req-1', 'sponsor-1', 'team', 'entity-1', 'pending'); + const request2 = createTestRequest('req-2', 'sponsor-2', 'team', 'entity-1', 'pending'); + const accepted = createTestRequest('req-3', 'sponsor-3', 'team', 'entity-1', 'accepted'); + await repository.create(request1); + await repository.create(request2); + await repository.create(accepted); + const result = await repository.countPendingByEntity('team', 'entity-1'); + expect(result).toBe(2); + expect(mockLogger.info).toHaveBeenCalledWith('Count of pending requests for entity team:entity-1: 2.'); + }); + }); + + describe('create', () => { + it('should create a new request', async () => { + const request = createTestRequest('req-1'); + const result = await repository.create(request); + expect(result).toEqual(request); + expect(mockLogger.info).toHaveBeenCalledWith('Sponsorship request req-1 created successfully.'); + }); + + it('should throw if request already exists', async () => { + const request = createTestRequest('req-1'); + await repository.create(request); + await expect(repository.create(request)).rejects.toThrow('Sponsorship request already exists'); + expect(mockLogger.warn).toHaveBeenCalledWith('Request with ID req-1 already exists.'); + }); + }); + + describe('update', () => { + it('should update existing request', async () => { + const request = createTestRequest('req-1'); + await repository.create(request); + const updated = request.accept('responder-1'); + const result = await repository.update(updated); + expect(result).toEqual(updated); + expect(mockLogger.info).toHaveBeenCalledWith('Sponsorship request req-1 updated successfully.'); + }); + + it('should throw if request not found', async () => { + const request = createTestRequest('req-1'); + await expect(repository.update(request)).rejects.toThrow('Sponsorship request not found'); + expect(mockLogger.warn).toHaveBeenCalledWith('Request with ID req-1 not found for update.'); + }); + }); + + describe('delete', () => { + it('should delete existing request', async () => { + const request = createTestRequest('req-1'); + await repository.create(request); + await repository.delete('req-1'); + expect(mockLogger.info).toHaveBeenCalledWith('Sponsorship request req-1 deleted successfully.'); + const found = await repository.findById('req-1'); + expect(found).toBeNull(); + }); + + it('should not throw if request not found', async () => { + await repository.delete('nonexistent'); + expect(mockLogger.warn).toHaveBeenCalledWith('Request with ID nonexistent not found for deletion.'); + }); + }); + + describe('exists', () => { + it('should return true if exists', async () => { + const request = createTestRequest('req-1'); + await repository.create(request); + const result = await repository.exists('req-1'); + expect(result).toBe(true); + }); + + it('should return false if not exists', async () => { + const result = await repository.exists('nonexistent'); + expect(result).toBe(false); + }); + }); +}); \ No newline at end of file diff --git a/adapters/racing/persistence/inmemory/InMemoryStandingRepository.test.ts b/adapters/racing/persistence/inmemory/InMemoryStandingRepository.test.ts new file mode 100644 index 000000000..1c431115a --- /dev/null +++ b/adapters/racing/persistence/inmemory/InMemoryStandingRepository.test.ts @@ -0,0 +1,296 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import { InMemoryStandingRepository } from './InMemoryStandingRepository'; +import { Standing } from '@core/racing/domain/entities/Standing'; +import type { Logger } from '@core/shared/application'; + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +describe('InMemoryStandingRepository', () => { + let repository: InMemoryStandingRepository; + let mockLogger: Logger; + let mockResultRepository: any; + let mockRaceRepository: any; + let mockLeagueRepository: any; + const testPointsSystems: Record> = { + 'f1-2024': { + 1: 25, 2: 18, 3: 15, 4: 12, 5: 10, + 6: 8, 7: 6, 8: 4, 9: 2, 10: 1 + }, + 'indycar': { + 1: 50, 2: 40, 3: 35, 4: 32, 5: 30, + 6: 28, 7: 26, 8: 24, 9: 22, 10: 20, + 11: 19, 12: 18, 13: 17, 14: 16, 15: 15 + } + }; + + beforeEach(() => { + mockLogger = { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + mockResultRepository = { + findById: vi.fn(), + findAll: vi.fn(), + findByRaceId: vi.fn(), + findByDriverId: vi.fn(), + findByDriverIdAndLeagueId: vi.fn(), + create: vi.fn(), + createMany: vi.fn(), + update: vi.fn(), + delete: vi.fn(), + deleteByRaceId: vi.fn(), + exists: vi.fn(), + existsByRaceId: vi.fn(), + } as any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + mockRaceRepository = { + findById: vi.fn(), + findAll: vi.fn(), + findByLeagueId: vi.fn(), + findUpcomingByLeagueId: vi.fn(), + findCompletedByLeagueId: vi.fn(), + findByStatus: vi.fn(), + findByDateRange: vi.fn(), + create: vi.fn(), + update: vi.fn(), + delete: vi.fn(), + exists: vi.fn(), + } as any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + mockLeagueRepository = { + findById: vi.fn(), + findByOwnerId: vi.fn(), + searchByName: vi.fn(), + findAll: vi.fn(), + create: vi.fn(), + update: vi.fn(), + delete: vi.fn(), + exists: vi.fn(), + } as any; + repository = new InMemoryStandingRepository( + mockLogger, + testPointsSystems, + mockResultRepository, + mockRaceRepository, + mockLeagueRepository + ); + }); + + const createTestStanding = (leagueId: string, driverId: string, position: number = 1, points: number = 0) => { + return Standing.create({ + leagueId, + driverId, + position, + points, + }); + }; + + describe('constructor', () => { + it('should initialize with required parameters', () => { + expect(repository).toBeDefined(); + expect(mockLogger.info).toHaveBeenCalledWith('InMemoryStandingRepository initialized.'); + }); + }); + + describe('findByLeagueId', () => { + it('should return standings for the specified league', async () => { + const standing1 = createTestStanding('league1', 'driver1', 1, 25); + const standing2 = createTestStanding('league1', 'driver2', 2, 18); + const standing3 = createTestStanding('league2', 'driver3', 1, 50); + await repository.save(standing1); + await repository.save(standing2); + await repository.save(standing3); + + const result = await repository.findByLeagueId('league1'); + expect(result).toHaveLength(2); + expect(result[0]).toEqual(standing1); + expect(result[1]).toEqual(standing2); + }); + + it('should return empty array if no standings for league', async () => { + const result = await repository.findByLeagueId('nonexistent'); + expect(result).toEqual([]); + }); + + it('should sort standings by position then points', async () => { + const standing1 = createTestStanding('league1', 'driver1', 2, 18); + const standing2 = createTestStanding('league1', 'driver2', 1, 25); + await repository.save(standing1); + await repository.save(standing2); + + const result = await repository.findByLeagueId('league1'); + expect(result[0]).toEqual(standing2); // position 1 + expect(result[1]).toEqual(standing1); // position 2 + }); + }); + + describe('findByDriverIdAndLeagueId', () => { + it('should return the standing if found', async () => { + const standing = createTestStanding('league1', 'driver1'); + await repository.save(standing); + + const result = await repository.findByDriverIdAndLeagueId('driver1', 'league1'); + expect(result).toEqual(standing); + }); + + it('should return null if not found', async () => { + const result = await repository.findByDriverIdAndLeagueId('driver1', 'league1'); + expect(result).toBeNull(); + }); + }); + + describe('findAll', () => { + it('should return all standings', async () => { + const standing1 = createTestStanding('league1', 'driver1'); + const standing2 = createTestStanding('league2', 'driver2'); + await repository.save(standing1); + await repository.save(standing2); + + const result = await repository.findAll(); + expect(result).toHaveLength(2); + expect(result).toContain(standing1); + expect(result).toContain(standing2); + }); + }); + + describe('save', () => { + it('should save a new standing', async () => { + const standing = createTestStanding('league1', 'driver1'); + const result = await repository.save(standing); + expect(result).toEqual(standing); + expect(mockLogger.info).toHaveBeenCalledWith('Standing for league league1, driver driver1 saved successfully.'); + }); + + it('should update existing standing', async () => { + const standing = createTestStanding('league1', 'driver1', 1, 25); + await repository.save(standing); + const updatedStanding = createTestStanding('league1', 'driver1', 1, 35); + const result = await repository.save(updatedStanding); + expect(result).toEqual(updatedStanding); + }); + }); + + describe('saveMany', () => { + it('should save multiple standings', async () => { + const standings = [ + createTestStanding('league1', 'driver1'), + createTestStanding('league1', 'driver2'), + ]; + const result = await repository.saveMany(standings); + expect(result).toEqual(standings); + expect(mockLogger.info).toHaveBeenCalledWith('2 standings saved successfully.'); + }); + }); + + describe('delete', () => { + it('should delete existing standing', async () => { + const standing = createTestStanding('league1', 'driver1'); + await repository.save(standing); + await repository.delete('league1', 'driver1'); + const found = await repository.findByDriverIdAndLeagueId('driver1', 'league1'); + expect(found).toBeNull(); + expect(mockLogger.info).toHaveBeenCalledWith('Standing for league league1, driver driver1 deleted successfully.'); + }); + + it('should log warning if standing not found', async () => { + await repository.delete('league1', 'driver1'); + expect(mockLogger.warn).toHaveBeenCalledWith('Standing for league league1, driver driver1 not found for deletion.'); + }); + }); + + describe('deleteByLeagueId', () => { + it('should delete all standings for a league', async () => { + const standing1 = createTestStanding('league1', 'driver1'); + const standing2 = createTestStanding('league1', 'driver2'); + const standing3 = createTestStanding('league2', 'driver3'); + await repository.save(standing1); + await repository.save(standing2); + await repository.save(standing3); + + await repository.deleteByLeagueId('league1'); + const league1Standings = await repository.findByLeagueId('league1'); + const league2Standings = await repository.findByLeagueId('league2'); + expect(league1Standings).toHaveLength(0); + expect(league2Standings).toHaveLength(1); + expect(mockLogger.info).toHaveBeenCalledWith('Deleted 2 standings for league id: league1.'); + }); + }); + + describe('exists', () => { + it('should return true if standing exists', async () => { + const standing = createTestStanding('league1', 'driver1'); + await repository.save(standing); + const result = await repository.exists('league1', 'driver1'); + expect(result).toBe(true); + }); + + it('should return false if standing does not exist', async () => { + const result = await repository.exists('league1', 'driver1'); + expect(result).toBe(false); + }); + }); + + describe('recalculate', () => { + it('should throw error if repositories are missing', async () => { + const repoWithoutDeps = new InMemoryStandingRepository(mockLogger, testPointsSystems); + await expect(repoWithoutDeps.recalculate('league1')).rejects.toThrow('Cannot recalculate standings: missing required repositories'); + }); + + it('should throw error if league not found', async () => { + mockLeagueRepository.findById.mockResolvedValue(null); + await expect(repository.recalculate('nonexistent')).rejects.toThrow('League with ID nonexistent not found'); + }); + + it('should return empty array if no completed races', async () => { + const mockLeague = { + id: 'league1', + settings: { pointsSystem: 'f1-2024', customPoints: null }, + }; + mockLeagueRepository.findById.mockResolvedValue(mockLeague); + mockRaceRepository.findCompletedByLeagueId.mockResolvedValue([]); + const result = await repository.recalculate('league1'); + expect(result).toEqual([]); + }); + + it('should recalculate standings based on race results', async () => { + const mockLeague = { + id: 'league1', + settings: { pointsSystem: 'f1-2024', customPoints: null }, + }; + const mockRace = { id: 'race1' }; + const mockResult1 = { driverId: 'driver1', position: 1 }; + const mockResult2 = { driverId: 'driver2', position: 2 }; + + mockLeagueRepository.findById.mockResolvedValue(mockLeague); + mockRaceRepository.findCompletedByLeagueId.mockResolvedValue([mockRace]); + mockResultRepository.findByRaceId.mockResolvedValue([mockResult1, mockResult2]); + + const result = await repository.recalculate('league1'); + expect(result).toHaveLength(2); + expect(result[0]?.driverId.toString()).toBe('driver1'); + expect(result[0]?.points.toNumber()).toBe(25); // 1st place in f1-2024 + expect(result[1]?.driverId.toString()).toBe('driver2'); + expect(result[1]?.points.toNumber()).toBe(18); // 2nd place + }); + + it('should use custom points if available', async () => { + const customPoints = { 1: 100, 2: 80 }; + const mockLeague = { + id: 'league1', + settings: { pointsSystem: 'f1-2024', customPoints }, + }; + const mockRace = { id: 'race1' }; + const mockResult = { driverId: 'driver1', position: 1 }; + + mockLeagueRepository.findById.mockResolvedValue(mockLeague); + mockRaceRepository.findCompletedByLeagueId.mockResolvedValue([mockRace]); + mockResultRepository.findByRaceId.mockResolvedValue([mockResult]); + + const result = await repository.recalculate('league1'); + expect(result[0]?.points.toNumber()).toBe(100); + }); + }); +}); \ No newline at end of file diff --git a/adapters/racing/persistence/inmemory/InMemoryStandingRepository.ts b/adapters/racing/persistence/inmemory/InMemoryStandingRepository.ts index b6a721bed..ca5f8513b 100644 --- a/adapters/racing/persistence/inmemory/InMemoryStandingRepository.ts +++ b/adapters/racing/persistence/inmemory/InMemoryStandingRepository.ts @@ -12,49 +12,28 @@ import type { IRaceRepository } from '@core/racing/domain/repositories/IRaceRepo import type { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository'; import type { Logger } from '@core/shared/application'; -/** - * Points systems presets - */ -const POINTS_SYSTEMS: Record> = { - 'f1-2024': { - 1: 25, 2: 18, 3: 15, 4: 12, 5: 10, - 6: 8, 7: 6, 8: 4, 9: 2, 10: 1 - }, - 'indycar': { - 1: 50, 2: 40, 3: 35, 4: 32, 5: 30, - 6: 28, 7: 26, 8: 24, 9: 22, 10: 20, - 11: 19, 12: 18, 13: 17, 14: 16, 15: 15 - } -}; - export class InMemoryStandingRepository implements IStandingRepository { private standings: Map; private resultRepository: IResultRepository | null; private raceRepository: IRaceRepository | null; private leagueRepository: ILeagueRepository | null; private readonly logger: Logger; + private readonly pointsSystems: Record>; constructor( logger: Logger, - seedData?: Standing[], + pointsSystems: Record>, resultRepository?: IResultRepository | null, raceRepository?: IRaceRepository | null, leagueRepository?: ILeagueRepository | null ) { this.logger = logger; + this.pointsSystems = pointsSystems; this.logger.info('InMemoryStandingRepository initialized.'); this.standings = new Map(); this.resultRepository = resultRepository ?? null; this.raceRepository = raceRepository ?? null; this.leagueRepository = leagueRepository ?? null; - - if (seedData) { - seedData.forEach(standing => { - const key = this.getKey(standing.leagueId, standing.driverId); - this.standings.set(key, standing); - this.logger.debug(`Seeded standing for league ${standing.leagueId}, driver ${standing.driverId}.`); - }); - } } private getKey(leagueId: string, driverId: string): string { @@ -65,14 +44,14 @@ export class InMemoryStandingRepository implements IStandingRepository { this.logger.debug(`Finding standings for league id: ${leagueId}`); try { const standings = Array.from(this.standings.values()) - .filter(standing => standing.leagueId === leagueId) + .filter(standing => standing.leagueId.toString() === leagueId) .sort((a, b) => { // Sort by position (lower is better) - if (a.position !== b.position) { - return a.position - b.position; + if (a.position.toNumber() !== b.position.toNumber()) { + return a.position.toNumber() - b.position.toNumber(); } // If positions are equal, sort by points (higher is better) - return b.points - a.points; + return b.points.toNumber() - a.points.toNumber(); }); this.logger.info(`Found ${standings.length} standings for league id: ${leagueId}.`); return standings; @@ -162,12 +141,11 @@ export class InMemoryStandingRepository implements IStandingRepository { async deleteByLeagueId(leagueId: string): Promise { this.logger.debug(`Deleting all standings for league id: ${leagueId}`); try { - const initialCount = Array.from(this.standings.values()).filter(s => s.leagueId === leagueId).length; const toDelete = Array.from(this.standings.values()) - .filter(standing => standing.leagueId === leagueId); - + .filter(standing => standing.leagueId.toString() === leagueId); + toDelete.forEach(standing => { - const key = this.getKey(standing.leagueId, standing.driverId); + const key = this.getKey(standing.leagueId.toString(), standing.driverId.toString()); this.standings.delete(key); }); this.logger.info(`Deleted ${toDelete.length} standings for league id: ${leagueId}.`); @@ -209,8 +187,8 @@ export class InMemoryStandingRepository implements IStandingRepository { // Get points system const resolvedPointsSystem = league.settings.customPoints ?? - POINTS_SYSTEMS[league.settings.pointsSystem] ?? - POINTS_SYSTEMS['f1-2024']; + this.pointsSystems[league.settings.pointsSystem] ?? + this.pointsSystems['f1-2024']; if (!resolvedPointsSystem) { this.logger.error(`No points system configured for league ${leagueId}.`); @@ -292,11 +270,4 @@ export class InMemoryStandingRepository implements IStandingRepository { throw error; } } - - /** - * Get available points systems - */ - static getPointsSystems(): Record> { - return POINTS_SYSTEMS; - } } \ No newline at end of file diff --git a/adapters/racing/persistence/inmemory/InMemoryTeamMembershipRepository.test.ts b/adapters/racing/persistence/inmemory/InMemoryTeamMembershipRepository.test.ts new file mode 100644 index 000000000..783f30968 --- /dev/null +++ b/adapters/racing/persistence/inmemory/InMemoryTeamMembershipRepository.test.ts @@ -0,0 +1,238 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import { InMemoryTeamMembershipRepository } from './InMemoryTeamMembershipRepository'; +import type { TeamMembership, TeamJoinRequest } from '@core/racing/domain/types/TeamMembership'; +import type { Logger } from '@core/shared/application'; + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +describe('InMemoryTeamMembershipRepository', () => { + let repository: InMemoryTeamMembershipRepository; + let mockLogger: Logger; + + beforeEach(() => { + mockLogger = { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; + repository = new InMemoryTeamMembershipRepository(mockLogger); + }); + + const createTestMembership = ( + teamId: string, + driverId: string, + role: 'owner' | 'manager' | 'driver' = 'driver', + status: 'active' | 'pending' | 'none' = 'active', + joinedAt: Date = new Date() + ): TeamMembership => ({ + teamId, + driverId, + role, + status, + joinedAt, + }); + + const createTestJoinRequest = ( + id: string, + teamId: string, + driverId: string, + requestedAt: Date = new Date(), + message?: string + ): TeamJoinRequest => ({ + id, + teamId, + driverId, + requestedAt, + ...(message !== undefined ? { message } : {}), + }); + + describe('constructor', () => { + it('should initialize with logger', () => { + expect(repository).toBeDefined(); + expect(mockLogger.info).toHaveBeenCalledWith('InMemoryTeamMembershipRepository initialized.'); + }); + }); + + describe('getMembership', () => { + it('should return membership if found', async () => { + const membership = createTestMembership('team1', 'driver1'); + await repository.saveMembership(membership); + + const result = await repository.getMembership('team1', 'driver1'); + expect(result).toEqual(membership); + }); + + it('should return null if membership not found', async () => { + const result = await repository.getMembership('team1', 'driver1'); + expect(result).toBeNull(); + }); + + it('should return null if team has no memberships', async () => { + const result = await repository.getMembership('nonexistent', 'driver1'); + expect(result).toBeNull(); + }); + }); + + describe('getActiveMembershipForDriver', () => { + it('should return active membership for driver', async () => { + const activeMembership = createTestMembership('team1', 'driver1', 'driver', 'active'); + const pendingMembership = createTestMembership('team2', 'driver1', 'driver', 'pending'); + await repository.saveMembership(activeMembership); + await repository.saveMembership(pendingMembership); + + const result = await repository.getActiveMembershipForDriver('driver1'); + expect(result).toEqual(activeMembership); + }); + + it('should return null if no active membership', async () => { + const pendingMembership = createTestMembership('team1', 'driver1', 'driver', 'pending'); + await repository.saveMembership(pendingMembership); + + const result = await repository.getActiveMembershipForDriver('driver1'); + expect(result).toBeNull(); + }); + + it('should return null if driver has no memberships', async () => { + const result = await repository.getActiveMembershipForDriver('driver1'); + expect(result).toBeNull(); + }); + }); + + describe('getTeamMembers', () => { + it('should return all members for team', async () => { + const member1 = createTestMembership('team1', 'driver1'); + const member2 = createTestMembership('team1', 'driver2'); + const member3 = createTestMembership('team2', 'driver3'); + await repository.saveMembership(member1); + await repository.saveMembership(member2); + await repository.saveMembership(member3); + + const result = await repository.getTeamMembers('team1'); + expect(result).toHaveLength(2); + expect(result).toContain(member1); + expect(result).toContain(member2); + }); + + it('should return empty array if team has no members', async () => { + const result = await repository.getTeamMembers('team1'); + expect(result).toEqual([]); + }); + }); + + describe('countByTeamId', () => { + it('should count active members for team', async () => { + const active1 = createTestMembership('team1', 'driver1', 'driver', 'active'); + const active2 = createTestMembership('team1', 'driver2', 'driver', 'active'); + const pending = createTestMembership('team1', 'driver3', 'driver', 'pending'); + await repository.saveMembership(active1); + await repository.saveMembership(active2); + await repository.saveMembership(pending); + + const result = await repository.countByTeamId('team1'); + expect(result).toBe(2); + }); + + it('should return 0 if team has no active members', async () => { + const pending = createTestMembership('team1', 'driver1', 'driver', 'pending'); + await repository.saveMembership(pending); + + const result = await repository.countByTeamId('team1'); + expect(result).toBe(0); + }); + + it('should return 0 if team has no members', async () => { + const result = await repository.countByTeamId('team1'); + expect(result).toBe(0); + }); + }); + + describe('saveMembership', () => { + it('should save new membership', async () => { + const membership = createTestMembership('team1', 'driver1'); + const result = await repository.saveMembership(membership); + expect(result).toEqual(membership); + expect(mockLogger.info).toHaveBeenCalledWith('[saveMembership] Success - created new membership for team: team1, driver: driver1.'); + }); + + it('should update existing membership', async () => { + const membership = createTestMembership('team1', 'driver1', 'driver', 'active'); + await repository.saveMembership(membership); + const updated = createTestMembership('team1', 'driver1', 'manager', 'active'); + const result = await repository.saveMembership(updated); + expect(result).toEqual(updated); + expect(mockLogger.info).toHaveBeenCalledWith('[saveMembership] Success - updated existing membership for team: team1, driver: driver1.'); + }); + }); + + describe('removeMembership', () => { + it('should remove existing membership', async () => { + const membership = createTestMembership('team1', 'driver1'); + await repository.saveMembership(membership); + await repository.removeMembership('team1', 'driver1'); + const found = await repository.getMembership('team1', 'driver1'); + expect(found).toBeNull(); + expect(mockLogger.info).toHaveBeenCalledWith('[removeMembership] Success - removed membership for team: team1, driver: driver1.'); + }); + + it('should log warning if membership not found', async () => { + await repository.removeMembership('team1', 'driver1'); + expect(mockLogger.warn).toHaveBeenCalledWith('[removeMembership] No membership list found for team: team1. Cannot remove.'); + }); + }); + + describe('getJoinRequests', () => { + it('should return join requests for team', async () => { + const request1 = createTestJoinRequest('req1', 'team1', 'driver1'); + const request2 = createTestJoinRequest('req2', 'team1', 'driver2'); + const request3 = createTestJoinRequest('req3', 'team2', 'driver3'); + await repository.saveJoinRequest(request1); + await repository.saveJoinRequest(request2); + await repository.saveJoinRequest(request3); + + const result = await repository.getJoinRequests('team1'); + expect(result).toHaveLength(2); + expect(result).toContain(request1); + expect(result).toContain(request2); + }); + + it('should return empty array if team has no requests', async () => { + const result = await repository.getJoinRequests('team1'); + expect(result).toEqual([]); + }); + }); + + describe('saveJoinRequest', () => { + it('should save new join request', async () => { + const request = createTestJoinRequest('req1', 'team1', 'driver1'); + const result = await repository.saveJoinRequest(request); + expect(result).toEqual(request); + expect(mockLogger.info).toHaveBeenCalledWith('[saveJoinRequest] Success - created new join request: req1.'); + }); + + it('should update existing join request', async () => { + const request = createTestJoinRequest('req1', 'team1', 'driver1'); + await repository.saveJoinRequest(request); + const updated = createTestJoinRequest('req1', 'team1', 'driver1', new Date(), 'updated message'); + const result = await repository.saveJoinRequest(updated); + expect(result).toEqual(updated); + expect(mockLogger.info).toHaveBeenCalledWith('[saveJoinRequest] Success - updated existing join request: req1.'); + }); + }); + + describe('removeJoinRequest', () => { + it('should remove join request by id', async () => { + const request = createTestJoinRequest('req1', 'team1', 'driver1'); + await repository.saveJoinRequest(request); + await repository.removeJoinRequest('req1'); + const requests = await repository.getJoinRequests('team1'); + expect(requests).toHaveLength(0); + expect(mockLogger.info).toHaveBeenCalledWith('[removeJoinRequest] Success - removed join request req1 from team team1.'); + }); + + it('should log warning if request not found', async () => { + await repository.removeJoinRequest('req1'); + expect(mockLogger.warn).toHaveBeenCalledWith('[removeJoinRequest] Not found - join request with ID req1 not found for removal.'); + }); + }); +}); \ No newline at end of file diff --git a/adapters/racing/persistence/inmemory/InMemoryTeamMembershipRepository.ts b/adapters/racing/persistence/inmemory/InMemoryTeamMembershipRepository.ts index 576bca0ba..224e13afe 100644 --- a/adapters/racing/persistence/inmemory/InMemoryTeamMembershipRepository.ts +++ b/adapters/racing/persistence/inmemory/InMemoryTeamMembershipRepository.ts @@ -17,29 +17,11 @@ export class InMemoryTeamMembershipRepository implements ITeamMembershipReposito private joinRequestsByTeam: Map; private readonly logger: Logger; - constructor(logger: Logger, seedMemberships?: TeamMembership[], seedJoinRequests?: TeamJoinRequest[]) { + constructor(logger: Logger) { this.logger = logger; this.logger.info('InMemoryTeamMembershipRepository initialized.'); this.membershipsByTeam = new Map(); this.joinRequestsByTeam = new Map(); - - if (seedMemberships) { - seedMemberships.forEach((membership) => { - const list = this.membershipsByTeam.get(membership.teamId) ?? []; - list.push(membership); - this.membershipsByTeam.set(membership.teamId, list); - this.logger.debug(`Seeded membership for team ${membership.teamId}, driver ${membership.driverId}.`); - }); - } - - if (seedJoinRequests) { - seedJoinRequests.forEach((request) => { - const list = this.joinRequestsByTeam.get(request.teamId) ?? []; - list.push(request); - this.joinRequestsByTeam.set(request.teamId, list); - this.logger.debug(`Seeded join request for team ${request.teamId}, driver ${request.driverId}.`); - }); - } } private getMembershipList(teamId: string): TeamMembership[] { @@ -78,7 +60,7 @@ async getMembership(teamId: string, driverId: string): Promise { + let repository: InMemoryTeamRepository; + let mockLogger: Logger; + + beforeEach(() => { + mockLogger = { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; + repository = new InMemoryTeamRepository(mockLogger); + }); + + describe('constructor', () => { + it('should initialize with a logger', () => { + expect(repository).toBeDefined(); + expect(mockLogger.info).toHaveBeenCalledWith('InMemoryTeamRepository initialized.'); + }); + }); + + describe('findById', () => { + it('should return null if team not found', async () => { + const result = await repository.findById('nonexistent'); + expect(result).toBeNull(); + expect(mockLogger.debug).toHaveBeenCalledWith('Finding team by id: nonexistent'); + expect(mockLogger.warn).toHaveBeenCalledWith('Team with id nonexistent not found.'); + }); + + it('should return the team if found', async () => { + const team = Team.create({ + id: 'team1', + name: 'Test Team', + tag: 'TT', + description: 'A test team', + ownerId: 'owner1', + leagues: ['league1'], + }); + await repository.create(team); + + const result = await repository.findById('team1'); + expect(result).toEqual(team); + expect(mockLogger.debug).toHaveBeenCalledWith('Finding team by id: team1'); + expect(mockLogger.info).toHaveBeenCalledWith('Found team: team1.'); + }); + }); + + describe('findAll', () => { + it('should return an empty array when no teams', async () => { + const result = await repository.findAll(); + expect(result).toEqual([]); + expect(mockLogger.debug).toHaveBeenCalledWith('Finding all teams.'); + expect(mockLogger.info).toHaveBeenCalledWith('Found 0 teams.'); + }); + + it('should return all teams', async () => { + const team1 = Team.create({ + id: 'team1', + name: 'Test Team 1', + tag: 'TT1', + description: 'A test team 1', + ownerId: 'owner1', + leagues: ['league1'], + }); + const team2 = Team.create({ + id: 'team2', + name: 'Test Team 2', + tag: 'TT2', + description: 'A test team 2', + ownerId: 'owner2', + leagues: ['league2'], + }); + await repository.create(team1); + await repository.create(team2); + + const result = await repository.findAll(); + expect(result).toEqual([team1, team2]); + expect(mockLogger.debug).toHaveBeenCalledWith('Finding all teams.'); + expect(mockLogger.info).toHaveBeenCalledWith('Found 2 teams.'); + }); + }); + + describe('findByLeagueId', () => { + it('should return empty array if no teams in league', async () => { + const result = await repository.findByLeagueId('league1'); + expect(result).toEqual([]); + expect(mockLogger.debug).toHaveBeenCalledWith('Finding teams by league id: league1'); + expect(mockLogger.info).toHaveBeenCalledWith('Found 0 teams for league id: league1.'); + }); + + it('should return teams in the league', async () => { + const team1 = Team.create({ + id: 'team1', + name: 'Test Team 1', + tag: 'TT1', + description: 'A test team 1', + ownerId: 'owner1', + leagues: ['league1'], + }); + const team2 = Team.create({ + id: 'team2', + name: 'Test Team 2', + tag: 'TT2', + description: 'A test team 2', + ownerId: 'owner2', + leagues: ['league1', 'league2'], + }); + await repository.create(team1); + await repository.create(team2); + + const result = await repository.findByLeagueId('league1'); + expect(result).toEqual([team1, team2]); + expect(mockLogger.debug).toHaveBeenCalledWith('Finding teams by league id: league1'); + expect(mockLogger.info).toHaveBeenCalledWith('Found 2 teams for league id: league1.'); + }); + }); + + describe('create', () => { + it('should create a new team', async () => { + const team = Team.create({ + id: 'team1', + name: 'Test Team', + tag: 'TT', + description: 'A test team', + ownerId: 'owner1', + leagues: ['league1'], + }); + + const result = await repository.create(team); + expect(result).toEqual(team); + expect(mockLogger.debug).toHaveBeenCalledWith('Creating team: team1'); + expect(mockLogger.info).toHaveBeenCalledWith('Team team1 created successfully.'); + }); + + it('should throw error if team already exists', async () => { + const team = Team.create({ + id: 'team1', + name: 'Test Team', + tag: 'TT', + description: 'A test team', + ownerId: 'owner1', + leagues: ['league1'], + }); + await repository.create(team); + + await expect(repository.create(team)).rejects.toThrow('Team with ID team1 already exists'); + expect(mockLogger.warn).toHaveBeenCalledWith('Team with ID team1 already exists.'); + }); + }); + + describe('update', () => { + it('should update an existing team', async () => { + const team = Team.create({ + id: 'team1', + name: 'Test Team', + tag: 'TT', + description: 'A test team', + ownerId: 'owner1', + leagues: ['league1'], + }); + await repository.create(team); + + const updatedTeam = team.update({ name: 'Updated Team' }); + const result = await repository.update(updatedTeam); + expect(result).toEqual(updatedTeam); + expect(mockLogger.debug).toHaveBeenCalledWith('Updating team: team1'); + expect(mockLogger.info).toHaveBeenCalledWith('Team team1 updated successfully.'); + }); + + it('should throw error if team does not exist', async () => { + const team = Team.create({ + id: 'team1', + name: 'Test Team', + tag: 'TT', + description: 'A test team', + ownerId: 'owner1', + leagues: ['league1'], + }); + + await expect(repository.update(team)).rejects.toThrow('Team with ID team1 not found'); + expect(mockLogger.warn).toHaveBeenCalledWith('Team with ID team1 not found for update.'); + }); + }); + + describe('delete', () => { + it('should delete an existing team', async () => { + const team = Team.create({ + id: 'team1', + name: 'Test Team', + tag: 'TT', + description: 'A test team', + ownerId: 'owner1', + leagues: ['league1'], + }); + await repository.create(team); + + await repository.delete('team1'); + expect(await repository.exists('team1')).toBe(false); + expect(mockLogger.debug).toHaveBeenCalledWith('Deleting team: team1'); + expect(mockLogger.info).toHaveBeenCalledWith('Team team1 deleted successfully.'); + }); + + it('should throw error if team does not exist', async () => { + await expect(repository.delete('nonexistent')).rejects.toThrow('Team with ID nonexistent not found'); + expect(mockLogger.warn).toHaveBeenCalledWith('Team with ID nonexistent not found for deletion.'); + }); + }); + + describe('exists', () => { + it('should return true if team exists', async () => { + const team = Team.create({ + id: 'team1', + name: 'Test Team', + tag: 'TT', + description: 'A test team', + ownerId: 'owner1', + leagues: ['league1'], + }); + await repository.create(team); + + const result = await repository.exists('team1'); + expect(result).toBe(true); + expect(mockLogger.debug).toHaveBeenCalledWith('Checking existence of team with id: team1'); + }); + + it('should return false if team does not exist', async () => { + const result = await repository.exists('nonexistent'); + expect(result).toBe(false); + expect(mockLogger.debug).toHaveBeenCalledWith('Checking existence of team with id: nonexistent'); + }); + }); +}); \ No newline at end of file diff --git a/adapters/racing/persistence/inmemory/InMemoryTeamRepository.ts b/adapters/racing/persistence/inmemory/InMemoryTeamRepository.ts index 7073c0140..4ff95c524 100644 --- a/adapters/racing/persistence/inmemory/InMemoryTeamRepository.ts +++ b/adapters/racing/persistence/inmemory/InMemoryTeamRepository.ts @@ -13,17 +13,10 @@ export class InMemoryTeamRepository implements ITeamRepository { private teams: Map; private readonly logger: Logger; - constructor(logger: Logger, seedData?: Team[]) { + constructor(logger: Logger) { this.logger = logger; this.logger.info('InMemoryTeamRepository initialized.'); this.teams = new Map(); - - if (seedData) { - seedData.forEach((team) => { - this.teams.set(team.id, team); - this.logger.debug(`Seeded team: ${team.id}.`); - }); - } } async findById(id: string): Promise { @@ -58,7 +51,7 @@ export class InMemoryTeamRepository implements ITeamRepository { this.logger.debug(`Finding teams by league id: ${leagueId}`); try { const teams = Array.from(this.teams.values()).filter((team) => - team.leagues.includes(leagueId), + team.leagues.some(league => league.toString() === leagueId), ); this.logger.info(`Found ${teams.length} teams for league id: ${leagueId}.`); return teams; diff --git a/adapters/racing/persistence/inmemory/InMemoryTrackRepository.test.ts b/adapters/racing/persistence/inmemory/InMemoryTrackRepository.test.ts new file mode 100644 index 000000000..a938ab647 --- /dev/null +++ b/adapters/racing/persistence/inmemory/InMemoryTrackRepository.test.ts @@ -0,0 +1,411 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import { InMemoryTrackRepository } from './InMemoryTrackRepository'; +import type { Logger } from '@core/shared/application'; +import { Track } from '@core/racing/domain/entities/Track'; + +describe('InMemoryTrackRepository', () => { + let repository: InMemoryTrackRepository; + let mockLogger: Logger; + + beforeEach(() => { + mockLogger = { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; + repository = new InMemoryTrackRepository(mockLogger); + }); + + describe('constructor', () => { + it('should initialize with a logger', () => { + expect(repository).toBeDefined(); + expect(mockLogger.info).toHaveBeenCalledWith('InMemoryTrackRepository initialized.'); + }); + }); + + describe('findById', () => { + it('should return null if track not found', async () => { + const result = await repository.findById('nonexistent'); + expect(result).toBeNull(); + expect(mockLogger.debug).toHaveBeenCalledWith('Finding track by id: nonexistent'); + expect(mockLogger.warn).toHaveBeenCalledWith('Track with id nonexistent not found.'); + }); + + it('should return the track if found', async () => { + const track = Track.create({ + id: 'track1', + name: 'Monza Circuit', + shortName: 'MON', + country: 'Italy', + category: 'road', + difficulty: 'advanced', + lengthKm: 5.793, + turns: 11, + imageUrl: 'https://example.com/monza.jpg', + gameId: 'iracing', + }); + await repository.create(track); + + const result = await repository.findById('track1'); + expect(result).toEqual(track); + expect(mockLogger.debug).toHaveBeenCalledWith('Finding track by id: track1'); + expect(mockLogger.info).toHaveBeenCalledWith('Found track: track1.'); + }); + }); + + describe('findAll', () => { + it('should return an empty array when no tracks', async () => { + const result = await repository.findAll(); + expect(result).toEqual([]); + expect(mockLogger.debug).toHaveBeenCalledWith('Finding all tracks.'); + expect(mockLogger.info).toHaveBeenCalledWith('Found 0 tracks.'); + }); + + it('should return all tracks', async () => { + const track1 = Track.create({ + id: 'track1', + name: 'Monza Circuit', + country: 'Italy', + category: 'road', + lengthKm: 5.793, + turns: 11, + gameId: 'iracing', + }); + const track2 = Track.create({ + id: 'track2', + name: 'Silverstone', + country: 'UK', + category: 'road', + lengthKm: 5.891, + turns: 18, + gameId: 'iracing', + }); + await repository.create(track1); + await repository.create(track2); + + const result = await repository.findAll(); + expect(result).toEqual([track1, track2]); + expect(mockLogger.debug).toHaveBeenCalledWith('Finding all tracks.'); + expect(mockLogger.info).toHaveBeenCalledWith('Found 2 tracks.'); + }); + }); + + describe('findByGameId', () => { + it('should return empty array if no tracks for game id', async () => { + const result = await repository.findByGameId('iracing'); + expect(result).toEqual([]); + expect(mockLogger.debug).toHaveBeenCalledWith('Finding tracks by game id: iracing'); + expect(mockLogger.info).toHaveBeenCalledWith('Found 0 tracks for game id: iracing.'); + }); + + it('should return tracks for the game id', async () => { + const track1 = Track.create({ + id: 'track1', + name: 'Monza Circuit', + country: 'Italy', + category: 'road', + lengthKm: 5.793, + turns: 11, + gameId: 'iracing', + }); + const track2 = Track.create({ + id: 'track2', + name: 'Silverstone', + country: 'UK', + category: 'road', + lengthKm: 5.891, + turns: 18, + gameId: 'iracing', + }); + const track3 = Track.create({ + id: 'track3', + name: 'Daytona', + country: 'USA', + category: 'oval', + lengthKm: 4.0, + turns: 4, + gameId: 'assetto', + }); + await repository.create(track1); + await repository.create(track2); + await repository.create(track3); + + const result = await repository.findByGameId('iracing'); + expect(result).toEqual([track1, track2]); + expect(mockLogger.debug).toHaveBeenCalledWith('Finding tracks by game id: iracing'); + expect(mockLogger.info).toHaveBeenCalledWith('Found 2 tracks for game id: iracing.'); + }); + }); + + describe('findByCategory', () => { + it('should return empty array if no tracks in category', async () => { + const result = await repository.findByCategory('oval'); + expect(result).toEqual([]); + expect(mockLogger.debug).toHaveBeenCalledWith('Finding tracks by category: oval'); + expect(mockLogger.info).toHaveBeenCalledWith('Found 0 tracks for category: oval.'); + }); + + it('should return tracks in the category', async () => { + const track1 = Track.create({ + id: 'track1', + name: 'Monza Circuit', + country: 'Italy', + category: 'road', + lengthKm: 5.793, + turns: 11, + gameId: 'iracing', + }); + const track2 = Track.create({ + id: 'track2', + name: 'Daytona', + country: 'USA', + category: 'oval', + lengthKm: 4.0, + turns: 4, + gameId: 'iracing', + }); + await repository.create(track1); + await repository.create(track2); + + const result = await repository.findByCategory('road'); + expect(result).toEqual([track1]); + expect(mockLogger.debug).toHaveBeenCalledWith('Finding tracks by category: road'); + expect(mockLogger.info).toHaveBeenCalledWith('Found 1 tracks for category: road.'); + }); + }); + + describe('findByCountry', () => { + it('should return empty array if no tracks in country', async () => { + const result = await repository.findByCountry('France'); + expect(result).toEqual([]); + expect(mockLogger.debug).toHaveBeenCalledWith('Finding tracks by country: France'); + expect(mockLogger.info).toHaveBeenCalledWith('Found 0 tracks for country: France.'); + }); + + it('should return tracks in the country', async () => { + const track1 = Track.create({ + id: 'track1', + name: 'Monza Circuit', + country: 'Italy', + category: 'road', + lengthKm: 5.793, + turns: 11, + gameId: 'iracing', + }); + const track2 = Track.create({ + id: 'track2', + name: 'Imola', + country: 'Italy', + category: 'road', + lengthKm: 4.909, + turns: 17, + gameId: 'iracing', + }); + const track3 = Track.create({ + id: 'track3', + name: 'Silverstone', + country: 'UK', + category: 'road', + lengthKm: 5.891, + turns: 18, + gameId: 'iracing', + }); + await repository.create(track1); + await repository.create(track2); + await repository.create(track3); + + const result = await repository.findByCountry('Italy'); + expect(result).toEqual([track2, track1]); + expect(mockLogger.debug).toHaveBeenCalledWith('Finding tracks by country: Italy'); + expect(mockLogger.info).toHaveBeenCalledWith('Found 2 tracks for country: Italy.'); + }); + }); + + describe('searchByName', () => { + it('should return empty array if no matches', async () => { + const result = await repository.searchByName('nonexistent'); + expect(result).toEqual([]); + expect(mockLogger.debug).toHaveBeenCalledWith('Searching tracks by name query: nonexistent'); + expect(mockLogger.info).toHaveBeenCalledWith('Found 0 tracks matching search query: nonexistent.'); + }); + + it('should return tracks matching the query in name', async () => { + const track1 = Track.create({ + id: 'track1', + name: 'Monza Circuit', + shortName: 'MON', + country: 'Italy', + category: 'road', + lengthKm: 5.793, + turns: 11, + gameId: 'iracing', + }); + const track2 = Track.create({ + id: 'track2', + name: 'Silverstone Circuit', + shortName: 'SIL', + country: 'UK', + category: 'road', + lengthKm: 5.891, + turns: 18, + gameId: 'iracing', + }); + await repository.create(track1); + await repository.create(track2); + + const result = await repository.searchByName('Circuit'); + expect(result).toEqual([track1, track2]); + expect(mockLogger.debug).toHaveBeenCalledWith('Searching tracks by name query: Circuit'); + expect(mockLogger.info).toHaveBeenCalledWith('Found 2 tracks matching search query: Circuit.'); + }); + + it('should return tracks matching the query in short name', async () => { + const track = Track.create({ + id: 'track1', + name: 'Monza Circuit', + shortName: 'MON', + country: 'Italy', + category: 'road', + lengthKm: 5.793, + turns: 11, + gameId: 'iracing', + }); + await repository.create(track); + + const result = await repository.searchByName('mon'); + expect(result).toEqual([track]); + expect(mockLogger.debug).toHaveBeenCalledWith('Searching tracks by name query: mon'); + expect(mockLogger.info).toHaveBeenCalledWith('Found 1 tracks matching search query: mon.'); + }); + }); + + describe('create', () => { + it('should create a new track', async () => { + const track = Track.create({ + id: 'track1', + name: 'Monza Circuit', + country: 'Italy', + category: 'road', + lengthKm: 5.793, + turns: 11, + gameId: 'iracing', + }); + + const result = await repository.create(track); + expect(result).toEqual(track); + expect(mockLogger.debug).toHaveBeenCalledWith('Creating track: track1'); + expect(mockLogger.info).toHaveBeenCalledWith('Track track1 created successfully.'); + }); + + it('should throw error if track already exists', async () => { + const track = Track.create({ + id: 'track1', + name: 'Monza Circuit', + country: 'Italy', + category: 'road', + lengthKm: 5.793, + turns: 11, + gameId: 'iracing', + }); + await repository.create(track); + + await expect(repository.create(track)).rejects.toThrow('Track with ID track1 already exists'); + expect(mockLogger.warn).toHaveBeenCalledWith('Track with ID track1 already exists.'); + }); + }); + + describe('update', () => { + it('should update an existing track', async () => { + const track = Track.create({ + id: 'track1', + name: 'Monza Circuit', + country: 'Italy', + category: 'road', + lengthKm: 5.793, + turns: 11, + gameId: 'iracing', + }); + await repository.create(track); + + const updatedTrack = Track.create({ + id: 'track1', + name: 'Monza Grand Prix Circuit', + country: 'Italy', + category: 'road', + lengthKm: 5.793, + turns: 11, + gameId: 'iracing', + }); + const result = await repository.update(updatedTrack); + expect(result).toEqual(updatedTrack); + expect(mockLogger.debug).toHaveBeenCalledWith('Updating track: track1'); + expect(mockLogger.info).toHaveBeenCalledWith('Track track1 updated successfully.'); + }); + + it('should throw error if track does not exist', async () => { + const track = Track.create({ + id: 'track1', + name: 'Monza Circuit', + country: 'Italy', + category: 'road', + lengthKm: 5.793, + turns: 11, + gameId: 'iracing', + }); + + await expect(repository.update(track)).rejects.toThrow('Track with ID track1 not found'); + expect(mockLogger.warn).toHaveBeenCalledWith('Track with ID track1 not found for update.'); + }); + }); + + describe('delete', () => { + it('should delete an existing track', async () => { + const track = Track.create({ + id: 'track1', + name: 'Monza Circuit', + country: 'Italy', + category: 'road', + lengthKm: 5.793, + turns: 11, + gameId: 'iracing', + }); + await repository.create(track); + + await repository.delete('track1'); + expect(await repository.exists('track1')).toBe(false); + expect(mockLogger.debug).toHaveBeenCalledWith('Deleting track: track1'); + expect(mockLogger.info).toHaveBeenCalledWith('Track track1 deleted successfully.'); + }); + + it('should throw error if track does not exist', async () => { + await expect(repository.delete('nonexistent')).rejects.toThrow('Track with ID nonexistent not found'); + expect(mockLogger.warn).toHaveBeenCalledWith('Track with ID nonexistent not found for deletion.'); + }); + }); + + describe('exists', () => { + it('should return true if track exists', async () => { + const track = Track.create({ + id: 'track1', + name: 'Monza Circuit', + country: 'Italy', + category: 'road', + lengthKm: 5.793, + turns: 11, + gameId: 'iracing', + }); + await repository.create(track); + + const result = await repository.exists('track1'); + expect(result).toBe(true); + expect(mockLogger.debug).toHaveBeenCalledWith('Checking existence of track with id: track1'); + }); + + it('should return false if track does not exist', async () => { + const result = await repository.exists('nonexistent'); + expect(result).toBe(false); + expect(mockLogger.debug).toHaveBeenCalledWith('Checking existence of track with id: nonexistent'); + }); + }); +}); \ No newline at end of file diff --git a/adapters/racing/persistence/inmemory/InMemoryTrackRepository.ts b/adapters/racing/persistence/inmemory/InMemoryTrackRepository.ts index 8e2556c5a..723e47622 100644 --- a/adapters/racing/persistence/inmemory/InMemoryTrackRepository.ts +++ b/adapters/racing/persistence/inmemory/InMemoryTrackRepository.ts @@ -14,17 +14,10 @@ export class InMemoryTrackRepository implements ITrackRepository { private tracks: Map; private readonly logger: Logger; - constructor(logger: Logger, seedData?: Track[]) { + constructor(logger: Logger) { this.logger = logger; this.logger.info('InMemoryTrackRepository initialized.'); this.tracks = new Map(); - - if (seedData) { - seedData.forEach(track => { - this.tracks.set(track.id, track); - this.logger.debug(`Seeded track: ${track.id}.`); - }); - } } async findById(id: string): Promise { @@ -59,8 +52,8 @@ export class InMemoryTrackRepository implements ITrackRepository { this.logger.debug(`Finding tracks by game id: ${gameId}`); try { const tracks = Array.from(this.tracks.values()) - .filter(track => track.gameId === gameId) - .sort((a, b) => a.name.localeCompare(b.name)); + .filter(track => track.gameId.props === gameId) + .sort((a, b) => a.name.props.localeCompare(b.name.props)); this.logger.info(`Found ${tracks.length} tracks for game id: ${gameId}.`); return tracks; } catch (error) { @@ -74,7 +67,7 @@ export class InMemoryTrackRepository implements ITrackRepository { try { const tracks = Array.from(this.tracks.values()) .filter(track => track.category === category) - .sort((a, b) => a.name.localeCompare(b.name)); + .sort((a, b) => a.name.props.localeCompare(b.name.props)); this.logger.info(`Found ${tracks.length} tracks for category: ${category}.`); return tracks; } catch (error) { @@ -87,8 +80,8 @@ export class InMemoryTrackRepository implements ITrackRepository { this.logger.debug(`Finding tracks by country: ${country}`); try { const tracks = Array.from(this.tracks.values()) - .filter(track => track.country.toLowerCase() === country.toLowerCase()) - .sort((a, b) => a.name.localeCompare(b.name)); + .filter(track => track.country.props.toLowerCase() === country.toLowerCase()) + .sort((a, b) => a.name.props.localeCompare(b.name.props)); this.logger.info(`Found ${tracks.length} tracks for country: ${country}.`); return tracks; } catch (error) { @@ -103,10 +96,10 @@ export class InMemoryTrackRepository implements ITrackRepository { const lowerQuery = query.toLowerCase(); const tracks = Array.from(this.tracks.values()) .filter(track => - track.name.toLowerCase().includes(lowerQuery) || - track.shortName.toLowerCase().includes(lowerQuery) + track.name.props.toLowerCase().includes(lowerQuery) || + track.shortName.props.toLowerCase().includes(lowerQuery) ) - .sort((a, b) => a.name.localeCompare(b.name)); + .sort((a, b) => a.name.props.localeCompare(b.name.props)); this.logger.info(`Found ${tracks.length} tracks matching search query: ${query}.`); return tracks; } catch (error) { diff --git a/adapters/racing/persistence/inmemory/InMemoryTransactionRepository.test.ts b/adapters/racing/persistence/inmemory/InMemoryTransactionRepository.test.ts new file mode 100644 index 000000000..801ae8aa6 --- /dev/null +++ b/adapters/racing/persistence/inmemory/InMemoryTransactionRepository.test.ts @@ -0,0 +1,156 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import { InMemoryTransactionRepository } from './InMemoryTransactionRepository'; +import { Transaction, TransactionType } from '@core/racing/domain/entities/league-wallet/Transaction'; +import { TransactionId } from '@core/racing/domain/entities/league-wallet/TransactionId'; +import { LeagueWalletId } from '@core/racing/domain/entities/league-wallet/LeagueWalletId'; +import { Money } from '@core/racing/domain/value-objects/Money'; +import type { Logger } from '@core/shared/application'; + +describe('InMemoryTransactionRepository', () => { + let repository: InMemoryTransactionRepository; + let mockLogger: Logger; + + beforeEach(() => { + mockLogger = { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; + repository = new InMemoryTransactionRepository(mockLogger); + }); + + const createTestTransaction = (id: string, walletId: string, type: TransactionType, amount: number) => { + return Transaction.create({ + id: TransactionId.create(id), + walletId: LeagueWalletId.create(walletId), + type, + amount: Money.create(amount), + completedAt: undefined, + description: undefined, + metadata: undefined, + }); + }; + + describe('constructor', () => { + it('should initialize with a logger', () => { + expect(repository).toBeDefined(); + expect(mockLogger.info).toHaveBeenCalledWith('InMemoryTransactionRepository initialized.'); + }); + }); + + describe('findById', () => { + it('should return null if transaction not found', async () => { + const result = await repository.findById('nonexistent'); + expect(result).toBeNull(); + expect(mockLogger.debug).toHaveBeenCalledWith('Finding transaction by id: nonexistent'); + expect(mockLogger.warn).toHaveBeenCalledWith('Transaction with id nonexistent not found.'); + }); + + it('should return the transaction if found', async () => { + const transaction = createTestTransaction('1', 'wallet1', 'sponsorship_payment', 100); + await repository.create(transaction); + + const result = await repository.findById('1'); + expect(result).toEqual(transaction); + expect(mockLogger.info).toHaveBeenCalledWith('Found transaction: 1.'); + }); + }); + + describe('findByWalletId', () => { + it('should return transactions filtered by wallet ID', async () => { + const transaction1 = createTestTransaction('1', 'wallet1', 'sponsorship_payment', 100); + const transaction2 = createTestTransaction('2', 'wallet2', 'prize_payout', 200); + await repository.create(transaction1); + await repository.create(transaction2); + + const result = await repository.findByWalletId('wallet1'); + expect(result).toHaveLength(1); + expect(result[0]).toEqual(transaction1); + }); + }); + + describe('findByType', () => { + it('should return transactions filtered by type', async () => { + const transaction1 = createTestTransaction('1', 'wallet1', 'sponsorship_payment', 100); + const transaction2 = createTestTransaction('2', 'wallet1', 'prize_payout', 200); + await repository.create(transaction1); + await repository.create(transaction2); + + const result = await repository.findByType('sponsorship_payment'); + expect(result).toHaveLength(1); + expect(result[0]).toEqual(transaction1); + }); + }); + + describe('create', () => { + it('should create a new transaction', async () => { + const transaction = createTestTransaction('1', 'wallet1', 'sponsorship_payment', 100); + const result = await repository.create(transaction); + expect(result).toEqual(transaction); + expect(mockLogger.info).toHaveBeenCalledWith('Transaction 1 created successfully.'); + }); + + it('should throw error if transaction already exists', async () => { + const transaction = createTestTransaction('1', 'wallet1', 'sponsorship_payment', 100); + await repository.create(transaction); + await expect(repository.create(transaction)).rejects.toThrow('Transaction with this ID already exists'); + }); + }); + + describe('update', () => { + it('should update an existing transaction', async () => { + const transaction = createTestTransaction('1', 'wallet1', 'sponsorship_payment', 100); + await repository.create(transaction); + + const updatedTransaction = Transaction.create({ + id: TransactionId.create('1'), + walletId: LeagueWalletId.create('wallet1'), + type: 'prize_payout', + amount: Money.create(150), + completedAt: undefined, + description: undefined, + metadata: undefined, + }); + const result = await repository.update(updatedTransaction); + expect(result).toEqual(updatedTransaction); + expect(mockLogger.info).toHaveBeenCalledWith('Transaction 1 updated successfully.'); + }); + + it('should throw error if transaction does not exist', async () => { + const transaction = createTestTransaction('1', 'wallet1', 'sponsorship_payment', 100); + await expect(repository.update(transaction)).rejects.toThrow('Transaction not found'); + }); + }); + + describe('delete', () => { + it('should delete an existing transaction', async () => { + const transaction = createTestTransaction('1', 'wallet1', 'sponsorship_payment', 100); + await repository.create(transaction); + + await repository.delete('1'); + expect(mockLogger.info).toHaveBeenCalledWith('Transaction 1 deleted successfully.'); + const found = await repository.findById('1'); + expect(found).toBeNull(); + }); + + it('should throw error if transaction does not exist', async () => { + await expect(repository.delete('nonexistent')).rejects.toThrow('Transaction with ID nonexistent not found'); + }); + }); + + describe('exists', () => { + it('should return true if transaction exists', async () => { + const transaction = createTestTransaction('1', 'wallet1', 'sponsorship_payment', 100); + await repository.create(transaction); + + const result = await repository.exists('1'); + expect(result).toBe(true); + }); + + it('should return false if transaction does not exist', async () => { + const result = await repository.exists('nonexistent'); + expect(result).toBe(false); + }); + }); +}); \ No newline at end of file diff --git a/adapters/racing/persistence/inmemory/InMemoryTransactionRepository.ts b/adapters/racing/persistence/inmemory/InMemoryTransactionRepository.ts index 7043d3f73..fae2b3f9e 100644 --- a/adapters/racing/persistence/inmemory/InMemoryTransactionRepository.ts +++ b/adapters/racing/persistence/inmemory/InMemoryTransactionRepository.ts @@ -4,21 +4,17 @@ * Mock repository for testing and development */ -import type { Transaction, TransactionType } from '../../domain/entities/Transaction'; -import type { ITransactionRepository } from '../../domain/repositories/ITransactionRepository'; +import type { Transaction, TransactionType } from '@core/racing/domain/entities/league-wallet/Transaction'; +import type { ITransactionRepository } from '@core/racing/domain/repositories/ITransactionRepository'; import type { Logger } from '@core/shared/application'; export class InMemoryTransactionRepository implements ITransactionRepository { private transactions: Map = new Map(); private readonly logger: Logger; - constructor(logger: Logger, seedData?: Transaction[]) { + constructor(logger: Logger) { this.logger = logger; this.logger.info('InMemoryTransactionRepository initialized.'); - if (seedData) { - seedData.forEach(t => this.transactions.set(t.id, t)); - this.logger.debug(`Seeded ${seedData.length} transactions.`); - } } async findById(id: string): Promise { @@ -40,7 +36,7 @@ export class InMemoryTransactionRepository implements ITransactionRepository { async findByWalletId(walletId: string): Promise { this.logger.debug(`Finding transactions by wallet id: ${walletId}`); try { - const transactions = Array.from(this.transactions.values()).filter(t => t.walletId === walletId); + const transactions = Array.from(this.transactions.values()).filter(t => t.walletId.toString() === walletId); this.logger.info(`Found ${transactions.length} transactions for wallet id: ${walletId}.`); return transactions; } catch (error) { @@ -64,11 +60,11 @@ export class InMemoryTransactionRepository implements ITransactionRepository { async create(transaction: Transaction): Promise { this.logger.debug(`Creating transaction: ${transaction.id}`); try { - if (this.transactions.has(transaction.id)) { + if (this.transactions.has(transaction.id.toString())) { this.logger.warn(`Transaction with ID ${transaction.id} already exists.`); throw new Error('Transaction with this ID already exists'); } - this.transactions.set(transaction.id, transaction); + this.transactions.set(transaction.id.toString(), transaction); this.logger.info(`Transaction ${transaction.id} created successfully.`); return transaction; } catch (error) { @@ -80,11 +76,11 @@ export class InMemoryTransactionRepository implements ITransactionRepository { async update(transaction: Transaction): Promise { this.logger.debug(`Updating transaction: ${transaction.id}`); try { - if (!this.transactions.has(transaction.id)) { + if (!this.transactions.has(transaction.id.toString())) { this.logger.warn(`Transaction with ID ${transaction.id} not found for update.`); throw new Error('Transaction not found'); } - this.transactions.set(transaction.id, transaction); + this.transactions.set(transaction.id.toString(), transaction); this.logger.info(`Transaction ${transaction.id} updated successfully.`); return transaction; } catch (error) { @@ -96,11 +92,13 @@ export class InMemoryTransactionRepository implements ITransactionRepository { async delete(id: string): Promise { this.logger.debug(`Deleting transaction: ${id}`); try { - if (this.transactions.delete(id)) { - this.logger.info(`Transaction ${id} deleted successfully.`); - } else { + if (!this.transactions.has(id)) { this.logger.warn(`Transaction with id ${id} not found for deletion.`); + throw new Error(`Transaction with ID ${id} not found`); } + + this.transactions.delete(id); + this.logger.info(`Transaction ${id} deleted successfully.`); } catch (error) { this.logger.error(`Error deleting transaction ${id}:`, error instanceof Error ? error : new Error(String(error))); throw error; diff --git a/core/racing/domain/entities/Driver.ts b/core/racing/domain/entities/Driver.ts index 55ec85eec..9e6f695b3 100644 --- a/core/racing/domain/entities/Driver.ts +++ b/core/racing/domain/entities/Driver.ts @@ -8,9 +8,9 @@ import { RacingDomainValidationError } from '../errors/RacingDomainError'; import type { IEntity } from '@core/shared/domain'; import { IRacingId } from '../value-objects/IRacingId'; -import { DriverName } from '../value-objects/DriverName'; +import { DriverName } from '../value-objects/driver/DriverName'; import { CountryCode } from '../value-objects/CountryCode'; -import { DriverBio } from '../value-objects/DriverBio'; +import { DriverBio } from '../value-objects/driver/DriverBio'; import { JoinedAt } from '../value-objects/JoinedAt'; export class Driver implements IEntity { diff --git a/core/racing/domain/entities/DriverLivery.ts b/core/racing/domain/entities/DriverLivery.ts index 0e379a5a9..07e72edf2 100644 --- a/core/racing/domain/entities/DriverLivery.ts +++ b/core/racing/domain/entities/DriverLivery.ts @@ -10,7 +10,7 @@ import type { IEntity } from '@core/shared/domain'; import { RacingDomainValidationError, RacingDomainInvariantError } from '../errors/RacingDomainError'; import { LiveryDecal } from '../value-objects/LiveryDecal'; import { DecalOverride } from '../value-objects/DecalOverride'; -import { DriverId } from '../value-objects/DriverId'; +import { DriverId } from '../value-objects/driver/DriverId'; import { GameId } from './GameId'; import { CarId } from '../value-objects/CarId'; import { ImageUrl } from '../value-objects/ImageUrl'; diff --git a/core/racing/domain/entities/LeagueScoringConfig.ts b/core/racing/domain/entities/LeagueScoringConfig.ts index dafd17e65..77631f48f 100644 --- a/core/racing/domain/entities/LeagueScoringConfig.ts +++ b/core/racing/domain/entities/LeagueScoringConfig.ts @@ -7,7 +7,7 @@ import type { IEntity } from '@core/shared/domain'; import { RacingDomainValidationError } from '../errors/RacingDomainError'; import type { ChampionshipConfig } from '../types/ChampionshipConfig'; -import { SeasonId } from './SeasonId'; +import { SeasonId } from './season/SeasonId'; import { ScoringPresetId } from './ScoringPresetId'; import { LeagueScoringConfigId } from './LeagueScoringConfigId'; diff --git a/core/racing/domain/entities/LiveryTemplate.ts b/core/racing/domain/entities/LiveryTemplate.ts index f12ee7e2a..b8bf515fa 100644 --- a/core/racing/domain/entities/LiveryTemplate.ts +++ b/core/racing/domain/entities/LiveryTemplate.ts @@ -10,8 +10,8 @@ import { RacingDomainValidationError, RacingDomainInvariantError } from '../erro import type { LiveryDecal } from '../value-objects/LiveryDecal'; import { LiveryTemplateId } from './LiveryTemplateId'; import { LeagueId } from './LeagueId'; -import { SeasonId } from './SeasonId'; -import { CarId } from './CarId'; +import { SeasonId } from './season/SeasonId'; +import { CarId } from '../value-objects/CarId'; import { ImageUrl } from './ImageUrl'; import { LiveryTemplateCreatedAt } from './LiveryTemplateCreatedAt'; import { LiveryTemplateUpdatedAt } from './LiveryTemplateUpdatedAt'; diff --git a/core/racing/domain/entities/Penalty.ts b/core/racing/domain/entities/Penalty.ts new file mode 100644 index 000000000..e69de29bb diff --git a/core/racing/domain/entities/league-wallet/LeagueWallet.ts b/core/racing/domain/entities/league-wallet/LeagueWallet.ts index 25929bff6..20f237d98 100644 --- a/core/racing/domain/entities/league-wallet/LeagueWallet.ts +++ b/core/racing/domain/entities/league-wallet/LeagueWallet.ts @@ -5,12 +5,12 @@ * Aggregate root for managing league finances and transactions. */ -import { RacingDomainValidationError, RacingDomainInvariantError } from '../errors/RacingDomainError'; +import { RacingDomainValidationError, RacingDomainInvariantError } from '../../errors/RacingDomainError'; import type { IEntity } from '@core/shared/domain'; -import type { Money } from '../value-objects/Money'; +import type { Money } from '../../value-objects/Money'; import { LeagueWalletId } from './LeagueWalletId'; -import { LeagueId } from './LeagueId'; +import { LeagueId } from '../LeagueId'; import { TransactionId } from './TransactionId'; export interface LeagueWalletProps { diff --git a/core/racing/domain/repositories/IPenaltyRepository.ts b/core/racing/domain/repositories/IPenaltyRepository.ts index d4e1a53db..d9aa640e5 100644 --- a/core/racing/domain/repositories/IPenaltyRepository.ts +++ b/core/racing/domain/repositories/IPenaltyRepository.ts @@ -4,7 +4,7 @@ * Defines the contract for persisting and retrieving Penalty entities. */ -import type { Penalty } from '../entities/Penalty'; +import type { Penalty } from '../entities/penalty/Penalty'; export interface IPenaltyRepository { /** diff --git a/core/racing/domain/repositories/IResultRepository.ts b/core/racing/domain/repositories/IResultRepository.ts index 15cd1f2a3..809427bf2 100644 --- a/core/racing/domain/repositories/IResultRepository.ts +++ b/core/racing/domain/repositories/IResultRepository.ts @@ -5,7 +5,7 @@ * Defines async methods using domain entities as types. */ -import type { Result } from '../entities/Result'; +import type { Result } from '../entities/result/Result'; export interface IResultRepository { /** diff --git a/core/racing/domain/repositories/ITransactionRepository.ts b/core/racing/domain/repositories/ITransactionRepository.ts index 8342a55da..4a8020bca 100644 --- a/core/racing/domain/repositories/ITransactionRepository.ts +++ b/core/racing/domain/repositories/ITransactionRepository.ts @@ -4,7 +4,7 @@ * Defines operations for Transaction entity persistence */ -import type { Transaction, TransactionType } from '../entities/Transaction'; +import type { Transaction, TransactionType } from '../entities/league-wallet/Transaction'; export interface ITransactionRepository { findById(id: string): Promise;