refactor
This commit is contained in:
@@ -1,20 +1,64 @@
|
||||
import type {
|
||||
LeagueConfigFormModel,
|
||||
} from '@gridpilot/racing/application';
|
||||
import type {
|
||||
CreateLeagueWithSeasonAndScoringCommand,
|
||||
CreateLeagueWithSeasonAndScoringResultDTO,
|
||||
} from '@gridpilot/racing/application/use-cases/CreateLeagueWithSeasonAndScoringUseCase';
|
||||
import {
|
||||
getDriverRepository,
|
||||
getCreateLeagueWithSeasonAndScoringUseCase,
|
||||
} from '@/lib/di-container';
|
||||
import { LeagueName } from '@gridpilot/racing/domain/value-objects/LeagueName';
|
||||
import { LeagueDescription } from '@gridpilot/racing/domain/value-objects/LeagueDescription';
|
||||
import { GameConstraints } from '@gridpilot/racing/domain/value-objects/GameConstraints';
|
||||
/**
|
||||
* League Wizard Service - Refactored to use API client
|
||||
*
|
||||
* This service handles league creation wizard logic without direct core dependencies.
|
||||
*/
|
||||
|
||||
import { apiClient } from '@/lib/apiClient';
|
||||
|
||||
export type WizardStep = 1 | 2 | 3 | 4 | 5 | 6 | 7;
|
||||
|
||||
export interface LeagueConfigFormModel {
|
||||
leagueId?: string;
|
||||
basics: {
|
||||
name: string;
|
||||
description?: string;
|
||||
visibility: 'public' | 'private' | 'unlisted';
|
||||
gameId: string;
|
||||
};
|
||||
structure: {
|
||||
mode: 'solo' | 'fixedTeams';
|
||||
maxDrivers?: number;
|
||||
maxTeams?: number;
|
||||
driversPerTeam?: number;
|
||||
};
|
||||
championships: {
|
||||
enableDriverChampionship: boolean;
|
||||
enableTeamChampionship: boolean;
|
||||
enableNationsChampionship: boolean;
|
||||
enableTrophyChampionship: boolean;
|
||||
};
|
||||
scoring: {
|
||||
patternId?: string;
|
||||
customScoringEnabled?: boolean;
|
||||
};
|
||||
dropPolicy: {
|
||||
strategy: 'none' | 'bestNResults' | 'dropWorstN';
|
||||
n?: number;
|
||||
};
|
||||
timings: {
|
||||
practiceMinutes?: number;
|
||||
qualifyingMinutes?: number;
|
||||
sprintRaceMinutes?: number;
|
||||
mainRaceMinutes?: number;
|
||||
sessionCount?: number;
|
||||
roundsPlanned?: number;
|
||||
raceDayOfWeek?: number;
|
||||
raceTimeUtc?: string;
|
||||
};
|
||||
stewarding: {
|
||||
decisionMode: 'owner_only' | 'admin_vote' | 'steward_panel';
|
||||
requiredVotes?: number;
|
||||
requireDefense: boolean;
|
||||
defenseTimeLimit: number;
|
||||
voteTimeLimit: number;
|
||||
protestDeadlineHours: number;
|
||||
stewardingClosesHours: number;
|
||||
notifyAccusedOnProtest: boolean;
|
||||
notifyOnVoteRequired: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface WizardErrors {
|
||||
basics?: {
|
||||
name?: string;
|
||||
@@ -51,16 +95,18 @@ export function validateLeagueWizardStep(
|
||||
if (step === 1) {
|
||||
const basicsErrors: NonNullable<WizardErrors['basics']> = {};
|
||||
|
||||
// Use LeagueName value object for validation
|
||||
const nameValidation = LeagueName.validate(form.basics.name);
|
||||
if (!nameValidation.valid && nameValidation.error) {
|
||||
basicsErrors.name = nameValidation.error;
|
||||
// Basic name validation
|
||||
if (!form.basics.name || form.basics.name.trim().length === 0) {
|
||||
basicsErrors.name = 'League name is required';
|
||||
} else if (form.basics.name.length < 3) {
|
||||
basicsErrors.name = 'League name must be at least 3 characters';
|
||||
} else if (form.basics.name.length > 100) {
|
||||
basicsErrors.name = 'League name must be less than 100 characters';
|
||||
}
|
||||
|
||||
// Use LeagueDescription value object for validation
|
||||
const descValidation = LeagueDescription.validate(form.basics.description ?? '');
|
||||
if (!descValidation.valid && descValidation.error) {
|
||||
basicsErrors.description = descValidation.error;
|
||||
// Description validation
|
||||
if (form.basics.description && form.basics.description.length > 500) {
|
||||
basicsErrors.description = 'Description must be less than 500 characters';
|
||||
}
|
||||
|
||||
if (Object.keys(basicsErrors).length > 0) {
|
||||
@@ -84,47 +130,23 @@ export function validateLeagueWizardStep(
|
||||
// Step 3: Structure (solo vs teams)
|
||||
if (step === 3) {
|
||||
const structureErrors: NonNullable<WizardErrors['structure']> = {};
|
||||
const gameConstraints = GameConstraints.forGame(form.basics.gameId);
|
||||
|
||||
if (form.structure.mode === 'solo') {
|
||||
if (!form.structure.maxDrivers || form.structure.maxDrivers <= 0) {
|
||||
structureErrors.maxDrivers =
|
||||
'Max drivers must be greater than 0 for solo leagues';
|
||||
} else {
|
||||
// Validate against game constraints
|
||||
const driverValidation = gameConstraints.validateDriverCount(
|
||||
form.structure.maxDrivers,
|
||||
);
|
||||
if (!driverValidation.valid && driverValidation.error) {
|
||||
structureErrors.maxDrivers = driverValidation.error;
|
||||
}
|
||||
} else if (form.structure.maxDrivers > 100) {
|
||||
structureErrors.maxDrivers = 'Max drivers cannot exceed 100';
|
||||
}
|
||||
} else if (form.structure.mode === 'fixedTeams') {
|
||||
if (!form.structure.maxTeams || form.structure.maxTeams <= 0) {
|
||||
structureErrors.maxTeams =
|
||||
'Max teams must be greater than 0 for team leagues';
|
||||
} else {
|
||||
// Validate against game constraints
|
||||
const teamValidation = gameConstraints.validateTeamCount(
|
||||
form.structure.maxTeams,
|
||||
);
|
||||
if (!teamValidation.valid && teamValidation.error) {
|
||||
structureErrors.maxTeams = teamValidation.error;
|
||||
}
|
||||
}
|
||||
if (!form.structure.driversPerTeam || form.structure.driversPerTeam <= 0) {
|
||||
structureErrors.driversPerTeam =
|
||||
'Drivers per team must be greater than 0';
|
||||
}
|
||||
// Validate total driver count
|
||||
if (form.structure.maxDrivers) {
|
||||
const driverValidation = gameConstraints.validateDriverCount(
|
||||
form.structure.maxDrivers,
|
||||
);
|
||||
if (!driverValidation.valid && driverValidation.error) {
|
||||
structureErrors.maxDrivers = driverValidation.error;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Object.keys(structureErrors).length > 0) {
|
||||
errors.structure = structureErrors;
|
||||
@@ -210,24 +232,27 @@ export function hasWizardErrors(errors: WizardErrors): boolean {
|
||||
});
|
||||
}
|
||||
|
||||
export interface CreateLeagueResult {
|
||||
leagueId: string;
|
||||
seasonId?: string;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pure mapping from LeagueConfigFormModel to the creation command.
|
||||
* Driver ownership is handled by the caller.
|
||||
* Create a league via API.
|
||||
*/
|
||||
export function buildCreateLeagueCommandFromConfig(
|
||||
export async function createLeagueFromConfig(
|
||||
form: LeagueConfigFormModel,
|
||||
ownerId: string,
|
||||
): CreateLeagueWithSeasonAndScoringCommand {
|
||||
): Promise<CreateLeagueResult> {
|
||||
const structure = form.structure;
|
||||
let maxDrivers: number;
|
||||
let maxTeams: number;
|
||||
|
||||
if (structure.mode === 'solo') {
|
||||
maxDrivers =
|
||||
typeof structure.maxDrivers === 'number' && structure.maxDrivers > 0
|
||||
? structure.maxDrivers
|
||||
: 0;
|
||||
maxTeams = 0;
|
||||
} else {
|
||||
const teams =
|
||||
typeof structure.maxTeams === 'number' && structure.maxTeams > 0
|
||||
@@ -237,52 +262,23 @@ export function buildCreateLeagueCommandFromConfig(
|
||||
typeof structure.driversPerTeam === 'number' && structure.driversPerTeam > 0
|
||||
? structure.driversPerTeam
|
||||
: 0;
|
||||
maxTeams = teams;
|
||||
maxDrivers = teams > 0 && perTeam > 0 ? teams * perTeam : 0;
|
||||
}
|
||||
|
||||
return {
|
||||
const result = await apiClient.leagues.create({
|
||||
name: form.basics.name.trim(),
|
||||
description: (form.basics.description ?? '').trim(),
|
||||
visibility: form.basics.visibility,
|
||||
isPublic: form.basics.visibility === 'public',
|
||||
maxMembers: maxDrivers,
|
||||
ownerId,
|
||||
gameId: form.basics.gameId,
|
||||
maxDrivers,
|
||||
maxTeams,
|
||||
enableDriverChampionship: form.championships.enableDriverChampionship,
|
||||
enableTeamChampionship: form.championships.enableTeamChampionship,
|
||||
enableNationsChampionship: form.championships.enableNationsChampionship,
|
||||
enableTrophyChampionship: form.championships.enableTrophyChampionship,
|
||||
scoringPresetId: form.scoring.patternId ?? 'custom',
|
||||
});
|
||||
|
||||
return {
|
||||
leagueId: result.leagueId,
|
||||
success: result.success,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Thin application-level facade that:
|
||||
* - pulls the current driver via repository
|
||||
* - builds the creation command
|
||||
* - delegates to the create-league use case
|
||||
*/
|
||||
export async function createLeagueFromConfig(
|
||||
form: LeagueConfigFormModel,
|
||||
): Promise<CreateLeagueWithSeasonAndScoringResultDTO> {
|
||||
const driverRepo = getDriverRepository();
|
||||
const drivers = await driverRepo.findAll();
|
||||
const currentDriver = drivers[0];
|
||||
|
||||
if (!currentDriver) {
|
||||
const error = new Error(
|
||||
'No driver profile found. Please create a driver profile first.',
|
||||
) as Error & { code?: string };
|
||||
error.code = 'NO_DRIVER';
|
||||
throw error;
|
||||
}
|
||||
|
||||
const useCase = getCreateLeagueWithSeasonAndScoringUseCase();
|
||||
const command = buildCreateLeagueCommandFromConfig(form, currentDriver.id);
|
||||
return useCase.execute(command);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply scoring preset selection and derive timings, returning a new form model.
|
||||
* This mirrors the previous React handler but keeps it in testable, non-UI logic.
|
||||
|
||||
Reference in New Issue
Block a user