import { Result } from '@core/shared/domain/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import { v4 as uuidv4 } from 'uuid'; import type { StewardingDecisionMode } from '../../domain/entities/League'; import { League } from '../../domain/entities/League'; import { Season } from '../../domain/entities/season/Season'; import type { LeagueRepository } from '../../domain/repositories/LeagueRepository'; import type { SeasonRepository } from '../../domain/repositories/SeasonRepository'; import type { Weekday } from '../../domain/types/Weekday'; import { LeagueTimezone } from '../../domain/value-objects/LeagueTimezone'; import { MonthlyRecurrencePattern } from '../../domain/value-objects/MonthlyRecurrencePattern'; import { RaceTimeOfDay } from '../../domain/value-objects/RaceTimeOfDay'; import { RecurrenceStrategyFactory } from '../../domain/value-objects/RecurrenceStrategy'; import { SeasonDropPolicy, type SeasonDropStrategy } from '../../domain/value-objects/SeasonDropPolicy'; import { SeasonSchedule } from '../../domain/value-objects/SeasonSchedule'; import { SeasonScoringConfig } from '../../domain/value-objects/SeasonScoringConfig'; import { SeasonStewardingConfig } from '../../domain/value-objects/SeasonStewardingConfig'; import { WeekdaySet } from '../../domain/value-objects/WeekdaySet'; export type LeagueConfigFormModel = { basics?: { name?: string; description?: string; visibility?: string; gameId?: string; }; structure?: { mode?: string; maxDrivers?: number; }; championships?: { enableDriverChampionship?: boolean; enableTeamChampionship?: boolean; enableNationsChampionship?: boolean; enableTrophyChampionship?: boolean; }; scoring?: { patternId?: string; customScoringEnabled?: boolean; }; dropPolicy?: { strategy?: string; n?: number; }; timings?: { qualifyingMinutes?: number; mainRaceMinutes?: number; sessionCount?: number; roundsPlanned?: number; seasonStartDate?: string; raceStartTime?: string; timezoneId?: string; recurrenceStrategy?: string; weekdays?: string[]; intervalWeeks?: number; monthlyOrdinal?: number; monthlyWeekday?: string; }; stewarding?: { decisionMode?: string; requiredVotes?: number; requireDefense?: boolean; defenseTimeLimit?: number; voteTimeLimit?: number; protestDeadlineHours?: number; stewardingClosesHours?: number; notifyAccusedOnProtest?: boolean; notifyOnVoteRequired?: boolean; }; }; export type CreateSeasonForLeagueInput = { leagueId: string; name: string; gameId: string; sourceSeasonId?: string; /** * Optional high-level wizard config used to derive schedule/scoring/drop/stewarding. * When omitted, the Season will be created with minimal metadata only. */ config?: LeagueConfigFormModel; }; export type CreateSeasonForLeagueResult = { league: League; season: Season; }; type CreateSeasonForLeagueErrorCode = 'LEAGUE_NOT_FOUND' | 'VALIDATION_ERROR' | 'REPOSITORY_ERROR'; /** * CreateSeasonForLeagueUseCase * * Creates a new Season for an existing League, optionally cloning or deriving * configuration from a source Season or a league config form. */ export class CreateSeasonForLeagueUseCase { constructor( private readonly leagueRepository: LeagueRepository, private readonly seasonRepository: SeasonRepository, ) {} async execute( input: CreateSeasonForLeagueInput, ): Promise< Result< CreateSeasonForLeagueResult, ApplicationErrorCode > > { try { const league = await this.leagueRepository.findById(input.leagueId); if (!league) { return Result.err({ code: 'LEAGUE_NOT_FOUND', details: { message: `League not found: ${input.leagueId}` }, }); } let baseSeasonProps: { schedule?: SeasonSchedule; scoringConfig?: SeasonScoringConfig; dropPolicy?: SeasonDropPolicy; stewardingConfig?: SeasonStewardingConfig; maxDrivers?: number; } = {}; if (input.sourceSeasonId) { const source = await this.seasonRepository.findById(input.sourceSeasonId); if (!source) { return Result.err({ code: 'VALIDATION_ERROR', details: { message: `Source Season not found: ${input.sourceSeasonId}` }, }); } baseSeasonProps = { ...(source.schedule !== undefined ? { schedule: source.schedule } : {}), ...(source.scoringConfig !== undefined ? { scoringConfig: source.scoringConfig } : {}), ...(source.dropPolicy !== undefined ? { dropPolicy: source.dropPolicy } : {}), ...(source.stewardingConfig !== undefined ? { stewardingConfig: source.stewardingConfig } : {}), ...(source.maxDrivers !== undefined ? { maxDrivers: source.maxDrivers } : {}), }; } else if (input.config) { baseSeasonProps = this.deriveSeasonPropsFromConfig(input.config); } const seasonId = uuidv4(); const season = Season.create({ id: seasonId, leagueId: league.id.toString(), gameId: input.gameId, name: input.name, year: new Date().getFullYear(), status: 'planned', ...(baseSeasonProps?.schedule ? { schedule: baseSeasonProps.schedule } : {}), ...(baseSeasonProps?.scoringConfig ? { scoringConfig: baseSeasonProps.scoringConfig } : {}), ...(baseSeasonProps?.dropPolicy ? { dropPolicy: baseSeasonProps.dropPolicy } : {}), ...(baseSeasonProps?.stewardingConfig ? { stewardingConfig: baseSeasonProps.stewardingConfig } : {}), ...(baseSeasonProps?.maxDrivers !== undefined ? { maxDrivers: baseSeasonProps.maxDrivers } : {}), }); await this.seasonRepository.add(season); const result: CreateSeasonForLeagueResult = { league, season, }; return Result.ok(result); } catch (error) { return Result.err({ code: 'REPOSITORY_ERROR', details: { message: error instanceof Error ? error.message : 'Unknown error', }, }); } } private parseDropStrategy(value: unknown): SeasonDropStrategy { if (value === 'none' || value === 'bestNResults' || value === 'dropWorstN') { return value; } return 'none'; } private parseDecisionMode(value: unknown): StewardingDecisionMode { if ( value === 'admin_only' || value === 'steward_decides' || value === 'steward_vote' || value === 'member_vote' || value === 'steward_veto' || value === 'member_veto' ) { return value; } return 'admin_only'; } private deriveSeasonPropsFromConfig(config: LeagueConfigFormModel): { schedule?: SeasonSchedule; scoringConfig?: SeasonScoringConfig; dropPolicy?: SeasonDropPolicy; stewardingConfig?: SeasonStewardingConfig; maxDrivers?: number; } { const schedule = this.buildScheduleFromTimings(config); const scoringConfig = new SeasonScoringConfig({ scoringPresetId: config.scoring?.patternId ?? 'custom', customScoringEnabled: config.scoring?.customScoringEnabled ?? false, }); const dropPolicy = new SeasonDropPolicy({ strategy: this.parseDropStrategy(config.dropPolicy?.strategy), ...(config.dropPolicy?.n !== undefined ? { n: config.dropPolicy.n } : {}), }); const stewardingConfig = new SeasonStewardingConfig({ decisionMode: this.parseDecisionMode(config.stewarding?.decisionMode), ...(config.stewarding?.requiredVotes !== undefined ? { requiredVotes: config.stewarding.requiredVotes } : {}), requireDefense: config.stewarding?.requireDefense ?? false, defenseTimeLimit: config.stewarding?.defenseTimeLimit ?? 0, voteTimeLimit: config.stewarding?.voteTimeLimit ?? 0, protestDeadlineHours: config.stewarding?.protestDeadlineHours ?? 0, stewardingClosesHours: config.stewarding?.stewardingClosesHours ?? 0, notifyAccusedOnProtest: config.stewarding?.notifyAccusedOnProtest ?? false, notifyOnVoteRequired: config.stewarding?.notifyOnVoteRequired ?? false, }); const structure = config.structure ?? {}; const maxDrivers = typeof structure.maxDrivers === 'number' && structure.maxDrivers > 0 ? structure.maxDrivers : undefined; return { ...(schedule !== undefined ? { schedule } : {}), scoringConfig, dropPolicy, stewardingConfig, ...(maxDrivers !== undefined ? { maxDrivers } : {}), }; } private buildScheduleFromTimings( config: LeagueConfigFormModel, ): SeasonSchedule | undefined { const { timings } = config; if (!timings || !timings.seasonStartDate || !timings.raceStartTime) { return undefined; } const startDate = new Date(timings.seasonStartDate); const timeOfDay = RaceTimeOfDay.fromString(timings.raceStartTime); const timezoneId = timings.timezoneId ?? 'UTC'; const timezone = LeagueTimezone.create(timezoneId); const plannedRounds = typeof timings.roundsPlanned === 'number' && timings.roundsPlanned > 0 ? timings.roundsPlanned : timings.sessionCount; const recurrence = (() => { const weekdays: WeekdaySet = timings.weekdays && timings.weekdays.length > 0 ? WeekdaySet.fromArray( timings.weekdays as unknown as Weekday[], ) : WeekdaySet.fromArray(['Mon']); switch (timings.recurrenceStrategy) { case 'everyNWeeks': return RecurrenceStrategyFactory.everyNWeeks( timings.intervalWeeks ?? 2, weekdays, ); case 'monthlyNthWeekday': { const pattern = MonthlyRecurrencePattern.create( (timings.monthlyOrdinal ?? 1) as 1 | 2 | 3 | 4, (timings.monthlyWeekday ?? 'Mon') as Weekday, ); return RecurrenceStrategyFactory.monthlyNthWeekday(pattern); } case 'weekly': default: return RecurrenceStrategyFactory.weekly(weekdays); } })(); return new SeasonSchedule({ startDate, timeOfDay, timezone, recurrence, plannedRounds: plannedRounds ?? 0, }); } }