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 '@core/shared/application'; import type { Logger } from '@core/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, private readonly logger: Logger, ) {} async execute( command: CreateLeagueWithSeasonAndScoringCommand, ): Promise { this.logger.debug('Executing CreateLeagueWithSeasonAndScoringUseCase', { command }); try { this.validate(command); this.logger.info('Command validated successfully.'); const leagueId = uuidv4(); this.logger.debug(`Generated leagueId: ${leagueId}`); const league = League.create({ id: leagueId, name: command.name, description: command.description ?? '', ownerId: command.ownerId, settings: { pointsSystem: 'custom', ...(command.maxDrivers !== undefined ? { maxDrivers: command.maxDrivers } : {}), }, }); await this.leagueRepository.create(league); this.logger.info(`League ${league.name} (${league.id}) created successfully.`); const seasonId = uuidv4(); this.logger.debug(`Generated seasonId: ${seasonId}`); 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); this.logger.info(`Season ${season.name} (${season.id}) created for league ${league.id}.`); const presetId = command.scoringPresetId ?? 'club-default'; this.logger.debug(`Attempting to retrieve scoring preset: ${presetId}`); const preset: LeagueScoringPresetDTO | undefined = this.presetProvider.getPresetById(presetId); if (!preset) { this.logger.error(`Unknown scoring preset: ${presetId}`); throw new Error(`Unknown scoring preset: ${presetId}`); } this.logger.info(`Scoring preset ${preset.name} (${preset.id}) retrieved.`); const finalConfig = this.presetProvider.createScoringConfigFromPreset(preset.id, seasonId); this.logger.debug(`Scoring configuration created from preset ${preset.id}.`); await this.leagueScoringConfigRepository.save(finalConfig); this.logger.info(`Scoring configuration saved for season ${seasonId}.`); const result = { leagueId: league.id, seasonId, scoringPresetId: preset.id, scoringPresetName: preset.name, }; this.logger.debug('CreateLeagueWithSeasonAndScoringUseCase completed successfully.', { result }); return result; } catch (error) { this.logger.error('Error during CreateLeagueWithSeasonAndScoringUseCase execution.', { command, error: error.message, stack: error.stack, }); throw error; } } private validate(command: CreateLeagueWithSeasonAndScoringCommand): void { this.logger.debug('Validating CreateLeagueWithSeasonAndScoringCommand', { command }); if (!command.name || command.name.trim().length === 0) { this.logger.warn('Validation failed: League name is required', { command }); throw new Error('League name is required'); } if (!command.ownerId || command.ownerId.trim().length === 0) { this.logger.warn('Validation failed: League ownerId is required', { command }); throw new Error('League ownerId is required'); } if (!command.gameId || command.gameId.trim().length === 0) { this.logger.warn('Validation failed: gameId is required', { command }); throw new Error('gameId is required'); } if (!command.visibility) { this.logger.warn('Validation failed: visibility is required', { command }); throw new Error('visibility is required'); } if (command.maxDrivers !== undefined && command.maxDrivers <= 0) { this.logger.warn('Validation failed: maxDrivers must be greater than 0 when provided', { command }); throw new Error('maxDrivers must be greater than 0 when provided'); } const visibility = LeagueVisibility.fromString(command.visibility); if (visibility.isRanked()) { const driverCount = command.maxDrivers ?? 0; if (driverCount < MIN_RANKED_LEAGUE_DRIVERS) { this.logger.warn( `Validation failed: Ranked leagues require at least ${MIN_RANKED_LEAGUE_DRIVERS} drivers. Current setting: ${driverCount}.`, { command } ); 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.` ); } } this.logger.debug('Validation successful.'); } }