188 lines
5.8 KiB
TypeScript
188 lines
5.8 KiB
TypeScript
/**
|
|
* 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<string, Record<number, number>> = {
|
|
'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<string, Standing>;
|
|
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<Standing[]> {
|
|
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<Standing | null> {
|
|
const key = this.getKey(leagueId, driverId);
|
|
return this.standings.get(key) ?? null;
|
|
}
|
|
|
|
async findAll(): Promise<Standing[]> {
|
|
return Array.from(this.standings.values());
|
|
}
|
|
|
|
async save(standing: Standing): Promise<Standing> {
|
|
const key = this.getKey(standing.leagueId, standing.driverId);
|
|
this.standings.set(key, standing);
|
|
return standing;
|
|
}
|
|
|
|
async saveMany(standings: Standing[]): Promise<Standing[]> {
|
|
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<void> {
|
|
const key = this.getKey(leagueId, driverId);
|
|
this.standings.delete(key);
|
|
}
|
|
|
|
async deleteByLeagueId(leagueId: string): Promise<void> {
|
|
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<boolean> {
|
|
const key = this.getKey(leagueId, driverId);
|
|
return this.standings.has(key);
|
|
}
|
|
|
|
async recalculate(leagueId: string): Promise<Standing[]> {
|
|
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<string, Standing>();
|
|
|
|
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<string, Record<number, number>> {
|
|
return POINTS_SYSTEMS;
|
|
}
|
|
} |