Files
gridpilot.gg/core/racing/application/use-cases/PreviewLeagueScheduleUseCase.ts
2025-12-23 15:38:50 +01:00

171 lines
5.2 KiB
TypeScript

import { SeasonScheduleGenerator } from '../../domain/services/SeasonScheduleGenerator';
import {
scheduleDTOToSeasonSchedule,
type SeasonScheduleConfigDTO,
} from '../dto/LeagueScheduleDTO';
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { Logger } from '@core/shared/application';
import type { SeasonSchedule } from '../../domain/value-objects/SeasonSchedule';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
export type PreviewLeagueScheduleSeasonConfig = SeasonScheduleConfigDTO;
export type PreviewLeagueScheduleInput = {
schedule: PreviewLeagueScheduleSeasonConfig;
maxRounds?: number;
};
export interface PreviewLeagueScheduleRound {
roundNumber: number;
scheduledAt: string;
timezoneId: string;
}
export interface PreviewLeagueScheduleResult {
rounds: PreviewLeagueScheduleRound[];
summary: string;
}
export type PreviewLeagueScheduleErrorCode =
| 'INVALID_SCHEDULE'
| 'REPOSITORY_ERROR';
export class PreviewLeagueScheduleUseCase {
constructor(
private readonly scheduleGenerator: Pick<
typeof SeasonScheduleGenerator,
'generateSlotsUpTo'
> = SeasonScheduleGenerator,
private readonly logger: Logger,
private readonly output: UseCaseOutputPort<PreviewLeagueScheduleResult>,
) {}
async execute(
params: PreviewLeagueScheduleInput,
): Promise<
Result<
void,
ApplicationErrorCode<PreviewLeagueScheduleErrorCode, { message: string }>
>
> {
this.logger.debug('Previewing league schedule', { params });
try {
let seasonSchedule: SeasonSchedule;
try {
seasonSchedule = scheduleDTOToSeasonSchedule(params.schedule);
} catch (error) {
this.logger.warn('Invalid schedule data provided', {
schedule: params.schedule,
error: error instanceof Error ? error.message : 'Unknown error',
});
return Result.err({
code: 'INVALID_SCHEDULE',
details: { message: 'Invalid schedule data' },
});
}
const maxRounds =
params.maxRounds && params.maxRounds > 0
? Math.min(params.maxRounds, seasonSchedule.plannedRounds)
: seasonSchedule.plannedRounds;
const slots = this.scheduleGenerator.generateSlotsUpTo(
seasonSchedule,
maxRounds,
);
const rounds: PreviewLeagueScheduleRound[] = slots.map(slot => ({
roundNumber: slot.roundNumber,
scheduledAt: slot.scheduledAt.toISOString(),
timezoneId: slot.timezone.id,
}));
const summary = this.buildSummary(params.schedule, rounds);
const result: PreviewLeagueScheduleResult = {
rounds,
summary,
};
this.logger.info('Successfully generated league schedule preview', {
roundCount: rounds.length,
});
this.output.present(result);
return Result.ok(undefined);
} catch (error) {
this.logger.error(
'Failed to preview league schedule due to an unexpected error',
error instanceof Error ? error : new Error('Unknown error'),
);
return Result.err({
code: 'REPOSITORY_ERROR',
details: {
message:
error instanceof Error
? error.message
: 'Failed to preview league schedule',
},
});
}
}
private buildSummary(
schedule: PreviewLeagueScheduleSeasonConfig,
rounds: Array<{ roundNumber: number; scheduledAt: string; timezoneId: string }>,
): string {
if (rounds.length === 0) {
return 'No rounds scheduled.';
}
const firstRound = rounds[0]!;
const lastRound = rounds[rounds.length - 1]!;
const first = new Date(firstRound.scheduledAt);
const last = new Date(lastRound.scheduledAt);
const firstDate = first.toISOString().slice(0, 10);
const lastDate = last.toISOString().slice(0, 10);
const timePart = schedule.raceStartTime;
const tz = schedule.timezoneId;
let recurrenceDescription: string;
if (schedule.recurrenceStrategy === 'weekly') {
const days = (schedule.weekdays ?? []).join(', ');
recurrenceDescription = `Every ${days}`;
} else if (schedule.recurrenceStrategy === 'everyNWeeks') {
const interval = schedule.intervalWeeks ?? 1;
const days = (schedule.weekdays ?? []).join(', ');
recurrenceDescription = `Every ${interval} week(s) on ${days}`;
} else if (schedule.recurrenceStrategy === 'monthlyNthWeekday') {
const ordinalLabel = this.ordinalToLabel(schedule.monthlyOrdinal ?? 1);
const weekday = schedule.monthlyWeekday ?? 'Mon';
recurrenceDescription = `Every ${ordinalLabel} ${weekday}`;
} else {
recurrenceDescription = 'Custom recurrence';
}
return `${recurrenceDescription} at ${timePart} ${tz}, starting ${firstDate}${rounds.length} rounds from ${firstDate} to ${lastDate}.`;
}
private ordinalToLabel(ordinal: 1 | 2 | 3 | 4): string {
switch (ordinal) {
case 1:
return '1st';
case 2:
return '2nd';
case 3:
return '3rd';
case 4:
return '4th';
default:
return `${ordinal}th`;
}
}
}