import { Season } from '../../domain/entities/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'; export interface CreateSeasonForLeagueCommand { leagueId: string; name: string; gameId: string; sourceSeasonId?: string; config?: LeagueConfigFormModel; } export interface CreateSeasonForLeagueResultDTO { seasonId: string; } export interface SeasonSummaryDTO { seasonId: string; leagueId: string; name: string; status: import('../../domain/entities/Season').SeasonStatus; startDate?: Date; endDate?: Date; isPrimary: boolean; } export interface ListSeasonsForLeagueQuery { leagueId: string; } export interface ListSeasonsForLeagueResultDTO { items: SeasonSummaryDTO[]; } export interface GetSeasonDetailsQuery { leagueId: string; seasonId: string; } export interface SeasonDetailsDTO { seasonId: string; leagueId: string; gameId: string; name: string; status: import('../../domain/entities/Season').SeasonStatus; startDate?: Date; endDate?: Date; maxDrivers?: number; schedule?: { startDate: Date; plannedRounds: number; }; scoring?: { scoringPresetId: string; customScoringEnabled: boolean; }; dropPolicy?: { strategy: import('../../domain/value-objects/SeasonDropPolicy').SeasonDropStrategy; n?: number; }; stewarding?: { decisionMode: import('../../domain/entities/League').StewardingDecisionMode; requiredVotes?: number; requireDefense: boolean; defenseTimeLimit: number; voteTimeLimit: number; protestDeadlineHours: number; stewardingClosesHours: number; notifyAccusedOnProtest: boolean; notifyOnVoteRequired: boolean; }; } export type SeasonLifecycleTransition = 'activate' | 'complete' | 'archive' | 'cancel'; export interface ManageSeasonLifecycleCommand { leagueId: string; seasonId: string; transition: SeasonLifecycleTransition; } export interface ManageSeasonLifecycleResultDTO { seasonId: string; status: import('../../domain/entities/Season').SeasonStatus; startDate?: Date; endDate?: Date; } export class SeasonApplicationService { constructor( private readonly leagueRepository: ILeagueRepository, private readonly seasonRepository: ISeasonRepository, ) {} async createSeasonForLeague(command: CreateSeasonForLeagueCommand): Promise { const league = await this.leagueRepository.findById(command.leagueId); if (!league) { throw new Error(`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) { throw new Error(`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); return { seasonId }; } async listSeasonsForLeague(query: ListSeasonsForLeagueQuery): Promise { const league = await this.leagueRepository.findById(query.leagueId); if (!league) { throw new Error(`League not found: ${query.leagueId}`); } const seasons = await this.seasonRepository.listByLeague(league.id); const items: SeasonSummaryDTO[] = seasons.map((s) => ({ seasonId: s.id, leagueId: s.leagueId, name: s.name, status: s.status, ...(s.startDate !== undefined ? { startDate: s.startDate } : {}), ...(s.endDate !== undefined ? { endDate: s.endDate } : {}), isPrimary: false, })); return { items }; } async getSeasonDetails(query: GetSeasonDetailsQuery): Promise { const league = await this.leagueRepository.findById(query.leagueId); if (!league) { throw new Error(`League not found: ${query.leagueId}`); } const season = await this.seasonRepository.findById(query.seasonId); if (!season || season.leagueId !== league.id) { throw new Error(`Season ${query.seasonId} does not belong to league ${league.id}`); } return { seasonId: season.id, leagueId: season.leagueId, gameId: season.gameId, name: season.name, status: season.status, ...(season.startDate !== undefined ? { startDate: season.startDate } : {}), ...(season.endDate !== undefined ? { endDate: season.endDate } : {}), ...(season.maxDrivers !== undefined ? { maxDrivers: season.maxDrivers } : {}), ...(season.schedule ? { schedule: { startDate: season.schedule.startDate, plannedRounds: season.schedule.plannedRounds, }, } : {}), ...(season.scoringConfig ? { scoring: { scoringPresetId: season.scoringConfig.scoringPresetId, customScoringEnabled: season.scoringConfig.customScoringEnabled ?? false, }, } : {}), ...(season.dropPolicy ? { dropPolicy: { strategy: season.dropPolicy.strategy, ...(season.dropPolicy.n !== undefined ? { n: season.dropPolicy.n } : {}), }, } : {}), ...(season.stewardingConfig ? { stewarding: { decisionMode: season.stewardingConfig.decisionMode, ...(season.stewardingConfig.requiredVotes !== undefined ? { requiredVotes: season.stewardingConfig.requiredVotes } : {}), requireDefense: season.stewardingConfig.requireDefense, defenseTimeLimit: season.stewardingConfig.defenseTimeLimit, voteTimeLimit: season.stewardingConfig.voteTimeLimit, protestDeadlineHours: season.stewardingConfig.protestDeadlineHours, stewardingClosesHours: season.stewardingConfig.stewardingClosesHours, notifyAccusedOnProtest: season.stewardingConfig.notifyAccusedOnProtest, notifyOnVoteRequired: season.stewardingConfig.notifyOnVoteRequired, }, } : {}), }; } async manageSeasonLifecycle(command: ManageSeasonLifecycleCommand): Promise { const league = await this.leagueRepository.findById(command.leagueId); if (!league) { throw new Error(`League not found: ${command.leagueId}`); } const season = await this.seasonRepository.findById(command.seasonId); if (!season || season.leagueId !== league.id) { throw new Error(`Season ${command.seasonId} does not belong to league ${league.id}`); } let updated: Season; 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: throw new Error('Unsupported Season lifecycle transition'); } await this.seasonRepository.update(updated); return { seasonId: updated.id, status: updated.status, ...(updated.startDate !== undefined ? { startDate: updated.startDate } : {}), ...(updated.endDate !== undefined ? { endDate: updated.endDate } : {}), }; } 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, }); } }