/** * Infrastructure Adapter: InMemoryStandingRepository * * In-memory implementation of IStandingRepository. * Stores data in Map structure and calculates standings from race results. */ import { Standing } from '../../domain/entities/Standing'; import { IStandingRepository } from '../../application/ports/IStandingRepository'; import { IResultRepository } from '../../application/ports/IResultRepository'; import { IRaceRepository } from '../../application/ports/IRaceRepository'; import { ILeagueRepository } from '../../application/ports/ILeagueRepository'; /** * 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; private raceRepository?: IRaceRepository; private leagueRepository?: ILeagueRepository; constructor( seedData?: Standing[], resultRepository?: IResultRepository, raceRepository?: IRaceRepository, leagueRepository?: ILeagueRepository ) { this.standings = new Map(); this.resultRepository = resultRepository; this.raceRepository = raceRepository; this.leagueRepository = leagueRepository; if (seedData) { seedData.forEach(standing => { const key = this.getKey(standing.leagueId, standing.driverId); this.standings.set(key, standing); }); } } private getKey(leagueId: string, driverId: string): string { return `${leagueId}:${driverId}`; } async findByLeagueId(leagueId: string): Promise { return 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; }); } async findByDriverIdAndLeagueId(driverId: string, leagueId: string): Promise { const key = this.getKey(leagueId, driverId); return this.standings.get(key) ?? null; } async findAll(): Promise { return Array.from(this.standings.values()); } async save(standing: Standing): Promise { const key = this.getKey(standing.leagueId, standing.driverId); this.standings.set(key, standing); return standing; } async saveMany(standings: Standing[]): Promise { standings.forEach(standing => { const key = this.getKey(standing.leagueId, standing.driverId); this.standings.set(key, standing); }); return standings; } async delete(leagueId: string, driverId: string): Promise { const key = this.getKey(leagueId, driverId); this.standings.delete(key); } async deleteByLeagueId(leagueId: string): Promise { 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); }); } async exists(leagueId: string, driverId: string): Promise { const key = this.getKey(leagueId, driverId); return this.standings.has(key); } async recalculate(leagueId: string): Promise { if (!this.resultRepository || !this.raceRepository || !this.leagueRepository) { throw new Error('Cannot recalculate standings: missing required repositories'); } // Get league to determine points system const league = await this.leagueRepository.findById(leagueId); if (!league) { throw new Error(`League with ID ${leagueId} not found`); } // Get points system const pointsSystem = league.settings.customPoints ?? POINTS_SYSTEMS[league.settings.pointsSystem] ?? POINTS_SYSTEMS['f1-2024']; // Get all completed races for the league const races = await this.raceRepository.findCompletedByLeagueId(leagueId); // Get all results for these races const allResults = await Promise.all( races.map(race => this.resultRepository!.findByRaceId(race.id)) ); const results = allResults.flat(); // 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, }); } // Add points from this result standing = standing.addRaceResult(result.position, pointsSystem); standingsMap.set(result.driverId, standing); }); // 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; }); // Assign positions const updatedStandings = sortedStandings.map((standing, index) => standing.updatePosition(index + 1) ); // Save all standings await this.saveMany(updatedStandings); return updatedStandings; } /** * Get available points systems */ static getPointsSystems(): Record> { return POINTS_SYSTEMS; } }