Files
gridpilot.gg/core/racing/application/use-cases/CreateLeagueWithSeasonAndScoringUseCase.ts
2025-12-21 00:43:42 +01:00

194 lines
7.7 KiB
TypeScript

import { v4 as uuidv4 } from 'uuid';
import { League } from '../../domain/entities/League';
import { Season } from '../../domain/entities/season/Season';
import { LeagueScoringConfig } from '../../domain/entities/LeagueScoringConfig';
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository';
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
import {
LeagueVisibility,
MIN_RANKED_LEAGUE_DRIVERS,
} from '../../domain/value-objects/LeagueVisibility';
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
export type CreateLeagueWithSeasonAndScoringCommand = {
name: string;
description?: string;
/**
* League visibility/ranking mode.
* - 'ranked' (or legacy 'public'): Competitive, public, affects ratings. Requires min 10 drivers.
* - 'unranked' (or legacy 'private'): Casual with friends, no rating impact.
*/
visibility: string;
ownerId: string;
gameId: string;
maxDrivers?: number;
maxTeams?: number;
enableDriverChampionship: boolean;
enableTeamChampionship: boolean;
enableNationsChampionship: boolean;
enableTrophyChampionship: boolean;
scoringPresetId?: string;
};
export type CreateLeagueWithSeasonAndScoringResult = {
league: League;
season: Season;
scoringConfig: LeagueScoringConfig;
};
type CreateLeagueWithSeasonAndScoringErrorCode =
| 'VALIDATION_ERROR'
| 'UNKNOWN_PRESET'
| 'REPOSITORY_ERROR';
type ScoringPreset = {
id: string;
name: string;
};
export class CreateLeagueWithSeasonAndScoringUseCase {
constructor(
private readonly leagueRepository: ILeagueRepository,
private readonly seasonRepository: ISeasonRepository,
private readonly leagueScoringConfigRepository: ILeagueScoringConfigRepository,
private readonly getLeagueScoringPresetById: (input: { presetId: string }) => Promise<ScoringPreset | undefined>,
private readonly logger: Logger,
private readonly output: UseCaseOutputPort<CreateLeagueWithSeasonAndScoringResult>,
) {}
async execute(
command: CreateLeagueWithSeasonAndScoringCommand,
): Promise<Result<void, ApplicationErrorCode<CreateLeagueWithSeasonAndScoringErrorCode>>> {
this.logger.debug('Executing CreateLeagueWithSeasonAndScoringUseCase', { command });
const validation = this.validate(command);
if (validation.isErr()) {
return Result.err(validation.unwrapErr());
}
this.logger.info('Command validated successfully.');
try {
const leagueId = uuidv4();
this.logger.debug(`Generated leagueId: ${leagueId}`);
const league = League.create({
id: leagueId,
name: command.name,
description: command.description ?? '',
ownerId: command.ownerId,
settings: {
pointsSystem: 'custom',
...(command.maxDrivers !== undefined ? { maxDrivers: command.maxDrivers } : {}),
},
});
await this.leagueRepository.create(league);
this.logger.info(`League ${league.name} (${league.id}) created successfully.`);
const seasonId = uuidv4();
this.logger.debug(`Generated seasonId: ${seasonId}`);
const season = Season.create({
id: seasonId,
leagueId: league.id.toString(),
gameId: command.gameId,
name: `${command.name} Season 1`,
year: new Date().getFullYear(),
order: 1,
status: 'active',
startDate: new Date(),
endDate: new Date(),
});
await this.seasonRepository.create(season);
this.logger.info(`Season ${season.name} (${season.id}) created for league ${league.id}.`);
const presetId = command.scoringPresetId ?? 'club-default';
this.logger.debug(`Attempting to retrieve scoring preset: ${presetId}`);
const preset = await this.getLeagueScoringPresetById({ presetId });
if (!preset) {
this.logger.error(`Unknown scoring preset: ${presetId}`);
return Result.err({ code: 'UNKNOWN_PRESET', details: { message: `Unknown scoring preset: ${presetId}` } });
}
this.logger.info(`Scoring preset ${preset.name} (${preset.id}) retrieved.`);
const scoringConfig = LeagueScoringConfig.create({
seasonId,
scoringPresetId: preset.id,
championships: {
driver: command.enableDriverChampionship,
team: command.enableTeamChampionship,
nations: command.enableNationsChampionship,
trophy: command.enableTrophyChampionship,
},
});
this.logger.debug(`Scoring configuration created from preset ${preset.id}.`);
await this.leagueScoringConfigRepository.save(scoringConfig);
this.logger.info(`Scoring configuration saved for season ${seasonId}.`);
const result: CreateLeagueWithSeasonAndScoringResult = {
league,
season,
scoringConfig,
};
this.logger.debug('CreateLeagueWithSeasonAndScoringUseCase completed successfully.', { result });
this.output.present(result);
return Result.ok(undefined);
} catch (error) {
return Result.err({
code: 'REPOSITORY_ERROR',
details: {
message: error instanceof Error ? error.message : 'Unknown error',
},
});
}
}
private validate(
command: CreateLeagueWithSeasonAndScoringCommand,
): Result<void, ApplicationErrorCode<'VALIDATION_ERROR', { message: string }>> {
this.logger.debug('Validating CreateLeagueWithSeasonAndScoringCommand', { command });
if (!command.name || command.name.trim().length === 0) {
this.logger.warn('Validation failed: League name is required', { command });
return Result.err({ code: 'VALIDATION_ERROR', details: { message: 'League name is required' } });
}
if (!command.ownerId || command.ownerId.trim().length === 0) {
this.logger.warn('Validation failed: League ownerId is required', { command });
return Result.err({ code: 'VALIDATION_ERROR', details: { message: 'League ownerId is required' } });
}
if (!command.gameId || command.gameId.trim().length === 0) {
this.logger.warn('Validation failed: gameId is required', { command });
return Result.err({ code: 'VALIDATION_ERROR', details: { message: 'gameId is required' } });
}
if (!command.visibility) {
this.logger.warn('Validation failed: visibility is required', { command });
return Result.err({ code: 'VALIDATION_ERROR', details: { message: 'visibility is required' } });
}
if (command.maxDrivers !== undefined && command.maxDrivers <= 0) {
this.logger.warn('Validation failed: maxDrivers must be greater than 0 when provided', { command });
return Result.err({ code: 'VALIDATION_ERROR', details: { message: 'maxDrivers must be greater than 0 when provided' } });
}
const visibility = LeagueVisibility.fromString(command.visibility);
if (visibility.isRanked()) {
const driverCount = command.maxDrivers ?? 0;
if (driverCount < MIN_RANKED_LEAGUE_DRIVERS) {
this.logger.warn(
`Validation failed: Ranked leagues require at least ${MIN_RANKED_LEAGUE_DRIVERS} drivers. Current setting: ${driverCount}.`,
{ command }
);
return Result.err({ code: 'VALIDATION_ERROR', details: { message:
`Ranked leagues require at least ${MIN_RANKED_LEAGUE_DRIVERS} drivers. ` +
`Current setting: ${driverCount}. ` +
`For smaller groups, consider creating an Unranked (Friends) league instead.`
} });
}
}
this.logger.debug('Validation successful.');
return Result.ok(undefined);
}
}