seed data
This commit is contained in:
@@ -61,5 +61,61 @@ export class EnsureInitialData {
|
||||
}
|
||||
|
||||
this.logger.info(`[Bootstrap] Achievements: ${createdCount} created, ${existingCount} already exist`);
|
||||
|
||||
// Create additional users for comprehensive seeding
|
||||
await this.createAdditionalUsers();
|
||||
|
||||
// Create user achievements linking users to achievements
|
||||
await this.createUserAchievements();
|
||||
}
|
||||
|
||||
private async createAdditionalUsers(): Promise<void> {
|
||||
const userConfigs = [
|
||||
// Driver with iRacing linked
|
||||
{ displayName: 'Max Verstappen', email: 'max@racing.com', password: 'Test123!', iracingCustomerId: '12345' },
|
||||
// Driver without email
|
||||
{ displayName: 'Lewis Hamilton', email: undefined, password: undefined, iracingCustomerId: '67890' },
|
||||
// Sponsor user
|
||||
{ displayName: 'Sponsor Inc', email: 'sponsor@example.com', password: 'Test123!', iracingCustomerId: undefined },
|
||||
// Various driver profiles
|
||||
{ displayName: 'Charles Leclerc', email: 'charles@ferrari.com', password: 'Test123!', iracingCustomerId: '11111' },
|
||||
{ displayName: 'Lando Norris', email: 'lando@mclaren.com', password: 'Test123!', iracingCustomerId: '22222' },
|
||||
{ displayName: 'George Russell', email: 'george@mercedes.com', password: 'Test123!', iracingCustomerId: '33333' },
|
||||
{ displayName: 'Carlos Sainz', email: 'carlos@ferrari.com', password: 'Test123!', iracingCustomerId: '44444' },
|
||||
{ displayName: 'Fernando Alonso', email: 'fernando@aston.com', password: 'Test123!', iracingCustomerId: '55555' },
|
||||
{ displayName: 'Sergio Perez', email: 'sergio@redbull.com', password: 'Test123!', iracingCustomerId: '66666' },
|
||||
{ displayName: 'Valtteri Bottas', email: 'valtteri@sauber.com', password: 'Test123!', iracingCustomerId: '77777' },
|
||||
];
|
||||
|
||||
let createdCount = 0;
|
||||
|
||||
for (const config of userConfigs) {
|
||||
if (!config.email) {
|
||||
// Skip users without email for now (would need different creation method)
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.signupUseCase.execute({
|
||||
email: config.email,
|
||||
password: config.password!,
|
||||
displayName: config.displayName,
|
||||
});
|
||||
|
||||
if (result.isOk()) {
|
||||
createdCount++;
|
||||
}
|
||||
} catch (error) {
|
||||
// User might already exist, skip
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.info(`[Bootstrap] Created ${createdCount} additional users`);
|
||||
}
|
||||
|
||||
private async createUserAchievements(): Promise<void> {
|
||||
// This would require access to user and achievement repositories
|
||||
// For now, we'll log that this would be done
|
||||
this.logger.info('[Bootstrap] User achievements would be created here (requires repository access)');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,9 @@ import type { IPenaltyRepository } from '@core/racing/domain/repositories/IPenal
|
||||
import type { IFeedRepository } from '@core/social/domain/repositories/IFeedRepository';
|
||||
import type { ISocialGraphRepository } from '@core/social/domain/repositories/ISocialGraphRepository';
|
||||
import { createRacingSeed } from './racing/RacingSeed';
|
||||
import { getApiPersistence } from '../../apps/api/src/env';
|
||||
import { seedId } from './racing/SeedIdHelper';
|
||||
import { DriverStatsStore } from '@adapters/racing/services/DriverStatsStore';
|
||||
import { TeamStatsStore } from '@adapters/racing/services/TeamStatsStore';
|
||||
|
||||
export type RacingSeedDependencies = {
|
||||
driverRepository: IDriverRepository;
|
||||
@@ -54,16 +55,56 @@ export class SeedRacingData {
|
||||
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();
|
||||
if (existingDrivers.length > 0) {
|
||||
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;
|
||||
}
|
||||
|
||||
const persistence = getApiPersistence();
|
||||
const seed = createRacingSeed({ persistence });
|
||||
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
|
||||
@@ -268,6 +309,36 @@ export class SeedRacingData {
|
||||
);
|
||||
}
|
||||
|
||||
private async clearExistingRacingData(): Promise<void> {
|
||||
// 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<void> {
|
||||
const leagues = await this.seedDeps.leagueRepository.findAll();
|
||||
|
||||
|
||||
@@ -2,6 +2,22 @@ import { Driver } from '@core/racing/domain/entities/Driver';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { seedId } from './SeedIdHelper';
|
||||
|
||||
export interface DriverStats {
|
||||
rating: number;
|
||||
safetyRating: number;
|
||||
sportsmanshipRating: number;
|
||||
totalRaces: number;
|
||||
wins: number;
|
||||
podiums: number;
|
||||
dnfs: number;
|
||||
avgFinish: number;
|
||||
bestFinish: number;
|
||||
worstFinish: number;
|
||||
consistency: number;
|
||||
experienceLevel: 'beginner' | 'intermediate' | 'advanced' | 'veteran';
|
||||
overallRank: number | null;
|
||||
}
|
||||
|
||||
export class RacingDriverFactory {
|
||||
constructor(
|
||||
private readonly driverCount: number,
|
||||
@@ -15,14 +31,156 @@ export class RacingDriverFactory {
|
||||
return Array.from({ length: this.driverCount }, (_, idx) => {
|
||||
const i = idx + 1;
|
||||
|
||||
return Driver.create({
|
||||
// Vary bio presence: 80% have bio, 20% empty
|
||||
const hasBio = i % 5 !== 0;
|
||||
|
||||
// Vary joined dates: some recent (new drivers), some old (veterans)
|
||||
let joinedAt: Date;
|
||||
if (i % 10 === 0) {
|
||||
// Very recent drivers (within last week)
|
||||
joinedAt = faker.date.recent({ days: 7, refDate: this.baseDate });
|
||||
} else if (i % 7 === 0) {
|
||||
// Recent drivers (within last month)
|
||||
joinedAt = faker.date.recent({ days: 30, refDate: this.baseDate });
|
||||
} else if (i % 5 === 0) {
|
||||
// Medium tenure (6-12 months ago)
|
||||
joinedAt = faker.date.between({
|
||||
from: new Date(this.baseDate.getTime() - 365 * 24 * 60 * 60 * 1000),
|
||||
to: new Date(this.baseDate.getTime() - 180 * 24 * 60 * 60 * 1000)
|
||||
});
|
||||
} else {
|
||||
// Veterans (1-2 years ago)
|
||||
joinedAt = faker.date.past({ years: 2, refDate: this.baseDate });
|
||||
}
|
||||
|
||||
const driverData: {
|
||||
id: string;
|
||||
iracingId: string;
|
||||
name: string;
|
||||
country: string;
|
||||
bio?: string;
|
||||
joinedAt?: Date;
|
||||
} = {
|
||||
id: seedId(`driver-${i}`, this.persistence),
|
||||
iracingId: String(100000 + i),
|
||||
name: faker.person.fullName(),
|
||||
country: faker.helpers.arrayElement(countries),
|
||||
bio: faker.lorem.sentences(2),
|
||||
joinedAt: faker.date.past({ years: 2, refDate: this.baseDate }),
|
||||
});
|
||||
joinedAt,
|
||||
};
|
||||
|
||||
if (hasBio) {
|
||||
driverData.bio = faker.lorem.sentences(2);
|
||||
}
|
||||
|
||||
return Driver.create(driverData);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate driver statistics for profile display
|
||||
* This would be stored in a separate stats table/service in production
|
||||
*/
|
||||
generateDriverStats(drivers: Driver[]): Map<string, DriverStats> {
|
||||
const statsMap = new Map<string, DriverStats>();
|
||||
|
||||
drivers.forEach((driver, idx) => {
|
||||
const i = idx + 1;
|
||||
|
||||
// Determine experience level based on join date and index
|
||||
let experienceLevel: 'beginner' | 'intermediate' | 'advanced' | 'veteran';
|
||||
let totalRaces: number;
|
||||
let rating: number;
|
||||
let safetyRating: number;
|
||||
let sportsmanshipRating: number;
|
||||
|
||||
if (i % 10 === 0) {
|
||||
// Very recent drivers
|
||||
experienceLevel = 'beginner';
|
||||
totalRaces = faker.number.int({ min: 1, max: 10 });
|
||||
rating = faker.number.int({ min: 800, max: 1200 });
|
||||
safetyRating = faker.number.int({ min: 60, max: 85 });
|
||||
sportsmanshipRating = Math.round(faker.number.float({ min: 3.0, max: 4.2 }) * 10) / 10;
|
||||
} else if (i % 7 === 0) {
|
||||
// Recent drivers
|
||||
experienceLevel = 'beginner';
|
||||
totalRaces = faker.number.int({ min: 5, max: 25 });
|
||||
rating = faker.number.int({ min: 1000, max: 1400 });
|
||||
safetyRating = faker.number.int({ min: 70, max: 90 });
|
||||
sportsmanshipRating = Math.round(faker.number.float({ min: 3.5, max: 4.5 }) * 10) / 10;
|
||||
} else if (i % 5 === 0) {
|
||||
// Medium tenure
|
||||
experienceLevel = 'intermediate';
|
||||
totalRaces = faker.number.int({ min: 20, max: 60 });
|
||||
rating = faker.number.int({ min: 1300, max: 1700 });
|
||||
safetyRating = faker.number.int({ min: 75, max: 95 });
|
||||
sportsmanshipRating = Math.round(faker.number.float({ min: 3.8, max: 4.8 }) * 10) / 10;
|
||||
} else if (i % 3 === 0) {
|
||||
// Advanced
|
||||
experienceLevel = 'advanced';
|
||||
totalRaces = faker.number.int({ min: 50, max: 120 });
|
||||
rating = faker.number.int({ min: 1600, max: 1900 });
|
||||
safetyRating = faker.number.int({ min: 80, max: 98 });
|
||||
sportsmanshipRating = Math.round(faker.number.float({ min: 4.0, max: 5.0 }) * 10) / 10;
|
||||
} else {
|
||||
// Veterans
|
||||
experienceLevel = 'veteran';
|
||||
totalRaces = faker.number.int({ min: 100, max: 200 });
|
||||
rating = faker.number.int({ min: 1700, max: 2000 });
|
||||
safetyRating = faker.number.int({ min: 85, max: 100 });
|
||||
sportsmanshipRating = Math.round(faker.number.float({ min: 4.2, max: 5.0 }) * 10) / 10;
|
||||
}
|
||||
|
||||
// Calculate performance stats based on total races
|
||||
const winRate = experienceLevel === 'beginner' ? faker.number.float({ min: 0, max: 5, fractionDigits: 1 }) :
|
||||
experienceLevel === 'intermediate' ? faker.number.float({ min: 2, max: 10, fractionDigits: 1 }) :
|
||||
experienceLevel === 'advanced' ? faker.number.float({ min: 5, max: 15, fractionDigits: 1 }) :
|
||||
faker.number.float({ min: 8, max: 20, fractionDigits: 1 });
|
||||
|
||||
const podiumRate = experienceLevel === 'beginner' ? faker.number.float({ min: 5, max: 15, fractionDigits: 1 }) :
|
||||
experienceLevel === 'intermediate' ? faker.number.float({ min: 10, max: 25, fractionDigits: 1 }) :
|
||||
experienceLevel === 'advanced' ? faker.number.float({ min: 15, max: 35, fractionDigits: 1 }) :
|
||||
faker.number.float({ min: 20, max: 45, fractionDigits: 1 });
|
||||
|
||||
const wins = Math.round((winRate / 100) * totalRaces);
|
||||
const podiums = Math.round((podiumRate / 100) * totalRaces);
|
||||
const dnfs = Math.round(faker.number.float({ min: 0.05, max: 0.15, fractionDigits: 2 }) * totalRaces);
|
||||
|
||||
const avgFinish = experienceLevel === 'beginner' ? faker.number.float({ min: 8, max: 15, fractionDigits: 1 }) :
|
||||
experienceLevel === 'intermediate' ? faker.number.float({ min: 5, max: 10, fractionDigits: 1 }) :
|
||||
experienceLevel === 'advanced' ? faker.number.float({ min: 3, max: 7, fractionDigits: 1 }) :
|
||||
faker.number.float({ min: 2, max: 5, fractionDigits: 1 });
|
||||
|
||||
const bestFinish = experienceLevel === 'beginner' ? faker.number.int({ min: 3, max: 8 }) :
|
||||
experienceLevel === 'intermediate' ? faker.number.int({ min: 1, max: 5 }) :
|
||||
faker.number.int({ min: 1, max: 3 });
|
||||
|
||||
const worstFinish = experienceLevel === 'beginner' ? faker.number.int({ min: 12, max: 20 }) :
|
||||
experienceLevel === 'intermediate' ? faker.number.int({ min: 8, max: 15 }) :
|
||||
experienceLevel === 'advanced' ? faker.number.int({ min: 5, max: 12 }) :
|
||||
faker.number.int({ min: 4, max: 10 });
|
||||
|
||||
const consistency = experienceLevel === 'beginner' ? faker.number.float({ min: 40, max: 65, fractionDigits: 0 }) :
|
||||
experienceLevel === 'intermediate' ? faker.number.float({ min: 60, max: 80, fractionDigits: 0 }) :
|
||||
experienceLevel === 'advanced' ? faker.number.float({ min: 75, max: 90, fractionDigits: 0 }) :
|
||||
faker.number.float({ min: 85, max: 98, fractionDigits: 0 });
|
||||
|
||||
statsMap.set(driver.id.toString(), {
|
||||
rating,
|
||||
safetyRating,
|
||||
sportsmanshipRating,
|
||||
totalRaces,
|
||||
wins,
|
||||
podiums,
|
||||
dnfs,
|
||||
avgFinish: parseFloat(avgFinish.toFixed(1)),
|
||||
bestFinish,
|
||||
worstFinish,
|
||||
consistency: parseFloat(consistency.toFixed(1)),
|
||||
experienceLevel,
|
||||
overallRank: null, // Will be calculated after all stats are loaded
|
||||
});
|
||||
});
|
||||
|
||||
return statsMap;
|
||||
}
|
||||
}
|
||||
@@ -11,29 +11,273 @@ export class RacingLeagueFactory {
|
||||
) {}
|
||||
|
||||
create(): League[] {
|
||||
const leagueCount = 20;
|
||||
const leagueCount = 30;
|
||||
|
||||
// Create diverse league configurations
|
||||
// Create diverse league configurations covering ALL enum combinations
|
||||
// Points systems: f1-2024, indycar, custom (3)
|
||||
// Qualifying formats: single-lap, open (2)
|
||||
// Visibility: ranked, unranked (2)
|
||||
// Decision modes: admin_only, steward_decides, steward_vote, member_vote, steward_veto, member_veto (6)
|
||||
// Total combinations: 3 * 2 * 2 * 6 = 72, but we'll sample 30 covering extremes
|
||||
|
||||
const leagueConfigs = [
|
||||
// Small sprint leagues
|
||||
{ maxDrivers: 16, sessionDuration: 30, pointsSystem: 'f1-2024' as const, qualifyingFormat: 'single-lap' as const },
|
||||
{ maxDrivers: 20, sessionDuration: 45, pointsSystem: 'f1-2024' as const, qualifyingFormat: 'open' as const },
|
||||
// Medium endurance leagues
|
||||
{ maxDrivers: 24, sessionDuration: 60, pointsSystem: 'indycar' as const, qualifyingFormat: 'open' as const },
|
||||
{ maxDrivers: 28, sessionDuration: 90, pointsSystem: 'custom' as const, qualifyingFormat: 'open' as const },
|
||||
// Large mixed leagues
|
||||
{ maxDrivers: 32, sessionDuration: 120, pointsSystem: 'f1-2024' as const, qualifyingFormat: 'open' as const },
|
||||
{ maxDrivers: 36, sessionDuration: 75, pointsSystem: 'indycar' as const, qualifyingFormat: 'single-lap' as const },
|
||||
{ maxDrivers: 40, sessionDuration: 100, pointsSystem: 'custom' as const, qualifyingFormat: 'open' as const },
|
||||
{ maxDrivers: 44, sessionDuration: 85, pointsSystem: 'f1-2024' as const, qualifyingFormat: 'open' as const },
|
||||
{ maxDrivers: 48, sessionDuration: 110, pointsSystem: 'indycar' as const, qualifyingFormat: 'single-lap' as const },
|
||||
{ maxDrivers: 50, sessionDuration: 95, pointsSystem: 'custom' as const, qualifyingFormat: 'open' as const },
|
||||
// 1-5: Ranked, F1-2024, various stewarding
|
||||
{
|
||||
pointsSystem: 'f1-2024' as const,
|
||||
qualifyingFormat: 'single-lap' as const,
|
||||
visibility: 'ranked' as const,
|
||||
maxDrivers: 40,
|
||||
sessionDuration: 60,
|
||||
stewarding: { decisionMode: 'admin_only' as const, requireDefense: false }
|
||||
},
|
||||
{
|
||||
pointsSystem: 'f1-2024' as const,
|
||||
qualifyingFormat: 'open' as const,
|
||||
visibility: 'ranked' as const,
|
||||
maxDrivers: 32,
|
||||
sessionDuration: 45,
|
||||
stewarding: { decisionMode: 'steward_vote' as const, requiredVotes: 3, requireDefense: true, defenseTimeLimit: 24 }
|
||||
},
|
||||
{
|
||||
pointsSystem: 'f1-2024' as const,
|
||||
qualifyingFormat: 'single-lap' as const,
|
||||
visibility: 'ranked' as const,
|
||||
maxDrivers: 50,
|
||||
sessionDuration: 90,
|
||||
stewarding: { decisionMode: 'member_vote' as const, requiredVotes: 5, requireDefense: false }
|
||||
},
|
||||
{
|
||||
pointsSystem: 'f1-2024' as const,
|
||||
qualifyingFormat: 'open' as const,
|
||||
visibility: 'ranked' as const,
|
||||
maxDrivers: 24,
|
||||
sessionDuration: 30,
|
||||
stewarding: { decisionMode: 'steward_veto' as const, requiredVotes: 2, requireDefense: true }
|
||||
},
|
||||
{
|
||||
pointsSystem: 'f1-2024' as const,
|
||||
qualifyingFormat: 'single-lap' as const,
|
||||
visibility: 'ranked' as const,
|
||||
maxDrivers: 60,
|
||||
sessionDuration: 120,
|
||||
stewarding: { decisionMode: 'member_veto' as const, requiredVotes: 4, requireDefense: false }
|
||||
},
|
||||
|
||||
// 6-10: Ranked, IndyCar, various stewarding
|
||||
{
|
||||
pointsSystem: 'indycar' as const,
|
||||
qualifyingFormat: 'open' as const,
|
||||
visibility: 'ranked' as const,
|
||||
maxDrivers: 28,
|
||||
sessionDuration: 75,
|
||||
stewarding: { decisionMode: 'steward_decides' as const, requireDefense: true, defenseTimeLimit: 48 }
|
||||
},
|
||||
{
|
||||
pointsSystem: 'indycar' as const,
|
||||
qualifyingFormat: 'single-lap' as const,
|
||||
visibility: 'ranked' as const,
|
||||
maxDrivers: 36,
|
||||
sessionDuration: 60,
|
||||
stewarding: { decisionMode: 'admin_only' as const, requireDefense: false }
|
||||
},
|
||||
{
|
||||
pointsSystem: 'indycar' as const,
|
||||
qualifyingFormat: 'open' as const,
|
||||
visibility: 'ranked' as const,
|
||||
maxDrivers: 44,
|
||||
sessionDuration: 100,
|
||||
stewarding: { decisionMode: 'steward_vote' as const, requiredVotes: 4, requireDefense: true, voteTimeLimit: 72 }
|
||||
},
|
||||
{
|
||||
pointsSystem: 'indycar' as const,
|
||||
qualifyingFormat: 'single-lap' as const,
|
||||
visibility: 'ranked' as const,
|
||||
maxDrivers: 20,
|
||||
sessionDuration: 45,
|
||||
stewarding: { decisionMode: 'member_vote' as const, requiredVotes: 6, requireDefense: false }
|
||||
},
|
||||
{
|
||||
pointsSystem: 'indycar' as const,
|
||||
qualifyingFormat: 'open' as const,
|
||||
visibility: 'ranked' as const,
|
||||
maxDrivers: 48,
|
||||
sessionDuration: 110,
|
||||
stewarding: { decisionMode: 'steward_veto' as const, requiredVotes: 3, requireDefense: true }
|
||||
},
|
||||
|
||||
// 11-15: Ranked, Custom, various stewarding
|
||||
{
|
||||
pointsSystem: 'custom' as const,
|
||||
qualifyingFormat: 'single-lap' as const,
|
||||
visibility: 'ranked' as const,
|
||||
maxDrivers: 32,
|
||||
sessionDuration: 50,
|
||||
stewarding: { decisionMode: 'member_veto' as const, requiredVotes: 5, requireDefense: false }
|
||||
},
|
||||
{
|
||||
pointsSystem: 'custom' as const,
|
||||
qualifyingFormat: 'open' as const,
|
||||
visibility: 'ranked' as const,
|
||||
maxDrivers: 40,
|
||||
sessionDuration: 85,
|
||||
stewarding: { decisionMode: 'admin_only' as const, requireDefense: false }
|
||||
},
|
||||
{
|
||||
pointsSystem: 'custom' as const,
|
||||
qualifyingFormat: 'single-lap' as const,
|
||||
visibility: 'ranked' as const,
|
||||
maxDrivers: 52,
|
||||
sessionDuration: 95,
|
||||
stewarding: { decisionMode: 'steward_decides' as const, requireDefense: true, defenseTimeLimit: 36 }
|
||||
},
|
||||
{
|
||||
pointsSystem: 'custom' as const,
|
||||
qualifyingFormat: 'open' as const,
|
||||
visibility: 'ranked' as const,
|
||||
maxDrivers: 26,
|
||||
sessionDuration: 65,
|
||||
stewarding: { decisionMode: 'steward_vote' as const, requiredVotes: 2, requireDefense: false }
|
||||
},
|
||||
{
|
||||
pointsSystem: 'custom' as const,
|
||||
qualifyingFormat: 'single-lap' as const,
|
||||
visibility: 'ranked' as const,
|
||||
maxDrivers: 56,
|
||||
sessionDuration: 105,
|
||||
stewarding: { decisionMode: 'member_vote' as const, requiredVotes: 7, requireDefense: true, protestDeadlineHours: 24 }
|
||||
},
|
||||
|
||||
// 16-20: Unranked, F1-2024, various stewarding (smaller, no participant requirements)
|
||||
{
|
||||
pointsSystem: 'f1-2024' as const,
|
||||
qualifyingFormat: 'open' as const,
|
||||
visibility: 'unranked' as const,
|
||||
maxDrivers: 8,
|
||||
sessionDuration: 30,
|
||||
stewarding: { decisionMode: 'admin_only' as const, requireDefense: false }
|
||||
},
|
||||
{
|
||||
pointsSystem: 'f1-2024' as const,
|
||||
qualifyingFormat: 'single-lap' as const,
|
||||
visibility: 'unranked' as const,
|
||||
maxDrivers: 12,
|
||||
sessionDuration: 45,
|
||||
stewarding: { decisionMode: 'steward_decides' as const, requireDefense: true }
|
||||
},
|
||||
{
|
||||
pointsSystem: 'f1-2024' as const,
|
||||
qualifyingFormat: 'open' as const,
|
||||
visibility: 'unranked' as const,
|
||||
maxDrivers: 16,
|
||||
sessionDuration: 60,
|
||||
stewarding: { decisionMode: 'steward_vote' as const, requiredVotes: 2, requireDefense: false }
|
||||
},
|
||||
{
|
||||
pointsSystem: 'f1-2024' as const,
|
||||
qualifyingFormat: 'single-lap' as const,
|
||||
visibility: 'unranked' as const,
|
||||
maxDrivers: 10,
|
||||
sessionDuration: 35,
|
||||
stewarding: { decisionMode: 'member_vote' as const, requiredVotes: 3, requireDefense: true }
|
||||
},
|
||||
{
|
||||
pointsSystem: 'f1-2024' as const,
|
||||
qualifyingFormat: 'open' as const,
|
||||
visibility: 'unranked' as const,
|
||||
maxDrivers: 20,
|
||||
sessionDuration: 55,
|
||||
stewarding: { decisionMode: 'steward_veto' as const, requiredVotes: 2, requireDefense: false }
|
||||
},
|
||||
|
||||
// 21-25: Unranked, IndyCar, various stewarding
|
||||
{
|
||||
pointsSystem: 'indycar' as const,
|
||||
qualifyingFormat: 'single-lap' as const,
|
||||
visibility: 'unranked' as const,
|
||||
maxDrivers: 14,
|
||||
sessionDuration: 40,
|
||||
stewarding: { decisionMode: 'member_veto' as const, requiredVotes: 3, requireDefense: true }
|
||||
},
|
||||
{
|
||||
pointsSystem: 'indycar' as const,
|
||||
qualifyingFormat: 'open' as const,
|
||||
visibility: 'unranked' as const,
|
||||
maxDrivers: 18,
|
||||
sessionDuration: 50,
|
||||
stewarding: { decisionMode: 'admin_only' as const, requireDefense: false }
|
||||
},
|
||||
{
|
||||
pointsSystem: 'indycar' as const,
|
||||
qualifyingFormat: 'single-lap' as const,
|
||||
visibility: 'unranked' as const,
|
||||
maxDrivers: 22,
|
||||
sessionDuration: 65,
|
||||
stewarding: { decisionMode: 'steward_decides' as const, requireDefense: true, defenseTimeLimit: 12 }
|
||||
},
|
||||
{
|
||||
pointsSystem: 'indycar' as const,
|
||||
qualifyingFormat: 'open' as const,
|
||||
visibility: 'unranked' as const,
|
||||
maxDrivers: 6,
|
||||
sessionDuration: 25,
|
||||
stewarding: { decisionMode: 'steward_vote' as const, requiredVotes: 2, requireDefense: false }
|
||||
},
|
||||
{
|
||||
pointsSystem: 'indycar' as const,
|
||||
qualifyingFormat: 'single-lap' as const,
|
||||
visibility: 'unranked' as const,
|
||||
maxDrivers: 24,
|
||||
sessionDuration: 70,
|
||||
stewarding: { decisionMode: 'member_vote' as const, requiredVotes: 4, requireDefense: true }
|
||||
},
|
||||
|
||||
// 26-30: Unranked, Custom, various stewarding
|
||||
{
|
||||
pointsSystem: 'custom' as const,
|
||||
qualifyingFormat: 'open' as const,
|
||||
visibility: 'unranked' as const,
|
||||
maxDrivers: 12,
|
||||
sessionDuration: 35,
|
||||
stewarding: { decisionMode: 'steward_veto' as const, requiredVotes: 2, requireDefense: false }
|
||||
},
|
||||
{
|
||||
pointsSystem: 'custom' as const,
|
||||
qualifyingFormat: 'single-lap' as const,
|
||||
visibility: 'unranked' as const,
|
||||
maxDrivers: 16,
|
||||
sessionDuration: 45,
|
||||
stewarding: { decisionMode: 'member_veto' as const, requiredVotes: 3, requireDefense: true }
|
||||
},
|
||||
{
|
||||
pointsSystem: 'custom' as const,
|
||||
qualifyingFormat: 'open' as const,
|
||||
visibility: 'unranked' as const,
|
||||
maxDrivers: 20,
|
||||
sessionDuration: 55,
|
||||
stewarding: { decisionMode: 'admin_only' as const, requireDefense: false }
|
||||
},
|
||||
{
|
||||
pointsSystem: 'custom' as const,
|
||||
qualifyingFormat: 'single-lap' as const,
|
||||
visibility: 'unranked' as const,
|
||||
maxDrivers: 8,
|
||||
sessionDuration: 30,
|
||||
stewarding: { decisionMode: 'steward_decides' as const, requireDefense: true, defenseTimeLimit: 18 }
|
||||
},
|
||||
{
|
||||
pointsSystem: 'custom' as const,
|
||||
qualifyingFormat: 'open' as const,
|
||||
visibility: 'unranked' as const,
|
||||
maxDrivers: 24,
|
||||
sessionDuration: 60,
|
||||
stewarding: { decisionMode: 'steward_vote' as const, requiredVotes: 3, requireDefense: false }
|
||||
},
|
||||
];
|
||||
|
||||
return Array.from({ length: leagueCount }, (_, idx) => {
|
||||
const i = idx + 1;
|
||||
const owner = faker.helpers.arrayElement(this.drivers);
|
||||
const config = leagueConfigs[idx % leagueConfigs.length]!;
|
||||
const config = leagueConfigs[idx]!;
|
||||
|
||||
const createdAt =
|
||||
// Ensure some "New" leagues (created within 7 days) so `/leagues` featured categories are populated.
|
||||
@@ -41,6 +285,31 @@ export class RacingLeagueFactory {
|
||||
? faker.date.recent({ days: 6, refDate: this.baseDate })
|
||||
: faker.date.past({ years: 2, refDate: this.baseDate });
|
||||
|
||||
// Calculate participant count based on visibility and league
|
||||
let participantCount: number;
|
||||
if (config.visibility === 'ranked') {
|
||||
// Ranked leagues need >= 10 participants
|
||||
// Some are full (max), some have minimum, some have none
|
||||
if (idx % 5 === 0) {
|
||||
participantCount = config.maxDrivers; // Full league
|
||||
} else if (idx % 3 === 0) {
|
||||
participantCount = 12; // Just meets minimum
|
||||
} else if (idx % 7 === 0) {
|
||||
participantCount = 0; // Empty but valid
|
||||
} else {
|
||||
participantCount = faker.number.int({ min: 10, max: config.maxDrivers - 5 });
|
||||
}
|
||||
} else {
|
||||
// Unranked can have any count, but must have at least 2 if not empty
|
||||
if (idx % 4 === 0) {
|
||||
participantCount = config.maxDrivers; // Full
|
||||
} else if (idx % 5 === 0) {
|
||||
participantCount = 0; // Empty
|
||||
} else {
|
||||
participantCount = faker.number.int({ min: 2, max: Math.min(config.maxDrivers, 15) });
|
||||
}
|
||||
}
|
||||
|
||||
const leagueData: {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -51,6 +320,18 @@ export class RacingLeagueFactory {
|
||||
maxDrivers: number;
|
||||
sessionDuration: number;
|
||||
qualifyingFormat: 'open' | 'single-lap';
|
||||
visibility?: 'ranked' | 'unranked';
|
||||
stewarding?: {
|
||||
decisionMode: 'admin_only' | 'steward_decides' | 'steward_vote' | 'member_vote' | 'steward_veto' | 'member_veto';
|
||||
requiredVotes?: number;
|
||||
requireDefense?: boolean;
|
||||
defenseTimeLimit?: number;
|
||||
voteTimeLimit?: number;
|
||||
protestDeadlineHours?: number;
|
||||
stewardingClosesHours?: number;
|
||||
notifyAccusedOnProtest?: boolean;
|
||||
notifyOnVoteRequired?: boolean;
|
||||
};
|
||||
};
|
||||
createdAt: Date;
|
||||
socialLinks?: { discordUrl?: string; youtubeUrl?: string; websiteUrl?: string };
|
||||
@@ -60,11 +341,23 @@ export class RacingLeagueFactory {
|
||||
name: faker.company.name() + ' Racing League',
|
||||
description: faker.lorem.sentences(2),
|
||||
ownerId: owner.id.toString(),
|
||||
settings: config,
|
||||
settings: {
|
||||
pointsSystem: config.pointsSystem,
|
||||
maxDrivers: config.maxDrivers,
|
||||
sessionDuration: config.sessionDuration,
|
||||
qualifyingFormat: config.qualifyingFormat,
|
||||
visibility: config.visibility,
|
||||
stewarding: {
|
||||
...config.stewarding,
|
||||
voteTimeLimit: 72,
|
||||
protestDeadlineHours: 48,
|
||||
stewardingClosesHours: 168,
|
||||
notifyAccusedOnProtest: true,
|
||||
notifyOnVoteRequired: true,
|
||||
},
|
||||
},
|
||||
createdAt,
|
||||
// Start with some participants for ranked leagues to meet minimum requirements
|
||||
// Note: ranked leagues require >= 10 participants (see LeagueVisibility)
|
||||
participantCount: i % 3 === 0 ? 12 : 0,
|
||||
participantCount,
|
||||
};
|
||||
|
||||
// Add social links with varying completeness
|
||||
|
||||
@@ -21,7 +21,7 @@ export class RacingRaceFactory {
|
||||
// Create races with systematic coverage of different statuses and scenarios
|
||||
const statuses: Array<'scheduled' | 'running' | 'completed' | 'cancelled'> = ['scheduled', 'running', 'completed', 'cancelled'];
|
||||
|
||||
for (let i = 1; i <= 50; i++) {
|
||||
for (let i = 1; i <= 100; i++) {
|
||||
const leagueId = leagueIds[(i - 1) % leagueIds.length] ?? demoLeagueId;
|
||||
const trackId = trackIds[(i - 1) % trackIds.length]!;
|
||||
const track = tracks.find(t => t.id === trackId)!;
|
||||
|
||||
@@ -19,111 +19,143 @@ export class RacingSeasonSponsorshipFactory {
|
||||
|
||||
for (const league of leagues) {
|
||||
const leagueId = league.id.toString();
|
||||
const leagueIndex = parseInt(leagueId.split('-')[1] || '0');
|
||||
|
||||
if (leagueId === seedId('league-5', this.persistence)) {
|
||||
seasons.push(
|
||||
Season.create({
|
||||
id: seedId('season-1', this.persistence),
|
||||
leagueId,
|
||||
gameId: 'iracing',
|
||||
name: 'Season 1 (GT Sprint)',
|
||||
year: 2025,
|
||||
order: 1,
|
||||
status: 'active',
|
||||
startDate: this.daysFromBase(-30),
|
||||
}),
|
||||
Season.create({
|
||||
id: seedId('season-2', this.persistence),
|
||||
leagueId,
|
||||
gameId: 'iracing',
|
||||
name: 'Season 2 (Endurance Cup)',
|
||||
year: 2024,
|
||||
order: 0,
|
||||
status: 'completed',
|
||||
startDate: this.daysFromBase(-120),
|
||||
endDate: this.daysFromBase(-60),
|
||||
}),
|
||||
Season.create({
|
||||
id: seedId('season-3', this.persistence),
|
||||
leagueId,
|
||||
gameId: 'iracing',
|
||||
name: 'Season 3 (Planned)',
|
||||
year: 2025,
|
||||
order: 2,
|
||||
status: 'planned',
|
||||
startDate: this.daysFromBase(14),
|
||||
}),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (leagueId === seedId('league-3', this.persistence)) {
|
||||
seasons.push(
|
||||
Season.create({
|
||||
id: seedId('league-3-season-a', this.persistence),
|
||||
leagueId,
|
||||
gameId: 'iracing',
|
||||
name: 'Split Season A',
|
||||
year: 2025,
|
||||
order: 1,
|
||||
status: 'active',
|
||||
startDate: this.daysFromBase(-10),
|
||||
}),
|
||||
Season.create({
|
||||
id: seedId('league-3-season-b', this.persistence),
|
||||
leagueId,
|
||||
gameId: 'iracing',
|
||||
name: 'Split Season B',
|
||||
year: 2025,
|
||||
order: 2,
|
||||
status: 'active',
|
||||
startDate: this.daysFromBase(-3),
|
||||
}),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const baseYear = this.baseDate.getUTCFullYear();
|
||||
const seasonCount = leagueId === seedId('league-2', this.persistence) ? 1 : faker.number.int({ min: 1, max: 3 });
|
||||
// Create 2-4 seasons per league to reach ~100 total seasons
|
||||
const seasonCount = faker.number.int({ min: 2, max: 4 });
|
||||
|
||||
for (let i = 0; i < seasonCount; i++) {
|
||||
const id = seedId(`${leagueId}-season-${i + 1}`, this.persistence);
|
||||
const isFirst = i === 0;
|
||||
|
||||
// Systematically cover all 5 statuses across all leagues
|
||||
// planned: 20%, active: 20%, completed: 30%, archived: 20%, cancelled: 10%
|
||||
let status: SeasonStatusValue;
|
||||
if (i === 0 && leagueIndex % 5 === 0) {
|
||||
status = 'planned';
|
||||
} else if (i === 0 && leagueIndex % 5 === 1) {
|
||||
status = 'active';
|
||||
} else if (i === 1 && leagueIndex % 5 === 2) {
|
||||
status = 'completed';
|
||||
} else if (i === 2 && leagueIndex % 5 === 3) {
|
||||
status = 'archived';
|
||||
} else if (i === 3 && leagueIndex % 5 === 4) {
|
||||
status = 'cancelled';
|
||||
} else {
|
||||
// Weighted random distribution
|
||||
const statusWeights = [
|
||||
{ weight: 2, value: 'planned' as const },
|
||||
{ weight: 2, value: 'active' as const },
|
||||
{ weight: 3, value: 'completed' as const },
|
||||
{ weight: 2, value: 'archived' as const },
|
||||
{ weight: 1, value: 'cancelled' as const },
|
||||
];
|
||||
const totalWeight = statusWeights.reduce((sum, item) => sum + item.weight, 0);
|
||||
const random = faker.number.int({ min: 1, max: totalWeight });
|
||||
let cumulative = 0;
|
||||
status = 'planned';
|
||||
for (const item of statusWeights) {
|
||||
cumulative += item.weight;
|
||||
if (random <= cumulative) {
|
||||
status = item.value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const status: SeasonStatusValue =
|
||||
leagueId === seedId('league-1', this.persistence) && isFirst
|
||||
? 'active'
|
||||
: leagueId === seedId('league-2', this.persistence)
|
||||
? 'planned'
|
||||
: isFirst
|
||||
? faker.helpers.arrayElement(['active', 'planned'] as const)
|
||||
: faker.helpers.arrayElement(['completed', 'archived', 'cancelled'] as const);
|
||||
const baseYear = this.baseDate.getUTCFullYear() + faker.number.int({ min: -1, max: 1 });
|
||||
|
||||
const startOffset =
|
||||
status === 'active'
|
||||
? faker.number.int({ min: -60, max: -1 })
|
||||
: status === 'planned'
|
||||
? faker.number.int({ min: 7, max: 60 })
|
||||
: faker.number.int({ min: -200, max: -90 });
|
||||
// Calculate dates based on status
|
||||
let startDate: Date | undefined;
|
||||
let endDate: Date | undefined;
|
||||
let schedulePublished: boolean | undefined;
|
||||
let participantCount: number | undefined;
|
||||
let maxDrivers: number | undefined;
|
||||
|
||||
const endOffset =
|
||||
status === 'completed' || status === 'archived' || status === 'cancelled'
|
||||
? faker.number.int({ min: -89, max: -7 })
|
||||
: undefined;
|
||||
switch (status) {
|
||||
case 'planned':
|
||||
startDate = this.daysFromBase(faker.number.int({ min: 7, max: 90 }));
|
||||
schedulePublished = faker.datatype.boolean({ probability: 0.6 });
|
||||
participantCount = 0;
|
||||
break;
|
||||
|
||||
seasons.push(
|
||||
Season.create({
|
||||
id,
|
||||
leagueId,
|
||||
gameId: 'iracing',
|
||||
name: `${faker.word.adjective()} ${faker.word.noun()} Season`,
|
||||
year: baseYear + faker.number.int({ min: -1, max: 1 }),
|
||||
order: i + 1,
|
||||
status,
|
||||
startDate: this.daysFromBase(startOffset),
|
||||
...(endOffset !== undefined ? { endDate: this.daysFromBase(endOffset) } : {}),
|
||||
}),
|
||||
);
|
||||
case 'active':
|
||||
startDate = this.daysFromBase(faker.number.int({ min: -60, max: -1 }));
|
||||
schedulePublished = true;
|
||||
maxDrivers = faker.number.int({
|
||||
min: 10,
|
||||
max: Math.max(10, Math.min(league.settings.maxDrivers || 32, 100))
|
||||
});
|
||||
participantCount = faker.number.int({ min: 5, max: maxDrivers });
|
||||
break;
|
||||
|
||||
case 'completed':
|
||||
startDate = this.daysFromBase(faker.number.int({ min: -180, max: -60 }));
|
||||
endDate = this.daysFromBase(faker.number.int({ min: -59, max: -7 }));
|
||||
schedulePublished = true;
|
||||
maxDrivers = faker.number.int({
|
||||
min: 10,
|
||||
max: Math.max(10, Math.min(league.settings.maxDrivers || 32, 100))
|
||||
});
|
||||
participantCount = faker.number.int({ min: 10, max: maxDrivers });
|
||||
break;
|
||||
|
||||
case 'archived':
|
||||
startDate = this.daysFromBase(faker.number.int({ min: -365, max: -200 }));
|
||||
endDate = this.daysFromBase(faker.number.int({ min: -199, max: -150 }));
|
||||
schedulePublished = true;
|
||||
maxDrivers = faker.number.int({
|
||||
min: 10,
|
||||
max: Math.max(10, Math.min(league.settings.maxDrivers || 32, 100))
|
||||
});
|
||||
participantCount = faker.number.int({ min: 8, max: maxDrivers });
|
||||
break;
|
||||
|
||||
case 'cancelled':
|
||||
startDate = this.daysFromBase(faker.number.int({ min: -30, max: -1 }));
|
||||
endDate = this.daysFromBase(faker.number.int({ min: -1, max: 1 })); // Cancelled early
|
||||
schedulePublished = faker.datatype.boolean({ probability: 0.3 });
|
||||
// Cancelled seasons can have maxDrivers but participantCount should be low
|
||||
maxDrivers = faker.number.int({
|
||||
min: 5,
|
||||
max: Math.max(5, Math.min(league.settings.maxDrivers || 32, 100))
|
||||
});
|
||||
participantCount = faker.number.int({ min: 0, max: Math.min(5, maxDrivers) }); // Minimal participants
|
||||
break;
|
||||
}
|
||||
|
||||
// Build season data with proper undefined handling
|
||||
const seasonData: {
|
||||
id: string;
|
||||
leagueId: string;
|
||||
gameId: string;
|
||||
name: string;
|
||||
year?: number;
|
||||
order?: number;
|
||||
status: SeasonStatusValue;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
schedulePublished?: boolean;
|
||||
participantCount?: number;
|
||||
maxDrivers?: number;
|
||||
} = {
|
||||
id,
|
||||
leagueId,
|
||||
gameId: 'iracing',
|
||||
name: `${faker.word.adjective()} ${faker.word.noun()} Season`,
|
||||
status,
|
||||
};
|
||||
|
||||
// Add optional fields only if they have values
|
||||
if (baseYear !== undefined) seasonData.year = baseYear;
|
||||
if (i + 1 !== undefined) seasonData.order = i + 1;
|
||||
if (startDate !== undefined) seasonData.startDate = startDate;
|
||||
if (endDate !== undefined) seasonData.endDate = endDate;
|
||||
if (schedulePublished !== undefined) seasonData.schedulePublished = schedulePublished;
|
||||
if (participantCount !== undefined) seasonData.participantCount = participantCount;
|
||||
if (maxDrivers !== undefined) seasonData.maxDrivers = maxDrivers;
|
||||
|
||||
const season = Season.create(seasonData);
|
||||
seasons.push(season);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import { Season } from '@core/racing/domain/entities/season/Season';
|
||||
import { SeasonSponsorship } from '@core/racing/domain/entities/season/SeasonSponsorship';
|
||||
import type { TeamJoinRequest, TeamMembership } from '@core/racing/domain/types/TeamMembership';
|
||||
import type { FeedItem } from '@core/social/domain/types/FeedItem';
|
||||
import { RacingDriverFactory } from './RacingDriverFactory';
|
||||
import { RacingDriverFactory, type DriverStats } from './RacingDriverFactory';
|
||||
import { RacingFeedFactory } from './RacingFeedFactory';
|
||||
import { RacingFriendshipFactory } from './RacingFriendshipFactory';
|
||||
import { RacingLeagueFactory } from './RacingLeagueFactory';
|
||||
@@ -22,7 +22,7 @@ import { RacingMembershipFactory } from './RacingMembershipFactory';
|
||||
import { RacingRaceFactory } from './RacingRaceFactory';
|
||||
import { RacingResultFactory } from './RacingResultFactory';
|
||||
import { RacingStandingFactory } from './RacingStandingFactory';
|
||||
import { RacingTeamFactory } from './RacingTeamFactory';
|
||||
import { RacingTeamFactory, type TeamStats } from './RacingTeamFactory';
|
||||
import { RacingTrackFactory } from './RacingTrackFactory';
|
||||
import { RacingSponsorFactory } from './RacingSponsorFactory';
|
||||
import { RacingSeasonSponsorshipFactory } from './RacingSeasonSponsorshipFactory';
|
||||
@@ -36,6 +36,7 @@ export type Friendship = {
|
||||
|
||||
export type RacingSeed = {
|
||||
drivers: Driver[];
|
||||
driverStats: Map<string, DriverStats>;
|
||||
leagues: League[];
|
||||
seasons: Season[];
|
||||
seasonSponsorships: SeasonSponsorship[];
|
||||
@@ -51,6 +52,7 @@ export type RacingSeed = {
|
||||
leagueJoinRequests: JoinRequest[];
|
||||
raceRegistrations: RaceRegistration[];
|
||||
teams: Team[];
|
||||
teamStats: Map<string, TeamStats>;
|
||||
teamMemberships: TeamMembership[];
|
||||
teamJoinRequests: TeamJoinRequest[];
|
||||
sponsors: Sponsor[];
|
||||
@@ -68,7 +70,7 @@ export type RacingSeedOptions = {
|
||||
export const racingSeedDefaults: Readonly<
|
||||
Required<RacingSeedOptions>
|
||||
> = {
|
||||
driverCount: 100,
|
||||
driverCount: 150, // Increased from 100 to 150
|
||||
baseDate: new Date(),
|
||||
persistence: 'inmemory',
|
||||
};
|
||||
@@ -98,6 +100,7 @@ class RacingSeedFactory {
|
||||
const feedFactory = new RacingFeedFactory(this.baseDate, this.persistence);
|
||||
|
||||
const drivers = driverFactory.create();
|
||||
const driverStats = driverFactory.generateDriverStats(drivers);
|
||||
const tracks = trackFactory.create();
|
||||
const leagueFactory = new RacingLeagueFactory(this.baseDate, drivers, this.persistence);
|
||||
const leagues = leagueFactory.create();
|
||||
@@ -113,6 +116,7 @@ class RacingSeedFactory {
|
||||
|
||||
const teamFactory = new RacingTeamFactory(this.baseDate, this.persistence);
|
||||
const teams = teamFactory.createTeams(drivers, leagues);
|
||||
const teamStats = teamFactory.generateTeamStats(teams);
|
||||
const races = raceFactory.create(leagues, tracks);
|
||||
const results = resultFactory.create(drivers, races);
|
||||
const standings = standingFactory.create(leagues, races, results);
|
||||
@@ -128,6 +132,7 @@ class RacingSeedFactory {
|
||||
|
||||
return {
|
||||
drivers,
|
||||
driverStats,
|
||||
leagues,
|
||||
seasons,
|
||||
seasonSponsorships,
|
||||
@@ -143,6 +148,7 @@ class RacingSeedFactory {
|
||||
leagueJoinRequests,
|
||||
raceRegistrations,
|
||||
teams,
|
||||
teamStats,
|
||||
teamMemberships,
|
||||
teamJoinRequests,
|
||||
sponsors,
|
||||
|
||||
@@ -259,6 +259,101 @@ export class RacingStewardingFactory {
|
||||
}
|
||||
}
|
||||
|
||||
// Add comprehensive penalty coverage for all types and statuses
|
||||
// This ensures we have examples of every penalty type in every status
|
||||
const penaltyTypes = ['time_penalty', 'grid_penalty', 'points_deduction', 'disqualification', 'warning', 'license_points', 'probation', 'fine', 'race_ban'] as const;
|
||||
const penaltyStatuses = ['pending', 'applied', 'appealed', 'overturned'] as const;
|
||||
|
||||
// Get some races and members for penalty seeding
|
||||
const allLeagueIds = Array.from(racesByLeague.keys());
|
||||
|
||||
for (const leagueId of allLeagueIds) {
|
||||
const members = activeMembersByLeague.get(leagueId) ?? [];
|
||||
if (members.length < 2) continue;
|
||||
|
||||
const leagueRaces = racesByLeague.get(leagueId) ?? [];
|
||||
const completedRaces = leagueRaces.filter((r) => r.status.toString() === 'completed');
|
||||
if (completedRaces.length === 0) continue;
|
||||
|
||||
const steward = members[0]!;
|
||||
const targetDriver = members[1]!;
|
||||
|
||||
// Create one penalty for each type/status combination
|
||||
let penaltyIndex = 0;
|
||||
for (const type of penaltyTypes) {
|
||||
for (const status of penaltyStatuses) {
|
||||
// Skip some combinations to avoid too many records
|
||||
if (faker.number.int({ min: 0, max: 2 }) > 0) continue;
|
||||
|
||||
const race = faker.helpers.arrayElement(completedRaces);
|
||||
const value = type === 'time_penalty' ? faker.number.int({ min: 5, max: 30 }) :
|
||||
type === 'grid_penalty' ? faker.number.int({ min: 1, max: 5 }) :
|
||||
type === 'points_deduction' ? faker.number.int({ min: 2, max: 10 }) :
|
||||
type === 'license_points' ? faker.number.int({ min: 1, max: 4 }) :
|
||||
type === 'fine' ? faker.number.int({ min: 50, max: 500 }) :
|
||||
type === 'race_ban' ? faker.number.int({ min: 1, max: 3 }) :
|
||||
type === 'warning' ? 1 :
|
||||
1; // disqualification, probation have no value
|
||||
|
||||
const penaltyData: {
|
||||
id: string;
|
||||
leagueId: string;
|
||||
raceId: string;
|
||||
driverId: string;
|
||||
type: typeof penaltyTypes[number];
|
||||
value?: number;
|
||||
reason: string;
|
||||
issuedBy: string;
|
||||
status: typeof penaltyStatuses[number];
|
||||
issuedAt: Date;
|
||||
appliedAt?: Date;
|
||||
notes?: string;
|
||||
} = {
|
||||
id: seedId(`penalty-${leagueId}-${type}-${status}-${penaltyIndex}`, this.persistence),
|
||||
leagueId,
|
||||
raceId: race.id.toString(),
|
||||
driverId: targetDriver,
|
||||
type,
|
||||
reason: this.getPenaltyReason(type),
|
||||
issuedBy: steward,
|
||||
status,
|
||||
issuedAt: faker.date.recent({ days: faker.number.int({ min: 1, max: 30 }), refDate: this.baseDate }),
|
||||
};
|
||||
|
||||
// Add value only for types that require it
|
||||
if (type !== 'warning') {
|
||||
penaltyData.value = value;
|
||||
}
|
||||
|
||||
if (status === 'applied') {
|
||||
penaltyData.appliedAt = faker.date.recent({ days: faker.number.int({ min: 1, max: 20 }), refDate: this.baseDate });
|
||||
}
|
||||
|
||||
if (type === 'race_ban') {
|
||||
penaltyData.notes = 'Multiple serious violations';
|
||||
}
|
||||
|
||||
penalties.push(Penalty.create(penaltyData));
|
||||
penaltyIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { protests, penalties };
|
||||
}
|
||||
|
||||
private getPenaltyReason(type: string): string {
|
||||
const reasons = {
|
||||
time_penalty: ['Avoidable contact', 'Track limits abuse', 'Unsafe rejoin', 'Blocking'],
|
||||
grid_penalty: ['Qualifying infringement', 'Parc fermé violation', 'Practice session breach'],
|
||||
points_deduction: ['Serious breach of rules', 'Multiple incidents', 'Unsportsmanlike conduct'],
|
||||
disqualification: ['Severe dangerous driving', 'Gross misconduct', 'Multiple serious violations'],
|
||||
warning: ['Track limits reminder', 'Minor contact', 'Procedure reminder'],
|
||||
license_points: ['General misconduct', 'Minor incidents', 'Warning escalation'],
|
||||
probation: ['Pattern of minor violations', 'Behavioral concerns', 'Conditional status'],
|
||||
fine: ['Financial penalty for rule breach', 'Administrative violation', 'Late entry fee'],
|
||||
race_ban: ['Multiple race bans', 'Severe dangerous driving', 'Gross misconduct'],
|
||||
};
|
||||
return faker.helpers.arrayElement(reasons[type as keyof typeof reasons] || ['Rule violation']);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,17 @@ import type { TeamJoinRequest, TeamMembership } from '@core/racing/domain/types/
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { seedId } from './SeedIdHelper';
|
||||
|
||||
export interface TeamStats {
|
||||
logoUrl: string;
|
||||
performanceLevel: 'beginner' | 'intermediate' | 'advanced' | 'pro';
|
||||
specialization: 'endurance' | 'sprint' | 'mixed';
|
||||
region: string;
|
||||
languages: string[];
|
||||
totalWins: number;
|
||||
totalRaces: number;
|
||||
rating: number;
|
||||
}
|
||||
|
||||
export class RacingTeamFactory {
|
||||
constructor(
|
||||
private readonly baseDate: Date,
|
||||
@@ -12,7 +23,7 @@ export class RacingTeamFactory {
|
||||
) {}
|
||||
|
||||
createTeams(drivers: Driver[], leagues: League[]): Team[] {
|
||||
const teamCount = 15;
|
||||
const teamCount = 50; // Increased from 15 to 50
|
||||
|
||||
return Array.from({ length: teamCount }, (_, idx) => {
|
||||
const i = idx + 1;
|
||||
@@ -177,8 +188,98 @@ export class RacingTeamFactory {
|
||||
return requests;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate team statistics and metadata for display
|
||||
* This would be stored in a separate stats table/service in production
|
||||
*/
|
||||
generateTeamStats(teams: Team[]): Map<string, TeamStats> {
|
||||
const statsMap = new Map<string, TeamStats>();
|
||||
|
||||
// Available logo URLs (simulating media uploads)
|
||||
const logoUrls = [
|
||||
'/images/ff1600.jpeg',
|
||||
'/images/header.jpeg',
|
||||
'/images/avatars/male-default-avatar.jpg',
|
||||
'/images/avatars/female-default-avatar.jpeg',
|
||||
'/images/avatars/neutral-default-avatar.jpeg',
|
||||
'/images/leagues/placeholder-cover.svg',
|
||||
];
|
||||
|
||||
// Available regions
|
||||
const regions = ['Europe', 'North America', 'South America', 'Asia', 'Oceania', 'Africa'];
|
||||
|
||||
// Available languages
|
||||
const allLanguages = ['English', 'German', 'French', 'Spanish', 'Italian', 'Portuguese', 'Japanese', 'Korean', 'Russian', 'Chinese'];
|
||||
|
||||
teams.forEach((team, idx) => {
|
||||
const i = idx + 1;
|
||||
|
||||
// Determine performance level based on index
|
||||
let performanceLevel: 'beginner' | 'intermediate' | 'advanced' | 'pro';
|
||||
let totalRaces: number;
|
||||
let rating: number;
|
||||
let totalWins: number;
|
||||
|
||||
if (i % 8 === 0) {
|
||||
// Pro teams
|
||||
performanceLevel = 'pro';
|
||||
totalRaces = faker.number.int({ min: 80, max: 150 });
|
||||
rating = faker.number.int({ min: 1700, max: 2000 });
|
||||
totalWins = faker.number.int({ min: 15, max: 40 });
|
||||
} else if (i % 5 === 0) {
|
||||
// Advanced teams
|
||||
performanceLevel = 'advanced';
|
||||
totalRaces = faker.number.int({ min: 40, max: 100 });
|
||||
rating = faker.number.int({ min: 1500, max: 1800 });
|
||||
totalWins = faker.number.int({ min: 8, max: 20 });
|
||||
} else if (i % 3 === 0) {
|
||||
// Intermediate teams
|
||||
performanceLevel = 'intermediate';
|
||||
totalRaces = faker.number.int({ min: 20, max: 60 });
|
||||
rating = faker.number.int({ min: 1200, max: 1600 });
|
||||
totalWins = faker.number.int({ min: 3, max: 12 });
|
||||
} else {
|
||||
// Beginner teams
|
||||
performanceLevel = 'beginner';
|
||||
totalRaces = faker.number.int({ min: 5, max: 25 });
|
||||
rating = faker.number.int({ min: 900, max: 1300 });
|
||||
totalWins = faker.number.int({ min: 0, max: 5 });
|
||||
}
|
||||
|
||||
// Determine specialization
|
||||
let specialization: 'endurance' | 'sprint' | 'mixed';
|
||||
if (i % 7 === 0) {
|
||||
specialization = 'endurance';
|
||||
} else if (i % 4 === 0) {
|
||||
specialization = 'sprint';
|
||||
} else {
|
||||
specialization = 'mixed';
|
||||
}
|
||||
|
||||
// Generate region and languages
|
||||
const region = faker.helpers.arrayElement(regions);
|
||||
const languageCount = faker.number.int({ min: 1, max: 3 });
|
||||
const languages = faker.helpers.arrayElements(allLanguages, languageCount);
|
||||
|
||||
// Generate logo URL (varied)
|
||||
const logoUrl = logoUrls[i % logoUrls.length] ?? logoUrls[0];
|
||||
|
||||
statsMap.set(team.id.toString(), {
|
||||
logoUrl: logoUrl!,
|
||||
performanceLevel,
|
||||
specialization,
|
||||
region,
|
||||
languages,
|
||||
totalWins,
|
||||
totalRaces,
|
||||
rating,
|
||||
});
|
||||
});
|
||||
|
||||
return statsMap;
|
||||
}
|
||||
|
||||
private addDays(date: Date, days: number): Date {
|
||||
return new Date(date.getTime() + days * 24 * 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user