import { Season } from '../../domain/entities/season/Season'; import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository'; import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository'; import type { LeagueConfigFormModel } from '../dto/LeagueConfigFormDTO'; 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 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, 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 scoringConfig = new SeasonScoringConfig({ scoringPresetId: config.scoring.patternId ?? 'custom', customScoringEnabled: config.scoring.customScoringEnabled ?? false, }); const dropPolicy = new SeasonDropPolicy({ strategy: config.dropPolicy.strategy, ...(config.dropPolicy.n !== undefined ? { n: config.dropPolicy.n } : {}), }); const stewardingConfig = new SeasonStewardingConfig({ decisionMode: config.stewarding.decisionMode, ...(config.stewarding.requiredVotes !== undefined ? { requiredVotes: config.stewarding.requiredVotes } : {}), requireDefense: config.stewarding.requireDefense, defenseTimeLimit: config.stewarding.defenseTimeLimit, voteTimeLimit: config.stewarding.voteTimeLimit, protestDeadlineHours: config.stewarding.protestDeadlineHours, stewardingClosesHours: config.stewarding.stewardingClosesHours, notifyAccusedOnProtest: config.stewarding.notifyAccusedOnProtest, notifyOnVoteRequired: config.stewarding.notifyOnVoteRequired, }); 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.seasonStartDate || !timings.raceStartTime) { return undefined; } const startDate = new Date(timings.seasonStartDate); const timeOfDay = RaceTimeOfDay.fromString(timings.raceStartTime); const timezoneId = timings.timezoneId ?? 'UTC'; const timezone = new LeagueTimezone(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 = new MonthlyRecurrencePattern({ ordinal: (timings.monthlyOrdinal ?? 1) as 1 | 2 | 3 | 4, weekday: (timings.monthlyWeekday ?? 'Mon') as Weekday, }); return RecurrenceStrategyFactory.monthlyNthWeekday(pattern); } case 'weekly': default: return RecurrenceStrategyFactory.weekly(weekdays); } })(); 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); 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) { 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) { 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', }, }); } } }