import { Season } from '../../domain/entities/season/Season'; import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository'; import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository'; import { SeasonSchedule } from '../../domain/value-objects/SeasonSchedule'; import { SeasonScoringConfig } from '../../domain/value-objects/SeasonScoringConfig'; import { SeasonDropPolicy } from '../../domain/value-objects/SeasonDropPolicy'; import { SeasonStewardingConfig } from '../../domain/value-objects/SeasonStewardingConfig'; import { RaceTimeOfDay } from '../../domain/value-objects/RaceTimeOfDay'; import { LeagueTimezone } from '../../domain/value-objects/LeagueTimezone'; import { RecurrenceStrategyFactory } from '../../domain/value-objects/RecurrenceStrategy'; import { WeekdaySet } from '../../domain/value-objects/WeekdaySet'; import { MonthlyRecurrencePattern } from '../../domain/value-objects/MonthlyRecurrencePattern'; import type { Weekday } from '../../domain/types/Weekday'; import { v4 as uuidv4 } from 'uuid'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort'; /** * Input, result and error models shared across Season-focused use cases. */ 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 interface 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; } // Backwards-compatible alias for existing wiring/tests. export type CreateSeasonForLeagueCommand = CreateSeasonForLeagueInput; export type CreateSeasonForLeagueResult = { season: Season; }; export type CreateSeasonForLeagueErrorCode = | 'LEAGUE_NOT_FOUND' | 'SOURCE_SEASON_NOT_FOUND' | 'REPOSITORY_ERROR'; export type CreateSeasonForLeagueApplicationError = ApplicationErrorCode< CreateSeasonForLeagueErrorCode, { message: string } >; export interface ListSeasonsForLeagueInput { leagueId: string; } // Backwards-compatible alias for existing wiring/tests. export type ListSeasonsForLeagueQuery = ListSeasonsForLeagueInput; export interface ListSeasonsForLeagueResult { seasons: Season[]; } export type ListSeasonsForLeagueErrorCode = | 'LEAGUE_NOT_FOUND' | 'REPOSITORY_ERROR'; export type ListSeasonsForLeagueApplicationError = ApplicationErrorCode< ListSeasonsForLeagueErrorCode, { message: string } >; export interface GetSeasonDetailsInput { leagueId: string; seasonId: string; } // Backwards-compatible alias for existing wiring/tests. export type GetSeasonDetailsQuery = GetSeasonDetailsInput; export interface GetSeasonDetailsResult { season: Season; } export type GetSeasonDetailsErrorCode = | 'LEAGUE_NOT_FOUND' | 'SEASON_NOT_FOUND' | 'REPOSITORY_ERROR'; export type GetSeasonDetailsApplicationError = ApplicationErrorCode< GetSeasonDetailsErrorCode, { message: string } >; export type SeasonLifecycleTransition = | 'activate' | 'complete' | 'archive' | 'cancel'; export interface ManageSeasonLifecycleInput { leagueId: string; seasonId: string; transition: SeasonLifecycleTransition; } // Backwards-compatible alias for existing wiring/tests. export type ManageSeasonLifecycleCommand = ManageSeasonLifecycleInput; export interface ManageSeasonLifecycleResult { season: Season; } export type ManageSeasonLifecycleErrorCode = | 'LEAGUE_NOT_FOUND' | 'SEASON_NOT_FOUND' | 'INVALID_LIFECYCLE_TRANSITION' | 'REPOSITORY_ERROR'; export type ManageSeasonLifecycleApplicationError = ApplicationErrorCode< ManageSeasonLifecycleErrorCode, { message: string } >; /** * 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: ILeagueRepository, private readonly seasonRepository: ISeasonRepository, private readonly output: UseCaseOutputPort, ) {} async execute( command: CreateSeasonForLeagueCommand, ): Promise> { try { const league = await this.leagueRepository.findById(command.leagueId); if (!league) { return Result.err({ code: 'LEAGUE_NOT_FOUND', details: { message: `League not found: ${command.leagueId}`, }, }); } let baseSeasonProps: { schedule?: SeasonSchedule; scoringConfig?: SeasonScoringConfig; dropPolicy?: SeasonDropPolicy; stewardingConfig?: SeasonStewardingConfig; maxDrivers?: number; } = {}; if (command.sourceSeasonId) { const source = await this.seasonRepository.findById(command.sourceSeasonId); if (!source) { return Result.err({ code: 'SOURCE_SEASON_NOT_FOUND', details: { message: `Source Season not found: ${command.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 (command.config) { baseSeasonProps = this.deriveSeasonPropsFromConfig(command.config); } const seasonId = uuidv4(); const season = Season.create({ id: seasonId, leagueId: league.id.toString(), gameId: command.gameId, name: command.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); this.output.present({ season }); return Result.ok(undefined); } catch (error) { return Result.err({ code: 'REPOSITORY_ERROR', details: { message: error instanceof Error ? error.message : 'Unknown error', }, }); } } private deriveSeasonPropsFromConfig(config: LeagueConfigFormModel): { schedule?: SeasonSchedule; scoringConfig?: SeasonScoringConfig; dropPolicy?: SeasonDropPolicy; stewardingConfig?: SeasonStewardingConfig; maxDrivers?: number; } { const schedule = this.buildScheduleFromTimings(config); const scoring = config.scoring ?? {}; const scoringConfig = new SeasonScoringConfig({ scoringPresetId: scoring.patternId ?? 'custom', customScoringEnabled: scoring.customScoringEnabled ?? false, }); const dropPolicyInput = config.dropPolicy ?? {}; const dropStrategy = dropPolicyInput.strategy; const dropPolicy = new SeasonDropPolicy({ strategy: dropStrategy === 'none' || dropStrategy === 'bestNResults' || dropStrategy === 'dropWorstN' ? dropStrategy : 'none', ...(dropPolicyInput.n !== undefined ? { n: dropPolicyInput.n } : {}), }); const stewardingInput = config.stewarding ?? {}; const decisionMode = stewardingInput.decisionMode; const stewardingConfig = new SeasonStewardingConfig({ decisionMode: decisionMode === 'admin_only' || decisionMode === 'steward_decides' || decisionMode === 'steward_vote' || decisionMode === 'member_vote' || decisionMode === 'steward_veto' || decisionMode === 'member_veto' ? decisionMode : 'admin_only', ...(stewardingInput.requiredVotes !== undefined ? { requiredVotes: stewardingInput.requiredVotes } : {}), requireDefense: stewardingInput.requireDefense ?? false, defenseTimeLimit: stewardingInput.defenseTimeLimit ?? 48, voteTimeLimit: stewardingInput.voteTimeLimit ?? 72, protestDeadlineHours: stewardingInput.protestDeadlineHours ?? 48, stewardingClosesHours: stewardingInput.stewardingClosesHours ?? 168, notifyAccusedOnProtest: stewardingInput.notifyAccusedOnProtest ?? true, notifyOnVoteRequired: stewardingInput.notifyOnVoteRequired ?? true, }); 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.timings; if (!timings?.seasonStartDate || !timings.raceStartTime) { return undefined; } const startDate = new Date(timings.seasonStartDate); const timeOfDay = RaceTimeOfDay.fromString(timings.raceStartTime); const timezone = LeagueTimezone.create(timings.timezoneId ?? 'UTC'); const plannedRounds = typeof timings.roundsPlanned === 'number' && timings.roundsPlanned > 0 ? timings.roundsPlanned : timings.sessionCount ?? 0; if (!Number.isInteger(plannedRounds) || plannedRounds <= 0) { return undefined; } const weekdaysRaw = timings.weekdays ?? []; const weekdays = WeekdaySet.fromArray( weekdaysRaw .filter((d): d is Weekday => (['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] as const).includes(d as Weekday)) .slice(0), ); const safeWeekdays = weekdays.getAll().length > 0 ? weekdays : WeekdaySet.fromArray(['Mon']); const recurrence = (() => { switch (timings.recurrenceStrategy) { case 'everyNWeeks': return RecurrenceStrategyFactory.everyNWeeks( timings.intervalWeeks ?? 2, safeWeekdays, ); 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(safeWeekdays); } })(); return new SeasonSchedule({ startDate, timeOfDay, timezone, recurrence, plannedRounds, }); } } /** * ListSeasonsForLeagueUseCase */ export class ListSeasonsForLeagueUseCase { constructor( private readonly leagueRepository: ILeagueRepository, private readonly seasonRepository: ISeasonRepository, private readonly output: UseCaseOutputPort, ) {} async execute( query: ListSeasonsForLeagueQuery, ): Promise> { try { const league = await this.leagueRepository.findById(query.leagueId); if (!league) { return Result.err({ code: 'LEAGUE_NOT_FOUND', details: { message: `League not found: ${query.leagueId}`, }, }); } const seasons = await this.seasonRepository.listByLeague(league.id.toString()); this.output.present({ seasons }); return Result.ok(undefined); } catch (error) { return Result.err({ code: 'REPOSITORY_ERROR', details: { message: error instanceof Error ? error.message : 'Unknown error', }, }); } } } /** * GetSeasonDetailsUseCase */ export class GetSeasonDetailsUseCase { constructor( private readonly leagueRepository: ILeagueRepository, private readonly seasonRepository: ISeasonRepository, private readonly output: UseCaseOutputPort, ) {} async execute( query: GetSeasonDetailsQuery, ): Promise> { try { const league = await this.leagueRepository.findById(query.leagueId); if (!league) { return Result.err({ code: 'LEAGUE_NOT_FOUND', details: { message: `League not found: ${query.leagueId}`, }, }); } const season = await this.seasonRepository.findById(query.seasonId); if (!season || season.leagueId !== league.id.toString()) { return Result.err({ code: 'SEASON_NOT_FOUND', details: { message: `Season ${query.seasonId} does not belong to league ${league.id}`, }, }); } this.output.present({ season }); return Result.ok(undefined); } catch (error) { return Result.err({ code: 'REPOSITORY_ERROR', details: { message: error instanceof Error ? error.message : 'Unknown error', }, }); } } } /** * ManageSeasonLifecycleUseCase */ export class ManageSeasonLifecycleUseCase { constructor( private readonly leagueRepository: ILeagueRepository, private readonly seasonRepository: ISeasonRepository, private readonly output: UseCaseOutputPort, ) {} async execute( command: ManageSeasonLifecycleCommand, ): Promise> { try { const league = await this.leagueRepository.findById(command.leagueId); if (!league) { return Result.err({ code: 'LEAGUE_NOT_FOUND', details: { message: `League not found: ${command.leagueId}`, }, }); } const season = await this.seasonRepository.findById(command.seasonId); if (!season || season.leagueId !== league.id.toString()) { return Result.err({ code: 'SEASON_NOT_FOUND', details: { message: `Season ${command.seasonId} does not belong to league ${league.id}`, }, }); } let updated: Season; try { switch (command.transition) { case 'activate': updated = season.activate(); break; case 'complete': updated = season.complete(); break; case 'archive': updated = season.archive(); break; case 'cancel': updated = season.cancel(); break; default: return Result.err({ code: 'INVALID_LIFECYCLE_TRANSITION', details: { message: `Unsupported Season lifecycle transition: ${command.transition}`, }, }); } } catch (error) { return Result.err({ code: 'INVALID_LIFECYCLE_TRANSITION', details: { message: error instanceof Error ? error.message : 'Invalid lifecycle transition', }, }); } try { await this.seasonRepository.update(updated); } catch (error) { return Result.err({ code: 'REPOSITORY_ERROR', details: { message: error instanceof Error ? error.message : 'Unknown error', }, }); } this.output.present({ season: updated }); return Result.ok(undefined); } catch (error) { return Result.err({ code: 'REPOSITORY_ERROR', details: { message: error instanceof Error ? error.message : 'Unknown error', }, }); } } }