/** * Infrastructure Adapter: InMemoryStandingRepository * * In-memory implementation of IStandingRepository. * Stores data in Map structure and calculates standings from race results. */ import { Standing } from '@core/racing/domain/entities/Standing'; import type { IStandingRepository } from '@core/racing/domain/repositories/IStandingRepository'; import type { IResultRepository } from '@core/racing/domain/repositories/IResultRepository'; import type { IRaceRepository } from '@core/racing/domain/repositories/IRaceRepository'; 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; constructor( logger: Logger, seedData?: Standing[], resultRepository?: IResultRepository | null, raceRepository?: IRaceRepository | null, leagueRepository?: ILeagueRepository | null ) { this.logger = logger; 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 { return `${leagueId}:${driverId}`; } async findByLeagueId(leagueId: string): Promise { this.logger.debug(`Finding standings for league id: ${leagueId}`); try { const standings = Array.from(this.standings.values()) .filter(standing => standing.leagueId === leagueId) .sort((a, b) => { // Sort by position (lower is better) if (a.position !== b.position) { return a.position - b.position; } // If positions are equal, sort by points (higher is better) return b.points - a.points; }); this.logger.info(`Found ${standings.length} standings for league id: ${leagueId}.`); return standings; } catch (error) { this.logger.error(`Error finding standings for league id ${leagueId}:`, error instanceof Error ? error : new Error(String(error))); throw error; } } async findByDriverIdAndLeagueId(driverId: string, leagueId: string): Promise { this.logger.debug(`Finding standing for driver: ${driverId}, league: ${leagueId}`); try { const key = this.getKey(leagueId, driverId); const standing = this.standings.get(key) ?? null; if (standing) { this.logger.info(`Found standing for driver: ${driverId}, league: ${leagueId}.`); } else { this.logger.warn(`Standing for driver ${driverId}, league ${leagueId} not found.`); } return standing; } catch (error) { this.logger.error(`Error finding standing for driver ${driverId}, league ${leagueId}:`, error); throw error; } } async findAll(): Promise { this.logger.debug('Finding all standings.'); try { const standings = Array.from(this.standings.values()); this.logger.info(`Found ${standings.length} standings.`); return standings; } catch (error) { this.logger.error('Error finding all standings:', error instanceof Error ? error : new Error(String(error))); throw error; } } async save(standing: Standing): Promise { this.logger.debug(`Saving standing for league: ${standing.leagueId}, driver: ${standing.driverId}`); try { const key = this.getKey(standing.leagueId, standing.driverId); if (this.standings.has(key)) { this.logger.debug(`Updating existing standing for league: ${standing.leagueId}, driver: ${standing.driverId}.`); } else { this.logger.debug(`Creating new standing for league: ${standing.leagueId}, driver: ${standing.driverId}.`); } this.standings.set(key, standing); this.logger.info(`Standing for league ${standing.leagueId}, driver ${standing.driverId} saved successfully.`); return standing; } catch (error) { this.logger.error(`Error saving standing for league ${standing.leagueId}, driver ${standing.driverId}:`, error); throw error; } } async saveMany(standings: Standing[]): Promise { this.logger.debug(`Saving ${standings.length} standings.`); try { standings.forEach(standing => { const key = this.getKey(standing.leagueId, standing.driverId); this.standings.set(key, standing); }); this.logger.info(`${standings.length} standings saved successfully.`); return standings; } catch (error) { this.logger.error(`Error saving many standings:`, error instanceof Error ? error : new Error(String(error))); throw error; } } async delete(leagueId: string, driverId: string): Promise { this.logger.debug(`Deleting standing for league: ${leagueId}, driver: ${driverId}`); try { const key = this.getKey(leagueId, driverId); if (this.standings.delete(key)) { this.logger.info(`Standing for league ${leagueId}, driver ${driverId} deleted successfully.`); } else { this.logger.warn(`Standing for league ${leagueId}, driver ${driverId} not found for deletion.`); } } catch (error) { this.logger.error(`Error deleting standing for league ${leagueId}, driver ${driverId}:`, error); throw error; } } 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); toDelete.forEach(standing => { const key = this.getKey(standing.leagueId, standing.driverId); this.standings.delete(key); }); this.logger.info(`Deleted ${toDelete.length} standings for league id: ${leagueId}.`); } catch (error) { this.logger.error(`Error deleting standings by league id ${leagueId}:`, error instanceof Error ? error : new Error(String(error))); throw error; } } async exists(leagueId: string, driverId: string): Promise { this.logger.debug(`Checking existence of standing for league: ${leagueId}, driver: ${driverId}`); try { const key = this.getKey(leagueId, driverId); const exists = this.standings.has(key); this.logger.debug(`Standing for league ${leagueId}, driver ${driverId} exists: ${exists}.`); return exists; } catch (error) { this.logger.error(`Error checking existence of standing for league ${leagueId}, driver ${driverId}:`, error); throw error; } } async recalculate(leagueId: string): Promise { this.logger.debug(`Recalculating standings for league id: ${leagueId}`); try { if (!this.resultRepository || !this.raceRepository || !this.leagueRepository) { this.logger.error('Cannot recalculate standings: missing required repositories.'); throw new Error('Cannot recalculate standings: missing required repositories'); } // Get league to determine points system const league = await this.leagueRepository.findById(leagueId); if (!league) { this.logger.warn(`League with ID ${leagueId} not found during recalculation.`); throw new Error(`League with ID ${leagueId} not found`); } this.logger.debug(`League ${leagueId} found for recalculation.`); // Get points system const resolvedPointsSystem = league.settings.customPoints ?? POINTS_SYSTEMS[league.settings.pointsSystem] ?? POINTS_SYSTEMS['f1-2024']; if (!resolvedPointsSystem) { this.logger.error(`No points system configured for league ${leagueId}.`); throw new Error('No points system configured for league'); } this.logger.debug(`Resolved points system for league ${leagueId}.`); // Get all completed races for the league const races = await this.raceRepository.findCompletedByLeagueId(leagueId); this.logger.debug(`Found ${races.length} completed races for league ${leagueId}.`); if (races.length === 0) { this.logger.warn(`No completed races found for league ${leagueId}. Standings will be empty.`); return []; } // Get all results for these races const allResults = await Promise.all( races.map(async race => { this.logger.debug(`Fetching results for race ${race.id}.`); const results = await this.resultRepository!.findByRaceId(race.id); this.logger.debug(`Found ${results.length} results for race ${race.id}.`); return results; }) ); const results = allResults.flat(); this.logger.debug(`Collected ${results.length} results from all completed races.`); // Calculate standings per driver const standingsMap = new Map(); results.forEach(result => { let standing = standingsMap.get(result.driverId); if (!standing) { standing = Standing.create({ leagueId, driverId: result.driverId, }); this.logger.debug(`Created new standing for driver ${result.driverId} in league ${leagueId}.`); } // Add points from this result standing = standing.addRaceResult(result.position, resolvedPointsSystem); standingsMap.set(result.driverId, standing); this.logger.debug(`Driver ${result.driverId} in league ${leagueId} accumulated ${standing.points} points.`); }); this.logger.debug(`Calculated initial standings for ${standingsMap.size} drivers.`); // Sort by points and assign positions const sortedStandings = Array.from(standingsMap.values()) .sort((a, b) => { if (b.points !== a.points) { return b.points - a.points; } // Tie-breaker: most wins if (b.wins !== a.wins) { return b.wins - a.wins; } // Tie-breaker: most races completed return b.racesCompleted - a.racesCompleted; }); this.logger.debug(`Sorted standings for ${sortedStandings.length} drivers.`); // Assign positions const updatedStandings = sortedStandings.map((standing, index) => { const newStanding = standing.updatePosition(index + 1); this.logger.debug(`Assigned position ${newStanding.position} to driver ${newStanding.driverId}.`); return newStanding; }); // Save all standings await this.saveMany(updatedStandings); this.logger.info(`Successfully recalculated and saved standings for league ${leagueId}.`); return updatedStandings; } catch (error) { this.logger.error(`Error recalculating standings for league ${leagueId}:`, error instanceof Error ? error : new Error(String(error))); throw error; } } /** * Get available points systems */ static getPointsSystems(): Record> { return POINTS_SYSTEMS; } }