import { v4 as uuidv4 } from 'uuid'; import { League } from '../../domain/entities/League'; import { Season } from '../../domain/entities/Season'; import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository'; import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository'; import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository'; import type { LeagueScoringConfig } from '../../domain/entities/LeagueScoringConfig'; import type { AsyncUseCase } from '@gridpilot/shared/application'; import type { LeagueScoringPresetProvider, LeagueScoringPresetDTO, } from '../ports/LeagueScoringPresetProvider'; import { LeagueVisibility, MIN_RANKED_LEAGUE_DRIVERS, } from '../../domain/value-objects/LeagueVisibility'; /** * League visibility/ranking mode. * - 'ranked' (or legacy 'public'): Competitive, public, affects driver ratings. Min 10 drivers. * - 'unranked' (or legacy 'private'): Casual with friends, no rating impact. */ export type LeagueVisibilityInput = 'ranked' | 'unranked' | 'public' | 'private'; export interface CreateLeagueWithSeasonAndScoringCommand { name: string; description?: string; /** * League visibility/ranking mode. * - 'ranked' (or legacy 'public'): Competitive, public, affects ratings. Requires min 10 drivers. * - 'unranked' (or legacy 'private'): Casual with friends, no rating impact. */ visibility: LeagueVisibilityInput; ownerId: string; gameId: string; maxDrivers?: number; maxTeams?: number; enableDriverChampionship: boolean; enableTeamChampionship: boolean; enableNationsChampionship: boolean; enableTrophyChampionship: boolean; scoringPresetId?: string; } export interface CreateLeagueWithSeasonAndScoringResultDTO { leagueId: string; seasonId: string; scoringPresetId?: string; scoringPresetName?: string; } export class CreateLeagueWithSeasonAndScoringUseCase implements AsyncUseCase { constructor( private readonly leagueRepository: ILeagueRepository, private readonly seasonRepository: ISeasonRepository, private readonly leagueScoringConfigRepository: ILeagueScoringConfigRepository, private readonly presetProvider: LeagueScoringPresetProvider, ) {} async execute( command: CreateLeagueWithSeasonAndScoringCommand, ): Promise { this.validate(command); const leagueId = uuidv4(); const league = League.create({ id: leagueId, name: command.name, description: command.description ?? '', ownerId: command.ownerId, settings: { // Presets are attached at scoring-config level; league settings use a stable points system id. pointsSystem: 'custom', ...(command.maxDrivers !== undefined ? { maxDrivers: command.maxDrivers } : {}), }, }); await this.leagueRepository.create(league); const seasonId = uuidv4(); const season = Season.create({ id: seasonId, leagueId: league.id, gameId: command.gameId, name: `${command.name} Season 1`, year: new Date().getFullYear(), order: 1, status: 'active', startDate: new Date(), endDate: new Date(), }); await this.seasonRepository.create(season); const presetId = command.scoringPresetId ?? 'club-default'; const preset: LeagueScoringPresetDTO | undefined = this.presetProvider.getPresetById(presetId); if (!preset) { throw new Error(`Unknown scoring preset: ${presetId}`); } const scoringConfig: LeagueScoringConfig = { id: uuidv4(), seasonId, scoringPresetId: preset.id, championships: [], }; // For the initial alpha slice, we keep using the preset's config shape from the in-memory registry. // The preset registry is responsible for building the full LeagueScoringConfig; we only attach the preset id here. const fullConfigFactory = (await import( '../../infrastructure/repositories/InMemoryScoringRepositories' )) as typeof import('../../infrastructure/repositories/InMemoryScoringRepositories'); const presetFromInfra = fullConfigFactory.getLeagueScoringPresetById( preset.id, ); if (!presetFromInfra) { throw new Error(`Preset registry missing preset: ${preset.id}`); } const infraConfig = presetFromInfra.createConfig({ seasonId }); const finalConfig: LeagueScoringConfig = { ...infraConfig, scoringPresetId: preset.id, }; await this.leagueScoringConfigRepository.save(finalConfig); return { leagueId: league.id, seasonId, scoringPresetId: preset.id, scoringPresetName: preset.name, }; } private validate(command: CreateLeagueWithSeasonAndScoringCommand): void { if (!command.name || command.name.trim().length === 0) { throw new Error('League name is required'); } if (!command.ownerId || command.ownerId.trim().length === 0) { throw new Error('League ownerId is required'); } if (!command.gameId || command.gameId.trim().length === 0) { throw new Error('gameId is required'); } if (!command.visibility) { throw new Error('visibility is required'); } if (command.maxDrivers !== undefined && command.maxDrivers <= 0) { throw new Error('maxDrivers must be greater than 0 when provided'); } // Validate visibility-specific constraints const visibility = LeagueVisibility.fromString(command.visibility); if (visibility.isRanked()) { // Ranked (public) leagues require minimum 10 drivers for competitive integrity const driverCount = command.maxDrivers ?? 0; if (driverCount < MIN_RANKED_LEAGUE_DRIVERS) { throw new Error( `Ranked leagues require at least ${MIN_RANKED_LEAGUE_DRIVERS} drivers. ` + `Current setting: ${driverCount}. ` + `For smaller groups, consider creating an Unranked (Friends) league instead.` ); } } } }