284 lines
10 KiB
TypeScript
284 lines
10 KiB
TypeScript
import type { Logger } from '@core/shared/domain/Logger';
|
|
import { Result } from '@core/shared/domain/Result';
|
|
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
import { League } from '../../domain/entities/League';
|
|
import { LeagueScoringConfig } from '../../domain/entities/LeagueScoringConfig';
|
|
import { Season } from '../../domain/entities/season/Season';
|
|
import { LeagueRepository } from '../../domain/repositories/LeagueRepository';
|
|
import { LeagueScoringConfigRepository } from '../../domain/repositories/LeagueScoringConfigRepository';
|
|
import { SeasonRepository } from '../../domain/repositories/SeasonRepository';
|
|
import type { BonusRule } from '../../domain/types/BonusRule';
|
|
import type { ChampionshipConfig } from '../../domain/types/ChampionshipConfig';
|
|
import type { SessionType } from '../../domain/types/SessionType';
|
|
import {
|
|
LeagueVisibility,
|
|
MIN_RANKED_LEAGUE_DRIVERS,
|
|
} from '../../domain/value-objects/LeagueVisibility';
|
|
import { PointsTable } from '../../domain/value-objects/PointsTable';
|
|
|
|
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: LeagueRepository,
|
|
private readonly seasonRepository: SeasonRepository,
|
|
private readonly leagueScoringConfigRepository: LeagueScoringConfigRepository,
|
|
private readonly getLeagueScoringPresetById: (input: { presetId: string }) => Promise<ScoringPreset | undefined>,
|
|
private readonly logger: Logger,
|
|
) {}
|
|
|
|
async execute(
|
|
command: CreateLeagueWithSeasonAndScoringCommand,
|
|
): Promise<
|
|
Result<
|
|
CreateLeagueWithSeasonAndScoringResult,
|
|
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 championships = this.createDefaultChampionshipConfigs(command);
|
|
const scoringConfig = LeagueScoringConfig.create({
|
|
seasonId,
|
|
scoringPresetId: preset.id,
|
|
championships,
|
|
});
|
|
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 });
|
|
|
|
return Result.ok(result);
|
|
} catch (error) {
|
|
return Result.err({
|
|
code: 'REPOSITORY_ERROR',
|
|
details: {
|
|
message: error instanceof Error ? error.message : 'Unknown error',
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
private createDefaultChampionshipConfigs(
|
|
command: CreateLeagueWithSeasonAndScoringCommand,
|
|
): ChampionshipConfig[] {
|
|
const sessionTypes: SessionType[] = ['main'];
|
|
|
|
const defaultPoints = [25, 18, 15, 12, 10, 8, 6, 4, 2, 1];
|
|
const pointsMap: Record<number, number> = {};
|
|
defaultPoints.forEach((points, index) => {
|
|
pointsMap[index + 1] = points;
|
|
});
|
|
|
|
const pointsTableBySessionType: Record<SessionType, PointsTable> =
|
|
{} as Record<SessionType, PointsTable>;
|
|
const bonusRulesBySessionType: Record<SessionType, BonusRule[]> =
|
|
{} as Record<SessionType, BonusRule[]>;
|
|
|
|
for (const sessionType of sessionTypes) {
|
|
pointsTableBySessionType[sessionType] = new PointsTable(pointsMap);
|
|
bonusRulesBySessionType[sessionType] = [];
|
|
}
|
|
|
|
const configs: ChampionshipConfig[] = [];
|
|
|
|
if (command.enableDriverChampionship) {
|
|
configs.push({
|
|
id: uuidv4(),
|
|
name: 'Driver Championship',
|
|
type: 'driver',
|
|
sessionTypes,
|
|
pointsTableBySessionType,
|
|
bonusRulesBySessionType,
|
|
dropScorePolicy: { strategy: 'none' },
|
|
});
|
|
}
|
|
|
|
if (command.enableTeamChampionship) {
|
|
configs.push({
|
|
id: uuidv4(),
|
|
name: 'Team Championship',
|
|
type: 'team',
|
|
sessionTypes,
|
|
pointsTableBySessionType,
|
|
bonusRulesBySessionType,
|
|
dropScorePolicy: { strategy: 'none' },
|
|
});
|
|
}
|
|
|
|
if (command.enableNationsChampionship) {
|
|
configs.push({
|
|
id: uuidv4(),
|
|
name: 'Nations Championship',
|
|
type: 'nations',
|
|
sessionTypes,
|
|
pointsTableBySessionType,
|
|
bonusRulesBySessionType,
|
|
dropScorePolicy: { strategy: 'none' },
|
|
});
|
|
}
|
|
|
|
if (command.enableTrophyChampionship) {
|
|
configs.push({
|
|
id: uuidv4(),
|
|
name: 'Trophy Championship',
|
|
type: 'trophy',
|
|
sessionTypes,
|
|
pointsTableBySessionType,
|
|
bonusRulesBySessionType,
|
|
dropScorePolicy: { strategy: 'none' },
|
|
});
|
|
}
|
|
|
|
if (configs.length === 0) {
|
|
configs.push({
|
|
id: uuidv4(),
|
|
name: 'Driver Championship',
|
|
type: 'driver',
|
|
sessionTypes,
|
|
pointsTableBySessionType,
|
|
bonusRulesBySessionType,
|
|
dropScorePolicy: { strategy: 'none' },
|
|
});
|
|
}
|
|
|
|
return configs;
|
|
}
|
|
|
|
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);
|
|
}
|
|
} |