league service

This commit is contained in:
2025-12-16 00:57:31 +01:00
parent 3b566c973d
commit 775d41e055
130 changed files with 4077 additions and 1036 deletions

View File

@@ -1,3 +1,5 @@
import type { LeagueScoringConfig } from '../../domain/entities/LeagueScoringConfig';
export type LeagueScoringPresetPrimaryChampionshipType =
| 'driver'
| 'team'
@@ -23,4 +25,5 @@ export interface LeagueScoringPresetDTO {
export interface LeagueScoringPresetProvider {
listPresets(): LeagueScoringPresetDTO[];
getPresetById(id: string): LeagueScoringPresetDTO | undefined;
createScoringConfigFromPreset(presetId: string, seasonId: string): LeagueScoringConfig;
}

View File

@@ -0,0 +1,13 @@
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
export interface ApproveLeagueJoinRequestViewModel {
success: boolean;
message: string;
}
export interface ApproveLeagueJoinRequestResultDTO {
success: boolean;
message: string;
}
export interface IApproveLeagueJoinRequestPresenter extends Presenter<ApproveLeagueJoinRequestResultDTO, ApproveLeagueJoinRequestViewModel> {}

View File

@@ -0,0 +1,17 @@
export interface CompleteDriverOnboardingViewModel {
success: boolean;
driverId?: string;
errorMessage?: string;
}
export interface CompleteDriverOnboardingResultDTO {
success: boolean;
driverId?: string;
errorMessage?: string;
}
export interface ICompleteDriverOnboardingPresenter {
present(dto: CompleteDriverOnboardingResultDTO): void;
get viewModel(): CompleteDriverOnboardingViewModel;
reset(): void;
}

View File

@@ -0,0 +1,13 @@
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
export interface GetLeagueAdminPermissionsViewModel {
canRemoveMember: boolean;
canUpdateRoles: boolean;
}
export interface GetLeagueAdminPermissionsResultDTO {
canRemoveMember: boolean;
canUpdateRoles: boolean;
}
export interface IGetLeagueAdminPermissionsPresenter extends Presenter<GetLeagueAdminPermissionsResultDTO, GetLeagueAdminPermissionsViewModel> {}

View File

@@ -0,0 +1,13 @@
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
export interface LeagueAdminViewModel {
leagueId: string;
ownerId: string;
}
export interface GetLeagueAdminResultDTO {
leagueId: string;
ownerId: string;
}
export interface IGetLeagueAdminPresenter extends Presenter<GetLeagueAdminResultDTO, LeagueAdminViewModel> {}

View File

@@ -0,0 +1,21 @@
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
export interface LeagueJoinRequestViewModel {
id: string;
leagueId: string;
driverId: string;
requestedAt: Date;
message: string;
driver: { id: string; name: string } | null;
}
export interface GetLeagueJoinRequestsViewModel {
joinRequests: LeagueJoinRequestViewModel[];
}
export interface GetLeagueJoinRequestsResultDTO {
joinRequests: any[];
drivers: { id: string; name: string }[];
}
export interface IGetLeagueJoinRequestsPresenter extends Presenter<GetLeagueJoinRequestsResultDTO, GetLeagueJoinRequestsViewModel> {}

View File

@@ -0,0 +1,21 @@
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
export interface LeagueMembershipsViewModel {
members: {
driverId: string;
driver: { id: string; name: string };
role: string;
joinedAt: Date;
}[];
}
export interface GetLeagueMembershipsViewModel {
memberships: LeagueMembershipsViewModel;
}
export interface GetLeagueMembershipsResultDTO {
memberships: any[];
drivers: { id: string; name: string }[];
}
export interface IGetLeagueMembershipsPresenter extends Presenter<GetLeagueMembershipsResultDTO, GetLeagueMembershipsViewModel> {}

View File

@@ -0,0 +1,17 @@
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
export interface LeagueOwnerSummaryViewModel {
driver: { id: string; name: string };
rating: number;
rank: number;
}
export interface GetLeagueOwnerSummaryViewModel {
summary: LeagueOwnerSummaryViewModel | null;
}
export interface GetLeagueOwnerSummaryResultDTO {
summary: LeagueOwnerSummaryViewModel | null;
}
export interface IGetLeagueOwnerSummaryPresenter extends Presenter<GetLeagueOwnerSummaryResultDTO, GetLeagueOwnerSummaryViewModel> {}

View File

@@ -0,0 +1,15 @@
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
export interface GetLeagueProtestsViewModel {
protests: any[];
racesById: Record<string, any>;
driversById: Record<string, any>;
}
export interface GetLeagueProtestsResultDTO {
protests: any[];
races: any[];
drivers: { id: string; name: string }[];
}
export interface IGetLeagueProtestsPresenter extends Presenter<GetLeagueProtestsResultDTO, GetLeagueProtestsViewModel> {}

View File

@@ -0,0 +1,19 @@
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
export interface LeagueScheduleViewModel {
races: Array<{
id: string;
name: string;
date: string;
}>;
}
export interface GetLeagueScheduleResultDTO {
races: Array<{
id: string;
name: string;
scheduledAt: Date;
}>;
}
export interface IGetLeagueSchedulePresenter extends Presenter<GetLeagueScheduleResultDTO, LeagueScheduleViewModel> {}

View File

@@ -0,0 +1,21 @@
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
export interface LeagueSeasonSummaryViewModel {
seasonId: string;
name: string;
status: string;
startDate: Date;
endDate: Date;
isPrimary: boolean;
isParallelActive: boolean;
}
export interface GetLeagueSeasonsViewModel {
seasons: LeagueSeasonSummaryViewModel[];
}
export interface GetLeagueSeasonsResultDTO {
seasons: any[];
}
export interface IGetLeagueSeasonsPresenter extends Presenter<GetLeagueSeasonsResultDTO, GetLeagueSeasonsViewModel> {}

View File

@@ -0,0 +1,11 @@
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
export interface GetTotalLeaguesViewModel {
totalLeagues: number;
}
export interface GetTotalLeaguesResultDTO {
totalLeagues: number;
}
export interface IGetTotalLeaguesPresenter extends Presenter<GetTotalLeaguesResultDTO, GetTotalLeaguesViewModel> {}

View File

@@ -2,24 +2,19 @@ import type { Standing } from '../../domain/entities/Standing';
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
export interface StandingItemViewModel {
id: string;
leagueId: string;
seasonId: string;
driverId: string;
position: number;
driver: { id: string; name: string };
points: number;
wins: number;
podiums: number;
racesCompleted: number;
rank: number;
}
export interface LeagueStandingsViewModel {
leagueId: string;
standings: StandingItemViewModel[];
}
export interface LeagueStandingsResultDTO {
standings: Standing[];
drivers: { id: string; name: string }[];
}
export interface ILeagueStandingsPresenter

View File

@@ -1,20 +1,16 @@
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
export interface LeagueStatsViewModel {
leagueId: string;
totalMembers: number;
totalRaces: number;
completedRaces: number;
scheduledRaces: number;
averageSOF: number | null;
highestSOF: number | null;
lowestSOF: number | null;
averageRating: number;
}
export interface ILeagueStatsPresenter {
present(
leagueId: string,
totalRaces: number,
completedRaces: number,
scheduledRaces: number,
sofValues: number[]
): LeagueStatsViewModel;
getViewModel(): LeagueStatsViewModel;
}
export interface LeagueStatsResultDTO {
totalMembers: number;
totalRaces: number;
averageRating: number;
}
export interface ILeagueStatsPresenter
extends Presenter<LeagueStatsResultDTO, LeagueStatsViewModel> {}

View File

@@ -0,0 +1,13 @@
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
export interface RejectLeagueJoinRequestViewModel {
success: boolean;
message: string;
}
export interface RejectLeagueJoinRequestResultDTO {
success: boolean;
message: string;
}
export interface IRejectLeagueJoinRequestPresenter extends Presenter<RejectLeagueJoinRequestResultDTO, RejectLeagueJoinRequestViewModel> {}

View File

@@ -0,0 +1,11 @@
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
export interface RemoveLeagueMemberViewModel {
success: boolean;
}
export interface RemoveLeagueMemberResultDTO {
success: boolean;
}
export interface IRemoveLeagueMemberPresenter extends Presenter<RemoveLeagueMemberResultDTO, RemoveLeagueMemberViewModel> {}

View File

@@ -0,0 +1,13 @@
export interface TotalDriversViewModel {
totalDrivers: number;
}
export interface TotalDriversResultDTO {
totalDrivers: number;
}
export interface ITotalDriversPresenter {
present(dto: TotalDriversResultDTO): void;
get viewModel(): TotalDriversViewModel;
reset(): void;
}

View File

@@ -0,0 +1,11 @@
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
export interface UpdateLeagueMemberRoleViewModel {
success: boolean;
}
export interface UpdateLeagueMemberRoleResultDTO {
success: boolean;
}
export interface IUpdateLeagueMemberRolePresenter extends Presenter<UpdateLeagueMemberRoleResultDTO, UpdateLeagueMemberRoleViewModel> {}

View File

@@ -0,0 +1,36 @@
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import type { IApproveLeagueJoinRequestPresenter, ApproveLeagueJoinRequestResultDTO, ApproveLeagueJoinRequestViewModel } from '../presenters/IApproveLeagueJoinRequestPresenter';
import type { UseCase } from '@gridpilot/shared/application/UseCase';
export interface ApproveLeagueJoinRequestUseCaseParams {
leagueId: string;
requestId: string;
}
export interface ApproveLeagueJoinRequestResultDTO {
success: boolean;
message: string;
}
export class ApproveLeagueJoinRequestUseCase implements UseCase<ApproveLeagueJoinRequestUseCaseParams, ApproveLeagueJoinRequestResultDTO, ApproveLeagueJoinRequestViewModel, IApproveLeagueJoinRequestPresenter> {
constructor(private readonly leagueMembershipRepository: ILeagueMembershipRepository) {}
async execute(params: ApproveLeagueJoinRequestUseCaseParams, presenter: IApproveLeagueJoinRequestPresenter): Promise<void> {
const requests = await this.leagueMembershipRepository.getJoinRequests(params.leagueId);
const request = requests.find(r => r.id === params.requestId);
if (!request) {
throw new Error('Join request not found');
}
await this.leagueMembershipRepository.removeJoinRequest(params.requestId);
await this.leagueMembershipRepository.saveMembership({
leagueId: params.leagueId,
driverId: request.driverId,
role: 'member',
status: 'active',
joinedAt: new Date(),
});
const dto: ApproveLeagueJoinRequestResultDTO = { success: true, message: 'Join request approved.' };
presenter.reset();
presenter.present(dto);
}
}

View File

@@ -0,0 +1,60 @@
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
import type { ICompleteDriverOnboardingPresenter, CompleteDriverOnboardingResultDTO } from '../presenters/ICompleteDriverOnboardingPresenter';
import type { UseCase } from '@gridpilot/shared/application/UseCase';
import { Driver } from '../../domain/entities/Driver';
export interface CompleteDriverOnboardingInput {
userId: string;
firstName: string;
lastName: string;
displayName: string;
country: string;
timezone?: string;
bio?: string;
}
/**
* Use Case for completing driver onboarding.
*/
export class CompleteDriverOnboardingUseCase
implements UseCase<CompleteDriverOnboardingInput, CompleteDriverOnboardingResultDTO, any, ICompleteDriverOnboardingPresenter>
{
constructor(private readonly driverRepository: IDriverRepository) {}
async execute(input: CompleteDriverOnboardingInput, presenter: ICompleteDriverOnboardingPresenter): Promise<void> {
presenter.reset();
try {
// Check if driver already exists
const existing = await this.driverRepository.findById(input.userId);
if (existing) {
presenter.present({
success: false,
errorMessage: 'Driver already exists',
});
return;
}
// Create new driver
const driver = Driver.create({
id: input.userId,
iracingId: input.userId, // Assuming userId is iracingId for now
name: input.displayName,
country: input.country,
bio: input.bio,
});
await this.driverRepository.save(driver);
presenter.present({
success: true,
driverId: driver.id,
});
} catch (error) {
presenter.present({
success: false,
errorMessage: error instanceof Error ? error.message : 'Unknown error',
});
}
}
}

View File

@@ -57,6 +57,7 @@ export class CreateLeagueWithSeasonAndScoringUseCase
private readonly seasonRepository: ISeasonRepository,
private readonly leagueScoringConfigRepository: ILeagueScoringConfigRepository,
private readonly presetProvider: LeagueScoringPresetProvider,
private readonly logger: Logger,
) {}
async execute(
@@ -113,31 +114,8 @@ export class CreateLeagueWithSeasonAndScoringUseCase
this.logger.info(`Scoring preset ${preset.name} (${preset.id}) retrieved.`);
const scoringConfig: LeagueScoringConfig = {
id: uuidv4(),
seasonId,
scoringPresetId: preset.id,
championships: [],
};
const fullConfigFactory = (await import(
'../../infrastructure/repositories/InMemoryScoringRepositories'
)) as typeof import('../../infrastructure/repositories/InMemoryScoringRepositories');
const presetFromInfra = fullConfigFactory.getLeagueScoringPresetById(
preset.id,
);
if (!presetFromInfra) {
this.logger.error(`Preset registry missing preset: ${preset.id}`);
throw new Error(`Preset registry missing preset: ${preset.id}`);
}
this.logger.debug(`Preset from infrastructure retrieved for ${preset.id}.`);
const infraConfig = presetFromInfra.createConfig({ seasonId });
const finalConfig: LeagueScoringConfig = {
...infraConfig,
scoringPresetId: preset.id,
};
const finalConfig = this.presetProvider.createScoringConfigFromPreset(preset.id, seasonId);
this.logger.debug(`Scoring configuration created from preset ${preset.id}.`);
await this.leagueScoringConfigRepository.save(finalConfig);
this.logger.info(`Scoring configuration saved for season ${seasonId}.`);

View File

@@ -0,0 +1,42 @@
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import type { IGetLeagueAdminPermissionsPresenter, GetLeagueAdminPermissionsResultDTO, GetLeagueAdminPermissionsViewModel } from '../presenters/IGetLeagueAdminPermissionsPresenter';
import type { UseCase } from '@gridpilot/shared/application/UseCase';
export interface GetLeagueAdminPermissionsUseCaseParams {
leagueId: string;
performerDriverId: string;
}
export interface GetLeagueAdminPermissionsResultDTO {
canRemoveMember: boolean;
canUpdateRoles: boolean;
}
export class GetLeagueAdminPermissionsUseCase implements UseCase<GetLeagueAdminPermissionsUseCaseParams, GetLeagueAdminPermissionsResultDTO, GetLeagueAdminPermissionsViewModel, IGetLeagueAdminPermissionsPresenter> {
constructor(
private readonly leagueRepository: ILeagueRepository,
private readonly leagueMembershipRepository: ILeagueMembershipRepository,
) {}
async execute(params: GetLeagueAdminPermissionsUseCaseParams, presenter: IGetLeagueAdminPermissionsPresenter): Promise<void> {
const league = await this.leagueRepository.findById(params.leagueId);
if (!league) {
presenter.present({ canRemoveMember: false, canUpdateRoles: false });
return;
}
const membership = await this.leagueMembershipRepository.getMembership(params.leagueId, params.performerDriverId);
if (!membership || membership.status !== 'active') {
presenter.present({ canRemoveMember: false, canUpdateRoles: false });
return;
}
// Business logic: owners and admins can remove members and update roles
const canRemoveMember = membership.role === 'owner' || membership.role === 'admin';
const canUpdateRoles = membership.role === 'owner' || membership.role === 'admin';
presenter.reset();
presenter.present({ canRemoveMember, canUpdateRoles });
}
}

View File

@@ -0,0 +1,33 @@
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
import type { IGetLeagueJoinRequestsPresenter, GetLeagueJoinRequestsResultDTO, GetLeagueJoinRequestsViewModel } from '../presenters/IGetLeagueJoinRequestsPresenter';
import type { UseCase } from '@gridpilot/shared/application/UseCase';
export interface GetLeagueJoinRequestsUseCaseParams {
leagueId: string;
}
export interface GetLeagueJoinRequestsResultDTO {
joinRequests: any[];
drivers: { id: string; name: string }[];
}
export class GetLeagueJoinRequestsUseCase implements UseCase<GetLeagueJoinRequestsUseCaseParams, GetLeagueJoinRequestsResultDTO, GetLeagueJoinRequestsViewModel, IGetLeagueJoinRequestsPresenter> {
constructor(
private readonly leagueMembershipRepository: ILeagueMembershipRepository,
private readonly driverRepository: IDriverRepository,
) {}
async execute(params: GetLeagueJoinRequestsUseCaseParams, presenter: IGetLeagueJoinRequestsPresenter): Promise<void> {
const joinRequests = await this.leagueMembershipRepository.getJoinRequests(params.leagueId);
const driverIds = joinRequests.map(r => r.driverId);
const drivers = await this.driverRepository.findByIds(driverIds);
const driverMap = new Map(drivers.map(d => [d.id, { id: d.id, name: d.name }]));
const dto: GetLeagueJoinRequestsResultDTO = {
joinRequests,
drivers: Array.from(driverMap.values()),
};
presenter.reset();
presenter.present(dto);
}
}

View File

@@ -0,0 +1,41 @@
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
import type { LeagueMembership } from '../../domain/entities/LeagueMembership';
import type { IGetLeagueMembershipsPresenter, GetLeagueMembershipsViewModel } from '../presenters/IGetLeagueMembershipsPresenter';
import type { UseCase } from '@gridpilot/shared/application/UseCase';
export interface GetLeagueMembershipsUseCaseParams {
leagueId: string;
}
export interface GetLeagueMembershipsResultDTO {
memberships: LeagueMembership[];
drivers: { id: string; name: string }[];
}
export class GetLeagueMembershipsUseCase implements UseCase<GetLeagueMembershipsUseCaseParams, GetLeagueMembershipsResultDTO, GetLeagueMembershipsViewModel, IGetLeagueMembershipsPresenter> {
constructor(
private readonly leagueMembershipRepository: ILeagueMembershipRepository,
private readonly driverRepository: IDriverRepository,
) {}
async execute(params: GetLeagueMembershipsUseCaseParams, presenter: IGetLeagueMembershipsPresenter): Promise<void> {
const memberships = await this.leagueMembershipRepository.getLeagueMembers(params.leagueId);
const drivers: { id: string; name: string }[] = [];
// Get driver details for each membership
for (const membership of memberships) {
const driver = await this.driverRepository.findById(membership.driverId);
if (driver) {
drivers.push({ id: driver.id, name: driver.name });
}
}
const dto: GetLeagueMembershipsResultDTO = {
memberships,
drivers,
};
presenter.reset();
presenter.present(dto);
}
}

View File

@@ -0,0 +1,23 @@
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
import type { IGetLeagueOwnerSummaryPresenter, GetLeagueOwnerSummaryResultDTO, GetLeagueOwnerSummaryViewModel } from '../presenters/IGetLeagueOwnerSummaryPresenter';
import type { UseCase } from '@gridpilot/shared/application/UseCase';
export interface GetLeagueOwnerSummaryUseCaseParams {
ownerId: string;
}
export interface GetLeagueOwnerSummaryResultDTO {
summary: { driver: { id: string; name: string }; rating: number; rank: number } | null;
}
export class GetLeagueOwnerSummaryUseCase implements UseCase<GetLeagueOwnerSummaryUseCaseParams, GetLeagueOwnerSummaryResultDTO, GetLeagueOwnerSummaryViewModel, IGetLeagueOwnerSummaryPresenter> {
constructor(private readonly driverRepository: IDriverRepository) {}
async execute(params: GetLeagueOwnerSummaryUseCaseParams, presenter: IGetLeagueOwnerSummaryPresenter): Promise<void> {
const driver = await this.driverRepository.findById(params.ownerId);
const summary = driver ? { driver: { id: driver.id, name: driver.name }, rating: 0, rank: 0 } : null;
const dto: GetLeagueOwnerSummaryResultDTO = { summary };
presenter.reset();
presenter.present(dto);
}
}

View File

@@ -0,0 +1,58 @@
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
import type { IProtestRepository } from '../../domain/repositories/IProtestRepository';
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
import type { IGetLeagueProtestsPresenter, GetLeagueProtestsResultDTO, GetLeagueProtestsViewModel } from '../presenters/IGetLeagueProtestsPresenter';
import type { UseCase } from '@gridpilot/shared/application/UseCase';
export interface GetLeagueProtestsUseCaseParams {
leagueId: string;
}
export interface GetLeagueProtestsResultDTO {
protests: any[];
races: any[];
drivers: { id: string; name: string }[];
}
export class GetLeagueProtestsUseCase implements UseCase<GetLeagueProtestsUseCaseParams, GetLeagueProtestsResultDTO, GetLeagueProtestsViewModel, IGetLeagueProtestsPresenter> {
constructor(
private readonly raceRepository: IRaceRepository,
private readonly protestRepository: IProtestRepository,
private readonly driverRepository: IDriverRepository,
) {}
async execute(params: GetLeagueProtestsUseCaseParams, presenter: IGetLeagueProtestsPresenter): Promise<void> {
const races = await this.raceRepository.findByLeagueId(params.leagueId);
const protests = [];
const raceMap = new Map();
const driverIds = new Set<string>();
for (const race of races) {
raceMap.set(race.id, { id: race.id, name: race.name, date: race.scheduledAt.toISOString() });
const raceProtests = await this.protestRepository.findByRaceId(race.id);
for (const protest of raceProtests) {
protests.push({
id: protest.id,
raceId: protest.raceId,
protestingDriverId: protest.protestingDriverId,
accusedDriverId: protest.accusedDriverId,
submittedAt: protest.filedAt,
description: protest.comment || '',
status: protest.status,
});
driverIds.add(protest.protestingDriverId);
driverIds.add(protest.accusedDriverId);
}
}
const drivers = await this.driverRepository.findByIds(Array.from(driverIds));
const driverMap = new Map(drivers.map(d => [d.id, { id: d.id, name: d.name }]));
const dto: GetLeagueProtestsResultDTO = {
protests,
races: Array.from(raceMap.values()),
drivers: Array.from(driverMap.values()),
};
presenter.reset();
presenter.present(dto);
}
}

View File

@@ -0,0 +1,32 @@
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
import type { IGetLeagueSchedulePresenter, GetLeagueScheduleResultDTO, GetLeagueScheduleViewModel } from '../presenters/IGetLeagueSchedulePresenter';
import type { UseCase } from '@gridpilot/shared/application/UseCase';
export interface GetLeagueScheduleUseCaseParams {
leagueId: string;
}
export interface GetLeagueScheduleResultDTO {
races: Array<{
id: string;
name: string;
scheduledAt: Date;
}>;
}
export class GetLeagueScheduleUseCase implements UseCase<GetLeagueScheduleUseCaseParams, GetLeagueScheduleResultDTO, GetLeagueScheduleViewModel, IGetLeagueSchedulePresenter> {
constructor(private readonly raceRepository: IRaceRepository) {}
async execute(params: GetLeagueScheduleUseCaseParams, presenter: IGetLeagueSchedulePresenter): Promise<void> {
const races = await this.raceRepository.findByLeagueId(params.leagueId);
const dto: GetLeagueScheduleResultDTO = {
races: races.map(race => ({
id: race.id,
name: `${race.track} - ${race.car}`,
scheduledAt: race.scheduledAt,
})),
};
presenter.reset();
presenter.present(dto);
}
}

View File

@@ -0,0 +1,22 @@
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
import type { IGetLeagueSeasonsPresenter, GetLeagueSeasonsResultDTO, GetLeagueSeasonsViewModel } from '../presenters/IGetLeagueSeasonsPresenter';
import type { UseCase } from '@gridpilot/shared/application/UseCase';
export interface GetLeagueSeasonsUseCaseParams {
leagueId: string;
}
export interface GetLeagueSeasonsResultDTO {
seasons: any[];
}
export class GetLeagueSeasonsUseCase implements UseCase<GetLeagueSeasonsUseCaseParams, GetLeagueSeasonsResultDTO, GetLeagueSeasonsViewModel, IGetLeagueSeasonsPresenter> {
constructor(private readonly seasonRepository: ISeasonRepository) {}
async execute(params: GetLeagueSeasonsUseCaseParams, presenter: IGetLeagueSeasonsPresenter): Promise<void> {
const seasons = await this.seasonRepository.findByLeagueId(params.leagueId);
const dto: GetLeagueSeasonsResultDTO = { seasons };
presenter.reset();
presenter.present(dto);
}
}

View File

@@ -1,4 +1,5 @@
import type { IStandingRepository } from '../../domain/repositories/IStandingRepository';
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
import type {
ILeagueStandingsPresenter,
LeagueStandingsResultDTO,
@@ -18,15 +19,22 @@ export class GetLeagueStandingsUseCase
implements
UseCase<GetLeagueStandingsUseCaseParams, LeagueStandingsResultDTO, LeagueStandingsViewModel, ILeagueStandingsPresenter>
{
constructor(private readonly standingRepository: IStandingRepository) {}
constructor(
private readonly standingRepository: IStandingRepository,
private readonly driverRepository: IDriverRepository,
) {}
async execute(
params: GetLeagueStandingsUseCaseParams,
presenter: ILeagueStandingsPresenter,
): Promise<void> {
const standings = await this.standingRepository.findByLeagueId(params.leagueId);
const driverIds = [...new Set(standings.map(s => s.driverId))];
const drivers = await this.driverRepository.findByIds(driverIds);
const driverMap = new Map(drivers.map(d => [d.id, { id: d.id, name: d.name }]));
const dto: LeagueStandingsResultDTO = {
standings,
drivers: Array.from(driverMap.values()),
};
presenter.reset();
presenter.present(dto);

View File

@@ -1,109 +1,28 @@
/**
* Use Case for retrieving league statistics.
* Orchestrates domain logic and delegates presentation to the presenter.
*/
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
import type { IResultRepository } from '../../domain/repositories/IRaceRepository';
import type { DriverRatingProvider } from '../ports/DriverRatingProvider';
import type { ILeagueStatsPresenter } from '../presenters/ILeagueStatsPresenter';
import type { AsyncUseCase } from '@gridpilot/shared/application';
import { Logger } from "@gridpilot/core/shared/application";
import {
AverageStrengthOfFieldCalculator,
type StrengthOfFieldCalculator,
} from '../../domain/services/StrengthOfFieldCalculator';
import type { ILeagueStatsPresenter, LeagueStatsResultDTO, LeagueStatsViewModel } from '../presenters/ILeagueStatsPresenter';
import type { UseCase } from '@gridpilot/shared/application/UseCase';
export interface GetLeagueStatsUseCaseParams {
leagueId: string;
}
/**
* Use Case for retrieving league statistics including average SOF across completed races.
*/
export class GetLeagueStatsUseCase
implements AsyncUseCase<GetLeagueStatsUseCaseParams, void> {
private readonly sofCalculator: StrengthOfFieldCalculator;
export class GetLeagueStatsUseCase implements UseCase<GetLeagueStatsUseCaseParams, LeagueStatsResultDTO, LeagueStatsViewModel, ILeagueStatsPresenter> {
constructor(
private readonly leagueRepository: ILeagueRepository,
private readonly leagueMembershipRepository: ILeagueMembershipRepository,
private readonly raceRepository: IRaceRepository,
private readonly resultRepository: IResultRepository,
private readonly driverRatingProvider: DriverRatingProvider,
public readonly presenter: ILeagueStatsPresenter,
private readonly logger: Logger,
sofCalculator?: StrengthOfFieldCalculator,
) {
this.sofCalculator = sofCalculator ?? new AverageStrengthOfFieldCalculator();
}
) {}
async execute(params: GetLeagueStatsUseCaseParams): Promise<void> {
this.logger.debug(
`Executing GetLeagueStatsUseCase with params: ${JSON.stringify(params)}`,
);
const { leagueId } = params;
try {
const league = await this.leagueRepository.findById(leagueId);
if (!league) {
this.logger.error(`League ${leagueId} not found`);
throw new Error(`League ${leagueId} not found`);
}
const races = await this.raceRepository.findByLeagueId(leagueId);
const completedRaces = races.filter(r => r.status === 'completed');
const scheduledRaces = races.filter(r => r.status === 'scheduled');
this.logger.info(
`Found ${races.length} races for league ${leagueId}: ${completedRaces.length} completed, ${scheduledRaces.length} scheduled. `,
);
// Calculate SOF for each completed race
const sofValues: number[] = [];
for (const race of completedRaces) {
// Use stored SOF if available
if (race.strengthOfField) {
this.logger.debug(
`Using stored Strength of Field for race ${race.id}: ${race.strengthOfField}`,
);
sofValues.push(race.strengthOfField);
continue;
}
// Otherwise calculate from results
const results = await this.resultRepository.findByRaceId(race.id);
if (results.length === 0) {
this.logger.debug(`No results found for race ${race.id}. Skipping SOF calculation.`);
continue;
}
const driverIds = results.map(r => r.driverId);
const ratings = this.driverRatingProvider.getRatings(driverIds);
const driverRatings = driverIds
.filter(id => ratings.has(id))
.map(id => ({ driverId: id, rating: ratings.get(id)! }));
const sof = this.sofCalculator.calculate(driverRatings);
if (sof !== null) {
this.logger.debug(`Calculated Strength of Field for race ${race.id}: ${sof}`);
sofValues.push(sof);
} else {
this.logger.warn(`Could not calculate Strength of Field for race ${race.id}`);
}
}
this.presenter.present(
leagueId,
races.length,
completedRaces.length,
scheduledRaces.length,
sofValues,
);
this.logger.info(`Successfully presented league statistics for league ${leagueId}.`);
} catch (error) {
this.logger.error(`Error in GetLeagueStatsUseCase: ${error.message}`);
throw error;
}
async execute(params: GetLeagueStatsUseCaseParams, presenter: ILeagueStatsPresenter): Promise<void> {
const memberships = await this.leagueMembershipRepository.getLeagueMembers(params.leagueId);
const races = await this.raceRepository.findByLeagueId(params.leagueId);
// TODO: Implement average rating calculation from driver ratings
const dto: LeagueStatsResultDTO = {
totalMembers: memberships.length,
totalRaces: races.length,
averageRating: 0,
};
presenter.reset();
presenter.present(dto);
}
}

View File

@@ -0,0 +1,23 @@
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
import type { ITotalDriversPresenter, TotalDriversResultDTO } from '../presenters/ITotalDriversPresenter';
import type { UseCase } from '@gridpilot/shared/application/UseCase';
/**
* Use Case for retrieving total number of drivers.
*/
export class GetTotalDriversUseCase
implements UseCase<void, TotalDriversResultDTO, any, ITotalDriversPresenter>
{
constructor(private readonly driverRepository: IDriverRepository) {}
async execute(_input: void, presenter: ITotalDriversPresenter): Promise<void> {
presenter.reset();
const drivers = await this.driverRepository.findAll();
const dto: TotalDriversResultDTO = {
totalDrivers: drivers.length,
};
presenter.present(dto);
}
}

View File

@@ -0,0 +1,20 @@
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
import type { IGetTotalLeaguesPresenter, GetTotalLeaguesResultDTO, GetTotalLeaguesViewModel } from '../presenters/IGetTotalLeaguesPresenter';
import type { UseCase } from '@gridpilot/shared/application/UseCase';
export interface GetTotalLeaguesUseCaseParams {}
export interface GetTotalLeaguesResultDTO {
totalLeagues: number;
}
export class GetTotalLeaguesUseCase implements UseCase<GetTotalLeaguesUseCaseParams, GetTotalLeaguesResultDTO, GetTotalLeaguesViewModel, IGetTotalLeaguesPresenter> {
constructor(private readonly leagueRepository: ILeagueRepository) {}
async execute(params: GetTotalLeaguesUseCaseParams, presenter: IGetTotalLeaguesPresenter): Promise<void> {
const leagues = await this.leagueRepository.findAll();
const dto: GetTotalLeaguesResultDTO = { totalLeagues: leagues.length };
presenter.reset();
presenter.present(dto);
}
}

View File

@@ -1,6 +1,7 @@
import type { IRaceRegistrationRepository } from '@gridpilot/racing/domain/repositories/IRaceRegistrationRepository';
import type { IRaceRegistrationRepository } from '../../domain/repositories/IRaceRegistrationRepository';
import type { IsDriverRegisteredForRaceQueryParamsDTO } from '../dto/RaceRegistrationQueryDTO';
import type { IDriverRegistrationStatusPresenter } from '../presenters/IDriverRegistrationStatusPresenter';
import type { UseCase } from '@gridpilot/shared/application/UseCase';
/**
* Use Case: IsDriverRegisteredForRaceUseCase
@@ -8,15 +9,15 @@ import type { IDriverRegistrationStatusPresenter } from '../presenters/IDriverRe
* Checks if a driver is registered for a specific race.
* Orchestrates domain logic and delegates presentation to the presenter.
*/
export class IsDriverRegisteredForRaceUseCase {
constructor(
private readonly registrationRepository: IRaceRegistrationRepository,
public readonly presenter: IDriverRegistrationStatusPresenter,
) {}
export class IsDriverRegisteredForRaceUseCase
implements UseCase<IsDriverRegisteredForRaceQueryParamsDTO, boolean, any, IDriverRegistrationStatusPresenter>
{
constructor(private readonly registrationRepository: IRaceRegistrationRepository) {}
async execute(params: IsDriverRegisteredForRaceQueryParamsDTO): Promise<void> {
async execute(params: IsDriverRegisteredForRaceQueryParamsDTO, presenter: IDriverRegistrationStatusPresenter): Promise<void> {
presenter.reset();
const { raceId, driverId } = params;
const isRegistered = await this.registrationRepository.isRegistered(raceId, driverId);
this.presenter.present(isRegistered, raceId, driverId);
presenter.present(isRegistered, raceId, driverId);
}
}

View File

@@ -0,0 +1,23 @@
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import type { IRejectLeagueJoinRequestPresenter, RejectLeagueJoinRequestResultDTO, RejectLeagueJoinRequestViewModel } from '../presenters/IRejectLeagueJoinRequestPresenter';
import type { UseCase } from '@gridpilot/shared/application/UseCase';
export interface RejectLeagueJoinRequestUseCaseParams {
requestId: string;
}
export interface RejectLeagueJoinRequestResultDTO {
success: boolean;
message: string;
}
export class RejectLeagueJoinRequestUseCase implements UseCase<RejectLeagueJoinRequestUseCaseParams, RejectLeagueJoinRequestResultDTO, RejectLeagueJoinRequestViewModel, IRejectLeagueJoinRequestPresenter> {
constructor(private readonly leagueMembershipRepository: ILeagueMembershipRepository) {}
async execute(params: RejectLeagueJoinRequestUseCaseParams, presenter: IRejectLeagueJoinRequestPresenter): Promise<void> {
await this.leagueMembershipRepository.removeJoinRequest(params.requestId);
const dto: RejectLeagueJoinRequestResultDTO = { success: true, message: 'Join request rejected.' };
presenter.reset();
presenter.present(dto);
}
}

View File

@@ -0,0 +1,31 @@
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import type { IRemoveLeagueMemberPresenter, RemoveLeagueMemberResultDTO, RemoveLeagueMemberViewModel } from '../presenters/IRemoveLeagueMemberPresenter';
import type { UseCase } from '@gridpilot/shared/application/UseCase';
export interface RemoveLeagueMemberUseCaseParams {
leagueId: string;
targetDriverId: string;
}
export interface RemoveLeagueMemberResultDTO {
success: boolean;
}
export class RemoveLeagueMemberUseCase implements UseCase<RemoveLeagueMemberUseCaseParams, RemoveLeagueMemberResultDTO, RemoveLeagueMemberViewModel, IRemoveLeagueMemberPresenter> {
constructor(private readonly leagueMembershipRepository: ILeagueMembershipRepository) {}
async execute(params: RemoveLeagueMemberUseCaseParams, presenter: IRemoveLeagueMemberPresenter): Promise<void> {
const memberships = await this.leagueMembershipRepository.getLeagueMembers(params.leagueId);
const membership = memberships.find(m => m.driverId === params.targetDriverId);
if (!membership) {
throw new Error('Membership not found');
}
await this.leagueMembershipRepository.saveMembership({
...membership,
status: 'inactive',
});
const dto: RemoveLeagueMemberResultDTO = { success: true };
presenter.reset();
presenter.present(dto);
}
}

View File

@@ -0,0 +1,32 @@
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import type { IUpdateLeagueMemberRolePresenter, UpdateLeagueMemberRoleResultDTO, UpdateLeagueMemberRoleViewModel } from '../presenters/IUpdateLeagueMemberRolePresenter';
import type { UseCase } from '@gridpilot/shared/application/UseCase';
export interface UpdateLeagueMemberRoleUseCaseParams {
leagueId: string;
targetDriverId: string;
newRole: string;
}
export interface UpdateLeagueMemberRoleResultDTO {
success: boolean;
}
export class UpdateLeagueMemberRoleUseCase implements UseCase<UpdateLeagueMemberRoleUseCaseParams, UpdateLeagueMemberRoleResultDTO, UpdateLeagueMemberRoleViewModel, IUpdateLeagueMemberRolePresenter> {
constructor(private readonly leagueMembershipRepository: ILeagueMembershipRepository) {}
async execute(params: UpdateLeagueMemberRoleUseCaseParams, presenter: IUpdateLeagueMemberRolePresenter): Promise<void> {
const memberships = await this.leagueMembershipRepository.getLeagueMembers(params.leagueId);
const membership = memberships.find(m => m.driverId === params.targetDriverId);
if (!membership) {
throw new Error('Membership not found');
}
await this.leagueMembershipRepository.saveMembership({
...membership,
role: params.newRole,
});
const dto: UpdateLeagueMemberRoleResultDTO = { success: true };
presenter.reset();
presenter.present(dto);
}
}