import type { Logger } from '@core/shared/application'; import type { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository'; import type { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository'; import type { IRaceRepository } from '@core/racing/domain/repositories/IRaceRepository'; import type { IResultRepository } from '@core/racing/domain/repositories/IResultRepository'; import type { IStandingRepository } from '@core/racing/domain/repositories/IStandingRepository'; import type { ILeagueMembershipRepository } from '@core/racing/domain/repositories/ILeagueMembershipRepository'; import type { IRaceRegistrationRepository } from '@core/racing/domain/repositories/IRaceRegistrationRepository'; import type { ITeamRepository } from '@core/racing/domain/repositories/ITeamRepository'; import type { ITeamMembershipRepository } from '@core/racing/domain/repositories/ITeamMembershipRepository'; import type { ISponsorRepository } from '@core/racing/domain/repositories/ISponsorRepository'; import type { ISeasonRepository } from '@core/racing/domain/repositories/ISeasonRepository'; import type { ILeagueScoringConfigRepository } from '@core/racing/domain/repositories/ILeagueScoringConfigRepository'; import type { ISeasonSponsorshipRepository } from '@core/racing/domain/repositories/ISeasonSponsorshipRepository'; import type { ISponsorshipRequestRepository } from '@core/racing/domain/repositories/ISponsorshipRequestRepository'; import type { ILeagueWalletRepository } from '@core/racing/domain/repositories/ILeagueWalletRepository'; import type { ITransactionRepository } from '@core/racing/domain/repositories/ITransactionRepository'; import type { Season } from '@core/racing/domain/entities/season/Season'; import { getLeagueScoringPresetById } from './LeagueScoringPresets'; import type { IProtestRepository } from '@core/racing/domain/repositories/IProtestRepository'; import type { IPenaltyRepository } from '@core/racing/domain/repositories/IPenaltyRepository'; import type { IFeedRepository } from '@core/social/domain/repositories/IFeedRepository'; import type { ISocialGraphRepository } from '@core/social/domain/repositories/ISocialGraphRepository'; import { createRacingSeed } from './racing/RacingSeed'; import { seedId } from './racing/SeedIdHelper'; import { DriverStatsStore } from '@adapters/racing/services/DriverStatsStore'; import { TeamStatsStore } from '@adapters/racing/services/TeamStatsStore'; export type RacingSeedDependencies = { driverRepository: IDriverRepository; leagueRepository: ILeagueRepository; seasonRepository: ISeasonRepository; leagueScoringConfigRepository: ILeagueScoringConfigRepository; seasonSponsorshipRepository: ISeasonSponsorshipRepository; sponsorshipRequestRepository: ISponsorshipRequestRepository; leagueWalletRepository: ILeagueWalletRepository; transactionRepository: ITransactionRepository; protestRepository: IProtestRepository; penaltyRepository: IPenaltyRepository; raceRepository: IRaceRepository; resultRepository: IResultRepository; standingRepository: IStandingRepository; leagueMembershipRepository: ILeagueMembershipRepository; raceRegistrationRepository: IRaceRegistrationRepository; teamRepository: ITeamRepository; teamMembershipRepository: ITeamMembershipRepository; sponsorRepository: ISponsorRepository; feedRepository: IFeedRepository; socialGraphRepository: ISocialGraphRepository; }; export class SeedRacingData { constructor( private readonly logger: Logger, private readonly seedDeps: RacingSeedDependencies, ) {} private getApiPersistence(): 'postgres' | 'inmemory' { const configured = process.env.GRIDPILOT_API_PERSISTENCE?.toLowerCase(); if (configured === 'postgres' || configured === 'inmemory') { return configured; } if (process.env.NODE_ENV === 'test') { return 'inmemory'; } return process.env.DATABASE_URL ? 'postgres' : 'inmemory'; } async execute(): Promise { const existingDrivers = await this.seedDeps.driverRepository.findAll(); const persistence = this.getApiPersistence(); // Check for force reseed via environment variable const forceReseedRaw = process.env.GRIDPILOT_API_FORCE_RESEED; const forceReseed = forceReseedRaw !== undefined && forceReseedRaw !== '0' && forceReseedRaw.toLowerCase() !== 'false'; if (existingDrivers.length > 0 && !forceReseed) { this.logger.info('[Bootstrap] Racing seed skipped (drivers already exist), ensuring scoring configs'); await this.ensureScoringConfigsForExistingData(); return; } if (forceReseed && existingDrivers.length > 0) { this.logger.info('[Bootstrap] Force reseed enabled - clearing existing racing data'); await this.clearExistingRacingData(); } const seed = createRacingSeed({ persistence, driverCount: 150 // Expanded from 100 to 150 }); // Populate the driver stats store for the InMemoryDriverStatsService const driverStatsStore = DriverStatsStore.getInstance(); driverStatsStore.clear(); // Clear any existing stats driverStatsStore.loadStats(seed.driverStats); this.logger.info(`[Bootstrap] Loaded driver stats for ${seed.driverStats.size} drivers`); // Populate the team stats store for the AllTeamsPresenter const teamStatsStore = TeamStatsStore.getInstance(); teamStatsStore.clear(); // Clear any existing stats teamStatsStore.loadStats(seed.teamStats); this.logger.info(`[Bootstrap] Loaded team stats for ${seed.teamStats.size} teams`); let sponsorshipRequestsSeededViaRepo = false; const seedableSponsorshipRequests = this.seedDeps .sponsorshipRequestRepository as unknown as { seed?: (input: unknown) => void }; if (typeof seedableSponsorshipRequests.seed === 'function') { seedableSponsorshipRequests.seed(seed.sponsorshipRequests); sponsorshipRequestsSeededViaRepo = true; } for (const driver of seed.drivers) { try { await this.seedDeps.driverRepository.create(driver); } catch { // ignore duplicates } } for (const league of seed.leagues) { try { await this.seedDeps.leagueRepository.create(league); } catch { // ignore duplicates } } for (const season of seed.seasons) { try { await this.seedDeps.seasonRepository.create(season); } catch { // ignore duplicates } } const activeSeasons = seed.seasons.filter((season) => season.status.isActive()); for (const season of activeSeasons) { const presetId = this.selectScoringPresetIdForSeason(season, persistence); const preset = getLeagueScoringPresetById(presetId); if (!preset) { this.logger.warn( `[Bootstrap] Scoring preset not found (presetId=${presetId}, seasonId=${season.id}, leagueId=${season.leagueId})`, ); continue; } const scoringConfig = preset.createConfig({ seasonId: season.id }); try { await this.seedDeps.leagueScoringConfigRepository.save(scoringConfig); } catch { // ignore duplicates } } for (const sponsorship of seed.seasonSponsorships) { try { await this.seedDeps.seasonSponsorshipRepository.create(sponsorship); } catch { // ignore duplicates } } if (!sponsorshipRequestsSeededViaRepo) { for (const request of seed.sponsorshipRequests) { try { await this.seedDeps.sponsorshipRequestRepository.create(request); } catch { // ignore duplicates } } } for (const wallet of seed.leagueWallets) { try { await this.seedDeps.leagueWalletRepository.create(wallet); } catch { // ignore duplicates } } for (const tx of seed.leagueWalletTransactions) { try { await this.seedDeps.transactionRepository.create(tx); } catch { // ignore duplicates } } for (const protest of seed.protests) { try { await this.seedDeps.protestRepository.create(protest); } catch { // ignore duplicates } } for (const penalty of seed.penalties) { try { await this.seedDeps.penaltyRepository.create(penalty); } catch { // ignore duplicates } } for (const race of seed.races) { try { await this.seedDeps.raceRepository.create(race); } catch { // ignore duplicates } } try { await this.seedDeps.resultRepository.createMany(seed.results); } catch { // ignore duplicates } for (const membership of seed.leagueMemberships) { try { await this.seedDeps.leagueMembershipRepository.saveMembership(membership); } catch { // ignore duplicates } } for (const request of seed.leagueJoinRequests) { try { await this.seedDeps.leagueMembershipRepository.saveJoinRequest(request); } catch { // ignore duplicates } } for (const team of seed.teams) { try { await this.seedDeps.teamRepository.create(team); } catch { // ignore duplicates } } for (const sponsor of seed.sponsors) { try { await this.seedDeps.sponsorRepository.create(sponsor); } catch { // ignore duplicates } } for (const membership of seed.teamMemberships) { try { await this.seedDeps.teamMembershipRepository.saveMembership(membership); } catch { // ignore duplicates } } for (const request of seed.teamJoinRequests) { try { await this.seedDeps.teamMembershipRepository.saveJoinRequest(request); } catch { // ignore duplicates } } for (const registration of seed.raceRegistrations) { try { await this.seedDeps.raceRegistrationRepository.register(registration); } catch { // ignore duplicates } } try { await this.seedDeps.standingRepository.saveMany(seed.standings); } catch { // ignore duplicates } const seedableFeed = this.seedDeps.feedRepository as unknown as { seed?: (input: unknown) => void }; if (typeof seedableFeed.seed === 'function') { seedableFeed.seed({ drivers: seed.drivers, friendships: seed.friendships, feedEvents: seed.feedEvents, }); } const seedableSocial = this.seedDeps.socialGraphRepository as unknown as { seed?: (input: unknown) => void }; if (typeof seedableSocial.seed === 'function') { seedableSocial.seed({ drivers: seed.drivers, friendships: seed.friendships, feedEvents: seed.feedEvents, }); } this.logger.info( `[Bootstrap] Seeded racing data: drivers=${seed.drivers.length}, leagues=${seed.leagues.length}, races=${seed.races.length}`, ); } private async clearExistingRacingData(): Promise { // Get all existing drivers const drivers = await this.seedDeps.driverRepository.findAll(); // Delete drivers first (this should cascade to related data in most cases) for (const driver of drivers) { try { await this.seedDeps.driverRepository.delete(driver.id); } catch { // Ignore errors } } // Try to clean up other data if repositories support it try { const leagues = await this.seedDeps.leagueRepository.findAll(); for (const league of leagues) { try { await this.seedDeps.leagueRepository.delete(league.id.toString()); } catch { // Ignore } } } catch { // Ignore } this.logger.info('[Bootstrap] Cleared existing racing data'); } private async ensureScoringConfigsForExistingData(): Promise { const leagues = await this.seedDeps.leagueRepository.findAll(); for (const league of leagues) { const seasons = await this.seedDeps.seasonRepository.findByLeagueId(league.id.toString()); const activeSeasons = seasons.filter((season) => season.status.isActive()); for (const season of activeSeasons) { const existing = await this.seedDeps.leagueScoringConfigRepository.findBySeasonId(season.id); if (existing) continue; const presetId = this.selectScoringPresetIdForSeason(season, 'postgres'); const preset = getLeagueScoringPresetById(presetId); if (!preset) { this.logger.warn( `[Bootstrap] Scoring preset not found (presetId=${presetId}, seasonId=${season.id}, leagueId=${season.leagueId})`, ); continue; } const scoringConfig = preset.createConfig({ seasonId: season.id }); try { await this.seedDeps.leagueScoringConfigRepository.save(scoringConfig); } catch { // ignore duplicates } } } } private selectScoringPresetIdForSeason(season: Season, persistence: 'postgres' | 'inmemory'): string { const expectedLeagueId = seedId('league-5', persistence); const expectedSeasonId = seedId('season-1-b', persistence); if (season.leagueId === expectedLeagueId && season.status.isActive()) { return 'sprint-main-driver'; } if (season.leagueId === seedId('league-3', persistence)) { return season.id === expectedSeasonId ? 'sprint-main-team' : 'club-default-nations'; } const match = /^league-(\d+)$/.exec(season.leagueId); const leagueNumber = match ? Number(match[1]) : undefined; if (leagueNumber !== undefined) { switch (leagueNumber % 4) { case 0: return 'sprint-main-team'; case 1: return 'endurance-main-trophy'; case 2: return 'sprint-main-driver'; case 3: return 'club-default-nations'; } } return 'club-default'; } }