Some checks failed
CI / lint-typecheck (pull_request) Failing after 12s
CI / tests (pull_request) Has been skipped
CI / contract-tests (pull_request) Has been skipped
CI / e2e-tests (pull_request) Has been skipped
CI / comment-pr (pull_request) Has been skipped
CI / commit-types (pull_request) Has been skipped
793 lines
29 KiB
TypeScript
793 lines
29 KiB
TypeScript
import type { DriverStats } from '@core/racing/application/use-cases/DriverStatsUseCase';
|
|
import { Driver } from '@core/racing/domain/entities/Driver';
|
|
import { Result } from '@core/racing/domain/entities/result/Result';
|
|
import type { Season } from '@core/racing/domain/entities/season/Season';
|
|
import { Standing } from '@core/racing/domain/entities/Standing';
|
|
import { Team } from '@core/racing/domain/entities/Team';
|
|
import type { DriverRepository } from '@core/racing/domain/repositories/DriverRepository';
|
|
import type { LeagueRepository } from '@core/racing/domain/repositories/LeagueRepository';
|
|
import type { SeasonRepository } from '@core/racing/domain/repositories/SeasonRepository';
|
|
import type { LeagueScoringConfigRepository } from '@core/racing/domain/repositories/LeagueScoringConfigRepository';
|
|
import type { SeasonSponsorshipRepository } from '@core/racing/domain/repositories/SeasonSponsorshipRepository';
|
|
import type { SponsorshipRequestRepository } from '@core/racing/domain/repositories/SponsorshipRequestRepository';
|
|
import type { LeagueWalletRepository } from '@core/racing/domain/repositories/LeagueWalletRepository';
|
|
import type { TransactionRepository } from '@core/racing/domain/repositories/TransactionRepository';
|
|
import type { ProtestRepository } from '@core/racing/domain/repositories/ProtestRepository';
|
|
import type { PenaltyRepository } from '@core/racing/domain/repositories/PenaltyRepository';
|
|
import type { RaceRepository } from '@core/racing/domain/repositories/RaceRepository';
|
|
import type { ResultRepository } from '@core/racing/domain/repositories/ResultRepository';
|
|
import type { StandingRepository } from '@core/racing/domain/repositories/StandingRepository';
|
|
import type { LeagueMembershipRepository } from '@core/racing/domain/repositories/LeagueMembershipRepository';
|
|
import type { RaceRegistrationRepository } from '@core/racing/domain/repositories/RaceRegistrationRepository';
|
|
import type { TeamRepository } from '@core/racing/domain/repositories/TeamRepository';
|
|
import type { TeamMembershipRepository } from '@core/racing/domain/repositories/TeamMembershipRepository';
|
|
import type { SponsorRepository } from '@core/racing/domain/repositories/SponsorRepository';
|
|
import type { FeedRepository } from '@core/social/domain/repositories/FeedRepository';
|
|
import type { SocialGraphRepository } from '@core/social/domain/repositories/SocialGraphRepository';
|
|
import type { DriverStatsRepository } from '@core/racing/domain/repositories/DriverStatsRepository';
|
|
import type { TeamStatsRepository, TeamStats } from '@core/racing/domain/repositories/TeamStatsRepository';
|
|
import type { MediaRepository } from '@core/racing/domain/repositories/MediaRepository';
|
|
import type { AuthRepository } from '@core/identity/domain/repositories/AuthRepository';
|
|
import type { PasswordHashingService } from '@core/identity/domain/services/PasswordHashingService';
|
|
import type { AdminUserRepository } from '@core/admin/domain/repositories/AdminUserRepository';
|
|
import type { Logger } from '@core/shared/domain/Logger';
|
|
import { getLeagueScoringPresetById } from './LeagueScoringPresets';
|
|
import { createRacingSeed } from './racing/RacingSeed';
|
|
import { seedId } from './racing/SeedIdHelper';
|
|
|
|
export type RacingSeedDependencies = {
|
|
driverRepository: DriverRepository;
|
|
leagueRepository: LeagueRepository;
|
|
seasonRepository: SeasonRepository;
|
|
leagueScoringConfigRepository: LeagueScoringConfigRepository;
|
|
seasonSponsorshipRepository: SeasonSponsorshipRepository;
|
|
sponsorshipRequestRepository: SponsorshipRequestRepository;
|
|
leagueWalletRepository: LeagueWalletRepository;
|
|
transactionRepository: TransactionRepository;
|
|
protestRepository: ProtestRepository;
|
|
penaltyRepository: PenaltyRepository;
|
|
raceRepository: RaceRepository;
|
|
resultRepository: ResultRepository;
|
|
standingRepository: StandingRepository;
|
|
leagueMembershipRepository: LeagueMembershipRepository;
|
|
raceRegistrationRepository: RaceRegistrationRepository;
|
|
teamRepository: TeamRepository;
|
|
teamMembershipRepository: TeamMembershipRepository;
|
|
sponsorRepository: SponsorRepository;
|
|
feedRepository: FeedRepository;
|
|
socialGraphRepository: SocialGraphRepository;
|
|
driverStatsRepository: DriverStatsRepository;
|
|
teamStatsRepository: TeamStatsRepository;
|
|
mediaRepository: MediaRepository;
|
|
// Identity dependencies for demo user seed
|
|
authRepository: AuthRepository;
|
|
passwordHashingService: PasswordHashingService;
|
|
adminUserRepository: AdminUserRepository;
|
|
};
|
|
|
|
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<void> {
|
|
const existingDrivers = await this.seedDeps.driverRepository.findAll();
|
|
const existingTeams = await this.seedDeps.teamRepository.findAll().catch(() => []);
|
|
const existingRaces = await this.seedDeps.raceRepository.findAll().catch(() => []);
|
|
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';
|
|
|
|
this.logger.info(
|
|
`[Bootstrap] Racing seed precheck: forceReseed=${forceReseed}, drivers=${existingDrivers.length}, teams=${existingTeams.length}, races=${existingRaces.length}, persistence=${persistence}`,
|
|
);
|
|
|
|
if (existingDrivers.length > 0 && !forceReseed) {
|
|
this.logger.info('[Bootstrap] Racing seed skipped (drivers already exist), ensuring scoring configs');
|
|
await this.ensureScoringConfigsForExistingData();
|
|
// Even when skipping full seed, ensure stats are computed and stored
|
|
this.logger.info('[Bootstrap] Computing and storing driver/team stats for existing data');
|
|
await this.computeAndStoreDriverStats();
|
|
await this.computeAndStoreTeamStats();
|
|
return;
|
|
}
|
|
|
|
// IMPORTANT:
|
|
// Force reseed must clear even when drivers are already empty.
|
|
// Otherwise stale teams can remain (e.g. with logoRef=system-default/logo),
|
|
// and the seed will "ignore duplicates" on create, leaving stale logoRefs in Postgres.
|
|
if (forceReseed) {
|
|
this.logger.info(
|
|
`[Bootstrap] Force reseed enabled - clearing existing racing data (drivers=${existingDrivers.length}, teams=${existingTeams.length})`,
|
|
);
|
|
await this.clearExistingRacingData();
|
|
}
|
|
|
|
const seed = createRacingSeed({
|
|
persistence,
|
|
driverCount: 150 // Expanded from 100 to 150
|
|
});
|
|
|
|
// Clear existing stats repositories
|
|
await this.seedDeps.driverStatsRepository.clear();
|
|
await this.seedDeps.teamStatsRepository.clear();
|
|
|
|
this.logger.info('[Bootstrap] Cleared existing stats repositories');
|
|
|
|
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,
|
|
});
|
|
}
|
|
|
|
// Compute and store driver stats from real data
|
|
await this.computeAndStoreDriverStats();
|
|
|
|
// Compute and store team stats from real data
|
|
await this.computeAndStoreTeamStats();
|
|
|
|
// Log race distribution for transparency
|
|
const raceStatusCounts = seed.races.reduce((acc, race) => {
|
|
const status = race.status.toString();
|
|
acc[status] = (acc[status] || 0) + 1;
|
|
return acc;
|
|
}, {} as Record<string, number>);
|
|
|
|
const upcomingRaces = seed.races.filter((r) => r.status.toString() === 'scheduled' && r.scheduledAt > new Date());
|
|
|
|
this.logger.info(
|
|
`[Bootstrap] Seeded racing data: drivers=${seed.drivers.length}, leagues=${seed.leagues.length}, races=${seed.races.length} (scheduled=${raceStatusCounts.scheduled || 0}, running=${raceStatusCounts.running || 0}, completed=${raceStatusCounts.completed || 0}, cancelled=${raceStatusCounts.cancelled || 0})`,
|
|
);
|
|
this.logger.info(
|
|
`[Bootstrap] Upcoming races: ${upcomingRaces.length} scheduled in the future`,
|
|
);
|
|
}
|
|
|
|
private async computeAndStoreDriverStats(): Promise<void> {
|
|
const drivers = await this.seedDeps.driverRepository.findAll();
|
|
const standings = await this.seedDeps.standingRepository.findAll();
|
|
const results = await this.seedDeps.resultRepository.findAll();
|
|
|
|
this.logger.info(`[Bootstrap] Computing stats for ${drivers.length} drivers from ${standings.length} standings and ${results.length} results`);
|
|
|
|
for (const driver of drivers) {
|
|
const driverResults = results.filter((r: Result) => r.driverId.toString() === driver.id);
|
|
const driverStandings = standings.filter((s: Standing) => s.driverId.toString() === driver.id);
|
|
|
|
if (driverResults.length === 0) continue;
|
|
|
|
const stats = this.calculateDriverStats(driver, driverResults, driverStandings);
|
|
await this.seedDeps.driverStatsRepository.saveDriverStats(driver.id, stats);
|
|
}
|
|
|
|
this.logger.info(`[Bootstrap] Computed and stored stats for ${drivers.length} drivers`);
|
|
}
|
|
|
|
private calculateDriverStats(driver: Driver, results: Result[], standings: Standing[]): DriverStats {
|
|
const wins = results.filter(r => r.position.toNumber() === 1).length;
|
|
const podiums = results.filter(r => r.position.toNumber() <= 3).length;
|
|
const dnfs = results.filter(r => r.position.toNumber() > 20).length;
|
|
const totalRaces = results.length;
|
|
|
|
const positions = results.map(r => r.position.toNumber());
|
|
const avgFinish = positions.reduce((sum, pos) => sum + pos, 0) / totalRaces;
|
|
const bestFinish = Math.min(...positions);
|
|
const worstFinish = Math.max(...positions);
|
|
|
|
// Calculate rating based on performance
|
|
let rating = 1000;
|
|
const driverStanding = standings.find(s => s.driverId.toString() === driver.id);
|
|
if (driverStanding) {
|
|
const pointsBonus = driverStanding.points.toNumber() * 2;
|
|
const positionBonus = Math.max(0, 50 - (driverStanding.position.toNumber() * 2));
|
|
const winBonus = driverStanding.wins * 100;
|
|
rating = Math.round(1000 + pointsBonus + positionBonus + winBonus);
|
|
} else {
|
|
const performanceBonus = ((totalRaces - wins) * 5) + ((totalRaces - podiums) * 2);
|
|
rating = Math.round(1000 + (wins * 100) + (podiums * 50) - performanceBonus);
|
|
}
|
|
|
|
// Calculate consistency
|
|
const avgPosition = avgFinish;
|
|
const variance = positions.reduce((sum, pos) => sum + Math.pow(pos - avgPosition, 2), 0) / totalRaces;
|
|
const consistency = Math.round(Math.max(0, 100 - (variance * 2)));
|
|
|
|
// Safety rating (based on incidents)
|
|
const totalIncidents = results.reduce((sum, r) => sum + r.incidents.toNumber(), 0);
|
|
const safetyRating = Math.round(Math.max(0, 100 - (totalIncidents / totalRaces)));
|
|
|
|
// Sportsmanship rating (placeholder)
|
|
const sportsmanshipRating = 4.5;
|
|
|
|
// Experience level
|
|
const experienceLevel = this.determineExperienceLevel(totalRaces);
|
|
|
|
// Overall rank
|
|
const overallRank = driverStanding ? driverStanding.position.toNumber() : null;
|
|
|
|
return {
|
|
rating,
|
|
safetyRating,
|
|
sportsmanshipRating,
|
|
totalRaces,
|
|
wins,
|
|
podiums,
|
|
dnfs,
|
|
avgFinish: Math.round(avgFinish * 10) / 10,
|
|
bestFinish,
|
|
worstFinish,
|
|
consistency,
|
|
experienceLevel,
|
|
overallRank
|
|
};
|
|
}
|
|
|
|
private async computeAndStoreTeamStats(): Promise<void> {
|
|
const teams = await this.seedDeps.teamRepository.findAll();
|
|
const results = await this.seedDeps.resultRepository.findAll();
|
|
const drivers = await this.seedDeps.driverRepository.findAll();
|
|
|
|
this.logger.info(`[Bootstrap] Computing stats for ${teams.length} teams`);
|
|
|
|
for (const team of teams) {
|
|
// Get team members using the correct method
|
|
const teamMemberships = await this.seedDeps.teamMembershipRepository.getTeamMembers(team.id);
|
|
const teamMemberIds = teamMemberships.map((m: any) => m.driverId.toString());
|
|
|
|
// Get results for team members
|
|
const teamResults = results.filter((r: Result) => teamMemberIds.includes(r.driverId.toString()));
|
|
|
|
// Get team drivers for name resolution
|
|
const teamDrivers = drivers.filter((d: Driver) => teamMemberIds.includes(d.id));
|
|
|
|
const stats = this.calculateTeamStats(team, teamResults, teamDrivers);
|
|
await this.seedDeps.teamStatsRepository.saveTeamStats(team.id, stats);
|
|
}
|
|
|
|
this.logger.info(`[Bootstrap] Computed and stored stats for ${teams.length} teams`);
|
|
}
|
|
|
|
private calculateTeamStats(_team: Team, results: Result[], drivers: Driver[]): TeamStats {
|
|
const wins = results.filter(r => r.position.toNumber() === 1).length;
|
|
const totalRaces = results.length;
|
|
|
|
// Calculate rating
|
|
const baseRating = 1000;
|
|
const winBonus = wins * 50;
|
|
const raceBonus = Math.min(totalRaces * 5, 200);
|
|
const rating = Math.round(baseRating + winBonus + raceBonus);
|
|
|
|
// Determine performance level
|
|
let performanceLevel: 'beginner' | 'intermediate' | 'advanced' | 'pro';
|
|
if (wins >= 20) performanceLevel = 'pro';
|
|
else if (wins >= 10) performanceLevel = 'advanced';
|
|
else if (wins >= 5) performanceLevel = 'intermediate';
|
|
else performanceLevel = 'beginner';
|
|
|
|
// Determine specialization (based on race types - simplified)
|
|
const specialization: 'endurance' | 'sprint' | 'mixed' = 'mixed';
|
|
|
|
// Get region from team name or first driver
|
|
const region = drivers.length > 0 && drivers[0] ? drivers[0].country.toString() : 'International';
|
|
|
|
// Languages (based on drivers)
|
|
const languages = Array.from(new Set(drivers.map(d => {
|
|
// Simplified language mapping based on country
|
|
const country = d.country.toString().toLowerCase();
|
|
if (country === 'us' || country === 'gb' || country === 'ca') return 'en';
|
|
if (country === 'de') return 'de';
|
|
if (country === 'fr') return 'fr';
|
|
if (country === 'es') return 'es';
|
|
if (country === 'it') return 'it';
|
|
if (country === 'jp') return 'ja';
|
|
return 'en';
|
|
})));
|
|
|
|
return {
|
|
performanceLevel,
|
|
specialization,
|
|
region,
|
|
languages,
|
|
totalWins: wins,
|
|
totalRaces,
|
|
rating
|
|
};
|
|
}
|
|
|
|
private determineExperienceLevel(totalRaces: number): string {
|
|
if (totalRaces >= 100) return 'Veteran';
|
|
if (totalRaces >= 50) return 'Experienced';
|
|
if (totalRaces >= 20) return 'Intermediate';
|
|
if (totalRaces >= 10) return 'Rookie';
|
|
return 'Beginner';
|
|
}
|
|
|
|
|
|
private async clearExistingRacingData(): Promise<void> {
|
|
this.logger.info('[Bootstrap] Starting comprehensive clearing of all racing data');
|
|
|
|
// Clear stats repositories first
|
|
try {
|
|
await this.seedDeps.driverStatsRepository.clear();
|
|
await this.seedDeps.teamStatsRepository.clear();
|
|
this.logger.info('[Bootstrap] Cleared stats repositories');
|
|
} catch (error) {
|
|
this.logger.warn('[Bootstrap] Could not clear stats repositories:', error);
|
|
}
|
|
|
|
// Clear race registrations - get all races first, then clear their registrations
|
|
try {
|
|
const races = await this.seedDeps.raceRepository.findAll();
|
|
for (const race of races) {
|
|
try {
|
|
await this.seedDeps.raceRegistrationRepository.clearRaceRegistrations(race.id.toString());
|
|
} catch {
|
|
// Ignore
|
|
}
|
|
}
|
|
this.logger.info('[Bootstrap] Cleared race registrations');
|
|
} catch (error) {
|
|
this.logger.warn('[Bootstrap] Could not clear race registrations:', error);
|
|
}
|
|
|
|
// Clear team join requests - get all teams first, then clear their join requests
|
|
try {
|
|
const teams = await this.seedDeps.teamRepository.findAll();
|
|
for (const team of teams) {
|
|
const joinRequests = await this.seedDeps.teamMembershipRepository.getJoinRequests(team.id.toString());
|
|
for (const request of joinRequests) {
|
|
try {
|
|
await this.seedDeps.teamMembershipRepository.removeJoinRequest(request.id);
|
|
} catch {
|
|
// Ignore
|
|
}
|
|
}
|
|
}
|
|
this.logger.info('[Bootstrap] Cleared team join requests');
|
|
} catch (error) {
|
|
this.logger.warn('[Bootstrap] Could not clear team join requests:', error);
|
|
}
|
|
|
|
// Clear team memberships
|
|
try {
|
|
const teams = await this.seedDeps.teamRepository.findAll();
|
|
for (const team of teams) {
|
|
const memberships = await this.seedDeps.teamMembershipRepository.getTeamMembers(team.id.toString());
|
|
for (const membership of memberships) {
|
|
try {
|
|
await this.seedDeps.teamMembershipRepository.removeMembership(team.id.toString(), membership.driverId.toString());
|
|
} catch {
|
|
// Ignore
|
|
}
|
|
}
|
|
}
|
|
this.logger.info('[Bootstrap] Cleared team memberships');
|
|
} catch (error) {
|
|
this.logger.warn('[Bootstrap] Could not clear team memberships:', error);
|
|
}
|
|
|
|
// Clear teams (this is critical - teams have stale logoRef)
|
|
try {
|
|
const teams = await this.seedDeps.teamRepository.findAll();
|
|
for (const team of teams) {
|
|
try {
|
|
await this.seedDeps.teamRepository.delete(team.id.toString());
|
|
} catch {
|
|
// Ignore
|
|
}
|
|
}
|
|
this.logger.info('[Bootstrap] Cleared teams');
|
|
} catch (error) {
|
|
this.logger.warn('[Bootstrap] Could not clear teams:', error);
|
|
}
|
|
|
|
// Clear results
|
|
try {
|
|
const results = await this.seedDeps.resultRepository.findAll();
|
|
for (const result of results) {
|
|
try {
|
|
await this.seedDeps.resultRepository.delete(result.id.toString());
|
|
} catch {
|
|
// Ignore
|
|
}
|
|
}
|
|
this.logger.info('[Bootstrap] Cleared results');
|
|
} catch (error) {
|
|
this.logger.warn('[Bootstrap] Could not clear results:', error);
|
|
}
|
|
|
|
// Clear standings
|
|
try {
|
|
const standings = await this.seedDeps.standingRepository.findAll();
|
|
for (const standing of standings) {
|
|
try {
|
|
await this.seedDeps.standingRepository.delete(standing.leagueId.toString(), standing.driverId.toString());
|
|
} catch {
|
|
// Ignore
|
|
}
|
|
}
|
|
this.logger.info('[Bootstrap] Cleared standings');
|
|
} catch (error) {
|
|
this.logger.warn('[Bootstrap] Could not clear standings:', error);
|
|
}
|
|
|
|
// Clear races
|
|
try {
|
|
const races = await this.seedDeps.raceRepository.findAll();
|
|
for (const race of races) {
|
|
try {
|
|
await this.seedDeps.raceRepository.delete(race.id.toString());
|
|
} catch {
|
|
// Ignore
|
|
}
|
|
}
|
|
this.logger.info('[Bootstrap] Cleared races');
|
|
} catch (error) {
|
|
this.logger.warn('[Bootstrap] Could not clear races:', error);
|
|
}
|
|
|
|
// Clear league join requests
|
|
try {
|
|
const leagues = await this.seedDeps.leagueRepository.findAll();
|
|
for (const league of leagues) {
|
|
const joinRequests = await this.seedDeps.leagueMembershipRepository.getJoinRequests(league.id.toString());
|
|
for (const request of joinRequests) {
|
|
try {
|
|
await this.seedDeps.leagueMembershipRepository.removeJoinRequest(request.id);
|
|
} catch {
|
|
// Ignore
|
|
}
|
|
}
|
|
}
|
|
this.logger.info('[Bootstrap] Cleared league join requests');
|
|
} catch (error) {
|
|
this.logger.warn('[Bootstrap] Could not clear league join requests:', error);
|
|
}
|
|
|
|
// Clear league memberships
|
|
try {
|
|
const leagues = await this.seedDeps.leagueRepository.findAll();
|
|
for (const league of leagues) {
|
|
const memberships = await this.seedDeps.leagueMembershipRepository.getLeagueMembers(league.id.toString());
|
|
for (const membership of memberships) {
|
|
try {
|
|
await this.seedDeps.leagueMembershipRepository.removeMembership(league.id.toString(), membership.driverId.toString());
|
|
} catch {
|
|
// Ignore
|
|
}
|
|
}
|
|
}
|
|
this.logger.info('[Bootstrap] Cleared league memberships');
|
|
} catch (error) {
|
|
this.logger.warn('[Bootstrap] Could not clear league memberships:', error);
|
|
}
|
|
|
|
// Note: Some repositories don't support direct deletion methods
|
|
// The key fix is clearing teams, team memberships, and join requests
|
|
// which resolves the logoRef issue
|
|
|
|
// Clear leagues
|
|
try {
|
|
const leagues = await this.seedDeps.leagueRepository.findAll();
|
|
for (const league of leagues) {
|
|
try {
|
|
await this.seedDeps.leagueRepository.delete(league.id.toString());
|
|
} catch {
|
|
// Ignore
|
|
}
|
|
}
|
|
this.logger.info('[Bootstrap] Cleared leagues');
|
|
} catch (error) {
|
|
this.logger.warn('[Bootstrap] Could not clear leagues:', error);
|
|
}
|
|
|
|
// Clear drivers (do this last as other data depends on it)
|
|
try {
|
|
const drivers = await this.seedDeps.driverRepository.findAll();
|
|
for (const driver of drivers) {
|
|
try {
|
|
await this.seedDeps.driverRepository.delete(driver.id);
|
|
} catch {
|
|
// Ignore
|
|
}
|
|
}
|
|
this.logger.info('[Bootstrap] Cleared drivers');
|
|
} catch (error) {
|
|
this.logger.warn('[Bootstrap] Could not clear drivers:', error);
|
|
}
|
|
|
|
// Clear social data if repositories support it
|
|
try {
|
|
const seedableFeed = this.seedDeps.feedRepository as unknown as { clear?: () => void };
|
|
if (typeof seedableFeed.clear === 'function') {
|
|
seedableFeed.clear();
|
|
this.logger.info('[Bootstrap] Cleared feed repository');
|
|
}
|
|
} catch (error) {
|
|
this.logger.warn('[Bootstrap] Could not clear feed repository:', error);
|
|
}
|
|
|
|
try {
|
|
const seedableSocial = this.seedDeps.socialGraphRepository as unknown as { clear?: () => void };
|
|
if (typeof seedableSocial.clear === 'function') {
|
|
seedableSocial.clear();
|
|
this.logger.info('[Bootstrap] Cleared social graph repository');
|
|
}
|
|
} catch (error) {
|
|
this.logger.warn('[Bootstrap] Could not clear social graph repository:', error);
|
|
}
|
|
|
|
this.logger.info('[Bootstrap] Completed comprehensive clearing of all racing data');
|
|
}
|
|
|
|
private async ensureScoringConfigsForExistingData(): Promise<void> {
|
|
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';
|
|
}
|
|
} |