This commit is contained in:
2025-12-11 21:06:25 +01:00
parent c49ea2598d
commit ec3ddc3a5c
227 changed files with 3496 additions and 2083 deletions

View File

@@ -10,7 +10,7 @@ import { Season } from '@gridpilot/racing/domain/entities/Season';
import { Sponsor } from '@gridpilot/racing/domain/entities/Sponsor';
import { SeasonSponsorship } from '@gridpilot/racing/domain/entities/SeasonSponsorship';
import { Money } from '@gridpilot/racing/domain/value-objects/Money';
import type { LeagueMembership, JoinRequest } from '@gridpilot/racing/domain/entities/LeagueMembership';
import type { JoinRequest } from '@gridpilot/racing/domain/entities/LeagueMembership';
import type { IDriverRepository } from '@gridpilot/racing/domain/repositories/IDriverRepository';
import type { ILeagueRepository } from '@gridpilot/racing/domain/repositories/ILeagueRepository';
@@ -139,7 +139,6 @@ import { TransferLeagueOwnershipUseCase } from '@gridpilot/racing/application/us
import { CancelRaceUseCase } from '@gridpilot/racing/application/use-cases/CancelRaceUseCase';
import { ImportRaceResultsUseCase } from '@gridpilot/racing/application/use-cases/ImportRaceResultsUseCase';
import { DriversLeaderboardPresenter } from './presenters/DriversLeaderboardPresenter';
import { TeamsLeaderboardPresenter } from './presenters/TeamsLeaderboardPresenter';
import { RacesPagePresenter } from './presenters/RacesPagePresenter';
import { AllRacesPagePresenter } from './presenters/AllRacesPagePresenter';
import { AllTeamsPresenter } from './presenters/AllTeamsPresenter';
@@ -195,7 +194,24 @@ export function configureDIContainer(): void {
const primaryDriverId = seedData.drivers[0]!.id;
// Create driver statistics from seed data
const driverStats = createDemoDriverStats(seedData.drivers);
type DemoDriverStatsEntry = {
rating?: number;
wins?: number;
podiums?: number;
dnfs?: number;
totalRaces?: number;
avgFinish?: number;
bestFinish?: number;
worstFinish?: number;
overallRank?: number;
consistency?: number;
percentile?: number;
driverId?: string;
};
type DemoDriverStatsMap = Record<string, DemoDriverStatsEntry>;
const driverStats: DemoDriverStatsMap = createDemoDriverStats(seedData.drivers);
// Register repositories
container.registerInstance<IDriverRepository>(
@@ -228,7 +244,11 @@ export function configureDIContainer(): void {
);
// Race registrations - seed from results for completed races, plus some upcoming races
const seedRaceRegistrations: Array<{ raceId: string; driverId: string; registeredAt: Date }> = [];
const seedRaceRegistrations: Array<{
raceId: string;
driverId: string;
registeredAt: Date;
}> = [];
// For completed races, extract driver registrations from results
for (const result of seedData.results) {
@@ -280,6 +300,8 @@ export function configureDIContainer(): void {
const seededPenalties: Penalty[] = [];
const seededProtests: Protest[] = [];
type ProtestProps = Parameters<(typeof Protest)['create']>[0];
racesForProtests.forEach(({ race, leagueIndex: leagueIdx }, raceIndex) => {
const raceResults = seedData.results.filter(r => r.raceId === race.id);
if (raceResults.length < 4) return;
@@ -291,33 +313,51 @@ export function configureDIContainer(): void {
if (!protestingResult || !accusedResult) continue;
const protestStatuses: Array<'pending' | 'under_review' | 'upheld' | 'dismissed'> = ['pending', 'under_review', 'upheld', 'dismissed'];
const status = protestStatuses[(raceIndex + i) % protestStatuses.length];
const protestStatuses = [
'pending',
'under_review',
'upheld',
'dismissed',
] as const;
const status =
protestStatuses[(raceIndex + i) % protestStatuses.length] ?? 'pending';
const protest = Protest.create({
const protestProps: ProtestProps = {
id: `protest-${race.id}-${i}`,
raceId: race.id,
protestingDriverId: protestingResult.driverId,
accusedDriverId: accusedResult.driverId,
incident: {
lap: 5 + i * 3,
description: i === 0
? 'Unsafe rejoining to the track after going off, causing contact'
: 'Aggressive defending, pushing competitor off track',
description:
i === 0
? 'Unsafe rejoining to the track after going off, causing contact'
: 'Aggressive defending, pushing competitor off track',
},
comment: i === 0
? 'Driver rejoined directly into my racing line, causing contact and damaging my front wing.'
: 'Driver moved under braking multiple times, forcing me off the circuit.',
comment:
i === 0
? 'Driver rejoined directly into my racing line, causing contact and damaging my front wing.'
: 'Driver moved under braking multiple times, forcing me off the circuit.',
status,
filedAt: new Date(Date.now() - (raceIndex + 1) * 24 * 60 * 60 * 1000),
reviewedBy: status !== 'pending' ? primaryDriverId : undefined,
decisionNotes: status === 'upheld'
? 'After reviewing the evidence, the accused driver is found at fault. Penalty applied.'
: status === 'dismissed'
? 'No clear fault found. Racing incident.'
: undefined,
reviewedAt: status !== 'pending' ? new Date(Date.now() - raceIndex * 24 * 60 * 60 * 1000) : undefined,
});
};
if (status !== 'pending') {
protestProps.reviewedBy = primaryDriverId;
protestProps.reviewedAt = new Date(
Date.now() - raceIndex * 24 * 60 * 60 * 1000,
);
}
if (status === 'upheld') {
protestProps.decisionNotes =
'After reviewing the evidence, the accused driver is found at fault. Penalty applied.';
} else if (status === 'dismissed') {
protestProps.decisionNotes =
'No clear fault found. Racing incident.';
}
const protest = Protest.create(protestProps);
seededProtests.push(protest);
@@ -448,7 +488,15 @@ export function configureDIContainer(): void {
);
// League memberships
const seededMemberships: LeagueMembership[] = seedData.memberships.map((m) => ({
type SeedMembership = {
leagueId: string;
driverId: string;
role: 'member' | 'owner' | 'admin' | 'steward';
status: 'active';
joinedAt: Date;
};
const seededMemberships: SeedMembership[] = seedData.memberships.map((m) => ({
leagueId: m.leagueId,
driverId: m.driverId,
role: 'member',
@@ -476,11 +524,11 @@ export function configureDIContainer(): void {
// Ensure primary driver owns at least one league
const hasPrimaryOwnerMembership = seededMemberships.some(
(m: LeagueMembership) => m.driverId === primaryDriverId && m.role === 'owner',
(m) => m.driverId === primaryDriverId && m.role === 'owner',
);
if (!hasPrimaryOwnerMembership && seedData.leagues.length > 0) {
const targetLeague =
seedData.leagues.find((l) => l.ownerId === primaryDriverId) ?? seedData.leagues[0];
seedData.leagues.find((l) => l.ownerId === primaryDriverId) ?? seedData.leagues[0]!;
const existingForPrimary = seededMemberships.find(
(m) => m.leagueId === targetLeague.id && m.driverId === primaryDriverId,
@@ -574,23 +622,36 @@ export function configureDIContainer(): void {
'Heard great things about this league. Can I join?',
'Experienced driver looking for competitive racing.',
'My friend recommended this league. Hope to race with you!',
];
] as const;
const message =
messages[(index + leagueIndex) % messages.length] ?? messages[0];
seededJoinRequests.push({
id: `join-${league.id}-${driver.id}`,
leagueId: league.id,
driverId: driver.id,
requestedAt: new Date(Date.now() - (index + 1 + leagueIndex) * 24 * 60 * 60 * 1000),
message: messages[(index + leagueIndex) % messages.length],
requestedAt: new Date(
Date.now() - (index + 1 + leagueIndex) * 24 * 60 * 60 * 1000,
),
message,
});
});
});
type InMemoryLeagueMembershipSeed = ConstructorParameters<
typeof InMemoryLeagueMembershipRepository
>[0];
container.registerInstance<ILeagueMembershipRepository>(
DI_TOKENS.LeagueMembershipRepository,
new InMemoryLeagueMembershipRepository(seededMemberships, seededJoinRequests)
new InMemoryLeagueMembershipRepository(
seededMemberships as InMemoryLeagueMembershipSeed,
seededJoinRequests,
)
);
// Team repositories
type InMemoryTeamSeed = ConstructorParameters<typeof InMemoryTeamRepository>[0];
container.registerInstance<ITeamRepository>(
DI_TOKENS.TeamRepository,
new InMemoryTeamRepository(
@@ -602,8 +663,8 @@ export function configureDIContainer(): void {
ownerId: seedData.drivers[0]!.id,
leagues: [t.primaryLeagueId],
createdAt: new Date(),
}))
)
})) as InMemoryTeamSeed,
),
);
container.registerInstance<ITeamMembershipRepository>(
@@ -644,16 +705,13 @@ export function configureDIContainer(): void {
);
const sponsorRepo = new InMemorySponsorRepository();
// Use synchronous seeding via internal method
seededSponsors.forEach(sponsor => {
(sponsorRepo as any).sponsors.set(sponsor.id, sponsor);
});
sponsorRepo.seed(seededSponsors);
container.registerInstance<ISponsorRepository>(
DI_TOKENS.SponsorRepository,
sponsorRepo
);
const seededSponsorships = seedData.seasonSponsorships.map(ss =>
const seededSponsorships = seedData.seasonSponsorships.map((ss) =>
SeasonSponsorship.create({
id: ss.id,
seasonId: ss.seasonId,
@@ -661,15 +719,12 @@ export function configureDIContainer(): void {
tier: ss.tier,
pricing: Money.create(ss.pricingAmount, ss.pricingCurrency),
status: ss.status,
description: ss.description,
})
description: ss.description ?? '',
}),
);
const seasonSponsorshipRepo = new InMemorySeasonSponsorshipRepository();
// Use synchronous seeding via internal method
seededSponsorships.forEach(sponsorship => {
(seasonSponsorshipRepo as any).sponsorships.set(sponsorship.id, sponsorship);
});
seasonSponsorshipRepo.seed(seededSponsorships);
container.registerInstance<ISeasonSponsorshipRepository>(
DI_TOKENS.SeasonSponsorshipRepository,
seasonSponsorshipRepo
@@ -691,9 +746,9 @@ export function configureDIContainer(): void {
);
// Seed sponsorship requests from demo data
seedData.sponsorshipRequests?.forEach(request => {
(sponsorshipRequestRepo as any).requests.set(request.id, request);
});
if (seedData.sponsorshipRequests && seedData.sponsorshipRequests.length > 0) {
sponsorshipRequestRepo.seed(seedData.sponsorshipRequests);
}
// Social repositories
container.registerInstance<IFeedRepository>(
@@ -732,7 +787,7 @@ export function configureDIContainer(): void {
);
// Register driver stats for access by utility functions
container.registerInstance(
container.registerInstance<DemoDriverStatsMap>(
DI_TOKENS.DriverStats,
driverStats
);
@@ -741,7 +796,8 @@ export function configureDIContainer(): void {
const driverRatingProvider: DriverRatingProvider = {
getRating: (driverId: string): number | null => {
const stats = driverStats[driverId];
return stats?.rating ?? null;
const rating = stats?.rating;
return typeof rating === 'number' ? rating : null;
},
getRatings: (driverIds: string[]): Map<string, number> => {
const result = new Map<string, number>();
@@ -905,7 +961,7 @@ export function configureDIContainer(): void {
const leagueStandingsPresenter = new LeagueStandingsPresenter();
container.registerInstance(
DI_TOKENS.GetLeagueStandingsUseCase,
new GetLeagueStandingsUseCase(standingRepository, leagueStandingsPresenter)
new GetLeagueStandingsUseCase(standingRepository),
);
const leagueDriverSeasonStatsPresenter = new LeagueDriverSeasonStatsPresenter();
@@ -919,7 +975,7 @@ export function configureDIContainer(): void {
{
getRating: (driverId: string) => {
const stats = driverStats[driverId];
if (!stats) {
if (!stats || typeof stats.rating !== 'number') {
return { rating: null, ratingChange: null };
}
const baseline = 1500;
@@ -930,8 +986,8 @@ export function configureDIContainer(): void {
};
},
},
leagueDriverSeasonStatsPresenter
)
leagueDriverSeasonStatsPresenter,
),
);
const allLeaguesWithCapacityPresenter = new AllLeaguesWithCapacityPresenter();
@@ -961,7 +1017,7 @@ export function configureDIContainer(): void {
const leagueScoringPresetsPresenter = new LeagueScoringPresetsPresenter();
container.registerInstance(
DI_TOKENS.ListLeagueScoringPresetsUseCase,
new ListLeagueScoringPresetsUseCase(leagueScoringPresetProvider, leagueScoringPresetsPresenter)
new ListLeagueScoringPresetsUseCase(leagueScoringPresetProvider)
);
const leagueScoringConfigPresenter = new LeagueScoringConfigPresenter();
@@ -977,7 +1033,6 @@ export function configureDIContainer(): void {
)
);
const leagueFullConfigPresenter = new LeagueFullConfigPresenter();
container.registerInstance(
DI_TOKENS.GetLeagueFullConfigUseCase,
new GetLeagueFullConfigUseCase(
@@ -985,14 +1040,13 @@ export function configureDIContainer(): void {
seasonRepository,
leagueScoringConfigRepository,
gameRepository,
leagueFullConfigPresenter
)
);
const leagueSchedulePreviewPresenter = new LeagueSchedulePreviewPresenter();
container.registerInstance(
DI_TOKENS.PreviewLeagueScheduleUseCase,
new PreviewLeagueScheduleUseCase(undefined, leagueSchedulePreviewPresenter)
new PreviewLeagueScheduleUseCase(undefined, leagueSchedulePreviewPresenter),
);
const raceWithSOFPresenter = new RaceWithSOFPresenter();
@@ -1031,6 +1085,8 @@ export function configureDIContainer(): void {
new GetAllRacesPageDataUseCase(raceRepository, leagueRepository, allRacesPagePresenter)
);
const imageService = container.resolve<ImageServicePort>(DI_TOKENS.ImageService);
const raceDetailPresenter = new RaceDetailPresenter();
container.registerInstance(
DI_TOKENS.GetRaceDetailUseCase,
@@ -1075,38 +1131,38 @@ export function configureDIContainer(): void {
// 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 stats = getDIContainer().resolve<DemoDriverStatsMap>(DI_TOKENS.DriverStats);
return Object.entries(stats)
.map(([driverId, stat]) => ({
driverId,
rating: stat.rating ?? 0,
overallRank: stat.overallRank ?? null,
}))
.sort((a, b) => (b.rating ?? 0) - (a.rating ?? 0));
},
};
const driverStatsService = {
getDriverStats: (driverId: string) => {
const stats = getDIContainer().resolve<Record<string, any>>(DI_TOKENS.DriverStats);
return stats[driverId] || null;
}
const stats = getDIContainer().resolve<DemoDriverStatsMap>(DI_TOKENS.DriverStats);
return stats[driverId] ?? null;
},
};
const imageService = getDIContainer().resolve<ImageServicePort>(DI_TOKENS.ImageService);
const driversPresenter = new DriversLeaderboardPresenter();
container.registerInstance(
DI_TOKENS.GetDriversLeaderboardUseCase,
new GetDriversLeaderboardUseCase(
driverRepository,
rankingService as any,
driverStatsService as any,
rankingService,
driverStatsService,
imageService,
driversPresenter
)
);
const getDriverStatsAdapter = (driverId: string) => {
const stats = getDIContainer().resolve<Record<string, any>>(DI_TOKENS.DriverStats);
const stats = getDIContainer().resolve<DemoDriverStatsMap>(DI_TOKENS.DriverStats);
const stat = stats[driverId];
if (!stat) return null;
return {
@@ -1116,7 +1172,6 @@ export function configureDIContainer(): void {
};
};
const teamsPresenter = new TeamsLeaderboardPresenter();
container.registerInstance(
DI_TOKENS.GetTeamsLeaderboardUseCase,
new GetTeamsLeaderboardUseCase(
@@ -1124,12 +1179,11 @@ export function configureDIContainer(): void {
teamMembershipRepository,
driverRepository,
getDriverStatsAdapter,
teamsPresenter
)
);
const getDriverStatsForDashboard = (driverId: string) => {
const stats = getDIContainer().resolve<Record<string, any>>(DI_TOKENS.DriverStats);
const stats = getDIContainer().resolve<DemoDriverStatsMap>(DI_TOKENS.DriverStats);
const stat = stats[driverId];
if (!stat) return null;
return {
@@ -1143,7 +1197,7 @@ export function configureDIContainer(): void {
};
const getDriverStatsForProfile = (driverId: string) => {
const stats = getDIContainer().resolve<Record<string, any>>(DI_TOKENS.DriverStats);
const stats = getDIContainer().resolve<DemoDriverStatsMap>(DI_TOKENS.DriverStats);
const stat = stats[driverId];
if (!stat) return null;
return {
@@ -1204,7 +1258,7 @@ export function configureDIContainer(): void {
const allTeamsPresenter = new AllTeamsPresenter();
container.registerInstance(
DI_TOKENS.GetAllTeamsUseCase,
new GetAllTeamsUseCase(teamRepository, teamMembershipRepository, allTeamsPresenter)
new GetAllTeamsUseCase(teamRepository, teamMembershipRepository),
);
const teamDetailsPresenter = new TeamDetailsPresenter();
@@ -1216,13 +1270,18 @@ export function configureDIContainer(): void {
const teamMembersPresenter = new TeamMembersPresenter();
container.registerInstance(
DI_TOKENS.GetTeamMembersUseCase,
new GetTeamMembersUseCase(teamMembershipRepository, driverRepository, imageService, teamMembersPresenter)
new GetTeamMembersUseCase(teamMembershipRepository, driverRepository, imageService, teamMembersPresenter),
);
const teamJoinRequestsPresenter = new TeamJoinRequestsPresenter();
container.registerInstance(
DI_TOKENS.GetTeamJoinRequestsUseCase,
new GetTeamJoinRequestsUseCase(teamMembershipRepository, driverRepository, imageService, teamJoinRequestsPresenter)
new GetTeamJoinRequestsUseCase(
teamMembershipRepository,
driverRepository,
imageService,
teamJoinRequestsPresenter,
),
);
const driverTeamPresenter = new DriverTeamPresenter();
@@ -1235,13 +1294,13 @@ export function configureDIContainer(): void {
const raceProtestsPresenter = new RaceProtestsPresenter();
container.registerInstance(
DI_TOKENS.GetRaceProtestsUseCase,
new GetRaceProtestsUseCase(protestRepository, driverRepository, raceProtestsPresenter)
new GetRaceProtestsUseCase(protestRepository, driverRepository)
);
const racePenaltiesPresenter = new RacePenaltiesPresenter();
container.registerInstance(
DI_TOKENS.GetRacePenaltiesUseCase,
new GetRacePenaltiesUseCase(penaltyRepository, driverRepository, racePenaltiesPresenter)
new GetRacePenaltiesUseCase(penaltyRepository, driverRepository)
);
// Register queries - Notifications
@@ -1286,13 +1345,11 @@ 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.GetPendingSponsorshipRequestsUseCase,
new GetPendingSponsorshipRequestsUseCase(
sponsorshipRequestRepository,
sponsorRepository,
pendingSponsorshipRequestsPresenter
)
);