wip league admin tools

This commit is contained in:
2025-12-28 12:04:12 +01:00
parent 5dc8c2399c
commit 6edf12fda8
401 changed files with 15365 additions and 6047 deletions

View File

@@ -95,7 +95,7 @@ export class SeedRacingData {
}
}
const activeSeasons = seed.seasons.filter((season) => season.status === 'active');
const activeSeasons = seed.seasons.filter((season) => season.status.isActive());
for (const season of activeSeasons) {
const presetId = this.selectScoringPresetIdForSeason(season);
const preset = getLeagueScoringPresetById(presetId);
@@ -270,7 +270,7 @@ export class SeedRacingData {
for (const league of leagues) {
const seasons = await this.seedDeps.seasonRepository.findByLeagueId(league.id.toString());
const activeSeasons = seasons.filter((season) => season.status === 'active');
const activeSeasons = seasons.filter((season) => season.status.isActive());
for (const season of activeSeasons) {
const existing = await this.seedDeps.leagueScoringConfigRepository.findBySeasonId(season.id);
@@ -298,7 +298,7 @@ export class SeedRacingData {
}
private selectScoringPresetIdForSeason(season: Season): string {
if (season.leagueId === 'league-5' && season.status === 'active') {
if (season.leagueId === 'league-5' && season.status.isActive()) {
return 'sprint-main-driver';
}

View File

@@ -11,8 +11,8 @@ export class RacingFeedFactory {
const items: FeedItem[] = [];
const friendMap = new Map(friendships.map((f) => [`${f.driverId}:${f.friendId}`, true] as const));
const completedRace = races.find((r) => r.status === 'completed');
const upcomingRace = races.find((r) => r.status === 'scheduled');
const completedRace = races.find((r) => r.status.toString() === 'completed');
const upcomingRace = races.find((r) => r.status.toString() === 'scheduled');
const league = leagues.find((l) => l.id.toString() === 'league-5') ?? leagues[0]!;
const now = this.addMinutes(this.baseDate, 10);

View File

@@ -61,7 +61,8 @@ export class RacingLeagueFactory {
settings: config,
createdAt,
// Start with some participants for ranked leagues to meet minimum requirements
participantCount: i % 3 === 0 ? 12 : i % 3 === 1 ? 8 : 0,
// Note: ranked leagues require >= 10 participants (see LeagueVisibility)
participantCount: i % 3 === 0 ? 12 : 0,
};
// Add social links with varying completeness

View File

@@ -249,7 +249,7 @@ export class RacingMembershipFactory {
.map(m => `${m.leagueId.toString()}:${m.driverId.toString()}`),
);
const scheduled = races.filter((r) => r.status === 'scheduled');
const scheduled = races.filter((r) => r.status.toString() === 'scheduled');
for (const race of scheduled) {
const leagueId = race.leagueId.toString();
@@ -311,7 +311,7 @@ export class RacingMembershipFactory {
}
// Keep a tiny curated "happy path" for the demo league as well
const upcomingDemoLeague = races.filter((r) => r.status === 'scheduled' && r.leagueId === 'league-5').slice(0, 3);
const upcomingDemoLeague = races.filter((r) => r.status.toString() === 'scheduled' && r.leagueId === 'league-5').slice(0, 3);
for (const race of upcomingDemoLeague) {
registrations.push(
RaceRegistration.create({

View File

@@ -5,7 +5,7 @@ import { Result as RaceResult } from '@core/racing/domain/entities/result/Result
export class RacingResultFactory {
create(drivers: Driver[], races: Race[]): RaceResult[] {
const results: RaceResult[] = [];
const completed = races.filter((r) => r.status === 'completed');
const completed = races.filter((r) => r.status.toString() === 'completed');
for (const race of completed) {
if (drivers.length === 0) continue;

View File

@@ -4,6 +4,7 @@ import { SponsorshipRequest } from '@core/racing/domain/entities/SponsorshipRequ
import { Season } from '@core/racing/domain/entities/season/Season';
import { SeasonSponsorship } from '@core/racing/domain/entities/season/SeasonSponsorship';
import type { Sponsor } from '@core/racing/domain/entities/sponsor/Sponsor';
import type { SeasonStatusValue } from '@core/racing/domain/value-objects/SeasonStatus';
import { Money } from '@core/racing/domain/value-objects/Money';
export class RacingSeasonSponsorshipFactory {
@@ -85,7 +86,7 @@ export class RacingSeasonSponsorshipFactory {
const id = `${leagueId}-season-${i + 1}`;
const isFirst = i === 0;
const status: Season['status'] =
const status: SeasonStatusValue =
leagueId === 'league-1' && isFirst
? 'active'
: leagueId === 'league-2'
@@ -133,7 +134,7 @@ export class RacingSeasonSponsorshipFactory {
const sponsorshipCount =
season.id === 'season-1'
? 2
: season.status === 'active'
: season.status.isActive()
? faker.number.int({ min: 0, max: 2 })
: faker.number.int({ min: 0, max: 1 });
@@ -162,12 +163,12 @@ export class RacingSeasonSponsorshipFactory {
),
createdAt: faker.date.recent({ days: 120, refDate: this.baseDate }),
description: tier === 'main' ? 'Main sponsor slot' : 'Secondary sponsor slot',
...(season.status === 'active'
...(season.status.isActive()
? {
status: faker.helpers.arrayElement(['active', 'pending'] as const),
activatedAt: faker.date.recent({ days: 30, refDate: this.baseDate }),
}
: season.status === 'completed' || season.status === 'archived'
: season.status.isCompleted() || season.status.isArchived()
? {
status: faker.helpers.arrayElement(['ended', 'cancelled'] as const),
endedAt: faker.date.recent({ days: 200, refDate: this.baseDate }),
@@ -191,7 +192,11 @@ export class RacingSeasonSponsorshipFactory {
for (const season of seasons) {
const isHighTrafficDemo = season.id === 'season-1';
const maxRequests =
isHighTrafficDemo ? 8 : season.status === 'active' ? faker.number.int({ min: 0, max: 4 }) : faker.number.int({ min: 0, max: 1 });
isHighTrafficDemo
? 8
: season.status.isActive()
? faker.number.int({ min: 0, max: 4 })
: faker.number.int({ min: 0, max: 1 });
for (let i = 0; i < maxRequests; i++) {
const tier: SponsorshipRequest['tier'] =
@@ -219,7 +224,7 @@ export class RacingSeasonSponsorshipFactory {
// A mix of statuses for edge cases (pending is what the UI lists)
const status =
season.status === 'active'
season.status.isActive()
? faker.helpers.arrayElement(['pending', 'pending', 'pending', 'rejected', 'withdrawn'] as const)
: faker.helpers.arrayElement(['pending', 'rejected'] as const);

View File

@@ -10,7 +10,7 @@ export class RacingStandingFactory {
const racesByLeague = new Map<string, Set<string>>();
for (const race of races) {
if (race.status !== 'completed') continue;
if (!race.status.isCompleted()) continue;
const set = racesByLeague.get(race.leagueId) ?? new Set<string>();
set.add(race.id);

View File

@@ -1,6 +1,6 @@
import { vi, describe, it, expect, beforeEach } from 'vitest';
import { InMemoryRaceRepository } from './InMemoryRaceRepository';
import { Race, RaceStatus } from '@core/racing/domain/entities/Race';
import { Race, type RaceStatusValue } from '@core/racing/domain/entities/Race';
import type { Logger } from '@core/shared/application';
describe('InMemoryRaceRepository', () => {
@@ -23,7 +23,7 @@ describe('InMemoryRaceRepository', () => {
track: string,
car: string,
scheduledAt: Date,
status: RaceStatus = 'scheduled'
status: RaceStatusValue = 'scheduled',
) => {
return Race.create({
id,
@@ -178,7 +178,7 @@ describe('InMemoryRaceRepository', () => {
const race = createTestRace('1', 'league1', 'Track1', 'Car1', new Date());
await repository.create(race);
const updatedRace = race.complete();
const updatedRace = race.start().complete();
const result = await repository.update(updatedRace);
expect(result).toEqual(updatedRace);
expect(mockLogger.info).toHaveBeenCalledWith('Race 1 updated successfully.');

View File

@@ -1,5 +1,5 @@
import { IRaceRepository } from '@core/racing/domain/repositories/IRaceRepository';
import { Race, RaceStatus } from '@core/racing/domain/entities/Race';
import { Race, type RaceStatusValue } from '@core/racing/domain/entities/Race';
import { Logger } from '@core/shared/application';
export class InMemoryRaceRepository implements IRaceRepository {
@@ -36,7 +36,7 @@ export class InMemoryRaceRepository implements IRaceRepository {
this.logger.debug(`[InMemoryRaceRepository] Finding upcoming races by league ID: ${leagueId}`);
const now = new Date();
const upcomingRaces = Array.from(this.races.values()).filter(race =>
race.leagueId === leagueId && race.status === 'scheduled' && race.scheduledAt > now
race.leagueId === leagueId && race.status.isScheduled() && race.scheduledAt > now
);
this.logger.info(`Found ${upcomingRaces.length} upcoming races for league ID: ${leagueId}.`);
return Promise.resolve(upcomingRaces);
@@ -45,15 +45,17 @@ export class InMemoryRaceRepository implements IRaceRepository {
async findCompletedByLeagueId(leagueId: string): Promise<Race[]> {
this.logger.debug(`[InMemoryRaceRepository] Finding completed races by league ID: ${leagueId}`);
const completedRaces = Array.from(this.races.values()).filter(race =>
race.leagueId === leagueId && race.status === 'completed'
race.leagueId === leagueId && race.status.isCompleted()
);
this.logger.info(`Found ${completedRaces.length} completed races for league ID: ${leagueId}.`);
return Promise.resolve(completedRaces);
}
async findByStatus(status: RaceStatus): Promise<Race[]> {
async findByStatus(status: RaceStatusValue): Promise<Race[]> {
this.logger.debug(`[InMemoryRaceRepository] Finding races by status: ${status}.`);
const races = Array.from(this.races.values()).filter(race => race.status === status);
const races = Array.from(this.races.values()).filter(
race => race.status.toString() === status,
);
this.logger.info(`Found ${races.length} races with status: ${status}.`);
return Promise.resolve(races);
}

View File

@@ -154,7 +154,7 @@ export class InMemorySeasonRepository implements ISeasonRepository {
this.logger.debug(`Listing active seasons by league id: ${leagueId}`);
try {
const seasons = this.seasons.filter(
(s) => s.leagueId === leagueId && s.status === 'active',
(s) => s.leagueId === leagueId && s.status.isActive(),
);
this.logger.info(`Found ${seasons.length} active seasons for league id: ${leagueId}.`);
return seasons;

View File

@@ -78,7 +78,9 @@ export class InMemorySeasonRepository implements ISeasonRepository {
async listActiveByLeague(leagueId: string): Promise<Season[]> {
this.logger.debug(`[InMemorySeasonRepository] Listing active seasons by league ID: ${leagueId}`);
const activeSeasons = Array.from(this.seasons.values()).filter(season => season.leagueId === leagueId && season.status === 'active');
const activeSeasons = Array.from(this.seasons.values()).filter(
season => season.leagueId === leagueId && season.status.isActive(),
);
return Promise.resolve(activeSeasons);
}
}

View File

@@ -52,8 +52,8 @@ export class InMemoryTrackRepository implements ITrackRepository {
this.logger.debug(`Finding tracks by game id: ${gameId}`);
try {
const tracks = Array.from(this.tracks.values())
.filter(track => track.gameId.props === gameId)
.sort((a, b) => a.name.props.localeCompare(b.name.props));
.filter(track => track.gameId.toString() === gameId)
.sort((a, b) => a.name.toString().localeCompare(b.name.toString()));
this.logger.info(`Found ${tracks.length} tracks for game id: ${gameId}.`);
return tracks;
} catch (error) {
@@ -67,7 +67,7 @@ export class InMemoryTrackRepository implements ITrackRepository {
try {
const tracks = Array.from(this.tracks.values())
.filter(track => track.category === category)
.sort((a, b) => a.name.props.localeCompare(b.name.props));
.sort((a, b) => a.name.toString().localeCompare(b.name.toString()));
this.logger.info(`Found ${tracks.length} tracks for category: ${category}.`);
return tracks;
} catch (error) {
@@ -80,8 +80,8 @@ export class InMemoryTrackRepository implements ITrackRepository {
this.logger.debug(`Finding tracks by country: ${country}`);
try {
const tracks = Array.from(this.tracks.values())
.filter(track => track.country.props.toLowerCase() === country.toLowerCase())
.sort((a, b) => a.name.props.localeCompare(b.name.props));
.filter(track => track.country.toString().toLowerCase() === country.toLowerCase())
.sort((a, b) => a.name.toString().localeCompare(b.name.toString()));
this.logger.info(`Found ${tracks.length} tracks for country: ${country}.`);
return tracks;
} catch (error) {
@@ -96,10 +96,10 @@ export class InMemoryTrackRepository implements ITrackRepository {
const lowerQuery = query.toLowerCase();
const tracks = Array.from(this.tracks.values())
.filter(track =>
track.name.props.toLowerCase().includes(lowerQuery) ||
track.shortName.props.toLowerCase().includes(lowerQuery)
track.name.toString().toLowerCase().includes(lowerQuery) ||
track.shortName.toString().toLowerCase().includes(lowerQuery)
)
.sort((a, b) => a.name.props.localeCompare(b.name.props));
.sort((a, b) => a.name.toString().localeCompare(b.name.toString()));
this.logger.info(`Found ${tracks.length} tracks matching search query: ${query}.`);
return tracks;
} catch (error) {