import 'reflect-metadata'; import { container } from 'tsyringe'; import { DI_TOKENS } from './di-tokens'; // Domain entities and repositories import { Penalty } from '@gridpilot/racing/domain/entities/Penalty'; import { Protest } from '@gridpilot/racing/domain/entities/Protest'; import { Game } from '@gridpilot/racing/domain/entities/Game'; 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 { 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'; import type { IRaceRepository } from '@gridpilot/racing/domain/repositories/IRaceRepository'; import type { IResultRepository } from '@gridpilot/racing/domain/repositories/IResultRepository'; import type { IStandingRepository } from '@gridpilot/racing/domain/repositories/IStandingRepository'; import type { IPenaltyRepository } from '@gridpilot/racing/domain/repositories/IPenaltyRepository'; import type { IProtestRepository } from '@gridpilot/racing/domain/repositories/IProtestRepository'; import type { IGameRepository } from '@gridpilot/racing/domain/repositories/IGameRepository'; import type { ISeasonRepository } from '@gridpilot/racing/domain/repositories/ISeasonRepository'; import type { ILeagueScoringConfigRepository } from '@gridpilot/racing/domain/repositories/ILeagueScoringConfigRepository'; import type { ITrackRepository } from '@gridpilot/racing/domain/repositories/ITrackRepository'; import type { ICarRepository } from '@gridpilot/racing/domain/repositories/ICarRepository'; import type { ITeamRepository, ITeamMembershipRepository, IRaceRegistrationRepository, ISponsorRepository, ISeasonSponsorshipRepository, ISponsorshipRequestRepository, ISponsorshipPricingRepository, } from '@gridpilot/racing'; import type { ILeagueMembershipRepository } from '@gridpilot/racing/domain/repositories/ILeagueMembershipRepository'; import type { IFeedRepository } from '@gridpilot/social/domain/repositories/IFeedRepository'; import type { ISocialGraphRepository } from '@gridpilot/social/domain/repositories/ISocialGraphRepository'; import type { ImageServicePort } from '@gridpilot/media'; // Notifications import type { INotificationRepository, INotificationPreferenceRepository } from '@gridpilot/notifications/application'; import { SendNotificationUseCase, MarkNotificationReadUseCase, GetUnreadNotificationsUseCase } from '@gridpilot/notifications/application'; import { InMemoryNotificationRepository, InMemoryNotificationPreferenceRepository, NotificationGatewayRegistry, InAppNotificationAdapter, } from '@gridpilot/notifications/infrastructure'; // Infrastructure repositories import { InMemoryDriverRepository } from '@gridpilot/racing/infrastructure/repositories/InMemoryDriverRepository'; import { InMemoryLeagueRepository } from '@gridpilot/racing/infrastructure/repositories/InMemoryLeagueRepository'; import { InMemoryRaceRepository } from '@gridpilot/racing/infrastructure/repositories/InMemoryRaceRepository'; import { InMemoryResultRepository } from '@gridpilot/racing/infrastructure/repositories/InMemoryResultRepository'; import { InMemoryStandingRepository } from '@gridpilot/racing/infrastructure/repositories/InMemoryStandingRepository'; import { InMemoryPenaltyRepository } from '@gridpilot/racing/infrastructure/repositories/InMemoryPenaltyRepository'; import { InMemoryProtestRepository } from '@gridpilot/racing/infrastructure/repositories/InMemoryProtestRepository'; import { InMemoryTrackRepository } from '@gridpilot/racing/infrastructure/repositories/InMemoryTrackRepository'; import { InMemoryCarRepository } from '@gridpilot/racing/infrastructure/repositories/InMemoryCarRepository'; import { InMemoryGameRepository, InMemorySeasonRepository, InMemoryLeagueScoringConfigRepository, getLeagueScoringPresetById, } from '@gridpilot/racing/infrastructure/repositories/InMemoryScoringRepositories'; import { InMemoryLeagueScoringPresetProvider } from '@gridpilot/racing/infrastructure/repositories/InMemoryLeagueScoringPresetProvider'; import { InMemorySponsorRepository } from '@gridpilot/racing/infrastructure/repositories/InMemorySponsorRepository'; import { InMemorySeasonSponsorshipRepository } from '@gridpilot/racing/infrastructure/repositories/InMemorySeasonSponsorshipRepository'; import { InMemorySponsorshipRequestRepository } from '@gridpilot/racing/infrastructure/repositories/InMemorySponsorshipRequestRepository'; import { InMemorySponsorshipPricingRepository } from '@gridpilot/racing/infrastructure/repositories/InMemorySponsorshipPricingRepository'; import { InMemoryTeamRepository } from '@gridpilot/racing/infrastructure/repositories/InMemoryTeamRepository'; import { InMemoryTeamMembershipRepository } from '@gridpilot/racing/infrastructure/repositories/InMemoryTeamMembershipRepository'; import { InMemoryRaceRegistrationRepository } from '@gridpilot/racing/infrastructure/repositories/InMemoryRaceRegistrationRepository'; import { InMemoryLeagueMembershipRepository } from '@gridpilot/racing/infrastructure/repositories/InMemoryLeagueMembershipRepository'; import { InMemoryFeedRepository, InMemorySocialGraphRepository, } from '@gridpilot/social/infrastructure/inmemory/InMemorySocialAndFeed'; import { DemoImageServiceAdapter } from '@gridpilot/testing-support'; // Application use cases and queries import { JoinLeagueUseCase, RegisterForRaceUseCase, WithdrawFromRaceUseCase, CreateTeamUseCase, JoinTeamUseCase, LeaveTeamUseCase, ApproveTeamJoinRequestUseCase, RejectTeamJoinRequestUseCase, UpdateTeamUseCase, GetAllTeamsUseCase, GetTeamDetailsUseCase, GetTeamMembersUseCase, GetTeamJoinRequestsUseCase, GetDriverTeamUseCase, CreateLeagueWithSeasonAndScoringUseCase, FileProtestUseCase, ReviewProtestUseCase, ApplyPenaltyUseCase, RequestProtestDefenseUseCase, SubmitProtestDefenseUseCase, GetSponsorDashboardUseCase, GetSponsorSponsorshipsUseCase, GetPendingSponsorshipRequestsUseCase, GetEntitySponsorshipPricingUseCase, ApplyForSponsorshipUseCase, AcceptSponsorshipRequestUseCase, RejectSponsorshipRequestUseCase, } from '@gridpilot/racing/application'; import { GetDashboardOverviewUseCase } from '@gridpilot/racing/application/use-cases/GetDashboardOverviewUseCase'; import { GetProfileOverviewUseCase } from '@gridpilot/racing/application/use-cases/GetProfileOverviewUseCase'; import { UpdateDriverProfileUseCase } from '@gridpilot/racing/application/use-cases/UpdateDriverProfileUseCase'; import { IsDriverRegisteredForRaceUseCase } from '@gridpilot/racing/application/use-cases/IsDriverRegisteredForRaceUseCase'; import { GetRaceRegistrationsUseCase } from '@gridpilot/racing/application/use-cases/GetRaceRegistrationsUseCase'; import { GetRaceWithSOFUseCase } from '@gridpilot/racing/application/use-cases/GetRaceWithSOFUseCase'; import { GetRaceProtestsUseCase } from '@gridpilot/racing/application/use-cases/GetRaceProtestsUseCase'; import { GetRacePenaltiesUseCase } from '@gridpilot/racing/application/use-cases/GetRacePenaltiesUseCase'; import { GetLeagueStandingsUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueStandingsUseCase'; import { GetLeagueDriverSeasonStatsUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueDriverSeasonStatsUseCase'; import { GetAllLeaguesWithCapacityUseCase } from '@gridpilot/racing/application/use-cases/GetAllLeaguesWithCapacityUseCase'; import { GetAllLeaguesWithCapacityAndScoringUseCase } from '@gridpilot/racing/application/use-cases/GetAllLeaguesWithCapacityAndScoringUseCase'; import { ListLeagueScoringPresetsUseCase } from '@gridpilot/racing/application/use-cases/ListLeagueScoringPresetsUseCase'; import { GetLeagueScoringConfigUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueScoringConfigUseCase'; import { GetLeagueFullConfigUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueFullConfigUseCase'; import { GetLeagueStatsUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueStatsUseCase'; import { GetRacesPageDataUseCase } from '@gridpilot/racing/application/use-cases/GetRacesPageDataUseCase'; import { GetRaceDetailUseCase } from '@gridpilot/racing/application/use-cases/GetRaceDetailUseCase'; import { GetRaceResultsDetailUseCase } from '@gridpilot/racing/application/use-cases/GetRaceResultsDetailUseCase'; import { GetAllRacesPageDataUseCase } from '@gridpilot/racing/application/use-cases/GetAllRacesPageDataUseCase'; 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 { CancelRaceUseCase } from '@gridpilot/racing/application/use-cases/CancelRaceUseCase'; import { ImportRaceResultsUseCase } from '@gridpilot/racing/application/use-cases/ImportRaceResultsUseCase'; import { DriversLeaderboardPresenter } from './presenters/DriversLeaderboardPresenter'; import { RacesPagePresenter } from './presenters/RacesPagePresenter'; import { AllRacesPagePresenter } from './presenters/AllRacesPagePresenter'; import { AllTeamsPresenter } from './presenters/AllTeamsPresenter'; import { TeamDetailsPresenter } from './presenters/TeamDetailsPresenter'; import { TeamMembersPresenter } from './presenters/TeamMembersPresenter'; import { TeamJoinRequestsPresenter } from './presenters/TeamJoinRequestsPresenter'; import { DriverTeamPresenter } from './presenters/DriverTeamPresenter'; import { AllLeaguesWithCapacityPresenter } from './presenters/AllLeaguesWithCapacityPresenter'; import { AllLeaguesWithCapacityAndScoringPresenter } from './presenters/AllLeaguesWithCapacityAndScoringPresenter'; import { LeagueStatsPresenter } from './presenters/LeagueStatsPresenter'; import { LeagueScoringConfigPresenter } from './presenters/LeagueScoringConfigPresenter'; import { LeagueFullConfigPresenter } from './presenters/LeagueFullConfigPresenter'; import { LeagueDriverSeasonStatsPresenter } from './presenters/LeagueDriverSeasonStatsPresenter'; import { LeagueStandingsPresenter } from './presenters/LeagueStandingsPresenter'; import { LeagueScoringPresetsPresenter } from './presenters/LeagueScoringPresetsPresenter'; import { RaceWithSOFPresenter } from './presenters/RaceWithSOFPresenter'; import { RaceProtestsPresenter } from './presenters/RaceProtestsPresenter'; import { RacePenaltiesPresenter } from './presenters/RacePenaltiesPresenter'; import { RaceRegistrationsPresenter } from './presenters/RaceRegistrationsPresenter'; import { DriverRegistrationStatusPresenter } from './presenters/DriverRegistrationStatusPresenter'; import { RaceDetailPresenter } from './presenters/RaceDetailPresenter'; import { RaceResultsDetailPresenter } from './presenters/RaceResultsDetailPresenter'; import { ImportRaceResultsPresenter } from './presenters/ImportRaceResultsPresenter'; import type { DriverRatingProvider } from '@gridpilot/racing/application'; import type { LeagueScoringPresetProvider } from '@gridpilot/racing/application/ports/LeagueScoringPresetProvider'; import { PreviewLeagueScheduleUseCase } from '@gridpilot/racing/application'; import { SponsorDashboardPresenter } from './presenters/SponsorDashboardPresenter'; import { SponsorSponsorshipsPresenter } from './presenters/SponsorSponsorshipsPresenter'; import { PendingSponsorshipRequestsPresenter } from './presenters/PendingSponsorshipRequestsPresenter'; import { EntitySponsorshipPricingPresenter } from './presenters/EntitySponsorshipPricingPresenter'; import { LeagueSchedulePreviewPresenter } from './presenters/LeagueSchedulePreviewPresenter'; import { DashboardOverviewPresenter } from './presenters/DashboardOverviewPresenter'; import { ProfileOverviewPresenter } from './presenters/ProfileOverviewPresenter'; // Demo infrastructure (runtime demo seed & helpers) import { createStaticRacingSeed, getDemoLeagueArchetypeByName, DEMO_TRACKS, DEMO_CARS, createDemoDriverStats, } from '@gridpilot/testing-support'; /** * Configure the DI container with all bindings for the website application */ export function configureDIContainer(): void { // Clear any existing registrations container.clearInstances(); // Create seed data const seedData = createStaticRacingSeed(42); const primaryDriverId = seedData.drivers[0]!.id; // Create driver statistics from seed data 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; const driverStats: DemoDriverStatsMap = createDemoDriverStats(seedData.drivers); // Register repositories container.registerInstance( DI_TOKENS.DriverRepository, new InMemoryDriverRepository(seedData.drivers) ); container.registerInstance( DI_TOKENS.LeagueRepository, new InMemoryLeagueRepository(seedData.leagues) ); const raceRepository = new InMemoryRaceRepository(seedData.races); container.registerInstance(DI_TOKENS.RaceRepository, raceRepository); // Result repository needs race repository for league-based queries const resultRepository = new InMemoryResultRepository(seedData.results, raceRepository); container.registerInstance(DI_TOKENS.ResultRepository, resultRepository); // Standing repository needs all three for recalculation const leagueRepository = container.resolve(DI_TOKENS.LeagueRepository); container.registerInstance( DI_TOKENS.StandingRepository, new InMemoryStandingRepository( seedData.standings, resultRepository, raceRepository, leagueRepository ) ); // Race registrations - seed from results for completed races, plus some upcoming races const seedRaceRegistrations: Array<{ raceId: string; driverId: string; registeredAt: Date; }> = []; // For completed races, extract driver registrations from results for (const result of seedData.results) { seedRaceRegistrations.push({ raceId: result.raceId, driverId: result.driverId, registeredAt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), }); } // For some upcoming races, add random registrations const upcomingRaces = seedData.races.filter(r => r.status === 'scheduled').slice(0, 10); for (const race of upcomingRaces) { const participantCount = Math.floor(Math.random() * 12) + 8; const shuffledDrivers = [...seedData.drivers].sort(() => Math.random() - 0.5).slice(0, participantCount); for (const driver of shuffledDrivers) { seedRaceRegistrations.push({ raceId: race.id, driverId: driver.id, registeredAt: new Date(Date.now() - Math.floor(Math.random() * 5) * 24 * 60 * 60 * 1000), }); } } container.registerInstance( DI_TOKENS.RaceRegistrationRepository, new InMemoryRaceRegistrationRepository(seedRaceRegistrations) ); // Seed penalties and protests const completedRaces = seedData.races.filter(r => r.status === 'completed'); const racesByLeague = new Map(); for (const race of completedRaces) { const existing = racesByLeague.get(race.leagueId) || []; existing.push(race); racesByLeague.set(race.leagueId, existing); } const racesForProtests: Array<{ race: typeof completedRaces[0]; leagueIndex: number }> = []; let leagueIndex = 0; for (const [, leagueRaces] of racesByLeague) { const sorted = [...leagueRaces].sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime()); for (const race of sorted.slice(0, 2)) { racesForProtests.push({ race, leagueIndex }); } leagueIndex++; } 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; const protestCount = Math.min(2, raceResults.length - 2); for (let i = 0; i < protestCount; i++) { const protestingResult = raceResults[i + 2]; const accusedResult = raceResults[i]; if (!protestingResult || !accusedResult) continue; const protestStatuses = [ 'pending', 'under_review', 'upheld', 'dismissed', ] as const; const status = protestStatuses[(raceIndex + i) % protestStatuses.length] ?? 'pending'; 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', }, 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), }; 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); if (status === 'upheld') { const penaltyType = i % 2 === 0 ? 'points_deduction' : 'time_penalty'; const penalty = Penalty.create({ id: `penalty-${race.id}-${i}`, raceId: race.id, driverId: accusedResult.driverId, type: penaltyType, value: penaltyType === 'points_deduction' ? 3 : 5, reason: protest.incident.description, protestId: protest.id, issuedBy: primaryDriverId, status: 'applied', issuedAt: new Date(Date.now() - raceIndex * 24 * 60 * 60 * 1000), appliedAt: new Date(Date.now() - raceIndex * 24 * 60 * 60 * 1000), }); seededPenalties.push(penalty); } } // Add direct penalties if (raceResults.length > 5) { if (raceIndex % 3 === 0) { const penalizedResult = raceResults[4]; if (penalizedResult) { const penalty = Penalty.create({ id: `penalty-direct-${race.id}`, raceId: race.id, driverId: penalizedResult.driverId, type: 'points_deduction', value: 5, reason: 'Causing avoidable collision', issuedBy: primaryDriverId, status: 'applied', issuedAt: new Date(Date.now() - (raceIndex + 1) * 12 * 60 * 60 * 1000), appliedAt: new Date(Date.now() - (raceIndex + 1) * 12 * 60 * 60 * 1000), }); seededPenalties.push(penalty); } } if (raceIndex % 3 === 1 && raceResults.length > 6) { const penalizedResult = raceResults[5]; if (penalizedResult) { const penalty = Penalty.create({ id: `penalty-direct-2-${race.id}`, raceId: race.id, driverId: penalizedResult.driverId, type: 'points_deduction', value: 2, reason: 'Track limits violation - gained lasting advantage', issuedBy: primaryDriverId, status: 'applied', issuedAt: new Date(Date.now() - (raceIndex + 1) * 12 * 60 * 60 * 1000), appliedAt: new Date(Date.now() - (raceIndex + 1) * 12 * 60 * 60 * 1000), }); seededPenalties.push(penalty); } } } }); container.registerInstance( DI_TOKENS.PenaltyRepository, new InMemoryPenaltyRepository(seededPenalties) ); container.registerInstance( DI_TOKENS.ProtestRepository, new InMemoryProtestRepository(seededProtests) ); // Scoring repositories const leagueScoringPresetProvider = new InMemoryLeagueScoringPresetProvider(); container.registerInstance( DI_TOKENS.LeagueScoringPresetProvider, leagueScoringPresetProvider ); const game = Game.create({ id: 'iracing', name: 'iRacing' }); const seededSeasons: Season[] = []; const seededScoringConfigs = []; for (const league of seedData.leagues) { const archetype = getDemoLeagueArchetypeByName(league.name); const season = Season.create({ id: `season-${league.id}-demo`, leagueId: league.id, gameId: game.id, name: `${league.name} Demo Season`, year: new Date().getFullYear(), order: 1, status: 'active', startDate: new Date(), endDate: new Date(), }); seededSeasons.push(season); const presetId = archetype?.scoringPresetId ?? 'club-default'; const infraPreset = getLeagueScoringPresetById(presetId); if (infraPreset) { const config = infraPreset.createConfig({ seasonId: season.id }); seededScoringConfigs.push(config); } } container.registerInstance( DI_TOKENS.GameRepository, new InMemoryGameRepository([game]) ); container.registerInstance( DI_TOKENS.SeasonRepository, new InMemorySeasonRepository(seededSeasons) ); container.registerInstance( DI_TOKENS.LeagueScoringConfigRepository, new InMemoryLeagueScoringConfigRepository(seededScoringConfigs) ); // League memberships 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', status: 'active', joinedAt: new Date(), })); // Ensure each league owner has an owner membership for (const league of seedData.leagues) { const existing = seededMemberships.find( (m) => m.leagueId === league.id && m.driverId === league.ownerId, ); if (!existing) { seededMemberships.push({ leagueId: league.id, driverId: league.ownerId, role: 'owner', status: 'active', joinedAt: new Date(), }); } else { existing.role = 'owner'; } } // Ensure primary driver owns at least one league const hasPrimaryOwnerMembership = seededMemberships.some( (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]!; const existingForPrimary = seededMemberships.find( (m) => m.leagueId === targetLeague.id && m.driverId === primaryDriverId, ); if (existingForPrimary) { existingForPrimary.role = 'owner'; } else { seededMemberships.push({ leagueId: targetLeague.id, driverId: primaryDriverId, role: 'owner', status: 'active', joinedAt: new Date(), }); } } // Add admins for primary league const primaryLeagueForAdmins = seedData.leagues.find((l) => l.ownerId === primaryDriverId) ?? seedData.leagues[0]; if (primaryLeagueForAdmins) { const adminCandidates = seedData.drivers .filter((d) => d.id !== primaryLeagueForAdmins.ownerId) .slice(0, 2); adminCandidates.forEach((driver) => { const existing = seededMemberships.find( (m) => m.leagueId === primaryLeagueForAdmins.id && m.driverId === driver.id, ); if (existing) { if (existing.role !== 'owner') { existing.role = 'admin'; } } else { seededMemberships.push({ leagueId: primaryLeagueForAdmins.id, driverId: driver.id, role: 'admin', status: 'active', joinedAt: new Date(), }); } }); } // Add stewards for primary league if (primaryLeagueForAdmins) { const stewardCandidates = seedData.drivers .filter((d) => d.id !== primaryLeagueForAdmins.ownerId) .slice(2, 5); stewardCandidates.forEach((driver) => { const existing = seededMemberships.find( (m) => m.leagueId === primaryLeagueForAdmins.id && m.driverId === driver.id, ); if (existing) { if (existing.role !== 'owner' && existing.role !== 'admin') { existing.role = 'steward'; } } else { seededMemberships.push({ leagueId: primaryLeagueForAdmins.id, driverId: driver.id, role: 'steward', status: 'active', joinedAt: new Date(), }); } }); } // Seed pending join requests const seededJoinRequests: JoinRequest[] = []; const demoLeagues = seedData.leagues.slice(0, 6); const extraDrivers = seedData.drivers.slice(5, 12); demoLeagues.forEach((league, leagueIndex) => { const memberDriverIds = seededMemberships .filter(m => m.leagueId === league.id) .map(m => m.driverId); const availableDrivers = extraDrivers.filter(d => !memberDriverIds.includes(d.id)); const driversForThisLeague = availableDrivers.slice(0, 3 + (leagueIndex % 3)); driversForThisLeague.forEach((driver, index) => { const messages = [ 'Would love to race in this series!', 'Looking to join for the upcoming season.', '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, }); }); }); type InMemoryLeagueMembershipSeed = ConstructorParameters< typeof InMemoryLeagueMembershipRepository >[0]; container.registerInstance( DI_TOKENS.LeagueMembershipRepository, new InMemoryLeagueMembershipRepository( seededMemberships as InMemoryLeagueMembershipSeed, seededJoinRequests, ) ); // Team repositories type InMemoryTeamSeed = ConstructorParameters[0]; container.registerInstance( DI_TOKENS.TeamRepository, new InMemoryTeamRepository( seedData.teams.map((t) => ({ id: t.id, name: t.name, tag: t.tag, description: t.description, ownerId: seedData.drivers[0]!.id, leagues: [t.primaryLeagueId], createdAt: new Date(), })) as InMemoryTeamSeed, ), ); container.registerInstance( DI_TOKENS.TeamMembershipRepository, new InMemoryTeamMembershipRepository( seedData.memberships .filter((m) => m.teamId) .map((m) => ({ teamId: m.teamId!, driverId: m.driverId, role: 'driver', status: 'active', joinedAt: new Date(), })) ) ); // Track and Car repositories container.registerInstance( DI_TOKENS.TrackRepository, new InMemoryTrackRepository(DEMO_TRACKS) ); container.registerInstance( DI_TOKENS.CarRepository, new InMemoryCarRepository(DEMO_CARS) ); // Sponsor repositories - seed with demo sponsors const seededSponsors = seedData.sponsors.map(s => Sponsor.create({ id: s.id, name: s.name, contactEmail: s.contactEmail, logoUrl: s.logoUrl, websiteUrl: s.websiteUrl, }) ); const sponsorRepo = new InMemorySponsorRepository(); sponsorRepo.seed(seededSponsors); container.registerInstance( DI_TOKENS.SponsorRepository, sponsorRepo ); const seededSponsorships = seedData.seasonSponsorships.map((ss) => SeasonSponsorship.create({ id: ss.id, seasonId: ss.seasonId, sponsorId: ss.sponsorId, tier: ss.tier, pricing: Money.create(ss.pricingAmount, ss.pricingCurrency), status: ss.status, description: ss.description ?? '', }), ); const seasonSponsorshipRepo = new InMemorySeasonSponsorshipRepository(); seasonSponsorshipRepo.seed(seededSponsorships); container.registerInstance( DI_TOKENS.SeasonSponsorshipRepository, seasonSponsorshipRepo ); // Sponsorship Request and Pricing repositories const sponsorshipRequestRepo = new InMemorySponsorshipRequestRepository(); container.registerInstance( DI_TOKENS.SponsorshipRequestRepository, sponsorshipRequestRepo ); const sponsorshipPricingRepo = new InMemorySponsorshipPricingRepository(); // Seed sponsorship pricings from demo data using domain SponsorshipPricing sponsorshipPricingRepo.seed(seedData.sponsorshipPricings ?? []); container.registerInstance( DI_TOKENS.SponsorshipPricingRepository, sponsorshipPricingRepo ); // Seed sponsorship requests from demo data if (seedData.sponsorshipRequests && seedData.sponsorshipRequests.length > 0) { sponsorshipRequestRepo.seed(seedData.sponsorshipRequests); } // Social repositories container.registerInstance( DI_TOKENS.FeedRepository, new InMemoryFeedRepository(seedData) ); container.registerInstance( DI_TOKENS.SocialRepository, new InMemorySocialGraphRepository(seedData) ); // Image service container.registerInstance( DI_TOKENS.ImageService, new DemoImageServiceAdapter() ); // Notification repositories container.registerInstance( DI_TOKENS.NotificationRepository, new InMemoryNotificationRepository() ); container.registerInstance( DI_TOKENS.NotificationPreferenceRepository, new InMemoryNotificationPreferenceRepository() ); const notificationGatewayRegistry = new NotificationGatewayRegistry([ new InAppNotificationAdapter(), ]); container.registerInstance( DI_TOKENS.NotificationGatewayRegistry, notificationGatewayRegistry ); // Register driver stats for access by utility functions container.registerInstance( DI_TOKENS.DriverStats, driverStats ); // Driver Rating Provider const driverRatingProvider: DriverRatingProvider = { getRating: (driverId: string): number | null => { const stats = driverStats[driverId]; const rating = stats?.rating; return typeof rating === 'number' ? rating : null; }, getRatings: (driverIds: string[]): Map => { const result = new Map(); for (const id of driverIds) { const stats = driverStats[id]; if (stats?.rating) { result.set(id, stats.rating); } } return result; }, }; container.registerInstance( DI_TOKENS.DriverRatingProvider, driverRatingProvider ); // Resolve dependencies for use cases const driverRepository = container.resolve(DI_TOKENS.DriverRepository); const raceRegistrationRepository = container.resolve(DI_TOKENS.RaceRegistrationRepository); const leagueMembershipRepository = container.resolve(DI_TOKENS.LeagueMembershipRepository); const standingRepository = container.resolve(DI_TOKENS.StandingRepository); const penaltyRepository = container.resolve(DI_TOKENS.PenaltyRepository); const protestRepository = container.resolve(DI_TOKENS.ProtestRepository); const teamRepository = container.resolve(DI_TOKENS.TeamRepository); const teamMembershipRepository = container.resolve(DI_TOKENS.TeamMembershipRepository); const seasonRepository = container.resolve(DI_TOKENS.SeasonRepository); const leagueScoringConfigRepository = container.resolve(DI_TOKENS.LeagueScoringConfigRepository); const gameRepository = container.resolve(DI_TOKENS.GameRepository); const notificationRepository = container.resolve(DI_TOKENS.NotificationRepository); const notificationPreferenceRepository = container.resolve(DI_TOKENS.NotificationPreferenceRepository); const feedRepository = container.resolve(DI_TOKENS.FeedRepository); const socialRepository = container.resolve(DI_TOKENS.SocialRepository); // Register use cases - Racing container.registerInstance( DI_TOKENS.JoinLeagueUseCase, new JoinLeagueUseCase(leagueMembershipRepository) ); container.registerInstance( DI_TOKENS.RegisterForRaceUseCase, new RegisterForRaceUseCase(raceRegistrationRepository, leagueMembershipRepository) ); container.registerInstance( DI_TOKENS.WithdrawFromRaceUseCase, new WithdrawFromRaceUseCase(raceRegistrationRepository) ); container.registerInstance( DI_TOKENS.CancelRaceUseCase, new CancelRaceUseCase(raceRepository) ); container.registerInstance( DI_TOKENS.CreateLeagueWithSeasonAndScoringUseCase, new CreateLeagueWithSeasonAndScoringUseCase( leagueRepository, seasonRepository, leagueScoringConfigRepository, leagueScoringPresetProvider ) ); container.registerInstance( DI_TOKENS.TransferLeagueOwnershipUseCase, new TransferLeagueOwnershipUseCase(leagueRepository, leagueMembershipRepository) ); // Register use cases - Teams container.registerInstance( DI_TOKENS.CreateTeamUseCase, new CreateTeamUseCase(teamRepository, teamMembershipRepository) ); container.registerInstance( DI_TOKENS.JoinTeamUseCase, new JoinTeamUseCase(teamRepository, teamMembershipRepository) ); container.registerInstance( DI_TOKENS.LeaveTeamUseCase, new LeaveTeamUseCase(teamMembershipRepository) ); container.registerInstance( DI_TOKENS.ApproveTeamJoinRequestUseCase, new ApproveTeamJoinRequestUseCase(teamMembershipRepository) ); container.registerInstance( DI_TOKENS.RejectTeamJoinRequestUseCase, new RejectTeamJoinRequestUseCase(teamMembershipRepository) ); container.registerInstance( DI_TOKENS.UpdateTeamUseCase, new UpdateTeamUseCase(teamRepository, teamMembershipRepository) ); // Register use cases - Stewarding container.registerInstance( DI_TOKENS.FileProtestUseCase, new FileProtestUseCase(protestRepository, raceRepository, leagueMembershipRepository) ); container.registerInstance( DI_TOKENS.ReviewProtestUseCase, new ReviewProtestUseCase(protestRepository, raceRepository, leagueMembershipRepository) ); container.registerInstance( DI_TOKENS.ApplyPenaltyUseCase, new ApplyPenaltyUseCase( penaltyRepository, protestRepository, raceRepository, leagueMembershipRepository ) ); container.registerInstance( DI_TOKENS.RequestProtestDefenseUseCase, new RequestProtestDefenseUseCase(protestRepository, raceRepository, leagueMembershipRepository) ); container.registerInstance( DI_TOKENS.SubmitProtestDefenseUseCase, new SubmitProtestDefenseUseCase(protestRepository) ); // Register use cases - Notifications container.registerInstance( DI_TOKENS.SendNotificationUseCase, new SendNotificationUseCase( notificationRepository, notificationPreferenceRepository, notificationGatewayRegistry ) ); container.registerInstance( DI_TOKENS.MarkNotificationReadUseCase, new MarkNotificationReadUseCase(notificationRepository) ); // Register queries - Racing const driverRegistrationStatusPresenter = new DriverRegistrationStatusPresenter(); container.registerInstance( DI_TOKENS.IsDriverRegisteredForRaceUseCase, new IsDriverRegisteredForRaceUseCase(raceRegistrationRepository, driverRegistrationStatusPresenter) ); const raceRegistrationsPresenter = new RaceRegistrationsPresenter(); container.registerInstance( DI_TOKENS.GetRaceRegistrationsUseCase, new GetRaceRegistrationsUseCase(raceRegistrationRepository, raceRegistrationsPresenter) ); const leagueStandingsPresenter = new LeagueStandingsPresenter(); container.registerInstance( DI_TOKENS.GetLeagueStandingsUseCase, new GetLeagueStandingsUseCase(standingRepository), ); const leagueDriverSeasonStatsPresenter = new LeagueDriverSeasonStatsPresenter(); container.registerInstance( DI_TOKENS.GetLeagueDriverSeasonStatsUseCase, new GetLeagueDriverSeasonStatsUseCase( standingRepository, resultRepository, penaltyRepository, raceRepository, { getRating: (driverId: string) => { const stats = driverStats[driverId]; if (!stats || typeof stats.rating !== 'number') { return { rating: null, ratingChange: null }; } const baseline = 1500; const delta = stats.rating - baseline; return { rating: stats.rating, ratingChange: delta !== 0 ? delta : null, }; }, }, leagueDriverSeasonStatsPresenter, ), ); const allLeaguesWithCapacityPresenter = new AllLeaguesWithCapacityPresenter(); container.registerInstance( DI_TOKENS.GetAllLeaguesWithCapacityUseCase, new GetAllLeaguesWithCapacityUseCase( leagueRepository, leagueMembershipRepository, allLeaguesWithCapacityPresenter ) ); const allLeaguesWithCapacityAndScoringPresenter = new AllLeaguesWithCapacityAndScoringPresenter(); container.registerInstance( DI_TOKENS.GetAllLeaguesWithCapacityAndScoringUseCase, new GetAllLeaguesWithCapacityAndScoringUseCase( leagueRepository, leagueMembershipRepository, seasonRepository, leagueScoringConfigRepository, gameRepository, leagueScoringPresetProvider, allLeaguesWithCapacityAndScoringPresenter ) ); const leagueScoringPresetsPresenter = new LeagueScoringPresetsPresenter(); container.registerInstance( DI_TOKENS.ListLeagueScoringPresetsUseCase, new ListLeagueScoringPresetsUseCase(leagueScoringPresetProvider) ); const leagueScoringConfigPresenter = new LeagueScoringConfigPresenter(); container.registerInstance( DI_TOKENS.GetLeagueScoringConfigUseCase, new GetLeagueScoringConfigUseCase( leagueRepository, seasonRepository, leagueScoringConfigRepository, gameRepository, leagueScoringPresetProvider, leagueScoringConfigPresenter ) ); container.registerInstance( DI_TOKENS.GetLeagueFullConfigUseCase, new GetLeagueFullConfigUseCase( leagueRepository, seasonRepository, leagueScoringConfigRepository, gameRepository, ) ); const leagueSchedulePreviewPresenter = new LeagueSchedulePreviewPresenter(); container.registerInstance( DI_TOKENS.PreviewLeagueScheduleUseCase, new PreviewLeagueScheduleUseCase(undefined, leagueSchedulePreviewPresenter), ); const raceWithSOFPresenter = new RaceWithSOFPresenter(); container.registerInstance( DI_TOKENS.GetRaceWithSOFUseCase, new GetRaceWithSOFUseCase( raceRepository, raceRegistrationRepository, resultRepository, driverRatingProvider, raceWithSOFPresenter ) ); const leagueStatsPresenter = new LeagueStatsPresenter(); container.registerInstance( DI_TOKENS.GetLeagueStatsUseCase, new GetLeagueStatsUseCase( leagueRepository, raceRepository, resultRepository, driverRatingProvider, leagueStatsPresenter ) ); const racesPresenter = new RacesPagePresenter(); container.registerInstance( DI_TOKENS.GetRacesPageDataUseCase, new GetRacesPageDataUseCase(raceRepository, leagueRepository, racesPresenter) ); const allRacesPagePresenter = new AllRacesPagePresenter(); container.registerInstance( DI_TOKENS.GetAllRacesPageDataUseCase, new GetAllRacesPageDataUseCase(raceRepository, leagueRepository, allRacesPagePresenter) ); const imageService = container.resolve(DI_TOKENS.ImageService); const raceDetailPresenter = new RaceDetailPresenter(); container.registerInstance( DI_TOKENS.GetRaceDetailUseCase, new GetRaceDetailUseCase( raceRepository, leagueRepository, driverRepository, raceRegistrationRepository, resultRepository, leagueMembershipRepository, driverRatingProvider, imageService, raceDetailPresenter ) ); const raceResultsDetailPresenter = new RaceResultsDetailPresenter(); container.registerInstance( DI_TOKENS.GetRaceResultsDetailUseCase, new GetRaceResultsDetailUseCase( raceRepository, leagueRepository, resultRepository, driverRepository, penaltyRepository, raceResultsDetailPresenter ) ); const importRaceResultsPresenter = new ImportRaceResultsPresenter(); container.registerInstance( DI_TOKENS.ImportRaceResultsUseCase, new ImportRaceResultsUseCase( raceRepository, leagueRepository, resultRepository, standingRepository, importRaceResultsPresenter ) ); // Create services for driver leaderboard query const rankingService = { getAllDriverRankings: () => { const stats = getDIContainer().resolve(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(DI_TOKENS.DriverStats); return stats[driverId] ?? null; }, }; const driversPresenter = new DriversLeaderboardPresenter(); container.registerInstance( DI_TOKENS.GetDriversLeaderboardUseCase, new GetDriversLeaderboardUseCase( driverRepository, rankingService, driverStatsService, imageService, driversPresenter ) ); const getDriverStatsAdapter = (driverId: string) => { const stats = getDIContainer().resolve(DI_TOKENS.DriverStats); const stat = stats[driverId]; if (!stat) return null; return { rating: stat.rating ?? null, wins: stat.wins ?? 0, totalRaces: stat.totalRaces ?? 0, }; }; container.registerInstance( DI_TOKENS.GetTeamsLeaderboardUseCase, new GetTeamsLeaderboardUseCase( teamRepository, teamMembershipRepository, driverRepository, getDriverStatsAdapter, ) ); const getDriverStatsForDashboard = (driverId: string) => { const stats = getDIContainer().resolve(DI_TOKENS.DriverStats); const stat = stats[driverId]; if (!stat) return null; return { rating: stat.rating ?? null, wins: stat.wins ?? 0, podiums: stat.podiums ?? 0, totalRaces: stat.totalRaces ?? 0, overallRank: stat.overallRank ?? null, consistency: stat.consistency ?? null, }; }; const getDriverStatsForProfile = (driverId: string) => { const stats = getDIContainer().resolve(DI_TOKENS.DriverStats); const stat = stats[driverId]; if (!stat) return null; return { rating: stat.rating ?? null, wins: stat.wins ?? 0, podiums: stat.podiums ?? 0, dnfs: stat.dnfs ?? 0, totalRaces: stat.totalRaces ?? 0, avgFinish: stat.avgFinish ?? null, bestFinish: stat.bestFinish ?? null, worstFinish: stat.worstFinish ?? null, overallRank: stat.overallRank ?? null, consistency: stat.consistency ?? null, percentile: stat.percentile ?? null, }; }; const dashboardOverviewPresenter = new DashboardOverviewPresenter(); container.registerInstance( DI_TOKENS.GetDashboardOverviewUseCase, new GetDashboardOverviewUseCase( driverRepository, raceRepository, resultRepository, leagueRepository, standingRepository, leagueMembershipRepository, raceRegistrationRepository, feedRepository, socialRepository, imageService, getDriverStatsForDashboard, dashboardOverviewPresenter ) ); const profileOverviewPresenter = new ProfileOverviewPresenter(); container.registerInstance( DI_TOKENS.GetProfileOverviewUseCase, new GetProfileOverviewUseCase( driverRepository, teamRepository, teamMembershipRepository, socialRepository, imageService, getDriverStatsForProfile, rankingService.getAllDriverRankings, profileOverviewPresenter ) ); container.registerInstance( DI_TOKENS.UpdateDriverProfileUseCase, new UpdateDriverProfileUseCase(driverRepository) ); // Register use cases - Teams (Query-like with Presenters) const allTeamsPresenter = new AllTeamsPresenter(); container.registerInstance( DI_TOKENS.GetAllTeamsUseCase, new GetAllTeamsUseCase(teamRepository, teamMembershipRepository), ); const teamDetailsPresenter = new TeamDetailsPresenter(); container.registerInstance( 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.GetRaceProtestsUseCase, new GetRaceProtestsUseCase(protestRepository, driverRepository) ); const racePenaltiesPresenter = new RacePenaltiesPresenter(); container.registerInstance( DI_TOKENS.GetRacePenaltiesUseCase, new GetRacePenaltiesUseCase(penaltyRepository, driverRepository) ); // Register queries - Notifications container.registerInstance( DI_TOKENS.GetUnreadNotificationsUseCase, new GetUnreadNotificationsUseCase(notificationRepository) ); // Register use cases - Sponsors const sponsorRepository = container.resolve(DI_TOKENS.SponsorRepository); const seasonSponsorshipRepository = container.resolve(DI_TOKENS.SeasonSponsorshipRepository); const sponsorDashboardPresenter = new SponsorDashboardPresenter(); container.registerInstance( DI_TOKENS.GetSponsorDashboardUseCase, new GetSponsorDashboardUseCase( sponsorRepository, seasonSponsorshipRepository, seasonRepository, leagueRepository, leagueMembershipRepository, raceRepository, sponsorDashboardPresenter ) ); const sponsorSponsorshipsPresenter = new SponsorSponsorshipsPresenter(); container.registerInstance( DI_TOKENS.GetSponsorSponsorshipsUseCase, new GetSponsorSponsorshipsUseCase( sponsorRepository, seasonSponsorshipRepository, seasonRepository, leagueRepository, leagueMembershipRepository, raceRepository, sponsorSponsorshipsPresenter ) ); // Sponsorship request repositories and use cases const sponsorshipRequestRepository = container.resolve(DI_TOKENS.SponsorshipRequestRepository); const sponsorshipPricingRepository = container.resolve(DI_TOKENS.SponsorshipPricingRepository); container.registerInstance( DI_TOKENS.GetPendingSponsorshipRequestsUseCase, new GetPendingSponsorshipRequestsUseCase( sponsorshipRequestRepository, sponsorRepository, ) ); const entitySponsorshipPricingPresenter = new EntitySponsorshipPricingPresenter(); container.registerInstance( DI_TOKENS.GetEntitySponsorshipPricingUseCase, new GetEntitySponsorshipPricingUseCase( sponsorshipPricingRepository, sponsorshipRequestRepository, seasonSponsorshipRepository, entitySponsorshipPricingPresenter ) ); container.registerInstance( DI_TOKENS.ApplyForSponsorshipUseCase, new ApplyForSponsorshipUseCase( sponsorshipRequestRepository, sponsorshipPricingRepository, sponsorRepository ) ); container.registerInstance( DI_TOKENS.AcceptSponsorshipRequestUseCase, new AcceptSponsorshipRequestUseCase(sponsorshipRequestRepository, seasonSponsorshipRepository) ); container.registerInstance( DI_TOKENS.RejectSponsorshipRequestUseCase, new RejectSponsorshipRequestUseCase(sponsorshipRequestRepository) ); } /** * Reset the container (for testing) */ export function resetDIContainer(): void { container.clearInstances(); } /** * Get the TSyringe container instance */ export function getDIContainer() { return container; }