171 lines
5.2 KiB
TypeScript
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`;
|
|
}
|
|
}
|
|
} |