190 lines
5.8 KiB
TypeScript
190 lines
5.8 KiB
TypeScript
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<LeagueScoringConfigDTO | null> {
|
|
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<string, any>,
|
|
): 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, BonusRule[]>,
|
|
): 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';
|
|
}
|
|
} |