more seeds
This commit is contained in:
@@ -256,6 +256,177 @@ export const leagueScoringPresets: LeagueScoringPreset[] = [
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'sprint-main-team',
|
||||
name: 'Sprint + Main (Teams)',
|
||||
description: 'Teams championship using the Sprint + Main weekend format.',
|
||||
primaryChampionshipType: 'team',
|
||||
dropPolicySummary: 'Best 6 results of 8 count towards the championship.',
|
||||
sessionSummary: 'Sprint + Main',
|
||||
bonusSummary: 'Fastest lap +1 point in main race if finishing P10 or better.',
|
||||
defaultTimings: {
|
||||
practiceMinutes: 20,
|
||||
qualifyingMinutes: 30,
|
||||
sprintRaceMinutes: 20,
|
||||
mainRaceMinutes: 40,
|
||||
sessionCount: 2,
|
||||
},
|
||||
createConfig: ({ seasonId }) => {
|
||||
const fastestLapBonus: BonusRule = {
|
||||
id: 'fastest-lap-main-team',
|
||||
type: 'fastestLap',
|
||||
points: 1,
|
||||
requiresFinishInTopN: 10,
|
||||
};
|
||||
|
||||
const sessionTypes: SessionType[] = ['sprint', 'main'];
|
||||
|
||||
const pointsTableBySessionType: Record<SessionType, PointsTable> = {
|
||||
sprint: sprintPointsSprintMain,
|
||||
main: mainPointsSprintMain,
|
||||
practice: new PointsTable({}),
|
||||
qualifying: new PointsTable({}),
|
||||
q1: new PointsTable({}),
|
||||
q2: new PointsTable({}),
|
||||
q3: new PointsTable({}),
|
||||
timeTrial: new PointsTable({}),
|
||||
};
|
||||
|
||||
const bonusRulesBySessionType: Record<SessionType, BonusRule[]> = {
|
||||
sprint: [],
|
||||
main: [fastestLapBonus],
|
||||
practice: [],
|
||||
qualifying: [],
|
||||
q1: [],
|
||||
q2: [],
|
||||
q3: [],
|
||||
timeTrial: [],
|
||||
};
|
||||
|
||||
const dropScorePolicy: DropScorePolicy = {
|
||||
strategy: 'bestNResults',
|
||||
count: 6,
|
||||
};
|
||||
|
||||
const championship: ChampionshipConfig = {
|
||||
id: 'team-champ-sprint-main',
|
||||
name: 'Team Championship',
|
||||
type: 'team' as ChampionshipType,
|
||||
sessionTypes,
|
||||
pointsTableBySessionType,
|
||||
bonusRulesBySessionType,
|
||||
dropScorePolicy,
|
||||
};
|
||||
|
||||
return LeagueScoringConfig.create({
|
||||
id: `lsc-${seasonId}-sprint-main-team`,
|
||||
seasonId,
|
||||
scoringPresetId: 'sprint-main-team',
|
||||
championships: [championship],
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'club-default-nations',
|
||||
name: 'Club ladder (Nations)',
|
||||
description: 'Nation vs nation ladder with a single main race and no bonuses.',
|
||||
primaryChampionshipType: 'nations',
|
||||
dropPolicySummary: 'All race results count, no drop scores.',
|
||||
sessionSummary: 'Main race only',
|
||||
bonusSummary: 'No bonus points.',
|
||||
defaultTimings: {
|
||||
practiceMinutes: 20,
|
||||
qualifyingMinutes: 20,
|
||||
sprintRaceMinutes: 0,
|
||||
mainRaceMinutes: 40,
|
||||
sessionCount: 1,
|
||||
},
|
||||
createConfig: ({ seasonId }) => {
|
||||
const sessionTypes: SessionType[] = ['main'];
|
||||
|
||||
const pointsTableBySessionType: Record<SessionType, PointsTable> = {
|
||||
sprint: new PointsTable({}),
|
||||
main: clubMainPoints,
|
||||
practice: new PointsTable({}),
|
||||
qualifying: new PointsTable({}),
|
||||
q1: new PointsTable({}),
|
||||
q2: new PointsTable({}),
|
||||
q3: new PointsTable({}),
|
||||
timeTrial: new PointsTable({}),
|
||||
};
|
||||
|
||||
const dropScorePolicy: DropScorePolicy = {
|
||||
strategy: 'none',
|
||||
};
|
||||
|
||||
const championship: ChampionshipConfig = {
|
||||
id: 'nations-champ-club-default',
|
||||
name: 'Nations Championship',
|
||||
type: 'nations' as ChampionshipType,
|
||||
sessionTypes,
|
||||
pointsTableBySessionType,
|
||||
dropScorePolicy,
|
||||
};
|
||||
|
||||
return LeagueScoringConfig.create({
|
||||
id: `lsc-${seasonId}-club-default-nations`,
|
||||
seasonId,
|
||||
scoringPresetId: 'club-default-nations',
|
||||
championships: [championship],
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'endurance-main-trophy',
|
||||
name: 'Endurance Trophy Event',
|
||||
description: 'Trophy-style endurance event with a single long main race.',
|
||||
primaryChampionshipType: 'trophy',
|
||||
dropPolicySummary: 'Best 4 results of 6 count towards the championship.',
|
||||
sessionSummary: 'Main race only',
|
||||
bonusSummary: 'No bonus points.',
|
||||
defaultTimings: {
|
||||
practiceMinutes: 30,
|
||||
qualifyingMinutes: 20,
|
||||
sprintRaceMinutes: 0,
|
||||
mainRaceMinutes: 120,
|
||||
sessionCount: 1,
|
||||
},
|
||||
createConfig: ({ seasonId }) => {
|
||||
const sessionTypes: SessionType[] = ['main'];
|
||||
|
||||
const pointsTableBySessionType: Record<SessionType, PointsTable> = {
|
||||
sprint: new PointsTable({}),
|
||||
main: enduranceMainPoints,
|
||||
practice: new PointsTable({}),
|
||||
qualifying: new PointsTable({}),
|
||||
q1: new PointsTable({}),
|
||||
q2: new PointsTable({}),
|
||||
q3: new PointsTable({}),
|
||||
timeTrial: new PointsTable({}),
|
||||
};
|
||||
|
||||
const dropScorePolicy: DropScorePolicy = {
|
||||
strategy: 'bestNResults',
|
||||
count: 4,
|
||||
};
|
||||
|
||||
const championship: ChampionshipConfig = {
|
||||
id: 'trophy-champ-endurance-main',
|
||||
name: 'Trophy Championship',
|
||||
type: 'trophy' as ChampionshipType,
|
||||
sessionTypes,
|
||||
pointsTableBySessionType,
|
||||
dropScorePolicy,
|
||||
};
|
||||
|
||||
return LeagueScoringConfig.create({
|
||||
id: `lsc-${seasonId}-endurance-main-trophy`,
|
||||
seasonId,
|
||||
scoringPresetId: 'endurance-main-trophy',
|
||||
championships: [championship],
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export function listLeagueScoringPresets(): LeagueScoringPreset[] {
|
||||
|
||||
@@ -10,10 +10,13 @@ import type { ITeamRepository } from '@core/racing/domain/repositories/ITeamRepo
|
||||
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';
|
||||
@@ -24,6 +27,7 @@ export type RacingSeedDependencies = {
|
||||
driverRepository: IDriverRepository;
|
||||
leagueRepository: ILeagueRepository;
|
||||
seasonRepository: ISeasonRepository;
|
||||
leagueScoringConfigRepository: ILeagueScoringConfigRepository;
|
||||
seasonSponsorshipRepository: ISeasonSponsorshipRepository;
|
||||
sponsorshipRequestRepository: ISponsorshipRequestRepository;
|
||||
leagueWalletRepository: ILeagueWalletRepository;
|
||||
@@ -51,7 +55,8 @@ export class SeedRacingData {
|
||||
async execute(): Promise<void> {
|
||||
const existingDrivers = await this.seedDeps.driverRepository.findAll();
|
||||
if (existingDrivers.length > 0) {
|
||||
this.logger.info('[Bootstrap] Racing seed skipped (drivers already exist)');
|
||||
this.logger.info('[Bootstrap] Racing seed skipped (drivers already exist), ensuring scoring configs');
|
||||
await this.ensureScoringConfigsForExistingData();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -90,6 +95,26 @@ export class SeedRacingData {
|
||||
}
|
||||
}
|
||||
|
||||
const activeSeasons = seed.seasons.filter((season) => season.status === 'active');
|
||||
for (const season of activeSeasons) {
|
||||
const presetId = this.selectScoringPresetIdForSeason(season);
|
||||
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 {
|
||||
@@ -239,4 +264,64 @@ export class SeedRacingData {
|
||||
`[Bootstrap] Seeded racing data: drivers=${seed.drivers.length}, leagues=${seed.leagues.length}, races=${seed.races.length}`,
|
||||
);
|
||||
}
|
||||
|
||||
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 === 'active');
|
||||
|
||||
for (const season of activeSeasons) {
|
||||
const existing = await this.seedDeps.leagueScoringConfigRepository.findBySeasonId(season.id);
|
||||
if (existing) continue;
|
||||
|
||||
const presetId = this.selectScoringPresetIdForSeason(season);
|
||||
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): string {
|
||||
if (season.leagueId === 'league-5' && season.status === 'active') {
|
||||
return 'sprint-main-driver';
|
||||
}
|
||||
|
||||
if (season.leagueId === 'league-3') {
|
||||
return season.id.endsWith('-b') ? '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';
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,12 @@ export class RacingLeagueFactory {
|
||||
const owner = faker.helpers.arrayElement(this.drivers);
|
||||
const config = leagueConfigs[idx % leagueConfigs.length]!;
|
||||
|
||||
const createdAt =
|
||||
// Ensure some "New" leagues (created within 7 days) so `/leagues` featured categories are populated.
|
||||
idx % 6 === 0
|
||||
? faker.date.recent({ days: 6, refDate: this.baseDate })
|
||||
: faker.date.past({ years: 2, refDate: this.baseDate });
|
||||
|
||||
const leagueData: {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -52,7 +58,7 @@ export class RacingLeagueFactory {
|
||||
description: faker.lorem.sentences(2),
|
||||
ownerId: owner.id.toString(),
|
||||
settings: config,
|
||||
createdAt: faker.date.past({ years: 2, refDate: this.baseDate }),
|
||||
createdAt,
|
||||
};
|
||||
|
||||
// Add social links with varying completeness
|
||||
|
||||
@@ -68,7 +68,7 @@ export const racingSeedDefaults: Readonly<
|
||||
Required<RacingSeedOptions>
|
||||
> = {
|
||||
driverCount: 100,
|
||||
baseDate: new Date('2025-01-15T12:00:00.000Z'),
|
||||
baseDate: new Date(),
|
||||
};
|
||||
|
||||
class RacingSeedFactory {
|
||||
|
||||
@@ -33,9 +33,13 @@ describe('InMemoryGameRepository', () => {
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should return an empty array', async () => {
|
||||
it('should return default seeded games', async () => {
|
||||
const result = await repository.findAll();
|
||||
expect(result).toEqual([]);
|
||||
|
||||
const ids = result.map((g) => g.id.toString());
|
||||
expect(ids).toEqual(expect.arrayContaining(['iracing', 'acc', 'f1-24', 'f1-23']));
|
||||
expect(result).toHaveLength(4);
|
||||
|
||||
expect(mockLogger.debug).toHaveBeenCalledWith('[InMemoryGameRepository] Finding all games.');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,20 @@ export class InMemoryGameRepository implements IGameRepository {
|
||||
|
||||
constructor(private readonly logger: Logger) {
|
||||
this.logger.info('InMemoryGameRepository initialized.');
|
||||
this.seedDefaults();
|
||||
}
|
||||
|
||||
private seedDefaults(): void {
|
||||
const defaults = [
|
||||
Game.create({ id: 'iracing', name: 'iRacing' }),
|
||||
Game.create({ id: 'acc', name: 'Assetto Corsa Competizione' }),
|
||||
Game.create({ id: 'f1-24', name: 'F1 24' }),
|
||||
Game.create({ id: 'f1-23', name: 'F1 23' }),
|
||||
];
|
||||
|
||||
for (const game of defaults) {
|
||||
this.games.set(game.id.toString(), game);
|
||||
}
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<Game | null> {
|
||||
|
||||
@@ -8,7 +8,7 @@ describe('InMemoryLeagueScoringPresetProvider', () => {
|
||||
it('should return all available presets', () => {
|
||||
const presets = provider.listPresets();
|
||||
|
||||
expect(presets).toHaveLength(3);
|
||||
expect(presets.length).toBeGreaterThanOrEqual(3);
|
||||
expect(presets).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
|
||||
Reference in New Issue
Block a user