This commit is contained in:
2025-12-16 18:17:48 +01:00
parent 362894d1a5
commit ec7c0b8f2a
94 changed files with 4240 additions and 983 deletions

View File

@@ -14,43 +14,13 @@ import {
LeagueVisibility,
MIN_RANKED_LEAGUE_DRIVERS,
} from '../../domain/value-objects/LeagueVisibility';
/**
* League visibility/ranking mode.
* - 'ranked' (or legacy 'public'): Competitive, public, affects driver ratings. Min 10 drivers.
* - 'unranked' (or legacy 'private'): Casual with friends, no rating impact.
*/
export type LeagueVisibilityInput = 'ranked' | 'unranked' | 'public' | 'private';
export interface 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: LeagueVisibilityInput;
ownerId: string;
gameId: string;
maxDrivers?: number;
maxTeams?: number;
enableDriverChampionship: boolean;
enableTeamChampionship: boolean;
enableNationsChampionship: boolean;
enableTrophyChampionship: boolean;
scoringPresetId?: string;
}
export interface CreateLeagueWithSeasonAndScoringResultDTO {
leagueId: string;
seasonId: string;
scoringPresetId?: string;
scoringPresetName?: string;
}
import { Result } from '@core/shared/result/Result';
import { RacingDomainValidationError } from '../../domain/errors/RacingDomainError';
import type { CreateLeagueWithSeasonAndScoringCommand } from './CreateLeagueWithSeasonAndScoringCommand';
import type { CreateLeagueWithSeasonAndScoringResultDTO } from '../dto/CreateLeagueWithSeasonAndScoringResultDTO';
export class CreateLeagueWithSeasonAndScoringUseCase
implements AsyncUseCase<CreateLeagueWithSeasonAndScoringCommand, CreateLeagueWithSeasonAndScoringResultDTO> {
implements AsyncUseCase<CreateLeagueWithSeasonAndScoringCommand, Result<CreateLeagueWithSeasonAndScoringResultDTO, RacingDomainValidationError>> {
constructor(
private readonly leagueRepository: ILeagueRepository,
private readonly seasonRepository: ISeasonRepository,
@@ -61,11 +31,14 @@ export class CreateLeagueWithSeasonAndScoringUseCase
async execute(
command: CreateLeagueWithSeasonAndScoringCommand,
): Promise<CreateLeagueWithSeasonAndScoringResultDTO> {
): Promise<Result<CreateLeagueWithSeasonAndScoringResultDTO, RacingDomainValidationError>> {
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 {
this.validate(command);
this.logger.info('Command validated successfully.');
const leagueId = uuidv4();
this.logger.debug(`Generated leagueId: ${leagueId}`);
@@ -108,7 +81,7 @@ export class CreateLeagueWithSeasonAndScoringUseCase
if (!preset) {
this.logger.error(`Unknown scoring preset: ${presetId}`);
throw new Error(`Unknown scoring preset: ${presetId}`);
return Result.err(new RacingDomainValidationError(`Unknown scoring preset: ${presetId}`));
}
this.logger.info(`Scoring preset ${preset.name} (${preset.id}) retrieved.`);
@@ -119,45 +92,44 @@ export class CreateLeagueWithSeasonAndScoringUseCase
await this.leagueScoringConfigRepository.save(finalConfig);
this.logger.info(`Scoring configuration saved for season ${seasonId}.`);
const result = {
const result: CreateLeagueWithSeasonAndScoringResultDTO = {
leagueId: league.id,
seasonId,
scoringPresetId: preset.id,
scoringPresetName: preset.name,
};
this.logger.debug('CreateLeagueWithSeasonAndScoringUseCase completed successfully.', { result });
return result;
return Result.ok(result);
} catch (error) {
this.logger.error('Error during CreateLeagueWithSeasonAndScoringUseCase execution.', error, { command });
throw error;
return Result.err(new RacingDomainValidationError(error instanceof Error ? error.message : 'Unknown error'));
}
}
private validate(command: CreateLeagueWithSeasonAndScoringCommand): void {
private validate(command: CreateLeagueWithSeasonAndScoringCommand): Result<void, RacingDomainValidationError> {
this.logger.debug('Validating CreateLeagueWithSeasonAndScoringCommand', { command });
if (!command.name || command.name.trim().length === 0) {
this.logger.warn('Validation failed: League name is required', { command });
throw new Error('League name is required');
return Result.err(new RacingDomainValidationError('League name is required'));
}
if (!command.ownerId || command.ownerId.trim().length === 0) {
this.logger.warn('Validation failed: League ownerId is required', { command });
throw new Error('League ownerId is required');
return Result.err(new RacingDomainValidationError('League ownerId is required'));
}
if (!command.gameId || command.gameId.trim().length === 0) {
this.logger.warn('Validation failed: gameId is required', { command });
throw new Error('gameId is required');
return Result.err(new RacingDomainValidationError('gameId is required'));
}
if (!command.visibility) {
this.logger.warn('Validation failed: visibility is required', { command });
throw new Error('visibility is required');
return Result.err(new RacingDomainValidationError('visibility is required'));
}
if (command.maxDrivers !== undefined && command.maxDrivers <= 0) {
this.logger.warn('Validation failed: maxDrivers must be greater than 0 when provided', { command });
throw new Error('maxDrivers must be greater than 0 when provided');
return Result.err(new RacingDomainValidationError('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) {
@@ -165,13 +137,14 @@ export class CreateLeagueWithSeasonAndScoringUseCase
`Validation failed: Ranked leagues require at least ${MIN_RANKED_LEAGUE_DRIVERS} drivers. Current setting: ${driverCount}.`,
{ command }
);
throw new Error(
return Result.err(new RacingDomainValidationError(
`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);
}
}