This commit is contained in:
2025-12-10 18:28:32 +01:00
parent 6d61be9c51
commit 1303a14493
108 changed files with 3366 additions and 1559 deletions

View File

@@ -88,48 +88,77 @@ import {
JoinLeagueUseCase,
RegisterForRaceUseCase,
WithdrawFromRaceUseCase,
IsDriverRegisteredForRaceQuery,
GetRaceRegistrationsQuery,
CreateTeamUseCase,
JoinTeamUseCase,
LeaveTeamUseCase,
ApproveTeamJoinRequestUseCase,
RejectTeamJoinRequestUseCase,
UpdateTeamUseCase,
GetAllTeamsQuery,
GetTeamDetailsQuery,
GetTeamMembersQuery,
GetTeamJoinRequestsQuery,
GetDriverTeamQuery,
GetLeagueStandingsQuery,
GetLeagueDriverSeasonStatsQuery,
GetAllLeaguesWithCapacityQuery,
GetAllLeaguesWithCapacityAndScoringQuery,
ListLeagueScoringPresetsQuery,
GetLeagueScoringConfigQuery,
GetAllTeamsUseCase,
GetTeamDetailsUseCase,
GetTeamMembersUseCase,
GetTeamJoinRequestsUseCase,
GetDriverTeamUseCase,
CreateLeagueWithSeasonAndScoringUseCase,
GetLeagueFullConfigQuery,
GetRaceWithSOFQuery,
GetLeagueStatsQuery,
FileProtestUseCase,
ReviewProtestUseCase,
ApplyPenaltyUseCase,
GetRaceProtestsQuery,
GetRacePenaltiesQuery,
RequestProtestDefenseUseCase,
SubmitProtestDefenseUseCase,
GetSponsorDashboardQuery,
GetSponsorSponsorshipsQuery,
GetPendingSponsorshipRequestsQuery,
GetEntitySponsorshipPricingQuery,
GetSponsorDashboardUseCase,
GetSponsorSponsorshipsUseCase,
GetPendingSponsorshipRequestsUseCase,
GetEntitySponsorshipPricingUseCase,
ApplyForSponsorshipUseCase,
AcceptSponsorshipRequestUseCase,
RejectSponsorshipRequestUseCase,
} from '@gridpilot/racing/application';
import { IsDriverRegisteredForRaceUseCase } from '@gridpilot/racing/application/use-cases/IsDriverRegisteredForRaceQuery';
import { GetRaceRegistrationsUseCase } from '@gridpilot/racing/application/use-cases/GetRaceRegistrationsQuery';
import { GetRaceWithSOFUseCase } from '@gridpilot/racing/application/use-cases/GetRaceWithSOFQuery';
import { GetRaceProtestsUseCase } from '@gridpilot/racing/application/use-cases/GetRaceProtestsQuery';
import { GetRacePenaltiesUseCase } from '@gridpilot/racing/application/use-cases/GetRacePenaltiesQuery';
import { GetLeagueStandingsUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueStandingsQuery';
import { GetLeagueDriverSeasonStatsUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueDriverSeasonStatsQuery';
import { GetAllLeaguesWithCapacityUseCase } from '@gridpilot/racing/application/use-cases/GetAllLeaguesWithCapacityQuery';
import { GetAllLeaguesWithCapacityAndScoringUseCase } from '@gridpilot/racing/application/use-cases/GetAllLeaguesWithCapacityAndScoringQuery';
import { ListLeagueScoringPresetsUseCase } from '@gridpilot/racing/application/use-cases/ListLeagueScoringPresetsQuery';
import { GetLeagueScoringConfigUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueScoringConfigQuery';
import { GetLeagueFullConfigUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueFullConfigQuery';
import { GetLeagueStatsUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueStatsQuery';
import { GetRacesPageDataUseCase } from '@gridpilot/racing/application/use-cases/GetRacesPageDataUseCase';
import { GetDriversLeaderboardUseCase } from '@gridpilot/racing/application/use-cases/GetDriversLeaderboardUseCase';
import { GetTeamsLeaderboardUseCase } from '@gridpilot/racing/application/use-cases/GetTeamsLeaderboardUseCase';
import { TransferLeagueOwnershipUseCase } from '@gridpilot/racing/application/use-cases/TransferLeagueOwnershipUseCase';
import { DriversLeaderboardPresenter } from '../../lib/presenters/DriversLeaderboardPresenter';
import { TeamsLeaderboardPresenter } from '../../lib/presenters/TeamsLeaderboardPresenter';
import { RacesPagePresenter } from '../../lib/presenters/RacesPagePresenter';
import { AllTeamsPresenter } from '../../lib/presenters/AllTeamsPresenter';
import { TeamDetailsPresenter } from '../../lib/presenters/TeamDetailsPresenter';
import { TeamMembersPresenter } from '../../lib/presenters/TeamMembersPresenter';
import { TeamJoinRequestsPresenter } from '../../lib/presenters/TeamJoinRequestsPresenter';
import { DriverTeamPresenter } from '../../lib/presenters/DriverTeamPresenter';
import { AllLeaguesWithCapacityPresenter } from '../../lib/presenters/AllLeaguesWithCapacityPresenter';
import { AllLeaguesWithCapacityAndScoringPresenter } from '../../lib/presenters/AllLeaguesWithCapacityAndScoringPresenter';
import { LeagueStatsPresenter } from '../../lib/presenters/LeagueStatsPresenter';
import { LeagueScoringConfigPresenter } from '../../lib/presenters/LeagueScoringConfigPresenter';
import { LeagueFullConfigPresenter } from '../../lib/presenters/LeagueFullConfigPresenter';
import { LeagueDriverSeasonStatsPresenter } from '../../lib/presenters/LeagueDriverSeasonStatsPresenter';
import { LeagueStandingsPresenter } from '../../lib/presenters/LeagueStandingsPresenter';
import { LeagueScoringPresetsPresenter } from '../../lib/presenters/LeagueScoringPresetsPresenter';
import { RaceWithSOFPresenter } from '../../lib/presenters/RaceWithSOFPresenter';
import { RaceProtestsPresenter } from '../../lib/presenters/RaceProtestsPresenter';
import { RacePenaltiesPresenter } from '../../lib/presenters/RacePenaltiesPresenter';
import { RaceRegistrationsPresenter } from '../../lib/presenters/RaceRegistrationsPresenter';
import { DriverRegistrationStatusPresenter } from '../../lib/presenters/DriverRegistrationStatusPresenter';
import type { DriverRatingProvider } from '@gridpilot/racing/application';
import type { LeagueScoringPresetProvider } from '@gridpilot/racing/application/ports/LeagueScoringPresetProvider';
import { PreviewLeagueScheduleQuery } from '@gridpilot/racing/application';
import { PreviewLeagueScheduleUseCase } from '@gridpilot/racing/application';
import { SponsorDashboardPresenter } from '../../lib/presenters/SponsorDashboardPresenter';
import { SponsorSponsorshipsPresenter } from '../../lib/presenters/SponsorSponsorshipsPresenter';
import { PendingSponsorshipRequestsPresenter } from '../../lib/presenters/PendingSponsorshipRequestsPresenter';
import { EntitySponsorshipPricingPresenter } from '../../lib/presenters/EntitySponsorshipPricingPresenter';
import { LeagueSchedulePreviewPresenter } from '../../lib/presenters/LeagueSchedulePreviewPresenter';
// Testing support
import {
@@ -840,24 +869,28 @@ export function configureDIContainer(): void {
);
// Register queries - Racing
const driverRegistrationStatusPresenter = new DriverRegistrationStatusPresenter();
container.registerInstance(
DI_TOKENS.IsDriverRegisteredForRaceQuery,
new IsDriverRegisteredForRaceQuery(raceRegistrationRepository)
DI_TOKENS.IsDriverRegisteredForRaceUseCase,
new IsDriverRegisteredForRaceUseCase(raceRegistrationRepository, driverRegistrationStatusPresenter)
);
const raceRegistrationsPresenter = new RaceRegistrationsPresenter();
container.registerInstance(
DI_TOKENS.GetRaceRegistrationsQuery,
new GetRaceRegistrationsQuery(raceRegistrationRepository)
DI_TOKENS.GetRaceRegistrationsUseCase,
new GetRaceRegistrationsUseCase(raceRegistrationRepository, raceRegistrationsPresenter)
);
const leagueStandingsPresenter = new LeagueStandingsPresenter();
container.registerInstance(
DI_TOKENS.GetLeagueStandingsQuery,
new GetLeagueStandingsQuery(standingRepository)
DI_TOKENS.GetLeagueStandingsUseCase,
new GetLeagueStandingsUseCase(standingRepository, leagueStandingsPresenter)
);
const leagueDriverSeasonStatsPresenter = new LeagueDriverSeasonStatsPresenter();
container.registerInstance(
DI_TOKENS.GetLeagueDriverSeasonStatsQuery,
new GetLeagueDriverSeasonStatsQuery(
DI_TOKENS.GetLeagueDriverSeasonStatsUseCase,
new GetLeagueDriverSeasonStatsUseCase(
standingRepository,
resultRepository,
penaltyRepository,
@@ -875,113 +908,200 @@ export function configureDIContainer(): void {
ratingChange: delta !== 0 ? delta : null,
};
},
}
},
leagueDriverSeasonStatsPresenter
)
);
const allLeaguesWithCapacityPresenter = new AllLeaguesWithCapacityPresenter();
container.registerInstance(
DI_TOKENS.GetAllLeaguesWithCapacityQuery,
new GetAllLeaguesWithCapacityQuery(leagueRepository, leagueMembershipRepository)
DI_TOKENS.GetAllLeaguesWithCapacityUseCase,
new GetAllLeaguesWithCapacityUseCase(
leagueRepository,
leagueMembershipRepository,
allLeaguesWithCapacityPresenter
)
);
const allLeaguesWithCapacityAndScoringPresenter = new AllLeaguesWithCapacityAndScoringPresenter();
container.registerInstance(
DI_TOKENS.GetAllLeaguesWithCapacityAndScoringQuery,
new GetAllLeaguesWithCapacityAndScoringQuery(
DI_TOKENS.GetAllLeaguesWithCapacityAndScoringUseCase,
new GetAllLeaguesWithCapacityAndScoringUseCase(
leagueRepository,
leagueMembershipRepository,
seasonRepository,
leagueScoringConfigRepository,
gameRepository,
leagueScoringPresetProvider
leagueScoringPresetProvider,
allLeaguesWithCapacityAndScoringPresenter
)
);
const leagueScoringPresetsPresenter = new LeagueScoringPresetsPresenter();
container.registerInstance(
DI_TOKENS.ListLeagueScoringPresetsQuery,
new ListLeagueScoringPresetsQuery(leagueScoringPresetProvider)
DI_TOKENS.ListLeagueScoringPresetsUseCase,
new ListLeagueScoringPresetsUseCase(leagueScoringPresetProvider, leagueScoringPresetsPresenter)
);
const leagueScoringConfigPresenter = new LeagueScoringConfigPresenter();
container.registerInstance(
DI_TOKENS.GetLeagueScoringConfigQuery,
new GetLeagueScoringConfigQuery(
DI_TOKENS.GetLeagueScoringConfigUseCase,
new GetLeagueScoringConfigUseCase(
leagueRepository,
seasonRepository,
leagueScoringConfigRepository,
gameRepository,
leagueScoringPresetProvider
leagueScoringPresetProvider,
leagueScoringConfigPresenter
)
);
const leagueFullConfigPresenter = new LeagueFullConfigPresenter();
container.registerInstance(
DI_TOKENS.GetLeagueFullConfigQuery,
new GetLeagueFullConfigQuery(
DI_TOKENS.GetLeagueFullConfigUseCase,
new GetLeagueFullConfigUseCase(
leagueRepository,
seasonRepository,
leagueScoringConfigRepository,
gameRepository
gameRepository,
leagueFullConfigPresenter
)
);
const leagueSchedulePreviewPresenter = new LeagueSchedulePreviewPresenter();
container.registerInstance(
DI_TOKENS.PreviewLeagueScheduleQuery,
new PreviewLeagueScheduleQuery()
DI_TOKENS.PreviewLeagueScheduleUseCase,
new PreviewLeagueScheduleUseCase(undefined, leagueSchedulePreviewPresenter)
);
const raceWithSOFPresenter = new RaceWithSOFPresenter();
container.registerInstance(
DI_TOKENS.GetRaceWithSOFQuery,
new GetRaceWithSOFQuery(
DI_TOKENS.GetRaceWithSOFUseCase,
new GetRaceWithSOFUseCase(
raceRepository,
raceRegistrationRepository,
resultRepository,
driverRatingProvider
driverRatingProvider,
raceWithSOFPresenter
)
);
const leagueStatsPresenter = new LeagueStatsPresenter();
container.registerInstance(
DI_TOKENS.GetLeagueStatsQuery,
new GetLeagueStatsQuery(
DI_TOKENS.GetLeagueStatsUseCase,
new GetLeagueStatsUseCase(
leagueRepository,
raceRepository,
resultRepository,
driverRatingProvider
driverRatingProvider,
leagueStatsPresenter
)
);
// Register queries - Teams
const racesPresenter = new RacesPagePresenter();
container.registerInstance(
DI_TOKENS.GetAllTeamsQuery,
new GetAllTeamsQuery(teamRepository)
DI_TOKENS.GetRacesPageDataUseCase,
new GetRacesPageDataUseCase(raceRepository, leagueRepository, racesPresenter)
);
// Create services for driver leaderboard query
const rankingService = {
getAllDriverRankings: () => {
const stats = getDIContainer().resolve<Record<string, any>>(DI_TOKENS.DriverStats);
return Object.entries(stats).map(([driverId, stat]) => ({
driverId,
rating: stat.rating,
overallRank: stat.overallRank,
})).sort((a, b) => b.rating - a.rating);
}
};
const driverStatsService = {
getDriverStats: (driverId: string) => {
const stats = getDIContainer().resolve<Record<string, any>>(DI_TOKENS.DriverStats);
return stats[driverId] || null;
}
};
const imageService = getDIContainer().resolve<ImageServicePort>(DI_TOKENS.ImageService);
const driversPresenter = new DriversLeaderboardPresenter();
container.registerInstance(
DI_TOKENS.GetTeamDetailsQuery,
new GetTeamDetailsQuery(teamRepository, teamMembershipRepository)
DI_TOKENS.GetDriversLeaderboardUseCase,
new GetDriversLeaderboardUseCase(
driverRepository,
rankingService as any,
driverStatsService as any,
imageService,
driversPresenter
)
);
const getDriverStatsAdapter = (driverId: string) => {
const stats = getDIContainer().resolve<Record<string, any>>(DI_TOKENS.DriverStats);
const stat = stats[driverId];
if (!stat) return null;
return {
rating: stat.rating ?? null,
wins: stat.wins ?? 0,
totalRaces: stat.totalRaces ?? 0,
};
};
const teamsPresenter = new TeamsLeaderboardPresenter();
container.registerInstance(
DI_TOKENS.GetTeamMembersQuery,
new GetTeamMembersQuery(teamMembershipRepository)
DI_TOKENS.GetTeamsLeaderboardUseCase,
new GetTeamsLeaderboardUseCase(
teamRepository,
teamMembershipRepository,
driverRepository,
getDriverStatsAdapter,
teamsPresenter
)
);
// Register use cases - Teams (Query-like with Presenters)
const allTeamsPresenter = new AllTeamsPresenter();
container.registerInstance(
DI_TOKENS.GetTeamJoinRequestsQuery,
new GetTeamJoinRequestsQuery(teamMembershipRepository)
DI_TOKENS.GetAllTeamsUseCase,
new GetAllTeamsUseCase(teamRepository, teamMembershipRepository, allTeamsPresenter)
);
const teamDetailsPresenter = new TeamDetailsPresenter();
container.registerInstance(
DI_TOKENS.GetDriverTeamQuery,
new GetDriverTeamQuery(teamRepository, teamMembershipRepository)
DI_TOKENS.GetTeamDetailsUseCase,
new GetTeamDetailsUseCase(teamRepository, teamMembershipRepository, teamDetailsPresenter)
);
const teamMembersPresenter = new TeamMembersPresenter();
container.registerInstance(
DI_TOKENS.GetTeamMembersUseCase,
new GetTeamMembersUseCase(teamMembershipRepository, driverRepository, imageService, teamMembersPresenter)
);
const teamJoinRequestsPresenter = new TeamJoinRequestsPresenter();
container.registerInstance(
DI_TOKENS.GetTeamJoinRequestsUseCase,
new GetTeamJoinRequestsUseCase(teamMembershipRepository, driverRepository, imageService, teamJoinRequestsPresenter)
);
const driverTeamPresenter = new DriverTeamPresenter();
container.registerInstance(
DI_TOKENS.GetDriverTeamUseCase,
new GetDriverTeamUseCase(teamRepository, teamMembershipRepository, driverTeamPresenter)
);
// Register queries - Stewarding
const raceProtestsPresenter = new RaceProtestsPresenter();
container.registerInstance(
DI_TOKENS.GetRaceProtestsQuery,
new GetRaceProtestsQuery(protestRepository, driverRepository)
DI_TOKENS.GetRaceProtestsUseCase,
new GetRaceProtestsUseCase(protestRepository, driverRepository, raceProtestsPresenter)
);
const racePenaltiesPresenter = new RacePenaltiesPresenter();
container.registerInstance(
DI_TOKENS.GetRacePenaltiesQuery,
new GetRacePenaltiesQuery(penaltyRepository, driverRepository)
DI_TOKENS.GetRacePenaltiesUseCase,
new GetRacePenaltiesUseCase(penaltyRepository, driverRepository, racePenaltiesPresenter)
);
// Register queries - Notifications
@@ -990,31 +1110,35 @@ export function configureDIContainer(): void {
new GetUnreadNotificationsQuery(notificationRepository)
);
// Register queries - Sponsors
// Register use cases - Sponsors
const sponsorRepository = container.resolve<ISponsorRepository>(DI_TOKENS.SponsorRepository);
const seasonSponsorshipRepository = container.resolve<ISeasonSponsorshipRepository>(DI_TOKENS.SeasonSponsorshipRepository);
const sponsorDashboardPresenter = new SponsorDashboardPresenter();
container.registerInstance(
DI_TOKENS.GetSponsorDashboardQuery,
new GetSponsorDashboardQuery(
DI_TOKENS.GetSponsorDashboardUseCase,
new GetSponsorDashboardUseCase(
sponsorRepository,
seasonSponsorshipRepository,
seasonRepository,
leagueRepository,
leagueMembershipRepository,
raceRepository
raceRepository,
sponsorDashboardPresenter
)
);
const sponsorSponsorshipsPresenter = new SponsorSponsorshipsPresenter();
container.registerInstance(
DI_TOKENS.GetSponsorSponsorshipsQuery,
new GetSponsorSponsorshipsQuery(
DI_TOKENS.GetSponsorSponsorshipsUseCase,
new GetSponsorSponsorshipsUseCase(
sponsorRepository,
seasonSponsorshipRepository,
seasonRepository,
leagueRepository,
leagueMembershipRepository,
raceRepository
raceRepository,
sponsorSponsorshipsPresenter
)
);
@@ -1022,20 +1146,24 @@ export function configureDIContainer(): void {
const sponsorshipRequestRepository = container.resolve<ISponsorshipRequestRepository>(DI_TOKENS.SponsorshipRequestRepository);
const sponsorshipPricingRepository = container.resolve<ISponsorshipPricingRepository>(DI_TOKENS.SponsorshipPricingRepository);
const pendingSponsorshipRequestsPresenter = new PendingSponsorshipRequestsPresenter();
container.registerInstance(
DI_TOKENS.GetPendingSponsorshipRequestsQuery,
new GetPendingSponsorshipRequestsQuery(
DI_TOKENS.GetPendingSponsorshipRequestsUseCase,
new GetPendingSponsorshipRequestsUseCase(
sponsorshipRequestRepository,
sponsorRepository
sponsorRepository,
pendingSponsorshipRequestsPresenter
)
);
const entitySponsorshipPricingPresenter = new EntitySponsorshipPricingPresenter();
container.registerInstance(
DI_TOKENS.GetEntitySponsorshipPricingQuery,
new GetEntitySponsorshipPricingQuery(
DI_TOKENS.GetEntitySponsorshipPricingUseCase,
new GetEntitySponsorshipPricingUseCase(
sponsorshipPricingRepository,
sponsorshipRequestRepository,
seasonSponsorshipRepository
seasonSponsorshipRepository,
entitySponsorshipPricingPresenter
)
);

View File

@@ -41,51 +41,54 @@ import type {
JoinLeagueUseCase,
RegisterForRaceUseCase,
WithdrawFromRaceUseCase,
IsDriverRegisteredForRaceQuery,
GetRaceRegistrationsQuery,
CreateTeamUseCase,
JoinTeamUseCase,
LeaveTeamUseCase,
ApproveTeamJoinRequestUseCase,
RejectTeamJoinRequestUseCase,
UpdateTeamUseCase,
GetAllTeamsQuery,
GetTeamDetailsQuery,
GetTeamMembersQuery,
GetTeamJoinRequestsQuery,
GetDriverTeamQuery,
GetLeagueStandingsQuery,
GetLeagueDriverSeasonStatsQuery,
GetAllLeaguesWithCapacityQuery,
GetAllLeaguesWithCapacityAndScoringQuery,
ListLeagueScoringPresetsQuery,
GetLeagueScoringConfigQuery,
GetAllTeamsUseCase,
GetTeamDetailsUseCase,
GetTeamMembersUseCase,
GetTeamJoinRequestsUseCase,
GetDriverTeamUseCase,
CreateLeagueWithSeasonAndScoringUseCase,
GetLeagueFullConfigQuery,
GetRaceWithSOFQuery,
GetLeagueStatsQuery,
FileProtestUseCase,
ReviewProtestUseCase,
ApplyPenaltyUseCase,
GetRaceProtestsQuery,
GetRacePenaltiesQuery,
RequestProtestDefenseUseCase,
SubmitProtestDefenseUseCase,
GetSponsorDashboardQuery,
GetSponsorSponsorshipsQuery,
GetSponsorDashboardUseCase,
GetSponsorSponsorshipsUseCase,
ApplyForSponsorshipUseCase,
AcceptSponsorshipRequestUseCase,
RejectSponsorshipRequestUseCase,
GetPendingSponsorshipRequestsQuery,
GetEntitySponsorshipPricingQuery,
GetPendingSponsorshipRequestsUseCase,
GetEntitySponsorshipPricingUseCase,
} from '@gridpilot/racing/application';
import type { IsDriverRegisteredForRaceUseCase } from '@gridpilot/racing/application/use-cases/IsDriverRegisteredForRaceQuery';
import type { GetRaceRegistrationsUseCase } from '@gridpilot/racing/application/use-cases/GetRaceRegistrationsQuery';
import type { GetRaceWithSOFUseCase } from '@gridpilot/racing/application/use-cases/GetRaceWithSOFQuery';
import type { GetRaceProtestsUseCase } from '@gridpilot/racing/application/use-cases/GetRaceProtestsQuery';
import type { GetRacePenaltiesUseCase } from '@gridpilot/racing/application/use-cases/GetRacePenaltiesQuery';
import type { GetRacesPageDataUseCase } from '@gridpilot/racing/application/use-cases/GetRacesPageDataUseCase';
import type { GetDriversLeaderboardUseCase } from '@gridpilot/racing/application/use-cases/GetDriversLeaderboardUseCase';
import type { GetTeamsLeaderboardUseCase } from '@gridpilot/racing/application/use-cases/GetTeamsLeaderboardUseCase';
import type { GetLeagueStandingsUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueStandingsQuery';
import type { GetLeagueDriverSeasonStatsUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueDriverSeasonStatsQuery';
import type { GetAllLeaguesWithCapacityUseCase } from '@gridpilot/racing/application/use-cases/GetAllLeaguesWithCapacityQuery';
import type { GetAllLeaguesWithCapacityAndScoringUseCase } from '@gridpilot/racing/application/use-cases/GetAllLeaguesWithCapacityAndScoringQuery';
import type { ListLeagueScoringPresetsUseCase } from '@gridpilot/racing/application/use-cases/ListLeagueScoringPresetsQuery';
import type { GetLeagueScoringConfigUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueScoringConfigQuery';
import type { GetLeagueFullConfigUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueFullConfigQuery';
import type { GetLeagueStatsUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueStatsQuery';
import type { ISponsorRepository } from '@gridpilot/racing/domain/repositories/ISponsorRepository';
import type { ISeasonSponsorshipRepository } from '@gridpilot/racing/domain/repositories/ISeasonSponsorshipRepository';
import type { ISponsorshipRequestRepository } from '@gridpilot/racing/domain/repositories/ISponsorshipRequestRepository';
import type { ISponsorshipPricingRepository } from '@gridpilot/racing/domain/repositories/ISponsorshipPricingRepository';
import type { TransferLeagueOwnershipUseCase } from '@gridpilot/racing/application/use-cases/TransferLeagueOwnershipUseCase';
import type { DriverRatingProvider } from '@gridpilot/racing/application';
import type { PreviewLeagueScheduleQuery } from '@gridpilot/racing/application';
import type { PreviewLeagueScheduleUseCase } from '@gridpilot/racing/application';
import type { LeagueScoringPresetProvider } from '@gridpilot/racing/application/ports/LeagueScoringPresetProvider';
import { createDemoDriverStats, getDemoLeagueRankings, type DriverStats } from '@gridpilot/testing-support';
@@ -211,64 +214,79 @@ class DIContainer {
return getDIContainer().resolve<WithdrawFromRaceUseCase>(DI_TOKENS.WithdrawFromRaceUseCase);
}
get isDriverRegisteredForRaceQuery(): IsDriverRegisteredForRaceQuery {
get isDriverRegisteredForRaceUseCase(): IsDriverRegisteredForRaceUseCase {
this.ensureInitialized();
return getDIContainer().resolve<IsDriverRegisteredForRaceQuery>(DI_TOKENS.IsDriverRegisteredForRaceQuery);
return getDIContainer().resolve<IsDriverRegisteredForRaceUseCase>(DI_TOKENS.IsDriverRegisteredForRaceUseCase);
}
get getRaceRegistrationsQuery(): GetRaceRegistrationsQuery {
get getRaceRegistrationsUseCase(): GetRaceRegistrationsUseCase {
this.ensureInitialized();
return getDIContainer().resolve<GetRaceRegistrationsQuery>(DI_TOKENS.GetRaceRegistrationsQuery);
return getDIContainer().resolve<GetRaceRegistrationsUseCase>(DI_TOKENS.GetRaceRegistrationsUseCase);
}
get getLeagueStandingsQuery(): GetLeagueStandingsQuery {
get getLeagueStandingsUseCase(): GetLeagueStandingsUseCase {
this.ensureInitialized();
return getDIContainer().resolve<GetLeagueStandingsQuery>(DI_TOKENS.GetLeagueStandingsQuery);
return getDIContainer().resolve<GetLeagueStandingsUseCase>(DI_TOKENS.GetLeagueStandingsUseCase);
}
get getLeagueDriverSeasonStatsQuery(): GetLeagueDriverSeasonStatsQuery {
get getLeagueDriverSeasonStatsUseCase(): GetLeagueDriverSeasonStatsUseCase {
this.ensureInitialized();
return getDIContainer().resolve<GetLeagueDriverSeasonStatsQuery>(DI_TOKENS.GetLeagueDriverSeasonStatsQuery);
return getDIContainer().resolve<GetLeagueDriverSeasonStatsUseCase>(DI_TOKENS.GetLeagueDriverSeasonStatsUseCase);
}
get getAllLeaguesWithCapacityQuery(): GetAllLeaguesWithCapacityQuery {
get getAllLeaguesWithCapacityUseCase(): GetAllLeaguesWithCapacityUseCase {
this.ensureInitialized();
return getDIContainer().resolve<GetAllLeaguesWithCapacityQuery>(DI_TOKENS.GetAllLeaguesWithCapacityQuery);
return getDIContainer().resolve<GetAllLeaguesWithCapacityUseCase>(DI_TOKENS.GetAllLeaguesWithCapacityUseCase);
}
get getAllLeaguesWithCapacityAndScoringQuery(): GetAllLeaguesWithCapacityAndScoringQuery {
get getAllLeaguesWithCapacityAndScoringUseCase(): GetAllLeaguesWithCapacityAndScoringUseCase {
this.ensureInitialized();
return getDIContainer().resolve<GetAllLeaguesWithCapacityAndScoringQuery>(DI_TOKENS.GetAllLeaguesWithCapacityAndScoringQuery);
return getDIContainer().resolve<GetAllLeaguesWithCapacityAndScoringUseCase>(DI_TOKENS.GetAllLeaguesWithCapacityAndScoringUseCase);
}
get listLeagueScoringPresetsQuery(): ListLeagueScoringPresetsQuery {
get listLeagueScoringPresetsUseCase(): ListLeagueScoringPresetsUseCase {
this.ensureInitialized();
return getDIContainer().resolve<ListLeagueScoringPresetsQuery>(DI_TOKENS.ListLeagueScoringPresetsQuery);
return getDIContainer().resolve<ListLeagueScoringPresetsUseCase>(DI_TOKENS.ListLeagueScoringPresetsUseCase);
}
get getLeagueScoringConfigQuery(): GetLeagueScoringConfigQuery {
get getLeagueScoringConfigUseCase(): GetLeagueScoringConfigUseCase {
this.ensureInitialized();
return getDIContainer().resolve<GetLeagueScoringConfigQuery>(DI_TOKENS.GetLeagueScoringConfigQuery);
return getDIContainer().resolve<GetLeagueScoringConfigUseCase>(DI_TOKENS.GetLeagueScoringConfigUseCase);
}
get getLeagueFullConfigQuery(): GetLeagueFullConfigQuery {
get getLeagueFullConfigUseCase(): GetLeagueFullConfigUseCase {
this.ensureInitialized();
return getDIContainer().resolve<GetLeagueFullConfigQuery>(DI_TOKENS.GetLeagueFullConfigQuery);
return getDIContainer().resolve<GetLeagueFullConfigUseCase>(DI_TOKENS.GetLeagueFullConfigUseCase);
}
get previewLeagueScheduleQuery(): PreviewLeagueScheduleQuery {
get previewLeagueScheduleUseCase(): PreviewLeagueScheduleUseCase {
this.ensureInitialized();
return getDIContainer().resolve<PreviewLeagueScheduleQuery>(DI_TOKENS.PreviewLeagueScheduleQuery);
return getDIContainer().resolve<PreviewLeagueScheduleUseCase>(DI_TOKENS.PreviewLeagueScheduleUseCase);
}
get getRaceWithSOFQuery(): GetRaceWithSOFQuery {
get getRaceWithSOFUseCase(): GetRaceWithSOFUseCase {
this.ensureInitialized();
return getDIContainer().resolve<GetRaceWithSOFQuery>(DI_TOKENS.GetRaceWithSOFQuery);
return getDIContainer().resolve<GetRaceWithSOFUseCase>(DI_TOKENS.GetRaceWithSOFUseCase);
}
get getLeagueStatsQuery(): GetLeagueStatsQuery {
get getLeagueStatsUseCase(): GetLeagueStatsUseCase {
this.ensureInitialized();
return getDIContainer().resolve<GetLeagueStatsQuery>(DI_TOKENS.GetLeagueStatsQuery);
return getDIContainer().resolve<GetLeagueStatsUseCase>(DI_TOKENS.GetLeagueStatsUseCase);
}
get getRacesPageDataUseCase(): GetRacesPageDataUseCase {
this.ensureInitialized();
return getDIContainer().resolve<GetRacesPageDataUseCase>(DI_TOKENS.GetRacesPageDataUseCase);
}
get getDriversLeaderboardUseCase(): GetDriversLeaderboardUseCase {
this.ensureInitialized();
return getDIContainer().resolve<GetDriversLeaderboardUseCase>(DI_TOKENS.GetDriversLeaderboardUseCase);
}
get getTeamsLeaderboardUseCase(): GetTeamsLeaderboardUseCase {
this.ensureInitialized();
return getDIContainer().resolve<GetTeamsLeaderboardUseCase>(DI_TOKENS.GetTeamsLeaderboardUseCase);
}
get driverRatingProvider(): DriverRatingProvider {
@@ -311,29 +329,29 @@ class DIContainer {
return getDIContainer().resolve<UpdateTeamUseCase>(DI_TOKENS.UpdateTeamUseCase);
}
get getAllTeamsQuery(): GetAllTeamsQuery {
get getAllTeamsUseCase(): GetAllTeamsUseCase {
this.ensureInitialized();
return getDIContainer().resolve<GetAllTeamsQuery>(DI_TOKENS.GetAllTeamsQuery);
return getDIContainer().resolve<GetAllTeamsUseCase>(DI_TOKENS.GetAllTeamsUseCase);
}
get getTeamDetailsQuery(): GetTeamDetailsQuery {
get getTeamDetailsUseCase(): GetTeamDetailsUseCase {
this.ensureInitialized();
return getDIContainer().resolve<GetTeamDetailsQuery>(DI_TOKENS.GetTeamDetailsQuery);
return getDIContainer().resolve<GetTeamDetailsUseCase>(DI_TOKENS.GetTeamDetailsUseCase);
}
get getTeamMembersQuery(): GetTeamMembersQuery {
get getTeamMembersUseCase(): GetTeamMembersUseCase {
this.ensureInitialized();
return getDIContainer().resolve<GetTeamMembersQuery>(DI_TOKENS.GetTeamMembersQuery);
return getDIContainer().resolve<GetTeamMembersUseCase>(DI_TOKENS.GetTeamMembersUseCase);
}
get getTeamJoinRequestsQuery(): GetTeamJoinRequestsQuery {
get getTeamJoinRequestsUseCase(): GetTeamJoinRequestsUseCase {
this.ensureInitialized();
return getDIContainer().resolve<GetTeamJoinRequestsQuery>(DI_TOKENS.GetTeamJoinRequestsQuery);
return getDIContainer().resolve<GetTeamJoinRequestsUseCase>(DI_TOKENS.GetTeamJoinRequestsUseCase);
}
get getDriverTeamQuery(): GetDriverTeamQuery {
get getDriverTeamUseCase(): GetDriverTeamUseCase {
this.ensureInitialized();
return getDIContainer().resolve<GetDriverTeamQuery>(DI_TOKENS.GetDriverTeamQuery);
return getDIContainer().resolve<GetDriverTeamUseCase>(DI_TOKENS.GetDriverTeamUseCase);
}
get teamRepository(): ITeamRepository {
@@ -411,14 +429,14 @@ class DIContainer {
return getDIContainer().resolve<ApplyPenaltyUseCase>(DI_TOKENS.ApplyPenaltyUseCase);
}
get getRaceProtestsQuery(): GetRaceProtestsQuery {
get getRaceProtestsUseCase(): GetRaceProtestsUseCase {
this.ensureInitialized();
return getDIContainer().resolve<GetRaceProtestsQuery>(DI_TOKENS.GetRaceProtestsQuery);
return getDIContainer().resolve<GetRaceProtestsUseCase>(DI_TOKENS.GetRaceProtestsUseCase);
}
get getRacePenaltiesQuery(): GetRacePenaltiesQuery {
get getRacePenaltiesUseCase(): GetRacePenaltiesUseCase {
this.ensureInitialized();
return getDIContainer().resolve<GetRacePenaltiesQuery>(DI_TOKENS.GetRacePenaltiesQuery);
return getDIContainer().resolve<GetRacePenaltiesUseCase>(DI_TOKENS.GetRacePenaltiesUseCase);
}
get requestProtestDefenseUseCase(): RequestProtestDefenseUseCase {
@@ -446,14 +464,14 @@ class DIContainer {
return getDIContainer().resolve<ISeasonSponsorshipRepository>(DI_TOKENS.SeasonSponsorshipRepository);
}
get getSponsorDashboardQuery(): GetSponsorDashboardQuery {
get getSponsorDashboardUseCase(): GetSponsorDashboardUseCase {
this.ensureInitialized();
return getDIContainer().resolve<GetSponsorDashboardQuery>(DI_TOKENS.GetSponsorDashboardQuery);
return getDIContainer().resolve<GetSponsorDashboardUseCase>(DI_TOKENS.GetSponsorDashboardUseCase);
}
get getSponsorSponsorshipsQuery(): GetSponsorSponsorshipsQuery {
get getSponsorSponsorshipsUseCase(): GetSponsorSponsorshipsUseCase {
this.ensureInitialized();
return getDIContainer().resolve<GetSponsorSponsorshipsQuery>(DI_TOKENS.GetSponsorSponsorshipsQuery);
return getDIContainer().resolve<GetSponsorSponsorshipsUseCase>(DI_TOKENS.GetSponsorSponsorshipsUseCase);
}
get sponsorshipRequestRepository(): ISponsorshipRequestRepository {
@@ -481,14 +499,14 @@ class DIContainer {
return getDIContainer().resolve<RejectSponsorshipRequestUseCase>(DI_TOKENS.RejectSponsorshipRequestUseCase);
}
get getPendingSponsorshipRequestsQuery(): GetPendingSponsorshipRequestsQuery {
get getPendingSponsorshipRequestsUseCase(): GetPendingSponsorshipRequestsUseCase {
this.ensureInitialized();
return getDIContainer().resolve<GetPendingSponsorshipRequestsQuery>(DI_TOKENS.GetPendingSponsorshipRequestsQuery);
return getDIContainer().resolve<GetPendingSponsorshipRequestsUseCase>(DI_TOKENS.GetPendingSponsorshipRequestsUseCase);
}
get getEntitySponsorshipPricingQuery(): GetEntitySponsorshipPricingQuery {
get getEntitySponsorshipPricingUseCase(): GetEntitySponsorshipPricingUseCase {
this.ensureInitialized();
return getDIContainer().resolve<GetEntitySponsorshipPricingQuery>(DI_TOKENS.GetEntitySponsorshipPricingQuery);
return getDIContainer().resolve<GetEntitySponsorshipPricingUseCase>(DI_TOKENS.GetEntitySponsorshipPricingUseCase);
}
}
@@ -543,57 +561,68 @@ export function getWithdrawFromRaceUseCase(): WithdrawFromRaceUseCase {
return DIContainer.getInstance().withdrawFromRaceUseCase;
}
export function getIsDriverRegisteredForRaceQuery(): IsDriverRegisteredForRaceQuery {
return DIContainer.getInstance().isDriverRegisteredForRaceQuery;
export function getIsDriverRegisteredForRaceUseCase(): IsDriverRegisteredForRaceUseCase {
return DIContainer.getInstance().isDriverRegisteredForRaceUseCase;
}
export function getGetRaceRegistrationsQuery(): GetRaceRegistrationsQuery {
return DIContainer.getInstance().getRaceRegistrationsQuery;
export function getGetRaceRegistrationsUseCase(): GetRaceRegistrationsUseCase {
return DIContainer.getInstance().getRaceRegistrationsUseCase;
}
export function getGetLeagueStandingsQuery(): GetLeagueStandingsQuery {
return DIContainer.getInstance().getLeagueStandingsQuery;
export function getGetLeagueStandingsUseCase(): GetLeagueStandingsUseCase {
return DIContainer.getInstance().getLeagueStandingsUseCase;
}
export function getGetLeagueDriverSeasonStatsQuery(): GetLeagueDriverSeasonStatsQuery {
return DIContainer.getInstance().getLeagueDriverSeasonStatsQuery;
export function getGetLeagueDriverSeasonStatsUseCase(): GetLeagueDriverSeasonStatsUseCase {
return DIContainer.getInstance().getLeagueDriverSeasonStatsUseCase;
}
export function getGetAllLeaguesWithCapacityQuery(): GetAllLeaguesWithCapacityQuery {
return DIContainer.getInstance().getAllLeaguesWithCapacityQuery;
export function getGetAllLeaguesWithCapacityUseCase(): GetAllLeaguesWithCapacityUseCase {
return DIContainer.getInstance().getAllLeaguesWithCapacityUseCase;
}
export function getGetAllLeaguesWithCapacityAndScoringQuery(): GetAllLeaguesWithCapacityAndScoringQuery {
return DIContainer.getInstance().getAllLeaguesWithCapacityAndScoringQuery;
export function getGetAllLeaguesWithCapacityAndScoringUseCase(): GetAllLeaguesWithCapacityAndScoringUseCase {
return DIContainer.getInstance().getAllLeaguesWithCapacityAndScoringUseCase;
}
export function getGetLeagueScoringConfigQuery(): GetLeagueScoringConfigQuery {
return DIContainer.getInstance().getLeagueScoringConfigQuery;
export function getGetLeagueScoringConfigUseCase(): GetLeagueScoringConfigUseCase {
return DIContainer.getInstance().getLeagueScoringConfigUseCase;
}
export function getGetLeagueFullConfigQuery(): GetLeagueFullConfigQuery {
return DIContainer.getInstance().getLeagueFullConfigQuery;
export function getGetLeagueFullConfigUseCase(): GetLeagueFullConfigUseCase {
return DIContainer.getInstance().getLeagueFullConfigUseCase;
}
// Placeholder export for future schedule preview API wiring.
export function getPreviewLeagueScheduleQuery(): PreviewLeagueScheduleQuery {
return DIContainer.getInstance().previewLeagueScheduleQuery;
export function getPreviewLeagueScheduleUseCase(): PreviewLeagueScheduleUseCase {
return DIContainer.getInstance().previewLeagueScheduleUseCase;
}
export function getListLeagueScoringPresetsQuery(): ListLeagueScoringPresetsQuery {
return DIContainer.getInstance().listLeagueScoringPresetsQuery;
export function getListLeagueScoringPresetsUseCase(): ListLeagueScoringPresetsUseCase {
return DIContainer.getInstance().listLeagueScoringPresetsUseCase;
}
export function getCreateLeagueWithSeasonAndScoringUseCase(): CreateLeagueWithSeasonAndScoringUseCase {
return DIContainer.getInstance().createLeagueWithSeasonAndScoringUseCase;
}
export function getGetRaceWithSOFQuery(): GetRaceWithSOFQuery {
return DIContainer.getInstance().getRaceWithSOFQuery;
export function getGetRaceWithSOFUseCase(): GetRaceWithSOFUseCase {
return DIContainer.getInstance().getRaceWithSOFUseCase;
}
export function getGetLeagueStatsQuery(): GetLeagueStatsQuery {
return DIContainer.getInstance().getLeagueStatsQuery;
export function getGetLeagueStatsUseCase(): GetLeagueStatsUseCase {
return DIContainer.getInstance().getLeagueStatsUseCase;
}
export function getGetRacesPageDataUseCase(): GetRacesPageDataUseCase {
return DIContainer.getInstance().getRacesPageDataUseCase;
}
export function getGetDriversLeaderboardUseCase(): GetDriversLeaderboardUseCase {
return DIContainer.getInstance().getDriversLeaderboardUseCase;
}
export function getGetTeamsLeaderboardUseCase(): GetTeamsLeaderboardUseCase {
return DIContainer.getInstance().getTeamsLeaderboardUseCase;
}
export function getDriverRatingProvider(): DriverRatingProvider {
@@ -632,24 +661,24 @@ export function getUpdateTeamUseCase(): UpdateTeamUseCase {
return DIContainer.getInstance().updateTeamUseCase;
}
export function getGetAllTeamsQuery(): GetAllTeamsQuery {
return DIContainer.getInstance().getAllTeamsQuery;
export function getGetAllTeamsUseCase(): GetAllTeamsUseCase {
return DIContainer.getInstance().getAllTeamsUseCase;
}
export function getGetTeamDetailsQuery(): GetTeamDetailsQuery {
return DIContainer.getInstance().getTeamDetailsQuery;
export function getGetTeamDetailsUseCase(): GetTeamDetailsUseCase {
return DIContainer.getInstance().getTeamDetailsUseCase;
}
export function getGetTeamMembersQuery(): GetTeamMembersQuery {
return DIContainer.getInstance().getTeamMembersQuery;
export function getGetTeamMembersUseCase(): GetTeamMembersUseCase {
return DIContainer.getInstance().getTeamMembersUseCase;
}
export function getGetTeamJoinRequestsQuery(): GetTeamJoinRequestsQuery {
return DIContainer.getInstance().getTeamJoinRequestsQuery;
export function getGetTeamJoinRequestsUseCase(): GetTeamJoinRequestsUseCase {
return DIContainer.getInstance().getTeamJoinRequestsUseCase;
}
export function getGetDriverTeamQuery(): GetDriverTeamQuery {
return DIContainer.getInstance().getDriverTeamQuery;
export function getGetDriverTeamUseCase(): GetDriverTeamUseCase {
return DIContainer.getInstance().getDriverTeamUseCase;
}
export function getFeedRepository(): IFeedRepository {
@@ -708,12 +737,12 @@ export function getApplyPenaltyUseCase(): ApplyPenaltyUseCase {
return DIContainer.getInstance().applyPenaltyUseCase;
}
export function getGetRaceProtestsQuery(): GetRaceProtestsQuery {
return DIContainer.getInstance().getRaceProtestsQuery;
export function getGetRaceProtestsUseCase(): GetRaceProtestsUseCase {
return DIContainer.getInstance().getRaceProtestsUseCase;
}
export function getGetRacePenaltiesQuery(): GetRacePenaltiesQuery {
return DIContainer.getInstance().getRacePenaltiesQuery;
export function getGetRacePenaltiesUseCase(): GetRacePenaltiesUseCase {
return DIContainer.getInstance().getRacePenaltiesUseCase;
}
export function getRequestProtestDefenseUseCase(): RequestProtestDefenseUseCase {
@@ -736,12 +765,12 @@ export function getSeasonSponsorshipRepository(): ISeasonSponsorshipRepository {
return DIContainer.getInstance().seasonSponsorshipRepository;
}
export function getGetSponsorDashboardQuery(): GetSponsorDashboardQuery {
return DIContainer.getInstance().getSponsorDashboardQuery;
export function getGetSponsorDashboardUseCase(): GetSponsorDashboardUseCase {
return DIContainer.getInstance().getSponsorDashboardUseCase;
}
export function getGetSponsorSponsorshipsQuery(): GetSponsorSponsorshipsQuery {
return DIContainer.getInstance().getSponsorSponsorshipsQuery;
export function getGetSponsorSponsorshipsUseCase(): GetSponsorSponsorshipsUseCase {
return DIContainer.getInstance().getSponsorSponsorshipsUseCase;
}
export function getSponsorshipRequestRepository(): ISponsorshipRequestRepository {
@@ -764,12 +793,12 @@ export function getRejectSponsorshipRequestUseCase(): RejectSponsorshipRequestUs
return DIContainer.getInstance().rejectSponsorshipRequestUseCase;
}
export function getGetPendingSponsorshipRequestsQuery(): GetPendingSponsorshipRequestsQuery {
return DIContainer.getInstance().getPendingSponsorshipRequestsQuery;
export function getGetPendingSponsorshipRequestsUseCase(): GetPendingSponsorshipRequestsUseCase {
return DIContainer.getInstance().getPendingSponsorshipRequestsUseCase;
}
export function getGetEntitySponsorshipPricingQuery(): GetEntitySponsorshipPricingQuery {
return DIContainer.getInstance().getEntitySponsorshipPricingQuery;
export function getGetEntitySponsorshipPricingUseCase(): GetEntitySponsorshipPricingUseCase {
return DIContainer.getInstance().getEntitySponsorshipPricingUseCase;
}
/**

View File

@@ -64,38 +64,41 @@ export const DI_TOKENS = {
MarkNotificationReadUseCase: Symbol.for('MarkNotificationReadUseCase'),
// Queries - Racing
IsDriverRegisteredForRaceQuery: Symbol.for('IsDriverRegisteredForRaceQuery'),
GetRaceRegistrationsQuery: Symbol.for('GetRaceRegistrationsQuery'),
GetLeagueStandingsQuery: Symbol.for('GetLeagueStandingsQuery'),
GetLeagueDriverSeasonStatsQuery: Symbol.for('GetLeagueDriverSeasonStatsQuery'),
GetAllLeaguesWithCapacityQuery: Symbol.for('GetAllLeaguesWithCapacityQuery'),
GetAllLeaguesWithCapacityAndScoringQuery: Symbol.for('GetAllLeaguesWithCapacityAndScoringQuery'),
ListLeagueScoringPresetsQuery: Symbol.for('ListLeagueScoringPresetsQuery'),
GetLeagueScoringConfigQuery: Symbol.for('GetLeagueScoringConfigQuery'),
GetLeagueFullConfigQuery: Symbol.for('GetLeagueFullConfigQuery'),
PreviewLeagueScheduleQuery: Symbol.for('PreviewLeagueScheduleQuery'),
GetRaceWithSOFQuery: Symbol.for('GetRaceWithSOFQuery'),
GetLeagueStatsQuery: Symbol.for('GetLeagueStatsQuery'),
IsDriverRegisteredForRaceUseCase: Symbol.for('IsDriverRegisteredForRaceUseCase'),
GetRaceRegistrationsUseCase: Symbol.for('GetRaceRegistrationsUseCase'),
GetLeagueStandingsUseCase: Symbol.for('GetLeagueStandingsUseCase'),
GetLeagueDriverSeasonStatsUseCase: Symbol.for('GetLeagueDriverSeasonStatsUseCase'),
GetAllLeaguesWithCapacityUseCase: Symbol.for('GetAllLeaguesWithCapacityUseCase'),
GetAllLeaguesWithCapacityAndScoringUseCase: Symbol.for('GetAllLeaguesWithCapacityAndScoringUseCase'),
ListLeagueScoringPresetsUseCase: Symbol.for('ListLeagueScoringPresetsUseCase'),
GetLeagueScoringConfigUseCase: Symbol.for('GetLeagueScoringConfigUseCase'),
GetLeagueFullConfigUseCase: Symbol.for('GetLeagueFullConfigUseCase'),
PreviewLeagueScheduleUseCase: Symbol.for('PreviewLeagueScheduleUseCase'),
GetRaceWithSOFUseCase: Symbol.for('GetRaceWithSOFUseCase'),
GetLeagueStatsUseCase: Symbol.for('GetLeagueStatsUseCase'),
GetRacesPageDataUseCase: Symbol.for('GetRacesPageDataUseCase'),
GetDriversLeaderboardUseCase: Symbol.for('GetDriversLeaderboardUseCase'),
GetTeamsLeaderboardUseCase: Symbol.for('GetTeamsLeaderboardUseCase'),
// Queries - Teams
GetAllTeamsQuery: Symbol.for('GetAllTeamsQuery'),
GetTeamDetailsQuery: Symbol.for('GetTeamDetailsQuery'),
GetTeamMembersQuery: Symbol.for('GetTeamMembersQuery'),
GetTeamJoinRequestsQuery: Symbol.for('GetTeamJoinRequestsQuery'),
GetDriverTeamQuery: Symbol.for('GetDriverTeamQuery'),
// Use Cases - Teams (Query-like)
GetAllTeamsUseCase: Symbol.for('GetAllTeamsUseCase'),
GetTeamDetailsUseCase: Symbol.for('GetTeamDetailsUseCase'),
GetTeamMembersUseCase: Symbol.for('GetTeamMembersUseCase'),
GetTeamJoinRequestsUseCase: Symbol.for('GetTeamJoinRequestsUseCase'),
GetDriverTeamUseCase: Symbol.for('GetDriverTeamUseCase'),
// Queries - Stewarding
GetRaceProtestsQuery: Symbol.for('GetRaceProtestsQuery'),
GetRacePenaltiesQuery: Symbol.for('GetRacePenaltiesQuery'),
GetRaceProtestsUseCase: Symbol.for('GetRaceProtestsUseCase'),
GetRacePenaltiesUseCase: Symbol.for('GetRacePenaltiesUseCase'),
// Queries - Notifications
GetUnreadNotificationsQuery: Symbol.for('GetUnreadNotificationsQuery'),
// Queries - Sponsors
GetSponsorDashboardQuery: Symbol.for('GetSponsorDashboardQuery'),
GetSponsorSponsorshipsQuery: Symbol.for('GetSponsorSponsorshipsQuery'),
GetPendingSponsorshipRequestsQuery: Symbol.for('GetPendingSponsorshipRequestsQuery'),
GetEntitySponsorshipPricingQuery: Symbol.for('GetEntitySponsorshipPricingQuery'),
// Use Cases - Sponsors
GetSponsorDashboardUseCase: Symbol.for('GetSponsorDashboardUseCase'),
GetSponsorSponsorshipsUseCase: Symbol.for('GetSponsorSponsorshipsUseCase'),
GetPendingSponsorshipRequestsUseCase: Symbol.for('GetPendingSponsorshipRequestsUseCase'),
GetEntitySponsorshipPricingUseCase: Symbol.for('GetEntitySponsorshipPricingUseCase'),
// Use Cases - Sponsorship
ApplyForSponsorshipUseCase: Symbol.for('ApplyForSponsorshipUseCase'),
@@ -104,6 +107,20 @@ export const DI_TOKENS = {
// Data
DriverStats: Symbol.for('DriverStats'),
// Presenters - Racing
RaceWithSOFPresenter: Symbol.for('IRaceWithSOFPresenter'),
RaceProtestsPresenter: Symbol.for('IRaceProtestsPresenter'),
RacePenaltiesPresenter: Symbol.for('IRacePenaltiesPresenter'),
RaceRegistrationsPresenter: Symbol.for('IRaceRegistrationsPresenter'),
DriverRegistrationStatusPresenter: Symbol.for('IDriverRegistrationStatusPresenter'),
// Presenters - Sponsors
SponsorDashboardPresenter: Symbol.for('ISponsorDashboardPresenter'),
SponsorSponsorshipsPresenter: Symbol.for('ISponsorSponsorshipsPresenter'),
PendingSponsorshipRequestsPresenter: Symbol.for('IPendingSponsorshipRequestsPresenter'),
EntitySponsorshipPricingPresenter: Symbol.for('IEntitySponsorshipPricingPresenter'),
LeagueSchedulePreviewPresenter: Symbol.for('ILeagueSchedulePreviewPresenter'),
} as const;
export type DITokens = typeof DI_TOKENS;

View File

@@ -0,0 +1,112 @@
import type { League } from '@gridpilot/racing/domain/entities/League';
import type {
IAllLeaguesWithCapacityAndScoringPresenter,
LeagueEnrichedData,
LeagueSummaryViewModel,
AllLeaguesWithCapacityAndScoringViewModel,
} from '@gridpilot/racing/application/presenters/IAllLeaguesWithCapacityAndScoringPresenter';
export class AllLeaguesWithCapacityAndScoringPresenter implements IAllLeaguesWithCapacityAndScoringPresenter {
private viewModel: AllLeaguesWithCapacityAndScoringViewModel | null = null;
present(enrichedLeagues: LeagueEnrichedData[]): AllLeaguesWithCapacityAndScoringViewModel {
const leagueItems: LeagueSummaryViewModel[] = enrichedLeagues.map((data) => {
const { league, usedDriverSlots, season, scoringConfig, game, preset } = data;
const configuredMaxDrivers = league.settings.maxDrivers ?? usedDriverSlots;
const safeMaxDrivers = Math.max(configuredMaxDrivers, usedDriverSlots);
const structureSummary = `Solo • ${safeMaxDrivers} drivers`;
const qualifyingMinutes = 30;
const mainRaceMinutes =
typeof league.settings.sessionDuration === 'number'
? league.settings.sessionDuration
: 40;
const timingSummary = `${qualifyingMinutes} min Quali • ${mainRaceMinutes} min Race`;
let scoringSummary: LeagueSummaryViewModel['scoring'] | undefined;
let scoringPatternSummary: string | undefined;
if (season && scoringConfig && game) {
const dropPolicySummary =
preset?.dropPolicySummary ?? this.deriveDropPolicySummary(scoringConfig);
const primaryChampionshipType =
preset?.primaryChampionshipType ??
(scoringConfig.championships[0]?.type ?? 'driver');
const scoringPresetName = preset?.name ?? 'Custom';
scoringPatternSummary = `${scoringPresetName}${dropPolicySummary}`;
scoringSummary = {
gameId: game.id,
gameName: game.name,
primaryChampionshipType,
scoringPresetId: scoringConfig.scoringPresetId ?? 'custom',
scoringPresetName,
dropPolicySummary,
scoringPatternSummary,
};
}
return {
id: league.id,
name: league.name,
description: league.description,
ownerId: league.ownerId,
createdAt: league.createdAt,
maxDrivers: safeMaxDrivers,
usedDriverSlots,
maxTeams: undefined,
usedTeamSlots: undefined,
structureSummary,
scoringPatternSummary,
timingSummary,
scoring: scoringSummary,
};
});
this.viewModel = {
leagues: leagueItems,
totalCount: leagueItems.length,
};
return this.viewModel;
}
getViewModel(): AllLeaguesWithCapacityAndScoringViewModel {
if (!this.viewModel) {
throw new Error('Presenter has not been called yet');
}
return this.viewModel;
}
private deriveDropPolicySummary(config: {
championships: Array<{
dropScorePolicy: { strategy: string; count?: number; dropCount?: number };
}>;
}): string {
const championship = config.championships[0];
if (!championship) {
return 'All results count';
}
const policy = championship.dropScorePolicy;
if (!policy || policy.strategy === 'none') {
return 'All results count';
}
if (policy.strategy === 'bestNResults' && typeof policy.count === 'number') {
return `Best ${policy.count} results count`;
}
if (
policy.strategy === 'dropWorstN' &&
typeof policy.dropCount === 'number'
) {
return `Worst ${policy.dropCount} results are dropped`;
}
return 'Custom drop score rules';
}
}

View File

@@ -0,0 +1,58 @@
import type { League } from '@gridpilot/racing/domain/entities/League';
import type {
IAllLeaguesWithCapacityPresenter,
LeagueWithCapacityViewModel,
AllLeaguesWithCapacityViewModel,
} from '@gridpilot/racing/application/presenters/IAllLeaguesWithCapacityPresenter';
export class AllLeaguesWithCapacityPresenter implements IAllLeaguesWithCapacityPresenter {
private viewModel: AllLeaguesWithCapacityViewModel | null = null;
present(
leagues: League[],
memberCounts: Map<string, number>
): AllLeaguesWithCapacityViewModel {
const leagueItems: LeagueWithCapacityViewModel[] = leagues.map((league) => {
const usedSlots = memberCounts.get(league.id) ?? 0;
// Ensure we never expose an impossible state like 26/24:
// clamp maxDrivers to at least usedSlots at the application boundary.
const configuredMax = league.settings.maxDrivers ?? usedSlots;
const safeMaxDrivers = Math.max(configuredMax, usedSlots);
return {
id: league.id,
name: league.name,
description: league.description,
ownerId: league.ownerId,
settings: {
...league.settings,
maxDrivers: safeMaxDrivers,
},
createdAt: league.createdAt.toISOString(),
socialLinks: league.socialLinks
? {
discordUrl: league.socialLinks.discordUrl,
youtubeUrl: league.socialLinks.youtubeUrl,
websiteUrl: league.socialLinks.websiteUrl,
}
: undefined,
usedSlots,
};
});
this.viewModel = {
leagues: leagueItems,
totalCount: leagueItems.length,
};
return this.viewModel;
}
getViewModel(): AllLeaguesWithCapacityViewModel {
if (!this.viewModel) {
throw new Error('Presenter has not been called yet');
}
return this.viewModel;
}
}

View File

@@ -0,0 +1,38 @@
import type { Team } from '@gridpilot/racing/domain/entities/Team';
import type {
IAllTeamsPresenter,
TeamListItemViewModel,
AllTeamsViewModel,
} from '@gridpilot/racing/application/presenters/IAllTeamsPresenter';
export class AllTeamsPresenter implements IAllTeamsPresenter {
private viewModel: AllTeamsViewModel | null = null;
present(teams: Array<Team & { memberCount?: number }>): AllTeamsViewModel {
const teamItems: TeamListItemViewModel[] = teams.map((team) => ({
id: team.id,
name: team.name,
tag: team.tag,
description: team.description,
memberCount: team.memberCount ?? 0,
leagues: team.leagues,
specialization: team.specialization as 'endurance' | 'sprint' | 'mixed' | undefined,
region: team.region,
languages: team.languages,
}));
this.viewModel = {
teams: teamItems,
totalCount: teamItems.length,
};
return this.viewModel;
}
getViewModel(): AllTeamsViewModel {
if (!this.viewModel) {
throw new Error('Presenter has not been called yet');
}
return this.viewModel;
}
}

View File

@@ -0,0 +1,29 @@
import type {
IDriverRegistrationStatusPresenter,
DriverRegistrationStatusViewModel,
} from '@gridpilot/racing/application/presenters/IDriverRegistrationStatusPresenter';
export class DriverRegistrationStatusPresenter implements IDriverRegistrationStatusPresenter {
private viewModel: DriverRegistrationStatusViewModel | null = null;
present(
isRegistered: boolean,
raceId: string,
driverId: string
): DriverRegistrationStatusViewModel {
this.viewModel = {
isRegistered,
raceId,
driverId,
};
return this.viewModel;
}
getViewModel(): DriverRegistrationStatusViewModel {
if (!this.viewModel) {
throw new Error('Presenter has not been called yet');
}
return this.viewModel;
}
}

View File

@@ -0,0 +1,48 @@
import type { Team, TeamMembership } from '@gridpilot/racing/domain/entities/Team';
import type {
IDriverTeamPresenter,
DriverTeamViewModel,
} from '@gridpilot/racing/application/presenters/IDriverTeamPresenter';
export class DriverTeamPresenter implements IDriverTeamPresenter {
private viewModel: DriverTeamViewModel | null = null;
present(
team: Team,
membership: TeamMembership,
driverId: string
): DriverTeamViewModel {
const isOwner = team.ownerId === driverId;
const canManage = membership.role === 'owner' || membership.role === 'manager';
this.viewModel = {
team: {
id: team.id,
name: team.name,
tag: team.tag,
description: team.description,
ownerId: team.ownerId,
leagues: team.leagues,
specialization: team.specialization as 'endurance' | 'sprint' | 'mixed' | undefined,
region: team.region,
languages: team.languages,
},
membership: {
role: membership.role,
joinedAt: membership.joinedAt.toISOString(),
isActive: membership.isActive,
},
isOwner,
canManage,
};
return this.viewModel;
}
getViewModel(): DriverTeamViewModel {
if (!this.viewModel) {
throw new Error('Presenter has not been called yet');
}
return this.viewModel;
}
}

View File

@@ -0,0 +1,81 @@
import type { Driver } from '@gridpilot/racing/domain/entities/Driver';
import type { SkillLevel } from '@gridpilot/racing/domain/services/SkillLevelService';
import { SkillLevelService } from '@gridpilot/racing/domain/services/SkillLevelService';
import type {
IDriversLeaderboardPresenter,
DriverLeaderboardItemViewModel,
DriversLeaderboardViewModel,
} from '@gridpilot/racing/application/presenters/IDriversLeaderboardPresenter';
export class DriversLeaderboardPresenter implements IDriversLeaderboardPresenter {
private viewModel: DriversLeaderboardViewModel | null = null;
present(
drivers: Driver[],
rankings: Array<{ driverId: string; rating: number; overallRank: number }>,
stats: Record<string, { rating: number; wins: number; podiums: number; totalRaces: number; overallRank: number }>,
avatarUrls: Record<string, string>
): DriversLeaderboardViewModel {
const items: DriverLeaderboardItemViewModel[] = drivers.map((driver) => {
const driverStats = stats[driver.id];
const rating = driverStats?.rating ?? 0;
const wins = driverStats?.wins ?? 0;
const podiums = driverStats?.podiums ?? 0;
const totalRaces = driverStats?.totalRaces ?? 0;
let effectiveRank = Number.POSITIVE_INFINITY;
if (typeof driverStats?.overallRank === 'number' && driverStats.overallRank > 0) {
effectiveRank = driverStats.overallRank;
} else {
const indexInGlobal = rankings.findIndex((entry) => entry.driverId === driver.id);
if (indexInGlobal !== -1) {
effectiveRank = indexInGlobal + 1;
}
}
const skillLevel = SkillLevelService.getSkillLevel(rating);
const isActive = rankings.some((r) => r.driverId === driver.id);
return {
id: driver.id,
name: driver.name,
rating,
skillLevel,
nationality: driver.country,
racesCompleted: totalRaces,
wins,
podiums,
isActive,
rank: effectiveRank,
avatarUrl: avatarUrls[driver.id] ?? '',
};
});
items.sort((a, b) => {
const rankA = Number.isFinite(a.rank) && a.rank > 0 ? a.rank : Number.POSITIVE_INFINITY;
const rankB = Number.isFinite(b.rank) && b.rank > 0 ? b.rank : Number.POSITIVE_INFINITY;
if (rankA !== rankB) return rankA - rankB;
return b.rating - a.rating;
});
const totalRaces = items.reduce((sum, d) => sum + d.racesCompleted, 0);
const totalWins = items.reduce((sum, d) => sum + d.wins, 0);
const activeCount = items.filter((d) => d.isActive).length;
this.viewModel = {
drivers: items,
totalRaces,
totalWins,
activeCount,
};
return this.viewModel;
}
getViewModel(): DriversLeaderboardViewModel {
if (!this.viewModel) {
throw new Error('Presenter has not been called yet');
}
return this.viewModel;
}
}

View File

@@ -0,0 +1,14 @@
import type { IEntitySponsorshipPricingPresenter } from '@racing/application/presenters/IEntitySponsorshipPricingPresenter';
import type { GetEntitySponsorshipPricingResultDTO } from '@racing/application/use-cases/GetEntitySponsorshipPricingQuery';
export class EntitySponsorshipPricingPresenter implements IEntitySponsorshipPricingPresenter {
private data: GetEntitySponsorshipPricingResultDTO | null = null;
present(data: GetEntitySponsorshipPricingResultDTO | null): void {
this.data = data;
}
getData(): GetEntitySponsorshipPricingResultDTO | null {
return this.data;
}
}

View File

@@ -0,0 +1,78 @@
import type {
ILeagueDriverSeasonStatsPresenter,
LeagueDriverSeasonStatsItemViewModel,
LeagueDriverSeasonStatsViewModel,
} from '@gridpilot/racing/application/presenters/ILeagueDriverSeasonStatsPresenter';
export class LeagueDriverSeasonStatsPresenter implements ILeagueDriverSeasonStatsPresenter {
private viewModel: LeagueDriverSeasonStatsViewModel | null = null;
present(
leagueId: string,
standings: Array<{
driverId: string;
position: number;
points: number;
racesCompleted: number;
}>,
penalties: Map<string, { baseDelta: number; bonusDelta: number }>,
driverResults: Map<string, Array<{ position: number }>>,
driverRatings: Map<string, { rating: number | null; ratingChange: number | null }>
): LeagueDriverSeasonStatsViewModel {
const stats: LeagueDriverSeasonStatsItemViewModel[] = standings.map((standing) => {
const penalty = penalties.get(standing.driverId) ?? { baseDelta: 0, bonusDelta: 0 };
const totalPenaltyPoints = penalty.baseDelta;
const bonusPoints = penalty.bonusDelta;
const racesCompleted = standing.racesCompleted;
const pointsPerRace = racesCompleted > 0 ? standing.points / racesCompleted : 0;
const ratingInfo = driverRatings.get(standing.driverId) ?? { rating: null, ratingChange: null };
const results = driverResults.get(standing.driverId) ?? [];
let avgFinish: number | null = null;
if (results.length > 0) {
const totalPositions = results.reduce((sum, r) => sum + r.position, 0);
const avg = totalPositions / results.length;
avgFinish = Number.isFinite(avg) ? Number(avg.toFixed(2)) : null;
}
return {
leagueId,
driverId: standing.driverId,
position: standing.position,
driverName: '',
teamId: undefined,
teamName: undefined,
totalPoints: standing.points + totalPenaltyPoints + bonusPoints,
basePoints: standing.points,
penaltyPoints: Math.abs(totalPenaltyPoints),
bonusPoints,
pointsPerRace,
racesStarted: results.length,
racesFinished: results.length,
dnfs: 0,
noShows: 0,
avgFinish,
rating: ratingInfo.rating,
ratingChange: ratingInfo.ratingChange,
};
});
stats.sort((a, b) => a.position - b.position);
this.viewModel = {
leagueId,
stats,
};
return this.viewModel;
}
getViewModel(): LeagueDriverSeasonStatsViewModel {
if (!this.viewModel) {
throw new Error('Presenter has not been called yet');
}
return this.viewModel;
}
}

View File

@@ -0,0 +1,119 @@
import type { DropScorePolicy } from '@gridpilot/racing/domain/value-objects/DropScorePolicy';
import type {
ILeagueFullConfigPresenter,
LeagueFullConfigData,
LeagueConfigFormViewModel,
} from '@gridpilot/racing/application/presenters/ILeagueFullConfigPresenter';
export class LeagueFullConfigPresenter implements ILeagueFullConfigPresenter {
private viewModel: LeagueConfigFormViewModel | null = null;
present(data: LeagueFullConfigData): LeagueConfigFormViewModel {
const { league, activeSeason, scoringConfig, game } = data;
const patternId = scoringConfig?.scoringPresetId;
const primaryChampionship =
scoringConfig && scoringConfig.championships && scoringConfig.championships.length > 0
? scoringConfig.championships[0]
: undefined;
const dropPolicy = primaryChampionship?.dropScorePolicy ?? undefined;
const dropPolicyForm = this.mapDropPolicy(dropPolicy);
const defaultQualifyingMinutes = 30;
const defaultMainRaceMinutes = 40;
const mainRaceMinutes =
typeof league.settings.sessionDuration === 'number'
? league.settings.sessionDuration
: defaultMainRaceMinutes;
const qualifyingMinutes = defaultQualifyingMinutes;
const roundsPlanned = 8;
let sessionCount = 2;
if (
primaryChampionship &&
Array.isArray((primaryChampionship as any).sessionTypes) &&
(primaryChampionship as any).sessionTypes.length > 0
) {
sessionCount = (primaryChampionship as any).sessionTypes.length;
}
const practiceMinutes = 20;
const sprintRaceMinutes = patternId === 'sprint-main-driver' ? 20 : undefined;
this.viewModel = {
leagueId: league.id,
basics: {
name: league.name,
description: league.description,
visibility: 'public',
gameId: game?.id ?? 'iracing',
},
structure: {
mode: 'solo',
maxDrivers: league.settings.maxDrivers ?? 32,
maxTeams: undefined,
driversPerTeam: undefined,
multiClassEnabled: false,
},
championships: {
enableDriverChampionship: true,
enableTeamChampionship: false,
enableNationsChampionship: false,
enableTrophyChampionship: false,
},
scoring: {
patternId: patternId ?? undefined,
customScoringEnabled: !patternId,
},
dropPolicy: dropPolicyForm,
timings: {
practiceMinutes,
qualifyingMinutes,
sprintRaceMinutes,
mainRaceMinutes,
sessionCount,
roundsPlanned,
},
stewarding: {
decisionMode: 'admin_only',
requireDefense: true,
defenseTimeLimit: 48,
voteTimeLimit: 72,
protestDeadlineHours: 72,
stewardingClosesHours: 168,
notifyAccusedOnProtest: true,
notifyOnVoteRequired: true,
},
};
return this.viewModel;
}
getViewModel(): LeagueConfigFormViewModel {
if (!this.viewModel) {
throw new Error('Presenter has not been called yet');
}
return this.viewModel;
}
private mapDropPolicy(policy: DropScorePolicy | undefined): { strategy: string; n?: number } {
if (!policy || policy.strategy === 'none') {
return { strategy: 'none' };
}
if (policy.strategy === 'bestNResults') {
const n = typeof policy.count === 'number' ? policy.count : undefined;
return n !== undefined ? { strategy: 'bestNResults', n } : { strategy: 'none' };
}
if (policy.strategy === 'dropWorstN') {
const n = typeof policy.dropCount === 'number' ? policy.dropCount : undefined;
return n !== undefined ? { strategy: 'dropWorstN', n } : { strategy: 'none' };
}
return { strategy: 'none' };
}
}

View File

@@ -0,0 +1,14 @@
import type { ILeagueSchedulePreviewPresenter } from '@racing/application/presenters/ILeagueSchedulePreviewPresenter';
import type { LeagueSchedulePreviewDTO } from '@racing/application/dto/LeagueScheduleDTO';
export class LeagueSchedulePreviewPresenter implements ILeagueSchedulePreviewPresenter {
private data: LeagueSchedulePreviewDTO | null = null;
present(data: LeagueSchedulePreviewDTO): void {
this.data = data;
}
getData(): LeagueSchedulePreviewDTO | null {
return this.data;
}
}

View File

@@ -0,0 +1,149 @@
import type { ChampionshipConfig } from '@gridpilot/racing/domain/value-objects/ChampionshipConfig';
import type { BonusRule } from '@gridpilot/racing/domain/value-objects/BonusRule';
import type {
ILeagueScoringConfigPresenter,
LeagueScoringConfigData,
LeagueScoringConfigViewModel,
LeagueScoringChampionshipViewModel,
} from '@gridpilot/racing/application/presenters/ILeagueScoringConfigPresenter';
export class LeagueScoringConfigPresenter implements ILeagueScoringConfigPresenter {
private viewModel: LeagueScoringConfigViewModel | null = null;
present(data: LeagueScoringConfigData): LeagueScoringConfigViewModel {
const championships: LeagueScoringChampionshipViewModel[] =
data.championships.map((champ) => this.mapChampionship(champ));
const dropPolicySummary =
data.preset?.dropPolicySummary ??
this.deriveDropPolicyDescriptionFromChampionships(data.championships);
this.viewModel = {
leagueId: data.leagueId,
seasonId: data.seasonId,
gameId: data.gameId,
gameName: data.gameName,
scoringPresetId: data.scoringPresetId,
scoringPresetName: data.preset?.name,
dropPolicySummary,
championships,
};
return this.viewModel;
}
getViewModel(): LeagueScoringConfigViewModel {
if (!this.viewModel) {
throw new Error('Presenter has not been called yet');
}
return this.viewModel;
}
private mapChampionship(championship: ChampionshipConfig): LeagueScoringChampionshipViewModel {
const sessionTypes = championship.sessionTypes.map((s) => s.toString());
const pointsPreview = this.buildPointsPreview(championship.pointsTableBySessionType);
const bonusSummary = this.buildBonusSummary(
championship.bonusRulesBySessionType ?? {},
);
const dropPolicyDescription = this.deriveDropPolicyDescription(
championship.dropScorePolicy,
);
return {
id: championship.id,
name: championship.name,
type: championship.type,
sessionTypes,
pointsPreview,
bonusSummary,
dropPolicyDescription,
};
}
private buildPointsPreview(
tables: Record<string, any>,
): Array<{ sessionType: string; position: number; points: number }> {
const preview: Array<{
sessionType: string;
position: number;
points: number;
}> = [];
const maxPositions = 10;
for (const [sessionType, table] of Object.entries(tables)) {
for (let pos = 1; pos <= maxPositions; pos++) {
const points = table.getPointsForPosition(pos);
if (points && points !== 0) {
preview.push({
sessionType,
position: pos,
points,
});
}
}
}
return preview;
}
private buildBonusSummary(
bonusRulesBySessionType: Record<string, BonusRule[]>,
): string[] {
const summaries: string[] = [];
for (const [sessionType, rules] of Object.entries(bonusRulesBySessionType)) {
for (const rule of rules) {
if (rule.type === 'fastestLap') {
const base = `Fastest lap in ${sessionType}`;
if (rule.requiresFinishInTopN) {
summaries.push(
`${base} +${rule.points} points if finishing P${rule.requiresFinishInTopN} or better`,
);
} else {
summaries.push(`${base} +${rule.points} points`);
}
} else {
summaries.push(
`${rule.type} bonus in ${sessionType} worth ${rule.points} points`,
);
}
}
}
return summaries;
}
private deriveDropPolicyDescriptionFromChampionships(
championships: ChampionshipConfig[],
): string {
const first = championships[0];
if (!first) {
return 'All results count';
}
return this.deriveDropPolicyDescription(first.dropScorePolicy);
}
private deriveDropPolicyDescription(policy: {
strategy: string;
count?: number;
dropCount?: number;
}): string {
if (!policy || policy.strategy === 'none') {
return 'All results count';
}
if (policy.strategy === 'bestNResults' && typeof policy.count === 'number') {
return `Best ${policy.count} results count towards the championship`;
}
if (
policy.strategy === 'dropWorstN' &&
typeof policy.dropCount === 'number'
) {
return `Worst ${policy.dropCount} results are dropped from the championship total`;
}
return 'Custom drop score rules apply';
}
}

View File

@@ -0,0 +1,25 @@
import type { LeagueScoringPresetDTO } from '@gridpilot/racing/application/ports/LeagueScoringPresetProvider';
import type {
ILeagueScoringPresetsPresenter,
LeagueScoringPresetsViewModel,
} from '@gridpilot/racing/application/presenters/ILeagueScoringPresetsPresenter';
export class LeagueScoringPresetsPresenter implements ILeagueScoringPresetsPresenter {
private viewModel: LeagueScoringPresetsViewModel | null = null;
present(presets: LeagueScoringPresetDTO[]): LeagueScoringPresetsViewModel {
this.viewModel = {
presets,
totalCount: presets.length,
};
return this.viewModel;
}
getViewModel(): LeagueScoringPresetsViewModel {
if (!this.viewModel) {
throw new Error('Presenter has not been called yet');
}
return this.viewModel;
}
}

View File

@@ -0,0 +1,38 @@
import type { Standing } from '@gridpilot/racing/domain/entities/Standing';
import type {
ILeagueStandingsPresenter,
StandingItemViewModel,
LeagueStandingsViewModel,
} from '@gridpilot/racing/application/presenters/ILeagueStandingsPresenter';
export class LeagueStandingsPresenter implements ILeagueStandingsPresenter {
private viewModel: LeagueStandingsViewModel | null = null;
present(standings: Standing[]): LeagueStandingsViewModel {
const standingItems: StandingItemViewModel[] = standings.map((standing) => ({
id: standing.id,
leagueId: standing.leagueId,
seasonId: standing.seasonId,
driverId: standing.driverId,
position: standing.position,
points: standing.points,
wins: standing.wins,
podiums: standing.podiums,
racesCompleted: standing.racesCompleted,
}));
this.viewModel = {
leagueId: standings[0]?.leagueId ?? '',
standings: standingItems,
};
return this.viewModel;
}
getViewModel(): LeagueStandingsViewModel {
if (!this.viewModel) {
throw new Error('Presenter has not been called yet');
}
return this.viewModel;
}
}

View File

@@ -0,0 +1,42 @@
import type {
ILeagueStatsPresenter,
LeagueStatsViewModel,
} from '@gridpilot/racing/application/presenters/ILeagueStatsPresenter';
export class LeagueStatsPresenter implements ILeagueStatsPresenter {
private viewModel: LeagueStatsViewModel | null = null;
present(
leagueId: string,
totalRaces: number,
completedRaces: number,
scheduledRaces: number,
sofValues: number[]
): LeagueStatsViewModel {
const averageSOF = sofValues.length > 0
? Math.round(sofValues.reduce((a, b) => a + b, 0) / sofValues.length)
: null;
const highestSOF = sofValues.length > 0 ? Math.max(...sofValues) : null;
const lowestSOF = sofValues.length > 0 ? Math.min(...sofValues) : null;
this.viewModel = {
leagueId,
totalRaces,
completedRaces,
scheduledRaces,
averageSOF,
highestSOF,
lowestSOF,
};
return this.viewModel;
}
getViewModel(): LeagueStatsViewModel {
if (!this.viewModel) {
throw new Error('Presenter has not been called yet');
}
return this.viewModel;
}
}

View File

@@ -0,0 +1,14 @@
import type { IPendingSponsorshipRequestsPresenter } from '@racing/application/presenters/IPendingSponsorshipRequestsPresenter';
import type { GetPendingSponsorshipRequestsResultDTO } from '@racing/application/use-cases/GetPendingSponsorshipRequestsQuery';
export class PendingSponsorshipRequestsPresenter implements IPendingSponsorshipRequestsPresenter {
private data: GetPendingSponsorshipRequestsResultDTO | null = null;
present(data: GetPendingSponsorshipRequestsResultDTO): void {
this.data = data;
}
getData(): GetPendingSponsorshipRequestsResultDTO | null {
return this.data;
}
}

View File

@@ -0,0 +1,60 @@
import type {
IRacePenaltiesPresenter,
RacePenaltyViewModel,
RacePenaltiesViewModel,
} from '@gridpilot/racing/application/presenters/IRacePenaltiesPresenter';
import type { PenaltyType, PenaltyStatus } from '@gridpilot/racing/domain/entities/Penalty';
export class RacePenaltiesPresenter implements IRacePenaltiesPresenter {
private viewModel: RacePenaltiesViewModel | null = null;
present(
penalties: Array<{
id: string;
raceId: string;
driverId: string;
type: PenaltyType;
value?: number;
reason: string;
protestId?: string;
issuedBy: string;
status: PenaltyStatus;
issuedAt: Date;
appliedAt?: Date;
notes?: string;
getDescription(): string;
}>,
driverMap: Map<string, string>
): RacePenaltiesViewModel {
const penaltyViewModels: RacePenaltyViewModel[] = penalties.map(penalty => ({
id: penalty.id,
raceId: penalty.raceId,
driverId: penalty.driverId,
driverName: driverMap.get(penalty.driverId) || 'Unknown',
type: penalty.type,
value: penalty.value,
reason: penalty.reason,
protestId: penalty.protestId,
issuedBy: penalty.issuedBy,
issuedByName: driverMap.get(penalty.issuedBy) || 'Unknown',
status: penalty.status,
description: penalty.getDescription(),
issuedAt: penalty.issuedAt.toISOString(),
appliedAt: penalty.appliedAt?.toISOString(),
notes: penalty.notes,
}));
this.viewModel = {
penalties: penaltyViewModels,
};
return this.viewModel;
}
getViewModel(): RacePenaltiesViewModel {
if (!this.viewModel) {
throw new Error('Presenter has not been called yet');
}
return this.viewModel;
}
}

View File

@@ -0,0 +1,59 @@
import type {
IRaceProtestsPresenter,
RaceProtestViewModel,
RaceProtestsViewModel,
} from '@gridpilot/racing/application/presenters/IRaceProtestsPresenter';
import type { ProtestStatus, ProtestIncident } from '@gridpilot/racing/domain/entities/Protest';
export class RaceProtestsPresenter implements IRaceProtestsPresenter {
private viewModel: RaceProtestsViewModel | null = null;
present(
protests: Array<{
id: string;
raceId: string;
protestingDriverId: string;
accusedDriverId: string;
incident: ProtestIncident;
comment?: string;
proofVideoUrl?: string;
status: ProtestStatus;
reviewedBy?: string;
decisionNotes?: string;
filedAt: Date;
reviewedAt?: Date;
}>,
driverMap: Map<string, string>
): RaceProtestsViewModel {
const protestViewModels: RaceProtestViewModel[] = protests.map(protest => ({
id: protest.id,
raceId: protest.raceId,
protestingDriverId: protest.protestingDriverId,
protestingDriverName: driverMap.get(protest.protestingDriverId) || 'Unknown',
accusedDriverId: protest.accusedDriverId,
accusedDriverName: driverMap.get(protest.accusedDriverId) || 'Unknown',
incident: protest.incident,
comment: protest.comment,
proofVideoUrl: protest.proofVideoUrl,
status: protest.status,
reviewedBy: protest.reviewedBy,
reviewedByName: protest.reviewedBy ? driverMap.get(protest.reviewedBy) : undefined,
decisionNotes: protest.decisionNotes,
filedAt: protest.filedAt.toISOString(),
reviewedAt: protest.reviewedAt?.toISOString(),
}));
this.viewModel = {
protests: protestViewModels,
};
return this.viewModel;
}
getViewModel(): RaceProtestsViewModel {
if (!this.viewModel) {
throw new Error('Presenter has not been called yet');
}
return this.viewModel;
}
}

View File

@@ -0,0 +1,24 @@
import type {
IRaceRegistrationsPresenter,
RaceRegistrationsViewModel,
} from '@gridpilot/racing/application/presenters/IRaceRegistrationsPresenter';
export class RaceRegistrationsPresenter implements IRaceRegistrationsPresenter {
private viewModel: RaceRegistrationsViewModel | null = null;
present(registeredDriverIds: string[]): RaceRegistrationsViewModel {
this.viewModel = {
registeredDriverIds,
count: registeredDriverIds.length,
};
return this.viewModel;
}
getViewModel(): RaceRegistrationsViewModel {
if (!this.viewModel) {
throw new Error('Presenter has not been called yet');
}
return this.viewModel;
}
}

View File

@@ -0,0 +1,49 @@
import type {
IRaceWithSOFPresenter,
RaceWithSOFViewModel,
} from '@gridpilot/racing/application/presenters/IRaceWithSOFPresenter';
export class RaceWithSOFPresenter implements IRaceWithSOFPresenter {
private viewModel: RaceWithSOFViewModel | null = null;
present(
raceId: string,
leagueId: string,
scheduledAt: Date,
track: string,
trackId: string,
car: string,
carId: string,
sessionType: string,
status: string,
strengthOfField: number | null,
registeredCount: number,
maxParticipants: number,
participantCount: number
): RaceWithSOFViewModel {
this.viewModel = {
id: raceId,
leagueId,
scheduledAt: scheduledAt.toISOString(),
track,
trackId,
car,
carId,
sessionType,
status,
strengthOfField,
registeredCount,
maxParticipants,
participantCount,
};
return this.viewModel;
}
getViewModel(): RaceWithSOFViewModel {
if (!this.viewModel) {
throw new Error('Presenter has not been called yet');
}
return this.viewModel;
}
}

View File

@@ -0,0 +1,64 @@
import type {
IRacesPagePresenter,
RacesPageViewModel,
RaceListItemViewModel,
} from '@gridpilot/racing/application/presenters/IRacesPagePresenter';
export class RacesPagePresenter implements IRacesPagePresenter {
private viewModel: RacesPageViewModel | null = null;
present(races: any[]): void {
const now = new Date();
const nextWeek = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
const raceViewModels: RaceListItemViewModel[] = races.map(race => ({
id: race.id,
track: race.track,
car: race.car,
scheduledAt: race.scheduledAt,
status: race.status,
leagueId: race.leagueId,
leagueName: race.leagueName,
strengthOfField: race.strengthOfField,
isUpcoming: race.isUpcoming,
isLive: race.isLive,
isPast: race.isPast,
}));
const stats = {
total: raceViewModels.length,
scheduled: raceViewModels.filter(r => r.status === 'scheduled').length,
running: raceViewModels.filter(r => r.status === 'running').length,
completed: raceViewModels.filter(r => r.status === 'completed').length,
};
const liveRaces = raceViewModels.filter(r => r.isLive);
const upcomingThisWeek = raceViewModels
.filter(r => {
const scheduledDate = new Date(r.scheduledAt);
return r.isUpcoming && scheduledDate >= now && scheduledDate <= nextWeek;
})
.slice(0, 5);
const recentResults = raceViewModels
.filter(r => r.status === 'completed')
.sort((a, b) => new Date(b.scheduledAt).getTime() - new Date(a.scheduledAt).getTime())
.slice(0, 3);
this.viewModel = {
races: raceViewModels,
stats,
liveRaces,
upcomingThisWeek,
recentResults,
};
}
getViewModel(): RacesPageViewModel {
if (!this.viewModel) {
throw new Error('ViewModel not yet generated. Call present() first.');
}
return this.viewModel;
}
}

View File

@@ -0,0 +1,14 @@
import type { ISponsorDashboardPresenter } from '@racing/application/presenters/ISponsorDashboardPresenter';
import type { SponsorDashboardDTO } from '@racing/application/use-cases/GetSponsorDashboardQuery';
export class SponsorDashboardPresenter implements ISponsorDashboardPresenter {
private data: SponsorDashboardDTO | null = null;
present(data: SponsorDashboardDTO | null): void {
this.data = data;
}
getData(): SponsorDashboardDTO | null {
return this.data;
}
}

View File

@@ -0,0 +1,14 @@
import type { ISponsorSponsorshipsPresenter } from '@racing/application/presenters/ISponsorSponsorshipsPresenter';
import type { SponsorSponsorshipsDTO } from '@racing/application/use-cases/GetSponsorSponsorshipsQuery';
export class SponsorSponsorshipsPresenter implements ISponsorSponsorshipsPresenter {
private data: SponsorSponsorshipsDTO | null = null;
present(data: SponsorSponsorshipsDTO | null): void {
this.data = data;
}
getData(): SponsorSponsorshipsDTO | null {
return this.data;
}
}

View File

@@ -0,0 +1,48 @@
import type { Team, TeamMembership } from '@gridpilot/racing/domain/entities/Team';
import type {
ITeamDetailsPresenter,
TeamDetailsViewModel,
} from '@gridpilot/racing/application/presenters/ITeamDetailsPresenter';
export class TeamDetailsPresenter implements ITeamDetailsPresenter {
private viewModel: TeamDetailsViewModel | null = null;
present(
team: Team,
membership: TeamMembership | null,
driverId: string
): TeamDetailsViewModel {
const canManage = membership?.role === 'owner' || membership?.role === 'manager';
this.viewModel = {
team: {
id: team.id,
name: team.name,
tag: team.tag,
description: team.description,
ownerId: team.ownerId,
leagues: team.leagues,
specialization: team.specialization as 'endurance' | 'sprint' | 'mixed' | undefined,
region: team.region,
languages: team.languages,
},
membership: membership
? {
role: membership.role,
joinedAt: membership.joinedAt.toISOString(),
isActive: membership.isActive,
}
: null,
canManage,
};
return this.viewModel;
}
getViewModel(): TeamDetailsViewModel {
if (!this.viewModel) {
throw new Error('Presenter has not been called yet');
}
return this.viewModel;
}
}

View File

@@ -0,0 +1,43 @@
import type { TeamJoinRequest } from '@gridpilot/racing/domain/entities/Team';
import type {
ITeamJoinRequestsPresenter,
TeamJoinRequestViewModel,
TeamJoinRequestsViewModel,
} from '@gridpilot/racing/application/presenters/ITeamJoinRequestsPresenter';
export class TeamJoinRequestsPresenter implements ITeamJoinRequestsPresenter {
private viewModel: TeamJoinRequestsViewModel | null = null;
present(
requests: TeamJoinRequest[],
driverNames: Record<string, string>,
avatarUrls: Record<string, string>
): TeamJoinRequestsViewModel {
const requestItems: TeamJoinRequestViewModel[] = requests.map((request) => ({
requestId: request.id,
driverId: request.driverId,
driverName: driverNames[request.driverId] ?? 'Unknown Driver',
teamId: request.teamId,
status: request.status,
requestedAt: request.requestedAt.toISOString(),
avatarUrl: avatarUrls[request.driverId] ?? '',
}));
const pendingCount = requestItems.filter((r) => r.status === 'pending').length;
this.viewModel = {
requests: requestItems,
pendingCount,
totalCount: requestItems.length,
};
return this.viewModel;
}
getViewModel(): TeamJoinRequestsViewModel {
if (!this.viewModel) {
throw new Error('Presenter has not been called yet');
}
return this.viewModel;
}
}

View File

@@ -0,0 +1,46 @@
import type { TeamMembership } from '@gridpilot/racing/domain/entities/Team';
import type {
ITeamMembersPresenter,
TeamMemberViewModel,
TeamMembersViewModel,
} from '@gridpilot/racing/application/presenters/ITeamMembersPresenter';
export class TeamMembersPresenter implements ITeamMembersPresenter {
private viewModel: TeamMembersViewModel | null = null;
present(
memberships: TeamMembership[],
driverNames: Record<string, string>,
avatarUrls: Record<string, string>
): TeamMembersViewModel {
const members: TeamMemberViewModel[] = memberships.map((membership) => ({
driverId: membership.driverId,
driverName: driverNames[membership.driverId] ?? 'Unknown Driver',
role: membership.role,
joinedAt: membership.joinedAt.toISOString(),
isActive: membership.isActive,
avatarUrl: avatarUrls[membership.driverId] ?? '',
}));
const ownerCount = members.filter((m) => m.role === 'owner').length;
const managerCount = members.filter((m) => m.role === 'manager').length;
const memberCount = members.filter((m) => m.role === 'member').length;
this.viewModel = {
members,
totalCount: members.length,
ownerCount,
managerCount,
memberCount,
};
return this.viewModel;
}
getViewModel(): TeamMembersViewModel {
if (!this.viewModel) {
throw new Error('Presenter has not been called yet');
}
return this.viewModel;
}
}

View File

@@ -0,0 +1,42 @@
import type {
ITeamsLeaderboardPresenter,
TeamsLeaderboardViewModel,
TeamLeaderboardItemViewModel,
SkillLevel,
} from '@gridpilot/racing/application/presenters/ITeamsLeaderboardPresenter';
export class TeamsLeaderboardPresenter implements ITeamsLeaderboardPresenter {
private viewModel: TeamsLeaderboardViewModel | null = null;
present(teams: any[], recruitingCount: number): void {
this.viewModel = {
teams: teams.map((team) => this.transformTeam(team)),
recruitingCount,
};
}
getViewModel(): TeamsLeaderboardViewModel {
if (!this.viewModel) {
throw new Error('ViewModel not yet generated. Call present() first.');
}
return this.viewModel;
}
private transformTeam(team: any): TeamLeaderboardItemViewModel {
return {
id: team.id,
name: team.name,
memberCount: team.memberCount,
rating: team.rating,
totalWins: team.totalWins,
totalRaces: team.totalRaces,
performanceLevel: team.performanceLevel as SkillLevel,
isRecruiting: team.isRecruiting,
createdAt: team.createdAt,
description: team.description,
specialization: team.specialization,
region: team.region,
languages: team.languages,
};
}
}