import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository'; import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository'; import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository'; import type { IGameRepository } from '../../domain/repositories/IGameRepository'; import type { LeagueScoringConfigDTO } from '../dto/LeagueScoringConfigDTO'; import type { LeagueScoringChampionshipDTO } from '../dto/LeagueScoringConfigDTO'; import type { LeagueScoringPresetProvider, LeagueScoringPresetDTO, } from '../ports/LeagueScoringPresetProvider'; import type { ChampionshipConfig } from '../../domain/value-objects/ChampionshipConfig'; import type { PointsTable } from '../../domain/value-objects/PointsTable'; import type { BonusRule } from '../../domain/value-objects/BonusRule'; /** * Query returning a league's scoring configuration for its active season. * * Designed for the league detail "Scoring" tab. */ export class GetLeagueScoringConfigQuery { constructor( private readonly leagueRepository: ILeagueRepository, private readonly seasonRepository: ISeasonRepository, private readonly leagueScoringConfigRepository: ILeagueScoringConfigRepository, private readonly gameRepository: IGameRepository, private readonly presetProvider: LeagueScoringPresetProvider, ) {} async execute(params: { leagueId: string }): Promise { const { leagueId } = params; const league = await this.leagueRepository.findById(leagueId); if (!league) { return null; } const seasons = await this.seasonRepository.findByLeagueId(leagueId); if (!seasons || seasons.length === 0) { return null; } const activeSeason = seasons.find((s) => s.status === 'active') ?? seasons[0]; const scoringConfig = await this.leagueScoringConfigRepository.findBySeasonId(activeSeason.id); if (!scoringConfig) { return null; } const game = await this.gameRepository.findById(activeSeason.gameId); if (!game) { return null; } const presetId = scoringConfig.scoringPresetId; const preset: LeagueScoringPresetDTO | undefined = presetId ? this.presetProvider.getPresetById(presetId) : undefined; const championships: LeagueScoringChampionshipDTO[] = scoringConfig.championships.map((champ) => this.mapChampionship(champ), ); const dropPolicySummary = preset?.dropPolicySummary ?? this.deriveDropPolicyDescriptionFromChampionships( scoringConfig.championships, ); return { leagueId: league.id, seasonId: activeSeason.id, gameId: game.id, gameName: game.name, scoringPresetId: presetId, scoringPresetName: preset?.name, dropPolicySummary, championships, }; } private mapChampionship(championship: ChampionshipConfig): LeagueScoringChampionshipDTO { const sessionTypes = championship.sessionTypes.map((s) => s.toString()); const pointsPreview = this.buildPointsPreview(championship.pointsTableBySessionType); const bonusSummary = this.buildBonusSummary( championship.bonusRulesBySessionType ?? {}, ); const dropPolicyDescription = this.deriveDropPolicyDescription( championship.dropScorePolicy, ); return { id: championship.id, name: championship.name, type: championship.type, sessionTypes, pointsPreview, bonusSummary, dropPolicyDescription, }; } private buildPointsPreview( tables: Record, ): Array<{ sessionType: string; position: number; points: number }> { const preview: Array<{ sessionType: string; position: number; points: number; }> = []; const maxPositions = 10; for (const [sessionType, table] of Object.entries(tables)) { for (let pos = 1; pos <= maxPositions; pos++) { const points = table.getPointsForPosition(pos); if (points && points !== 0) { preview.push({ sessionType, position: pos, points, }); } } } return preview; } private buildBonusSummary( bonusRulesBySessionType: Record, ): string[] { const summaries: string[] = []; for (const [sessionType, rules] of Object.entries(bonusRulesBySessionType)) { for (const rule of rules) { if (rule.type === 'fastestLap') { const base = `Fastest lap in ${sessionType}`; if (rule.requiresFinishInTopN) { summaries.push( `${base} +${rule.points} points if finishing P${rule.requiresFinishInTopN} or better`, ); } else { summaries.push(`${base} +${rule.points} points`); } } else { summaries.push( `${rule.type} bonus in ${sessionType} worth ${rule.points} points`, ); } } } return summaries; } private deriveDropPolicyDescriptionFromChampionships( championships: ChampionshipConfig[], ): string { const first = championships[0]; if (!first) { return 'All results count'; } return this.deriveDropPolicyDescription(first.dropScorePolicy); } private deriveDropPolicyDescription(policy: { strategy: string; count?: number; dropCount?: number; }): string { if (!policy || policy.strategy === 'none') { return 'All results count'; } if (policy.strategy === 'bestNResults' && typeof policy.count === 'number') { return `Best ${policy.count} results count towards the championship`; } if ( policy.strategy === 'dropWorstN' && typeof policy.dropCount === 'number' ) { return `Worst ${policy.dropCount} results are dropped from the championship total`; } return 'Custom drop score rules apply'; } }