This commit is contained in:
2025-12-09 10:32:59 +01:00
parent 35f988f885
commit a780139692
26 changed files with 2224 additions and 344 deletions

View File

@@ -5,6 +5,8 @@
* Allows easy swapping to persistent repositories later.
*/
import { Penalty } from '@gridpilot/racing/domain/entities/Penalty';
import { Protest } from '@gridpilot/racing/domain/entities/Protest';
import { Driver } from '@gridpilot/racing/domain/entities/Driver';
import { League } from '@gridpilot/racing/domain/entities/League';
import { Race } from '@gridpilot/racing/domain/entities/Race';
@@ -21,6 +23,7 @@ import type { IRaceRepository } from '@gridpilot/racing/domain/repositories/IRac
import type { IResultRepository } from '@gridpilot/racing/domain/repositories/IResultRepository';
import type { IStandingRepository } from '@gridpilot/racing/domain/repositories/IStandingRepository';
import type { IPenaltyRepository } from '@gridpilot/racing/domain/repositories/IPenaltyRepository';
import type { IProtestRepository } from '@gridpilot/racing/domain/repositories/IProtestRepository';
import type { IGameRepository } from '@gridpilot/racing/domain/repositories/IGameRepository';
import type { ISeasonRepository } from '@gridpilot/racing/domain/repositories/ISeasonRepository';
import type { ILeagueScoringConfigRepository } from '@gridpilot/racing/domain/repositories/ILeagueScoringConfigRepository';
@@ -43,6 +46,7 @@ import { InMemoryRaceRepository } from '@gridpilot/racing/infrastructure/reposit
import { InMemoryResultRepository } from '@gridpilot/racing/infrastructure/repositories/InMemoryResultRepository';
import { InMemoryStandingRepository } from '@gridpilot/racing/infrastructure/repositories/InMemoryStandingRepository';
import { InMemoryPenaltyRepository } from '@gridpilot/racing/infrastructure/repositories/InMemoryPenaltyRepository';
import { InMemoryProtestRepository } from '@gridpilot/racing/infrastructure/repositories/InMemoryProtestRepository';
import { InMemoryTrackRepository } from '@gridpilot/racing/infrastructure/repositories/InMemoryTrackRepository';
import { InMemoryCarRepository } from '@gridpilot/racing/infrastructure/repositories/InMemoryCarRepository';
import {
@@ -83,6 +87,11 @@ import {
GetLeagueFullConfigQuery,
GetRaceWithSOFQuery,
GetLeagueStatsQuery,
FileProtestUseCase,
ReviewProtestUseCase,
ApplyPenaltyUseCase,
GetRaceProtestsQuery,
GetRacePenaltiesQuery,
} from '@gridpilot/racing/application';
import type { DriverRatingProvider } from '@gridpilot/racing/application';
import {
@@ -170,6 +179,7 @@ class DIContainer {
private _resultRepository: IResultRepository;
private _standingRepository: IStandingRepository;
private _penaltyRepository: IPenaltyRepository;
private _protestRepository: IProtestRepository;
private _teamRepository: ITeamRepository;
private _teamMembershipRepository: ITeamMembershipRepository;
private _raceRegistrationRepository: IRaceRegistrationRepository;
@@ -204,6 +214,12 @@ class DIContainer {
private _getLeagueStatsQuery: GetLeagueStatsQuery;
private _driverRatingProvider: DriverRatingProvider;
private _fileProtestUseCase: FileProtestUseCase;
private _reviewProtestUseCase: ReviewProtestUseCase;
private _applyPenaltyUseCase: ApplyPenaltyUseCase;
private _getRaceProtestsQuery: GetRaceProtestsQuery;
private _getRacePenaltiesQuery: GetRacePenaltiesQuery;
private _createTeamUseCase: CreateTeamUseCase;
private _joinTeamUseCase: JoinTeamUseCase;
private _leaveTeamUseCase: LeaveTeamUseCase;
@@ -267,9 +283,123 @@ class DIContainer {
}
this._raceRegistrationRepository = new InMemoryRaceRegistrationRepository(seedRaceRegistrations);
// Penalties (seeded in-memory adapter)
this._penaltyRepository = new InMemoryPenaltyRepository();
// Seed sample penalties and protests for completed races across different leagues
// Group completed races by league, then take 1-2 from each league to ensure coverage
const completedRaces = seedData.races.filter(r => r.status === 'completed');
const racesByLeague = new Map<string, typeof completedRaces>();
for (const race of completedRaces) {
const existing = racesByLeague.get(race.leagueId) || [];
existing.push(race);
racesByLeague.set(race.leagueId, existing);
}
// Get up to 2 races per league for protest seeding
const racesForProtests: Array<{ race: typeof completedRaces[0]; leagueIndex: number }> = [];
let leagueIndex = 0;
for (const [, leagueRaces] of racesByLeague) {
// Sort by scheduled date, take earliest 2
const sorted = [...leagueRaces].sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime());
for (const race of sorted.slice(0, 2)) {
racesForProtests.push({ race, leagueIndex });
}
leagueIndex++;
}
const seededPenalties: Penalty[] = [];
const seededProtests: Protest[] = [];
racesForProtests.forEach(({ race, leagueIndex: leagueIdx }, raceIndex) => {
// Get results for this race to find drivers involved
const raceResults = seedData.results.filter(r => r.raceId === race.id);
if (raceResults.length < 4) return;
// Create 1-2 protests per race
const protestCount = Math.min(2, raceResults.length - 2);
for (let i = 0; i < protestCount; i++) {
const protestingResult = raceResults[i + 2]; // Driver who finished 3rd or 4th
const accusedResult = raceResults[i]; // Driver who finished 1st or 2nd
if (!protestingResult || !accusedResult) continue;
const protestStatuses: Array<'pending' | 'under_review' | 'upheld' | 'dismissed'> = ['pending', 'under_review', 'upheld', 'dismissed'];
const status = protestStatuses[(raceIndex + i) % protestStatuses.length];
const protest = Protest.create({
id: `protest-${race.id}-${i}`,
raceId: race.id,
protestingDriverId: protestingResult.driverId,
accusedDriverId: accusedResult.driverId,
incident: {
lap: 5 + i * 3,
description: i === 0
? 'Unsafe rejoining to the track after going off, causing contact'
: 'Aggressive defending, pushing competitor off track',
},
comment: i === 0
? 'Driver rejoined directly into my racing line, causing contact and damaging my front wing.'
: 'Driver moved under braking multiple times, forcing me off the circuit.',
status,
filedAt: new Date(Date.now() - (raceIndex + 1) * 24 * 60 * 60 * 1000),
reviewedBy: status !== 'pending' ? primaryDriverId : undefined,
decisionNotes: status === 'upheld'
? 'After reviewing the evidence, the accused driver is found at fault. Penalty applied.'
: status === 'dismissed'
? 'No clear fault found. Racing incident.'
: undefined,
reviewedAt: status !== 'pending' ? new Date(Date.now() - raceIndex * 24 * 60 * 60 * 1000) : undefined,
});
seededProtests.push(protest);
// If protest was upheld, create a penalty
if (status === 'upheld') {
const penaltyTypes: Array<'time_penalty' | 'points_deduction' | 'warning'> = ['time_penalty', 'points_deduction', 'warning'];
const penaltyType = penaltyTypes[i % penaltyTypes.length];
const penalty = Penalty.create({
id: `penalty-${race.id}-${i}`,
raceId: race.id,
driverId: accusedResult.driverId,
type: penaltyType,
value: penaltyType === 'time_penalty' ? 5 : penaltyType === 'points_deduction' ? 3 : undefined,
reason: protest.incident.description,
protestId: protest.id,
issuedBy: primaryDriverId,
status: 'applied',
issuedAt: new Date(Date.now() - raceIndex * 24 * 60 * 60 * 1000),
appliedAt: new Date(Date.now() - raceIndex * 24 * 60 * 60 * 1000),
});
seededPenalties.push(penalty);
}
}
// Add a direct penalty (not from protest) for some races
if (raceIndex % 2 === 0 && raceResults.length > 5) {
const penalizedResult = raceResults[4];
if (penalizedResult) {
const penalty = Penalty.create({
id: `penalty-direct-${race.id}`,
raceId: race.id,
driverId: penalizedResult.driverId,
type: 'time_penalty',
value: 10,
reason: 'Track limits violation - gained lasting advantage',
issuedBy: primaryDriverId,
status: 'applied',
issuedAt: new Date(Date.now() - (raceIndex + 1) * 12 * 60 * 60 * 1000),
appliedAt: new Date(Date.now() - (raceIndex + 1) * 12 * 60 * 60 * 1000),
});
seededPenalties.push(penalty);
}
}
});
// Penalties and protests with seeded data
this._penaltyRepository = new InMemoryPenaltyRepository(seededPenalties);
this._protestRepository = new InMemoryProtestRepository(seededProtests);
// Scoring preset provider and seeded game/season/scoring config repositories
this._leagueScoringPresetProvider = new InMemoryLeagueScoringPresetProvider();
@@ -396,22 +526,34 @@ class DIContainer {
});
}
// Seed a few pending join requests for demo leagues
// Seed a few pending join requests for demo leagues (expanded to more leagues)
const seededJoinRequests: JoinRequest[] = [];
const demoLeagues = seedData.leagues.slice(0, 2);
const extraDrivers = seedData.drivers.slice(3, 8);
const demoLeagues = seedData.leagues.slice(0, 6); // Expanded from 2 to 6 leagues
const extraDrivers = seedData.drivers.slice(5, 12); // More drivers for requests
demoLeagues.forEach((league) => {
extraDrivers.forEach((driver, index) => {
demoLeagues.forEach((league, leagueIndex) => {
// Skip leagues where these drivers are already members
const memberDriverIds = seededMemberships
.filter(m => m.leagueId === league.id)
.map(m => m.driverId);
const availableDrivers = extraDrivers.filter(d => !memberDriverIds.includes(d.id));
const driversForThisLeague = availableDrivers.slice(0, 3 + (leagueIndex % 3)); // 3-5 requests per league
driversForThisLeague.forEach((driver, index) => {
const messages = [
'Would love to race in this series!',
'Looking to join for the upcoming season.',
'Heard great things about this league. Can I join?',
'Experienced driver looking for competitive racing.',
'My friend recommended this league. Hope to race with you!',
];
seededJoinRequests.push({
id: `join-${league.id}-${driver.id}`,
leagueId: league.id,
driverId: driver.id,
requestedAt: new Date(Date.now() - (index + 1) * 24 * 60 * 60 * 1000),
message:
index % 2 === 0
? 'Would love to race in this series!'
: 'Looking to join for the upcoming season.',
requestedAt: new Date(Date.now() - (index + 1 + leagueIndex) * 24 * 60 * 60 * 1000),
message: messages[(index + leagueIndex) % messages.length],
});
});
});
@@ -467,6 +609,7 @@ class DIContainer {
this._standingRepository,
this._resultRepository,
this._penaltyRepository,
this._raceRepository,
{
getRating: (driverId: string) => {
const stats = driverStats[driverId];
@@ -595,6 +738,32 @@ class DIContainer {
this._teamMembershipRepository,
);
// Stewarding use cases and queries
this._fileProtestUseCase = new FileProtestUseCase(
this._protestRepository,
this._raceRepository,
this._leagueMembershipRepository,
);
this._reviewProtestUseCase = new ReviewProtestUseCase(
this._protestRepository,
this._raceRepository,
this._leagueMembershipRepository,
);
this._applyPenaltyUseCase = new ApplyPenaltyUseCase(
this._penaltyRepository,
this._protestRepository,
this._raceRepository,
this._leagueMembershipRepository,
);
this._getRaceProtestsQuery = new GetRaceProtestsQuery(
this._protestRepository,
this._driverRepository,
);
this._getRacePenaltiesQuery = new GetRacePenaltiesQuery(
this._penaltyRepository,
this._driverRepository,
);
// Social and feed adapters backed by static seed
this._feedRepository = new InMemoryFeedRepository(seedData);
this._socialRepository = new InMemorySocialGraphRepository(seedData);
@@ -825,6 +994,10 @@ class DIContainer {
return this._penaltyRepository;
}
get protestRepository(): IProtestRepository {
return this._protestRepository;
}
get raceRegistrationRepository(): IRaceRegistrationRepository {
return this._raceRegistrationRepository;
}
@@ -989,6 +1162,26 @@ class DIContainer {
get carRepository(): ICarRepository {
return this._carRepository;
}
get fileProtestUseCase(): FileProtestUseCase {
return this._fileProtestUseCase;
}
get reviewProtestUseCase(): ReviewProtestUseCase {
return this._reviewProtestUseCase;
}
get applyPenaltyUseCase(): ApplyPenaltyUseCase {
return this._applyPenaltyUseCase;
}
get getRaceProtestsQuery(): GetRaceProtestsQuery {
return this._getRaceProtestsQuery;
}
get getRacePenaltiesQuery(): GetRacePenaltiesQuery {
return this._getRacePenaltiesQuery;
}
}
/**
@@ -1018,6 +1211,10 @@ export function getPenaltyRepository(): IPenaltyRepository {
return DIContainer.getInstance().penaltyRepository;
}
export function getProtestRepository(): IProtestRepository {
return DIContainer.getInstance().protestRepository;
}
export function getRaceRegistrationRepository(): IRaceRegistrationRepository {
return DIContainer.getInstance().raceRegistrationRepository;
}
@@ -1167,6 +1364,26 @@ export function getCarRepository(): ICarRepository {
return DIContainer.getInstance().carRepository;
}
export function getFileProtestUseCase(): FileProtestUseCase {
return DIContainer.getInstance().fileProtestUseCase;
}
export function getReviewProtestUseCase(): ReviewProtestUseCase {
return DIContainer.getInstance().reviewProtestUseCase;
}
export function getApplyPenaltyUseCase(): ApplyPenaltyUseCase {
return DIContainer.getInstance().applyPenaltyUseCase;
}
export function getGetRaceProtestsQuery(): GetRaceProtestsQuery {
return DIContainer.getInstance().getRaceProtestsQuery;
}
export function getGetRacePenaltiesQuery(): GetRacePenaltiesQuery {
return DIContainer.getInstance().getRacePenaltiesQuery;
}
/**
* Reset function for testing
*/