more seeds

This commit is contained in:
2025-12-27 10:43:55 +01:00
parent 58d9a1c762
commit 91612e4256
16 changed files with 1713 additions and 72 deletions

View File

@@ -8,6 +8,14 @@ import type { ILeagueMembershipRepository } from '@core/racing/domain/repositori
import type { IRaceRegistrationRepository } from '@core/racing/domain/repositories/IRaceRegistrationRepository';
import type { ITeamRepository } from '@core/racing/domain/repositories/ITeamRepository';
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 { 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 { 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';
import type { ISocialGraphRepository } from '@core/social/domain/repositories/ISocialGraphRepository';
import { createRacingSeed } from './racing/RacingSeed';
@@ -15,6 +23,13 @@ import { createRacingSeed } from './racing/RacingSeed';
export type RacingSeedDependencies = {
driverRepository: IDriverRepository;
leagueRepository: ILeagueRepository;
seasonRepository: ISeasonRepository;
seasonSponsorshipRepository: ISeasonSponsorshipRepository;
sponsorshipRequestRepository: ISponsorshipRequestRepository;
leagueWalletRepository: ILeagueWalletRepository;
transactionRepository: ITransactionRepository;
protestRepository: IProtestRepository;
penaltyRepository: IPenaltyRepository;
raceRepository: IRaceRepository;
resultRepository: IResultRepository;
standingRepository: IStandingRepository;
@@ -22,6 +37,7 @@ export type RacingSeedDependencies = {
raceRegistrationRepository: IRaceRegistrationRepository;
teamRepository: ITeamRepository;
teamMembershipRepository: ITeamMembershipRepository;
sponsorRepository: ISponsorRepository;
feedRepository: IFeedRepository;
socialGraphRepository: ISocialGraphRepository;
};
@@ -41,6 +57,15 @@ export class SeedRacingData {
const seed = createRacingSeed();
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);
@@ -57,6 +82,65 @@ export class SeedRacingData {
}
}
for (const season of seed.seasons) {
try {
await this.seedDeps.seasonRepository.create(season);
} 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);
@@ -79,6 +163,14 @@ export class SeedRacingData {
}
}
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);
@@ -87,6 +179,14 @@ export class SeedRacingData {
}
}
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);
@@ -95,6 +195,14 @@ export class SeedRacingData {
}
}
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);

View File

@@ -0,0 +1,197 @@
import { faker } from '@faker-js/faker';
import type { League } from '@core/racing/domain/entities/League';
import { LeagueWallet } from '@core/racing/domain/entities/league-wallet/LeagueWallet';
import { LeagueWalletId } from '@core/racing/domain/entities/league-wallet/LeagueWalletId';
import { Transaction } from '@core/racing/domain/entities/league-wallet/Transaction';
import { TransactionId } from '@core/racing/domain/entities/league-wallet/TransactionId';
import { Money } from '@core/racing/domain/value-objects/Money';
type LeagueWalletSeed = {
wallets: LeagueWallet[];
transactions: Transaction[];
};
export class RacingLeagueWalletFactory {
constructor(private readonly baseDate: Date) {}
create(leagues: League[]): LeagueWalletSeed {
const wallets: LeagueWallet[] = [];
const transactions: Transaction[] = [];
for (const league of leagues) {
const leagueId = league.id.toString();
const walletId = `wallet-${leagueId}`;
const createdAt = faker.date.past({ years: 2, refDate: this.baseDate });
// Ensure coverage:
// - some leagues have no wallet (wallet use-case returns WALLET_NOT_FOUND)
// - some wallets have no transactions
// - some wallets have varied transaction types + statuses
const shouldHaveWallet = leagueId !== 'league-2' && leagueId !== 'league-7';
if (!shouldHaveWallet) {
continue;
}
const currency: 'USD' | 'EUR' | 'GBP' = leagueId === 'league-1' ? 'EUR' : 'USD';
const baselineBalance =
leagueId === 'league-5'
? Money.create(2880, 'USD')
: Money.create(faker.number.int({ min: 0, max: 12_000 }), currency);
let wallet = LeagueWallet.create({
id: walletId,
leagueId,
balance: baselineBalance,
createdAt,
});
const transactionCount =
leagueId === 'league-3'
? 0
: leagueId === 'league-5'
? 14
: faker.number.int({ min: 2, max: 18 });
for (let i = 0; i < transactionCount; i++) {
const id = TransactionId.create(`tx-${leagueId}-${i + 1}`);
const type = this.pickTransactionType(i, leagueId);
const amount = this.pickAmount(type, currency, leagueId);
let status = this.pickStatus(type, i, leagueId);
const isDebit = type === 'withdrawal' || type === 'refund';
if (status === 'completed' && isDebit) {
const expectedNetAmount = amount.calculateNetAmount();
if (!wallet.canWithdraw(expectedNetAmount)) {
status = type === 'withdrawal' ? 'failed' : 'cancelled';
}
}
const tx = Transaction.create({
id,
walletId: LeagueWalletId.create(walletId),
type,
amount,
status,
completedAt: status === 'completed' ? faker.date.recent({ days: 180, refDate: this.baseDate }) : undefined,
createdAt: faker.date.recent({ days: 240, refDate: this.baseDate }),
description: this.describe(type, leagueId),
metadata: {
leagueId,
source: type === 'sponsorship_payment' ? 'sponsorship' : 'wallet',
},
});
transactions.push(tx);
// Update wallet balance for completed transactions only.
// Use netAmount because wallet stores net proceeds after platform fee.
if (tx.status === 'completed') {
if (tx.type === 'withdrawal' || tx.type === 'refund') {
wallet = wallet.withdrawFunds(tx.netAmount, tx.id.toString());
} else {
wallet = wallet.addFunds(tx.netAmount, tx.id.toString());
}
}
}
// Explicit edge-case: pending prize payout shows up in "pending payouts"
if (leagueId === 'league-5') {
const pendingPrize = Transaction.create({
id: TransactionId.create(`tx-${leagueId}-pending-prize`),
walletId: LeagueWalletId.create(walletId),
type: 'prize_payout',
amount: Money.create(600, currency),
status: 'pending',
createdAt: faker.date.recent({ days: 15, refDate: this.baseDate }),
completedAt: undefined,
description: 'Season prize pool payout (pending)',
metadata: { seasonId: 'season-2', placement: 'P1-P3' },
});
transactions.push(pendingPrize);
}
wallets.push(wallet);
}
return { wallets, transactions };
}
private pickTransactionType(index: number, leagueId: string) {
if (leagueId === 'league-5') {
const types = [
'sponsorship_payment',
'membership_payment',
'membership_payment',
'sponsorship_payment',
'prize_payout',
'withdrawal',
] as const;
return types[index % types.length]!;
}
const types = [
'membership_payment',
'sponsorship_payment',
'membership_payment',
'withdrawal',
'refund',
'prize_payout',
] as const;
return types[index % types.length]!;
}
private pickAmount(type: Transaction['type'], currency: 'USD' | 'EUR' | 'GBP', leagueId: string): Money {
const asUsd = (amount: number) => Money.create(amount, currency);
switch (type) {
case 'sponsorship_payment':
return asUsd(leagueId === 'league-5' ? faker.number.int({ min: 400, max: 1600 }) : faker.number.int({ min: 150, max: 2400 }));
case 'membership_payment':
return asUsd(faker.number.int({ min: 10, max: 120 }));
case 'withdrawal':
return asUsd(faker.number.int({ min: 25, max: 800 }));
case 'refund':
return asUsd(faker.number.int({ min: 10, max: 150 }));
case 'prize_payout':
return asUsd(faker.number.int({ min: 100, max: 1500 }));
}
}
private pickStatus(type: Transaction['type'], index: number, leagueId: string): Transaction['status'] {
if (leagueId === 'league-5') {
if (type === 'withdrawal' && index % 2 === 0) return 'completed';
if (type === 'prize_payout' && index % 3 === 0) return 'pending';
return 'completed';
}
if (type === 'withdrawal') {
return faker.helpers.arrayElement(['completed', 'pending', 'failed'] as const);
}
if (type === 'refund') {
return faker.helpers.arrayElement(['completed', 'cancelled'] as const);
}
return faker.helpers.arrayElement(['completed', 'pending'] as const);
}
private describe(type: Transaction['type'], leagueId: string): string {
switch (type) {
case 'sponsorship_payment':
return leagueId === 'league-5' ? 'Season sponsorship payment' : 'Sponsorship payment';
case 'membership_payment':
return 'Membership fee';
case 'withdrawal':
return 'Owner withdrawal';
case 'refund':
return 'Refund processed';
case 'prize_payout':
return 'Prize payout';
}
}
}

View File

@@ -1,5 +1,6 @@
import { Driver } from '@core/racing/domain/entities/Driver';
import { League } from '@core/racing/domain/entities/League';
import { JoinRequest } from '@core/racing/domain/entities/JoinRequest';
import { LeagueMembership } from '@core/racing/domain/entities/LeagueMembership';
import { Race } from '@core/racing/domain/entities/Race';
import { RaceRegistration } from '@core/racing/domain/entities/RaceRegistration';
@@ -9,44 +10,308 @@ export class RacingMembershipFactory {
createLeagueMemberships(drivers: Driver[], leagues: League[]): LeagueMembership[] {
const memberships: LeagueMembership[] = [];
const leagueById = new Map(leagues.map(l => [l.id.toString(), l]));
for (const driver of drivers) {
const driverId = driver.id;
const add = (props: {
leagueId: string;
driverId: string;
role: 'owner' | 'admin' | 'steward' | 'member';
status: 'active' | 'inactive' | 'pending';
joinedDaysAgo: number;
id?: string;
}): void => {
memberships.push(
LeagueMembership.create({
leagueId: 'league-5',
driverId,
role: driverId === 'driver-1' ? 'owner' : 'member',
status: 'active',
joinedAt: this.addDays(this.baseDate, -60),
leagueId: props.leagueId,
driverId: props.driverId,
role: props.role,
status: props.status,
joinedAt: this.addDays(this.baseDate, -props.joinedDaysAgo),
...(props.id !== undefined ? { id: props.id } : {}),
}),
);
};
// Empty league: intentionally no memberships.
// (Keep `league-2` empty if it exists.)
// Widen the type to avoid TS2367 “no overlap” comparisons in some build modes.
const emptyLeagueId: string | undefined = leagueById.has('league-2') ? ('league-2' as string) : undefined;
// Demo league: "full" + overbooked with pending/inactive members.
const demoLeague = leagueById.get('league-5');
if (demoLeague) {
const maxDrivers = demoLeague.settings.maxDrivers ?? 32;
const activeDrivers = drivers.slice(0, Math.min(maxDrivers, drivers.length));
activeDrivers.forEach((driver, idx) => {
const driverId = driver.id.toString();
const role =
driverId === 'driver-1'
? 'owner'
: idx === 1 || idx === 2
? 'admin'
: idx === 3 || idx === 4
? 'steward'
: 'member';
add({ leagueId: demoLeague.id.toString(), driverId, role, status: 'active', joinedDaysAgo: 60 - idx });
});
// Over-cap edge cases (membership exists but not active / pending)
const overbooked = drivers.slice(activeDrivers.length, activeDrivers.length + 4);
overbooked.forEach((driver, idx) => {
add({
leagueId: demoLeague.id.toString(),
driverId: driver.id.toString(),
role: 'member',
status: idx % 2 === 0 ? 'pending' : 'inactive',
joinedDaysAgo: 10 + idx,
});
});
}
// League with mixed statuses and roles (but not full).
const league1 = leagueById.get('league-1');
if (league1) {
const pick = drivers.slice(15, 25);
pick.forEach((driver, idx) => {
add({
leagueId: league1.id.toString(),
driverId: driver.id.toString(),
role: idx === 0 ? 'owner' : idx === 1 ? 'steward' : 'member',
status: idx % 5 === 0 ? 'pending' : idx % 7 === 0 ? 'inactive' : 'active',
joinedDaysAgo: 30 + idx,
});
});
}
// League with only pending memberships (tests "pending list" UX).
const league4 = leagueById.get('league-4');
if (league4) {
drivers.slice(40, 48).forEach((driver, idx) => {
add({
leagueId: league4.id.toString(),
driverId: driver.id.toString(),
role: idx === 0 ? 'owner' : 'member',
status: 'pending',
joinedDaysAgo: 3 + idx,
});
});
}
// Spread remaining drivers across remaining leagues to create realistic overlap.
for (const driver of drivers) {
const driverId = driver.id.toString();
const driverNumber = Number(driverId.split('-')[1]);
const extraLeague = leagues[(driverNumber % (leagues.length - 1)) + 1];
if (extraLeague) {
memberships.push(
LeagueMembership.create({
leagueId: extraLeague.id.toString(),
for (const league of leagues) {
const leagueId = league.id.toString();
if (leagueId === 'league-5') continue;
if (emptyLeagueId && leagueId === emptyLeagueId) continue;
if (driverNumber % 11 === 0 && leagueId === 'league-3') {
add({
leagueId,
driverId,
role: 'member',
status: 'inactive',
joinedDaysAgo: 120,
});
continue;
}
// Sparse membership distribution (not every driver in every league)
if ((driverNumber + Number(leagueId.split('-')[1] ?? 0)) % 9 === 0) {
add({
leagueId,
driverId,
role: 'member',
status: 'active',
joinedAt: this.addDays(this.baseDate, -40),
}),
);
joinedDaysAgo: 45,
});
}
}
}
return memberships;
}
createRaceRegistrations(races: Race[]): RaceRegistration[] {
createLeagueJoinRequests(
drivers: Driver[],
leagues: League[],
leagueMemberships: LeagueMembership[],
): JoinRequest[] {
const membershipIds = new Set<string>(leagueMemberships.map(m => m.id.toString()));
const requests: JoinRequest[] = [];
const addRequest = (input: { leagueId: string; driverId: string; id?: string; message?: string; requestedAt?: Date }) => {
requests.push(
JoinRequest.create({
leagueId: input.leagueId,
driverId: input.driverId,
...(input.id !== undefined && { id: input.id }),
...(input.message !== undefined && { message: input.message }),
...(input.requestedAt !== undefined && { requestedAt: input.requestedAt }),
}),
);
};
// League with lots of requests + membership/request conflicts (everyone is a member of league-5 already).
const demoLeagueId = 'league-5';
const demoDrivers = drivers.slice(10, 35);
demoDrivers.forEach((driver, idx) => {
const message =
idx % 4 === 0
? 'Interested in consistent stewarding and clean racing.'
: idx % 4 === 1
? undefined
: idx % 4 === 2
? ''
: 'Can I join mid-season and still be eligible for points?';
addRequest({
leagueId: demoLeagueId,
driverId: driver.id.toString(),
requestedAt: this.addDays(this.baseDate, -(7 + idx)),
...(message !== undefined && { message }),
});
});
// League with a few "normal" requests (only drivers who are NOT members already).
const targetLeagueId = 'league-1';
const nonMembers = drivers
.filter(driver => !membershipIds.has(`${targetLeagueId}:${driver.id.toString()}`))
.slice(0, 6);
nonMembers.forEach((driver, idx) => {
addRequest({
leagueId: targetLeagueId,
driverId: driver.id.toString(),
requestedAt: this.addDays(this.baseDate, -(3 + idx)),
...(idx % 2 === 0 && { message: 'Looking for regular endurance rounds and stable race times.' }),
});
});
// Single request with no message (explicit id).
const league3Exists = leagues.some(l => l.id.toString() === 'league-3');
if (league3Exists && drivers[0]) {
addRequest({
id: 'league-3-join-req-1',
leagueId: 'league-3',
driverId: drivers[0].id.toString(),
requestedAt: this.addDays(this.baseDate, -9),
});
}
// Duplicate id edge case (last write wins in in-memory repo).
if (drivers[1]) {
addRequest({
id: 'dup-league-join-req-1',
leagueId: 'league-7',
driverId: drivers[1].id.toString(),
requestedAt: this.addDays(this.baseDate, -2),
message: 'First request message (will be overwritten).',
});
addRequest({
id: 'dup-league-join-req-1',
leagueId: 'league-7',
driverId: drivers[1].id.toString(),
requestedAt: this.addDays(this.baseDate, -1),
message: 'Updated request message (duplicate id).',
});
}
// Explicit conflict: join request exists even though membership exists.
const driver1 = drivers.find(d => d.id.toString() === 'driver-1');
if (driver1) {
addRequest({
id: 'conflict-req-league-5-driver-1',
leagueId: demoLeagueId,
driverId: driver1.id.toString(),
requestedAt: this.addDays(this.baseDate, -15),
message: 'Testing UI edge case: request exists for an existing member.',
});
}
return requests;
}
createRaceRegistrations(
races: Race[],
drivers: Driver[],
leagueMemberships: LeagueMembership[],
): RaceRegistration[] {
const registrations: RaceRegistration[] = [];
const activeMembershipKey = new Set(
leagueMemberships
.filter(m => m.status.toString() === 'active')
.map(m => `${m.leagueId.toString()}:${m.driverId.toString()}`),
);
const scheduled = races.filter((r) => r.status === 'scheduled');
for (const race of scheduled) {
const leagueId = race.leagueId.toString();
const targetCount = (race as unknown as { registeredCount?: number }).registeredCount ?? 0;
// 25%: intentionally no registrations
if (Number(race.id.toString().split('-')[1] ?? 0) % 4 === 0) {
continue;
}
const eligibleDrivers = drivers
.map(d => d.id.toString())
.filter(driverId => activeMembershipKey.has(`${leagueId}:${driverId}`));
const desired = Math.min(
eligibleDrivers.length,
Math.max(1, targetCount > 0 ? targetCount : 3),
);
const start = Number(race.id.toString().split('-')[1] ?? 0);
for (let i = 0; i < desired; i++) {
const driverId = eligibleDrivers[(start + i) % eligibleDrivers.length];
if (!driverId) continue;
registrations.push(
RaceRegistration.create({
raceId: race.id,
driverId,
}),
);
}
// Edge case: one "outsider" registration (driver not active in league)
if (eligibleDrivers.length > 0 && drivers.length > eligibleDrivers.length) {
const outsider = drivers
.map(d => d.id.toString())
.find(driverId => !activeMembershipKey.has(`${leagueId}:${driverId}`));
if (outsider && start % 7 === 0) {
registrations.push(
RaceRegistration.create({
raceId: race.id,
driverId: outsider,
}),
);
}
}
// Edge case: duplicate registration (should be ignored by repo if unique constrained)
if (start % 9 === 0 && registrations.length > 0) {
const last = registrations[registrations.length - 1]!;
registrations.push(
RaceRegistration.create({
raceId: last.raceId.toString(),
driverId: last.driverId.toString(),
}),
);
}
}
// 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);
for (const race of upcomingDemoLeague) {
registrations.push(
RaceRegistration.create({

View File

@@ -8,12 +8,45 @@ export class RacingResultFactory {
const completed = races.filter((r) => r.status === 'completed');
for (const race of completed) {
const participants = drivers.slice(0, Math.min(16, drivers.length));
if (drivers.length === 0) continue;
for (let idx = 0; idx < participants.length; idx++) {
const driver = participants[idx]!;
const rng = this.mulberry32(this.hashToSeed(`${race.id}:${race.leagueId}:${race.trackId}`));
const minSize = Math.min(6, drivers.length);
const maxSize = Math.min(26, drivers.length);
const participantCount = Math.max(
minSize,
Math.min(maxSize, minSize + Math.floor(rng() * (maxSize - minSize + 1))),
);
const offset = Math.floor(rng() * drivers.length);
const participants = Array.from({ length: participantCount }, (_, idx) => drivers[(offset + idx) % drivers.length]!);
const finishOrder = [...participants];
this.shuffleInPlace(finishOrder, rng);
const gridOrder = [...participants];
this.shuffleInPlace(gridOrder, rng);
const baseLap = 82_000 + Math.floor(rng() * 12_000); // 1:22.000 - 1:34.000-ish
for (let idx = 0; idx < finishOrder.length; idx++) {
const driver = finishOrder[idx]!;
const position = idx + 1;
const startPosition = ((idx + 3) % participants.length) + 1;
const startPosition = gridOrder.findIndex(d => d.id.toString() === driver.id.toString()) + 1;
const lapJitter = Math.floor(rng() * 900);
const fastestLap = baseLap + lapJitter + (position - 1) * 35;
const incidents =
rng() < 0.55
? 0
: rng() < 0.85
? 1
: rng() < 0.95
? 2
: 3 + Math.floor(rng() * 6);
results.push(
RaceResult.create({
@@ -21,9 +54,9 @@ export class RacingResultFactory {
raceId: race.id,
driverId: driver.id,
position,
startPosition,
fastestLap: 88_000 + idx * 120,
incidents: idx % 4 === 0 ? 2 : 0,
startPosition: Math.max(1, startPosition),
fastestLap,
incidents,
}),
);
}
@@ -31,4 +64,33 @@ export class RacingResultFactory {
return results;
}
private shuffleInPlace<T>(items: T[], rng: () => number): void {
for (let i = items.length - 1; i > 0; i--) {
const j = Math.floor(rng() * (i + 1));
const tmp = items[i]!;
items[i] = items[j]!;
items[j] = tmp;
}
}
private hashToSeed(input: string): number {
let hash = 2166136261;
for (let i = 0; i < input.length; i++) {
hash ^= input.charCodeAt(i);
hash = Math.imul(hash, 16777619);
}
return hash >>> 0;
}
private mulberry32(seed: number): () => number {
let a = seed >>> 0;
return () => {
a |= 0;
a = (a + 0x6D2B79F5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
}

View File

@@ -0,0 +1,248 @@
import { faker } from '@faker-js/faker';
import type { League } from '@core/racing/domain/entities/League';
import { SponsorshipRequest } from '@core/racing/domain/entities/SponsorshipRequest';
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 { Money } from '@core/racing/domain/value-objects/Money';
export class RacingSeasonSponsorshipFactory {
constructor(private readonly baseDate: Date) {}
createSeasons(leagues: League[]): Season[] {
const seasons: Season[] = [];
for (const league of leagues) {
const leagueId = league.id.toString();
if (leagueId === 'league-5') {
seasons.push(
Season.create({
id: 'season-1',
leagueId,
gameId: 'iracing',
name: 'Season 1 (GT Sprint)',
year: 2025,
order: 1,
status: 'active',
startDate: this.daysFromBase(-30),
}),
Season.create({
id: 'season-2',
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: 'season-3',
leagueId,
gameId: 'iracing',
name: 'Season 3 (Planned)',
year: 2025,
order: 2,
status: 'planned',
startDate: this.daysFromBase(14),
}),
);
continue;
}
if (leagueId === 'league-3') {
seasons.push(
Season.create({
id: 'league-3-season-a',
leagueId,
gameId: 'iracing',
name: 'Split Season A',
year: 2025,
order: 1,
status: 'active',
startDate: this.daysFromBase(-10),
}),
Season.create({
id: 'league-3-season-b',
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 === 'league-2' ? 1 : faker.number.int({ min: 1, max: 3 });
for (let i = 0; i < seasonCount; i++) {
const id = `${leagueId}-season-${i + 1}`;
const isFirst = i === 0;
const status: Season['status'] =
leagueId === 'league-1' && isFirst
? 'active'
: leagueId === 'league-2'
? 'planned'
: isFirst
? faker.helpers.arrayElement(['active', 'planned'] as const)
: faker.helpers.arrayElement(['completed', 'archived', 'cancelled'] as const);
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 });
const endOffset =
status === 'completed' || status === 'archived' || status === 'cancelled'
? faker.number.int({ min: -89, max: -7 })
: undefined;
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) } : {}),
}),
);
}
}
return seasons;
}
createSeasonSponsorships(seasons: Season[], sponsors: Sponsor[]): SeasonSponsorship[] {
const sponsorships: SeasonSponsorship[] = [];
const sponsorIds = sponsors.map((s) => s.id.toString());
for (const season of seasons) {
const sponsorshipCount =
season.id === 'season-1'
? 2
: season.status === 'active'
? faker.number.int({ min: 0, max: 2 })
: faker.number.int({ min: 0, max: 1 });
const usedSponsorIds = new Set<string>();
for (let i = 0; i < sponsorshipCount; i++) {
const tier: SeasonSponsorship['tier'] =
i === 0 ? 'main' : faker.helpers.arrayElement(['secondary', 'secondary', 'main'] as const);
const sponsorIdCandidate = faker.helpers.arrayElement(sponsorIds);
const sponsorId = usedSponsorIds.has(sponsorIdCandidate)
? faker.helpers.arrayElement(sponsorIds)
: sponsorIdCandidate;
usedSponsorIds.add(sponsorId);
const base = SeasonSponsorship.create({
id: `season-sponsorship-${season.id}-${i + 1}`,
seasonId: season.id,
leagueId: season.leagueId,
sponsorId,
tier,
pricing: Money.create(
tier === 'main' ? faker.number.int({ min: 900, max: 2500 }) : faker.number.int({ min: 250, max: 900 }),
'USD',
),
createdAt: faker.date.recent({ days: 120, refDate: this.baseDate }),
description: tier === 'main' ? 'Main sponsor slot' : 'Secondary sponsor slot',
...(season.status === 'active'
? {
status: faker.helpers.arrayElement(['active', 'pending'] as const),
activatedAt: faker.date.recent({ days: 30, refDate: this.baseDate }),
}
: season.status === 'completed' || season.status === 'archived'
? {
status: faker.helpers.arrayElement(['ended', 'cancelled'] as const),
endedAt: faker.date.recent({ days: 200, refDate: this.baseDate }),
}
: {
status: faker.helpers.arrayElement(['pending', 'cancelled'] as const),
}),
});
sponsorships.push(base);
}
}
return sponsorships;
}
createSponsorshipRequests(seasons: Season[], sponsors: Sponsor[]): SponsorshipRequest[] {
const requests: SponsorshipRequest[] = [];
const sponsorIds = sponsors.map((s) => s.id.toString());
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 });
for (let i = 0; i < maxRequests; i++) {
const tier: SponsorshipRequest['tier'] =
i === 0 ? 'main' : faker.helpers.arrayElement(['secondary', 'secondary', 'main'] as const);
const sponsorId =
isHighTrafficDemo && i === 0
? 'demo-sponsor-1'
: faker.helpers.arrayElement(sponsorIds);
const offeredAmount = Money.create(
tier === 'main' ? faker.number.int({ min: 1000, max: 3500 }) : faker.number.int({ min: 200, max: 1200 }),
'USD',
);
const includeMessage = i % 3 !== 0;
const message = includeMessage
? faker.helpers.arrayElement([
'We would love to sponsor this season — high-quality sim racing content fits our brand.',
'We can provide prize pool support + gear giveaways for podium finishers.',
'Interested in the main slot: branding on liveries + broadcast overlays.',
'We want a secondary slot to test engagement before a bigger commitment.',
])
: undefined;
// A mix of statuses for edge cases (pending is what the UI lists)
const status =
season.status === 'active'
? faker.helpers.arrayElement(['pending', 'pending', 'pending', 'rejected', 'withdrawn'] as const)
: faker.helpers.arrayElement(['pending', 'rejected'] as const);
requests.push(
SponsorshipRequest.create({
id: `sponsorship-request-${season.id}-${i + 1}`,
sponsorId,
entityType: 'season',
entityId: season.id,
tier,
offeredAmount,
...(message !== undefined ? { message } : {}),
status,
createdAt: faker.date.recent({ days: 60, refDate: this.baseDate }),
}),
);
}
}
return requests;
}
private daysFromBase(days: number): Date {
return new Date(this.baseDate.getTime() + days * 24 * 60 * 60 * 1000);
}
}

View File

@@ -7,7 +7,12 @@ import { Result as RaceResult } from '@core/racing/domain/entities/result/Result
import { Standing } from '@core/racing/domain/entities/Standing';
import { Team } from '@core/racing/domain/entities/Team';
import { Track } from '@core/racing/domain/entities/Track';
import type { TeamMembership } from '@core/racing/domain/types/TeamMembership';
import { JoinRequest } from '@core/racing/domain/entities/JoinRequest';
import { SponsorshipRequest } from '@core/racing/domain/entities/SponsorshipRequest';
import { Sponsor } from '@core/racing/domain/entities/sponsor/Sponsor';
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 { RacingFeedFactory } from './RacingFeedFactory';
@@ -19,6 +24,10 @@ import { RacingResultFactory } from './RacingResultFactory';
import { RacingStandingFactory } from './RacingStandingFactory';
import { RacingTeamFactory } from './RacingTeamFactory';
import { RacingTrackFactory } from './RacingTrackFactory';
import { RacingSponsorFactory } from './RacingSponsorFactory';
import { RacingSeasonSponsorshipFactory } from './RacingSeasonSponsorshipFactory';
import { RacingLeagueWalletFactory } from './RacingLeagueWalletFactory';
import { RacingStewardingFactory } from './RacingStewardingFactory';
export type Friendship = {
driverId: string;
@@ -28,13 +37,23 @@ export type Friendship = {
export type RacingSeed = {
drivers: Driver[];
leagues: League[];
seasons: Season[];
seasonSponsorships: SeasonSponsorship[];
sponsorshipRequests: SponsorshipRequest[];
leagueWallets: import('@core/racing/domain/entities/league-wallet/LeagueWallet').LeagueWallet[];
leagueWalletTransactions: import('@core/racing/domain/entities/league-wallet/Transaction').Transaction[];
protests: import('@core/racing/domain/entities/Protest').Protest[];
penalties: import('@core/racing/domain/entities/penalty/Penalty').Penalty[];
races: Race[];
results: RaceResult[];
standings: Standing[];
leagueMemberships: LeagueMembership[];
leagueJoinRequests: JoinRequest[];
raceRegistrations: RaceRegistration[];
teams: Team[];
teamMemberships: TeamMembership[];
teamJoinRequests: TeamJoinRequest[];
sponsors: Sponsor[];
tracks: Track[];
friendships: Friendship[];
feedEvents: FeedItem[];
@@ -68,6 +87,9 @@ class RacingSeedFactory {
const resultFactory = new RacingResultFactory();
const standingFactory = new RacingStandingFactory();
const membershipFactory = new RacingMembershipFactory(this.baseDate);
const sponsorFactory = new RacingSponsorFactory(this.baseDate);
const seasonSponsorshipFactory = new RacingSeasonSponsorshipFactory(this.baseDate);
const leagueWalletFactory = new RacingLeagueWalletFactory(this.baseDate);
const friendshipFactory = new RacingFriendshipFactory();
const feedFactory = new RacingFeedFactory(this.baseDate);
@@ -75,27 +97,51 @@ class RacingSeedFactory {
const tracks = trackFactory.create();
const leagueFactory = new RacingLeagueFactory(this.baseDate, drivers);
const leagues = leagueFactory.create();
const teamFactory = new RacingTeamFactory(this.baseDate, drivers, leagues);
const teams = teamFactory.createTeams();
const sponsors = sponsorFactory.create();
const seasons = seasonSponsorshipFactory.createSeasons(leagues);
const seasonSponsorships = seasonSponsorshipFactory.createSeasonSponsorships(seasons, sponsors);
const sponsorshipRequests = seasonSponsorshipFactory.createSponsorshipRequests(seasons, sponsors);
const leagueMemberships = membershipFactory.createLeagueMemberships(drivers, leagues);
const leagueJoinRequests = membershipFactory.createLeagueJoinRequests(drivers, leagues, leagueMemberships);
const { wallets: leagueWallets, transactions: leagueWalletTransactions } = leagueWalletFactory.create(leagues);
const teamFactory = new RacingTeamFactory(this.baseDate);
const teams = teamFactory.createTeams(drivers, leagues);
const races = raceFactory.create(leagues, tracks);
const results = resultFactory.create(drivers, races);
const standings = standingFactory.create(leagues, races, results);
const leagueMemberships = membershipFactory.createLeagueMemberships(drivers, leagues);
const raceRegistrations = membershipFactory.createRaceRegistrations(races);
const raceRegistrations = membershipFactory.createRaceRegistrations(races, drivers, leagueMemberships);
const teamMemberships = teamFactory.createTeamMemberships(drivers, teams);
const teamJoinRequests = teamFactory.createTeamJoinRequests(drivers, teams, teamMemberships);
const stewardingFactory = new RacingStewardingFactory(this.baseDate);
const { protests, penalties } = stewardingFactory.create(races, drivers, leagueMemberships);
const friendships = friendshipFactory.create(drivers);
const feedEvents = feedFactory.create(drivers, friendships, races, leagues);
return {
drivers,
leagues,
seasons,
seasonSponsorships,
sponsorshipRequests,
leagueWallets,
leagueWalletTransactions,
protests,
penalties,
races,
results,
standings,
leagueMemberships,
leagueJoinRequests,
raceRegistrations,
teams,
teamMemberships,
teamJoinRequests,
sponsors,
tracks,
friendships,
feedEvents,

View File

@@ -0,0 +1,125 @@
import { Sponsor } from '@core/racing/domain/entities/sponsor/Sponsor';
import { faker } from '@faker-js/faker';
export class RacingSponsorFactory {
constructor(private readonly baseDate: Date) {}
create(): Sponsor[] {
const demoSponsor = Sponsor.create({
id: 'demo-sponsor-1',
name: 'GridPilot Sim Racing Supply',
contactEmail: 'partnerships@gridpilot.example',
logoUrl: 'http://localhost:3000/images/header.jpeg',
websiteUrl: 'https://gridpilot.example/sponsors/gridpilot-sim-racing-supply',
createdAt: faker.date.past({ years: 2, refDate: this.baseDate }),
});
const sponsorCount = 49;
const sponsorNames = [
'Red Bull Energy',
'Shell Racing',
'Michelin Tires',
'Castrol Oil',
'Pirelli Tires',
'Goodyear Tires',
'Bridgestone',
'Continental Tires',
'Dunlop Tires',
'Yokohama Tires',
'Hankook Tires',
'BFGoodrich',
'Firestone Tires',
'Kumho Tires',
'Nexen Tires',
'Falken Tires',
'Toyo Tires',
'Maxxis Tires',
'Cooper Tires',
'General Tires',
'Sparco Motorsport',
'OMP Racing',
'Simucube',
'Fanatec',
'Moza Racing',
'Heusinkveld Engineering',
'Next Level Racing',
'Thrustmaster Racing',
'Logitech G Racing',
'GoPro Motorsports',
'VRS DirectForce',
'Ascher Racing',
'RaceRoom Store',
'iRacing Credits',
'Assetto Corsa Competizione',
'RaceDepartment',
'SimRacing.GP',
'Grid Finder',
'Racing Gloves Co.',
'Hydration Labs',
'Energy Bar Co.',
'Broadcast Overlay Studio',
'Carbon Fiber Works',
'Brake Pad Supply',
'Pit Strategy Analytics',
'Telemetry Tools',
'Setup Shop',
'Streaming Partner',
'Hardware Tuning Lab',
'Trackside Media',
];
const logoPaths = [
'http://localhost:3000/images/header.jpeg',
'http://localhost:3000/images/ff1600.jpeg',
'http://localhost:3000/images/avatars/male-default-avatar.jpg',
'http://localhost:3000/images/avatars/female-default-avatar.jpeg',
'http://localhost:3000/images/avatars/neutral-default-avatar.jpeg',
'http://localhost:3000/images/leagues/placeholder-cover.svg',
'http://localhost:3000/favicon.svg',
];
const websiteUrls = [
'https://www.redbull.com',
'https://www.shell.com',
'https://www.michelin.com',
'https://www.castrol.com',
'https://www.pirelli.com',
'https://www.goodyear.com',
'https://www.bridgestone.com',
'https://www.continental-tires.com',
'https://www.dunlop.eu',
'https://www.yokohamatire.com',
'https://www.hankooktire.com',
'https://www.sparco-official.com',
'https://www.omp.com',
'https://fanatec.com',
'https://mozaracing.com',
'https://heusinkveld.com',
'https://nextlevelracing.com',
'https://www.thrustmaster.com',
'https://www.logitechg.com',
'https://gopro.com',
];
const sponsors = Array.from({ length: sponsorCount }, (_, idx) => {
const i = idx + 1;
const name = sponsorNames[idx % sponsorNames.length]!;
const safeName = name.replace(/\s+/g, '').toLowerCase();
const logoUrl = logoPaths[idx % logoPaths.length]!;
const websiteUrl = websiteUrls[idx % websiteUrls.length]!;
return Sponsor.create({
id: `sponsor-${i}`,
name,
contactEmail: `partnerships+${safeName}@example.com`,
...(logoUrl ? { logoUrl } : {}),
...(websiteUrl ? { websiteUrl } : {}),
createdAt: faker.date.past({ years: 5, refDate: this.baseDate }),
});
});
return [demoSponsor, ...sponsors];
}
}

View File

@@ -0,0 +1,260 @@
import { faker } from '@faker-js/faker';
import type { Driver } from '@core/racing/domain/entities/Driver';
import type { LeagueMembership } from '@core/racing/domain/entities/LeagueMembership';
import { Protest } from '@core/racing/domain/entities/Protest';
import type { Race } from '@core/racing/domain/entities/Race';
import { Penalty } from '@core/racing/domain/entities/penalty/Penalty';
type StewardingSeed = {
protests: Protest[];
penalties: Penalty[];
};
export class RacingStewardingFactory {
constructor(private readonly baseDate: Date) {}
create(races: Race[], drivers: Driver[], leagueMemberships: LeagueMembership[]): StewardingSeed {
const protests: Protest[] = [];
const penalties: Penalty[] = [];
const driversById = new Map<string, Driver>(drivers.map((d) => [d.id.toString(), d]));
const activeMembersByLeague = new Map<string, string[]>();
for (const membership of leagueMemberships) {
if (membership.status.toString() !== 'active') continue;
const leagueId = membership.leagueId.toString();
const driverId = membership.driverId.toString();
if (!driversById.has(driverId)) continue;
const list = activeMembersByLeague.get(leagueId) ?? [];
list.push(driverId);
activeMembersByLeague.set(leagueId, list);
}
const racesByLeague = new Map<string, Race[]>();
for (const race of races) {
const leagueId = race.leagueId.toString();
const list = racesByLeague.get(leagueId) ?? [];
list.push(race);
racesByLeague.set(leagueId, list);
}
// Make league-5 the "busy demo league" for stewarding UIs:
// - at least one pending/under_review protest
// - some resolved protests
// - penalties with and without protest linkage
const demoLeagueRaces = (racesByLeague.get('league-5') ?? []).slice().sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime());
const demoMembers = activeMembersByLeague.get('league-5') ?? [];
if (demoLeagueRaces.length > 0 && demoMembers.length >= 4) {
const [firstRace, secondRace, thirdRace] = demoLeagueRaces;
const protester = demoMembers[0]!;
const accused = demoMembers[1]!;
const steward = demoMembers[2]!;
const spare = demoMembers[3]!;
if (firstRace) {
protests.push(
Protest.create({
id: 'protest-1',
raceId: firstRace.id.toString(),
protestingDriverId: protester,
accusedDriverId: accused,
incident: {
lap: 1,
description: 'Avoidable contact into Turn 1 causing a spin and damage.',
timeInRace: 85,
},
comment: 'I left a car width but got punted. Please review the onboard and external.',
proofVideoUrl: 'https://example.com/videos/protest-1',
status: 'pending',
filedAt: faker.date.recent({ days: 6, refDate: this.baseDate }),
}),
);
// No penalty yet (pending), but seed a direct steward warning on same race.
penalties.push(
Penalty.create({
id: 'penalty-1',
leagueId: 'league-5',
raceId: firstRace.id.toString(),
driverId: accused,
type: 'warning',
reason: 'Repeated track limits warnings (race director note).',
issuedBy: steward,
status: 'applied',
issuedAt: faker.date.recent({ days: 5, refDate: this.baseDate }),
appliedAt: faker.date.recent({ days: 5, refDate: this.baseDate }),
notes: 'Keep it within track limits; further offenses will escalate.',
}),
);
}
if (secondRace) {
protests.push(
Protest.create({
id: 'protest-2',
raceId: secondRace.id.toString(),
protestingDriverId: spare,
accusedDriverId: accused,
incident: {
lap: 7,
description: 'Unsafe rejoin after off-track caused a collision with following traffic.',
timeInRace: 610,
},
proofVideoUrl: 'https://example.com/videos/protest-2',
status: 'under_review',
reviewedBy: steward,
filedAt: faker.date.recent({ days: 12, refDate: this.baseDate }),
}),
);
// Under review penalty still pending (linked to protest)
penalties.push(
Penalty.create({
id: 'penalty-2',
leagueId: 'league-5',
raceId: secondRace.id.toString(),
driverId: accused,
type: 'time_penalty',
value: 10,
reason: 'Unsafe rejoin (protest pending review)',
protestId: 'protest-2',
issuedBy: steward,
status: 'pending',
issuedAt: faker.date.recent({ days: 10, refDate: this.baseDate }),
notes: 'Will be applied to results if upheld.',
}),
);
}
if (thirdRace) {
const upheld = Protest.create({
id: 'protest-3',
raceId: thirdRace.id.toString(),
protestingDriverId: protester,
accusedDriverId: spare,
incident: {
lap: 12,
description: 'Brake check on the straight leading to rear-end contact.',
timeInRace: 1280,
},
status: 'upheld',
reviewedBy: steward,
decisionNotes: 'Brake check is not acceptable. Penalty applied.',
reviewedAt: faker.date.recent({ days: 20, refDate: this.baseDate }),
filedAt: faker.date.recent({ days: 25, refDate: this.baseDate }),
});
protests.push(upheld);
penalties.push(
Penalty.create({
id: 'penalty-3',
leagueId: 'league-5',
raceId: thirdRace.id.toString(),
driverId: spare,
type: 'points_deduction',
value: 5,
reason: upheld.incident.description.toString(),
protestId: upheld.id,
issuedBy: steward,
status: 'applied',
issuedAt: faker.date.recent({ days: 20, refDate: this.baseDate }),
appliedAt: faker.date.recent({ days: 19, refDate: this.baseDate }),
notes: 'Applied after stewarding decision.',
}),
);
const dismissed = Protest.create({
id: 'protest-4',
raceId: thirdRace.id.toString(),
protestingDriverId: accused,
accusedDriverId: protester,
incident: {
lap: 3,
description: 'Minor side-by-side contact with no loss of control.',
timeInRace: 240,
},
status: 'dismissed',
reviewedBy: steward,
decisionNotes: 'Racing incident, no further action.',
reviewedAt: faker.date.recent({ days: 18, refDate: this.baseDate }),
filedAt: faker.date.recent({ days: 22, refDate: this.baseDate }),
});
protests.push(dismissed);
}
}
// Fill other leagues lightly with variety (including awaiting_defense and withdrawn).
for (const [leagueId, leagueRaces] of racesByLeague.entries()) {
if (leagueId === 'league-5') continue;
const members = activeMembersByLeague.get(leagueId) ?? [];
if (members.length < 3) continue;
const completedRaces = leagueRaces.filter((r) => r.status.toString() === 'completed').slice(0, 2);
if (completedRaces.length === 0) continue;
const [race] = completedRaces;
if (!race) continue;
const a = members[0]!;
const b = members[1]!;
const steward = members[2]!;
const seedOne = faker.number.int({ min: 0, max: 2 }) === 0;
if (!seedOne) continue;
const status = faker.helpers.arrayElement(['awaiting_defense', 'withdrawn'] as const);
const protest = Protest.create({
id: `protest-${leagueId}-${race.id.toString()}`,
raceId: race.id.toString(),
protestingDriverId: a,
accusedDriverId: b,
incident: {
lap: faker.number.int({ min: 1, max: 20 }),
description: faker.helpers.arrayElement([
'Divebomb attempt from too far back caused avoidable contact.',
'Blocking under braking and changing line multiple times.',
'Track limits abuse provided a sustained advantage.',
'Blue flag was ignored causing unnecessary time loss.',
]),
},
status,
...(status === 'awaiting_defense'
? {
defenseRequestedBy: steward,
defenseRequestedAt: faker.date.recent({ days: 3, refDate: this.baseDate }),
}
: {}),
filedAt: faker.date.recent({ days: 14, refDate: this.baseDate }),
});
protests.push(protest);
if (status === 'withdrawn') {
// A non-protest-linked penalty can still exist for the same race.
penalties.push(
Penalty.create({
id: `penalty-${leagueId}-${race.id.toString()}`,
leagueId,
raceId: race.id.toString(),
driverId: b,
type: faker.helpers.arrayElement(['grid_penalty', 'license_points'] as const),
value: faker.number.int({ min: 2, max: 5 }),
reason: 'Steward discretion: repeated behavior across sessions.',
issuedBy: steward,
status: 'applied',
issuedAt: faker.date.recent({ days: 9, refDate: this.baseDate }),
appliedAt: faker.date.recent({ days: 9, refDate: this.baseDate }),
}),
);
}
}
return { protests, penalties };
}
}

View File

@@ -1,25 +1,21 @@
import { Driver } from '@core/racing/domain/entities/Driver';
import { League } from '@core/racing/domain/entities/League';
import { Team } from '@core/racing/domain/entities/Team';
import type { TeamMembership } from '@core/racing/domain/types/TeamMembership';
import type { TeamJoinRequest, TeamMembership } from '@core/racing/domain/types/TeamMembership';
import { faker } from '@faker-js/faker';
export class RacingTeamFactory {
constructor(
private readonly baseDate: Date,
private readonly drivers: Driver[],
private readonly leagues: League[],
) {}
constructor(private readonly baseDate: Date) {}
createTeams(): Team[] {
createTeams(drivers: Driver[], leagues: League[]): Team[] {
const teamCount = 15;
return Array.from({ length: teamCount }, (_, idx) => {
const i = idx + 1;
const owner = faker.helpers.arrayElement(this.drivers);
const owner = faker.helpers.arrayElement(drivers);
const teamLeagues = faker.helpers.arrayElements(
this.leagues.map(l => l.id.toString()),
{ min: 0, max: 3 }
leagues.map((l) => l.id.toString()),
{ min: 0, max: 3 },
);
return Team.create({
@@ -111,4 +107,74 @@ export class RacingTeamFactory {
return memberships;
}
createTeamJoinRequests(
drivers: Driver[],
teams: Team[],
teamMemberships: TeamMembership[],
): TeamJoinRequest[] {
const membershipIds = new Set<string>(teamMemberships.map(m => `${m.teamId}:${m.driverId}`));
const requests: TeamJoinRequest[] = [];
const addRequest = (request: TeamJoinRequest): void => {
requests.push(request);
};
const team1 = teams[0];
if (team1) {
const candidateDriverIds = drivers
.map(d => d.id.toString())
.filter(driverId => !membershipIds.has(`${team1.id.toString()}:${driverId}`))
.slice(0, 8);
candidateDriverIds.forEach((driverId, idx) => {
addRequest({
id: `team-join-${team1.id.toString()}-${driverId}`,
teamId: team1.id.toString(),
driverId,
requestedAt: this.addDays(this.baseDate, -(5 + idx)),
...(idx % 3 === 0
? { message: 'Can I join as a substitute driver for endurance events?' }
: idx % 3 === 2
? { message: '' }
: {}),
});
});
// Conflict edge case: owner submits a join request to own team.
addRequest({
id: `team-join-${team1.id.toString()}-${team1.ownerId.toString()}-conflict`,
teamId: team1.id.toString(),
driverId: team1.ownerId.toString(),
requestedAt: this.addDays(this.baseDate, -1),
message: 'Testing edge case: owner submitted join request.',
});
}
const team3 = teams[2];
if (team3 && drivers[0]) {
const driverId = drivers[0].id.toString();
addRequest({
id: 'dup-team-join-req-1',
teamId: team3.id.toString(),
driverId,
requestedAt: this.addDays(this.baseDate, -10),
message: 'First request message (will be overwritten).',
});
addRequest({
id: 'dup-team-join-req-1',
teamId: team3.id.toString(),
driverId,
requestedAt: this.addDays(this.baseDate, -9),
message: 'Updated request message (duplicate id).',
});
}
return requests;
}
private addDays(date: Date, days: number): Date {
return new Date(date.getTime() + days * 24 * 60 * 60 * 1000);
}
}

View File

@@ -2,6 +2,10 @@ import { Track } from '@core/racing/domain/entities/Track';
export class RacingTrackFactory {
create(): Track[] {
// Only a subset of track images exist locally in `apps/website/public/images/tracks`.
// Use a real local image for a few, and a stable local placeholder for the rest.
const placeholderImageUrl = '/images/leagues/placeholder-cover.svg';
return [
// Road tracks - various difficulties
Track.create({
@@ -25,7 +29,7 @@ export class RacingTrackFactory {
difficulty: 'intermediate',
lengthKm: 5.793,
turns: 11,
imageUrl: '/images/tracks/monza.jpg',
imageUrl: placeholderImageUrl,
gameId: 'iracing',
}),
Track.create({
@@ -37,7 +41,7 @@ export class RacingTrackFactory {
difficulty: 'advanced',
lengthKm: 5.148,
turns: 15,
imageUrl: '/images/tracks/nurburgring.jpg',
imageUrl: placeholderImageUrl,
gameId: 'iracing',
}),
Track.create({
@@ -49,7 +53,7 @@ export class RacingTrackFactory {
difficulty: 'intermediate',
lengthKm: 5.891,
turns: 18,
imageUrl: '/images/tracks/silverstone.jpg',
imageUrl: placeholderImageUrl,
gameId: 'iracing',
}),
Track.create({
@@ -61,7 +65,7 @@ export class RacingTrackFactory {
difficulty: 'expert',
lengthKm: 5.807,
turns: 18,
imageUrl: '/images/tracks/suzuka.jpg',
imageUrl: placeholderImageUrl,
gameId: 'iracing',
}),
Track.create({
@@ -73,7 +77,7 @@ export class RacingTrackFactory {
difficulty: 'advanced',
lengthKm: 3.602,
turns: 11,
imageUrl: '/images/tracks/laguna.jpg',
imageUrl: placeholderImageUrl,
gameId: 'iracing',
}),
Track.create({
@@ -85,7 +89,7 @@ export class RacingTrackFactory {
difficulty: 'intermediate',
lengthKm: 4.259,
turns: 14,
imageUrl: '/images/tracks/zandvoort.jpg',
imageUrl: placeholderImageUrl,
gameId: 'iracing',
}),
Track.create({
@@ -97,7 +101,7 @@ export class RacingTrackFactory {
difficulty: 'advanced',
lengthKm: 4.909,
turns: 19,
imageUrl: '/images/tracks/imola.jpg',
imageUrl: placeholderImageUrl,
gameId: 'iracing',
}),
Track.create({
@@ -109,7 +113,7 @@ export class RacingTrackFactory {
difficulty: 'expert',
lengthKm: 13.626,
turns: 38,
imageUrl: '/images/tracks/le-mans.jpg',
imageUrl: placeholderImageUrl,
gameId: 'iracing',
}),
Track.create({
@@ -121,7 +125,7 @@ export class RacingTrackFactory {
difficulty: 'intermediate',
lengthKm: 4.574,
turns: 17,
imageUrl: '/images/tracks/hockenheim.jpg',
imageUrl: placeholderImageUrl,
gameId: 'iracing',
}),
// Oval tracks
@@ -146,7 +150,7 @@ export class RacingTrackFactory {
difficulty: 'advanced',
lengthKm: 4.192,
turns: 4,
imageUrl: '/images/tracks/indianapolis.jpg',
imageUrl: placeholderImageUrl,
gameId: 'iracing',
}),
Track.create({
@@ -158,7 +162,7 @@ export class RacingTrackFactory {
difficulty: 'beginner',
lengthKm: 4.280,
turns: 4,
imageUrl: '/images/tracks/talladega.jpg',
imageUrl: placeholderImageUrl,
gameId: 'iracing',
}),
// Street tracks
@@ -171,7 +175,7 @@ export class RacingTrackFactory {
difficulty: 'intermediate',
lengthKm: 5.410,
turns: 19,
imageUrl: '/images/tracks/miami.jpg',
imageUrl: placeholderImageUrl,
gameId: 'iracing',
}),
Track.create({
@@ -183,7 +187,7 @@ export class RacingTrackFactory {
difficulty: 'advanced',
lengthKm: 6.201,
turns: 17,
imageUrl: '/images/tracks/las-vegas.jpg',
imageUrl: placeholderImageUrl,
gameId: 'iracing',
}),
// Dirt tracks
@@ -196,7 +200,7 @@ export class RacingTrackFactory {
difficulty: 'beginner',
lengthKm: 0.805,
turns: 4,
imageUrl: '/images/tracks/eldora.jpg',
imageUrl: placeholderImageUrl,
gameId: 'iracing',
}),
];

View File

@@ -13,9 +13,9 @@ describe('InMemoryImageServiceAdapter', () => {
const adapter = new InMemoryImageServiceAdapter(logger);
expect(adapter.getDriverAvatar('driver-1')).toContain('/avatars/driver-1.png');
expect(adapter.getTeamLogo('team-1')).toContain('/logos/team-team-1.png');
expect(adapter.getLeagueCover('league-1')).toContain('/covers/league-league-1.png');
expect(adapter.getLeagueLogo('league-1')).toContain('/logos/league-league-1.png');
expect(adapter.getDriverAvatar('driver-1')).toContain('/images/avatars/');
expect(adapter.getTeamLogo('team-1')).toBe('/images/ff1600.jpeg');
expect(adapter.getLeagueCover('league-1')).toBe('/images/header.jpeg');
expect(adapter.getLeagueLogo('league-1')).toBe('/images/ff1600.jpeg');
});
});

View File

@@ -8,21 +8,30 @@ export class InMemoryImageServiceAdapter implements IImageServicePort {
getDriverAvatar(driverId: string): string {
this.logger.debug(`[InMemoryImageServiceAdapter] Getting avatar for driver: ${driverId}`);
return `https://cdn.example.com/avatars/${driverId}.png`; // Mock URL
const driverNumber = Number(driverId.replace('driver-', ''));
const index = Number.isFinite(driverNumber) ? driverNumber % 3 : 0;
const avatars = [
'/images/avatars/male-default-avatar.jpg',
'/images/avatars/female-default-avatar.jpeg',
'/images/avatars/neutral-default-avatar.jpeg',
] as const;
return avatars[index] ?? avatars[0];
}
getTeamLogo(teamId: string): string {
this.logger.debug(`[InMemoryImageServiceAdapter] Getting logo for team: ${teamId}`);
return `https://cdn.example.com/logos/team-${teamId}.png`; // Mock URL
return '/images/ff1600.jpeg';
}
getLeagueCover(leagueId: string): string {
this.logger.debug(`[InMemoryImageServiceAdapter] Getting cover for league: ${leagueId}`);
return `https://cdn.example.com/covers/league-${leagueId}.png`; // Mock URL
return '/images/header.jpeg';
}
getLeagueLogo(leagueId: string): string {
this.logger.debug(`[InMemoryImageServiceAdapter] Getting logo for league: ${leagueId}`);
return `https://cdn.example.com/logos/league-${leagueId}.png`; // Mock URL
return '/images/ff1600.jpeg';
}
}

View File

@@ -46,6 +46,13 @@ export const BootstrapProviders: Provider[] = [
useFactory: (
driverRepository: RacingSeedDependencies['driverRepository'],
leagueRepository: RacingSeedDependencies['leagueRepository'],
seasonRepository: RacingSeedDependencies['seasonRepository'],
seasonSponsorshipRepository: RacingSeedDependencies['seasonSponsorshipRepository'],
sponsorshipRequestRepository: RacingSeedDependencies['sponsorshipRequestRepository'],
leagueWalletRepository: RacingSeedDependencies['leagueWalletRepository'],
transactionRepository: RacingSeedDependencies['transactionRepository'],
protestRepository: RacingSeedDependencies['protestRepository'],
penaltyRepository: RacingSeedDependencies['penaltyRepository'],
raceRepository: RacingSeedDependencies['raceRepository'],
resultRepository: RacingSeedDependencies['resultRepository'],
standingRepository: RacingSeedDependencies['standingRepository'],
@@ -53,11 +60,19 @@ export const BootstrapProviders: Provider[] = [
raceRegistrationRepository: RacingSeedDependencies['raceRegistrationRepository'],
teamRepository: RacingSeedDependencies['teamRepository'],
teamMembershipRepository: RacingSeedDependencies['teamMembershipRepository'],
sponsorRepository: RacingSeedDependencies['sponsorRepository'],
feedRepository: RacingSeedDependencies['feedRepository'],
socialGraphRepository: RacingSeedDependencies['socialGraphRepository'],
): RacingSeedDependencies => ({
driverRepository,
leagueRepository,
seasonRepository,
seasonSponsorshipRepository,
sponsorshipRequestRepository,
leagueWalletRepository,
transactionRepository,
protestRepository,
penaltyRepository,
raceRepository,
resultRepository,
standingRepository,
@@ -65,12 +80,20 @@ export const BootstrapProviders: Provider[] = [
raceRegistrationRepository,
teamRepository,
teamMembershipRepository,
sponsorRepository,
feedRepository,
socialGraphRepository,
}),
inject: [
'IDriverRepository',
'ILeagueRepository',
'ISeasonRepository',
'ISeasonSponsorshipRepository',
'ISponsorshipRequestRepository',
'ILeagueWalletRepository',
'ITransactionRepository',
'IProtestRepository',
'IPenaltyRepository',
'IRaceRepository',
'IResultRepository',
'IStandingRepository',
@@ -78,6 +101,7 @@ export const BootstrapProviders: Provider[] = [
'IRaceRegistrationRepository',
'ITeamRepository',
'ITeamMembershipRepository',
'ISponsorRepository',
'IFeedRepository',
'ISocialGraphRepository',
],

View File

@@ -0,0 +1,224 @@
import { describe, expect, it } from 'vitest';
import { createRacingSeed } from '@adapters/bootstrap/racing/RacingSeed';
describe('Racing seed (bootstrap)', () => {
it('creates a large, internally consistent seed', () => {
const seed = createRacingSeed();
expect(seed.drivers.length).toBeGreaterThanOrEqual(50);
expect(seed.leagues.length).toBeGreaterThanOrEqual(10);
expect(seed.races.length).toBeGreaterThan(0);
const driverIds = new Set(seed.drivers.map((d) => d.id.toString()));
const leagueIds = new Set(seed.leagues.map((l) => l.id.toString()));
const teamIds = new Set(seed.teams.map((t) => t.id.toString()));
const raceById = new Map(seed.races.map((r) => [r.id.toString(), r] as const));
const sponsorIds = new Set(seed.sponsors.map((s) => s.id.toString()));
const seasonById = new Map(seed.seasons.map((s) => [s.id, s] as const));
const walletById = new Map(seed.leagueWallets.map((w) => [w.id.toString(), w] as const));
const protestById = new Map(seed.protests.map((p) => [p.id, p] as const));
// Deterministic demo IDs used by the website
expect(seed.sponsors.some((s) => s.id.toString() === 'demo-sponsor-1')).toBe(true);
// Seasons + sponsorship ecosystem
expect(seed.seasons.some((s) => s.id === 'season-1')).toBe(true);
expect(seed.seasons.some((s) => s.id === 'season-2')).toBe(true);
// Referential integrity: seasons must reference real leagues
for (const season of seed.seasons) {
expect(leagueIds.has(season.leagueId)).toBe(true);
}
// Referential integrity: season sponsorships must reference real season + sponsor
for (const sponsorship of seed.seasonSponsorships) {
expect(seasonById.has(sponsorship.seasonId)).toBe(true);
expect(sponsorIds.has(sponsorship.sponsorId)).toBe(true);
const season = seasonById.get(sponsorship.seasonId);
if (season && sponsorship.leagueId !== undefined) {
expect(sponsorship.leagueId).toBe(season.leagueId);
}
}
// Referential integrity: sponsorship requests must reference a real sponsor and target entity
for (const request of seed.sponsorshipRequests) {
expect(sponsorIds.has(request.sponsorId)).toBe(true);
if (request.entityType === 'season') {
expect(seasonById.has(request.entityId)).toBe(true);
} else if (request.entityType === 'team') {
expect(teamIds.has(request.entityId)).toBe(true);
} else if (request.entityType === 'race') {
expect(raceById.has(request.entityId)).toBe(true);
} else if (request.entityType === 'driver') {
expect(driverIds.has(request.entityId)).toBe(true);
}
}
const season1PendingRequests = seed.sponsorshipRequests.filter(
(r) => r.entityType === 'season' && r.entityId === 'season-1' && r.status === 'pending',
);
expect(season1PendingRequests.length).toBeGreaterThan(0);
// Wallet edge cases:
// - some leagues have no wallet at all
expect(seed.leagueWallets.some((w) => w.leagueId.toString() === 'league-2')).toBe(false);
expect(seed.leagueWallets.some((w) => w.leagueId.toString() === 'league-7')).toBe(false);
// - some wallets have no transactions (league-3 is intentionally quiet)
const league3Wallet = seed.leagueWallets.find((w) => w.leagueId.toString() === 'league-3');
if (league3Wallet) {
const league3TxCount = seed.leagueWalletTransactions.filter(
(tx) => tx.walletId.toString() === league3Wallet.id.toString(),
).length;
expect(league3TxCount).toBe(0);
}
// - demo wallet has a pending prize payout
const league5Wallet = seed.leagueWallets.find((w) => w.leagueId.toString() === 'league-5');
expect(league5Wallet).toBeDefined();
if (league5Wallet) {
const league5PendingPrizes = seed.leagueWalletTransactions.filter(
(tx) =>
tx.walletId.toString() === league5Wallet.id.toString() &&
tx.type === 'prize_payout' &&
tx.status === 'pending',
);
expect(league5PendingPrizes.length).toBeGreaterThan(0);
}
// Referential integrity: all transactions reference an existing wallet
for (const tx of seed.leagueWalletTransactions) {
expect(walletById.has(tx.walletId.toString())).toBe(true);
}
// Wallet invariant safety: no completed debit causes an overdraft when applied in factory order.
for (const wallet of seed.leagueWallets) {
const walletId = wallet.id.toString();
const completed = seed.leagueWalletTransactions
.filter((t) => t.walletId.toString() === walletId && t.status === 'completed')
.filter((t) => t.type !== 'prize_payout');
const sum = (values: number[]) => values.reduce((acc, v) => acc + v, 0);
const credits = completed.filter((t) => t.type === 'membership_payment' || t.type === 'sponsorship_payment');
const debits = completed.filter((t) => t.type === 'withdrawal' || t.type === 'refund');
const creditNet = sum(credits.map((t) => t.netAmount.amount));
const debitNet = sum(debits.map((t) => t.netAmount.amount));
const baseline = wallet.balance.amount - creditNet + debitNet;
expect(baseline).toBeGreaterThanOrEqual(0);
const parseSeq = (id: string): number | undefined => {
const match = id.match(/tx-league-\d+-(\d+)$/);
if (!match) return undefined;
const seq = Number(match[1]);
return Number.isFinite(seq) ? seq : undefined;
};
const inFactoryOrder = [...completed].sort((a, b) => {
const aSeq = parseSeq(a.id.toString());
const bSeq = parseSeq(b.id.toString());
if (aSeq !== undefined && bSeq !== undefined) return aSeq - bSeq;
if (aSeq !== undefined) return -1;
if (bSeq !== undefined) return 1;
return a.createdAt.getTime() - b.createdAt.getTime();
});
let running = baseline;
for (const tx of inFactoryOrder) {
const net = tx.netAmount.amount;
if (tx.type === 'withdrawal' || tx.type === 'refund') {
running -= net;
expect(running).toBeGreaterThanOrEqual(0);
} else {
running += net;
}
}
expect(running).toBeCloseTo(wallet.balance.amount, 6);
}
// Membership edge cases:
// - league-2 is intentionally empty
expect(seed.leagueMemberships.some((m) => m.leagueId.toString() === 'league-2')).toBe(false);
// - league-5 is "busy" (full/overbooked mix of statuses)
const league5Memberships = seed.leagueMemberships.filter((m) => m.leagueId.toString() === 'league-5');
expect(league5Memberships.length).toBeGreaterThan(0);
expect(league5Memberships.some((m) => m.status.toString() === 'active')).toBe(true);
expect(league5Memberships.some((m) => m.status.toString() === 'pending')).toBe(true);
// Stewarding referential integrity
for (const protest of seed.protests) {
expect(raceById.has(protest.raceId)).toBe(true);
expect(driverIds.has(protest.protestingDriverId)).toBe(true);
expect(driverIds.has(protest.accusedDriverId)).toBe(true);
const race = raceById.get(protest.raceId);
if (race) {
const leagueId = race.leagueId.toString();
const hasMembership = (driverId: string) =>
seed.leagueMemberships.some(
(m) => m.leagueId.toString() === leagueId && m.driverId.toString() === driverId,
);
expect(hasMembership(protest.protestingDriverId)).toBe(true);
expect(hasMembership(protest.accusedDriverId)).toBe(true);
}
}
for (const penalty of seed.penalties) {
expect(raceById.has(penalty.raceId)).toBe(true);
expect(driverIds.has(penalty.driverId)).toBe(true);
const race = raceById.get(penalty.raceId);
if (race) {
expect(penalty.leagueId).toBe(race.leagueId.toString());
}
if (penalty.protestId !== undefined) {
expect(protestById.has(penalty.protestId)).toBe(true);
const protest = protestById.get(penalty.protestId);
if (protest) {
expect(penalty.raceId).toBe(protest.raceId);
}
}
}
// Stewarding edge cases: demo league has multiple protest statuses
const protestStatusesLeague5 = new Set(
seed.protests
.filter((p) =>
seed.races.some((r) => r.id.toString() === p.raceId && r.leagueId.toString() === 'league-5'),
)
.map((p) => p.status.toString()),
);
expect(protestStatusesLeague5.has('pending')).toBe(true);
expect(protestStatusesLeague5.has('under_review') || protestStatusesLeague5.has('upheld')).toBe(true);
// Penalties should include at least one linked to a protest
expect(seed.penalties.some((p) => (p as unknown as { protestId?: string }).protestId)).toBe(true);
// Basic ID sanity (no duplicates per collection)
const assertNoDuplicateIds = (ids: string[]) => {
const unique = new Set(ids);
expect(unique.size).toBe(ids.length);
};
assertNoDuplicateIds(seed.drivers.map((d) => d.id.toString()));
assertNoDuplicateIds(seed.leagues.map((l) => l.id.toString()));
assertNoDuplicateIds(seed.teams.map((t) => t.id.toString()));
assertNoDuplicateIds(seed.sponsors.map((s) => s.id.toString()));
assertNoDuplicateIds(seed.seasons.map((s) => s.id));
assertNoDuplicateIds(seed.seasonSponsorships.map((s) => s.id));
assertNoDuplicateIds(seed.sponsorshipRequests.map((r) => r.id));
assertNoDuplicateIds(seed.protests.map((p) => p.id));
assertNoDuplicateIds(seed.penalties.map((p) => p.id));
assertNoDuplicateIds(seed.leagueWallets.map((w) => w.id.toString()));
assertNoDuplicateIds(seed.leagueWalletTransactions.map((t) => t.id.toString()));
});
});

View File

@@ -1,11 +1,12 @@
import { describe, expect, it, vi } from 'vitest';
import { Result } from '@core/shared/application/Result';
import { DriverService } from './DriverService';
describe('DriverService', () => {
const logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() };
it('getDriversLeaderboard executes use case and returns presenter model', async () => {
const getDriversLeaderboardUseCase = { execute: vi.fn(async () => {}) };
const getDriversLeaderboardUseCase = { execute: vi.fn(async () => Result.ok(undefined)) };
const driversLeaderboardPresenter = { getResponseModel: vi.fn(() => ({ items: [] })) };
const service = new DriverService(
@@ -31,7 +32,7 @@ describe('DriverService', () => {
});
it('getTotalDrivers executes use case and returns presenter model', async () => {
const getTotalDriversUseCase = { execute: vi.fn(async () => {}) };
const getTotalDriversUseCase = { execute: vi.fn(async () => Result.ok(undefined)) };
const driverStatsPresenter = { getResponseModel: vi.fn(() => ({ totalDrivers: 123 })) };
const service = new DriverService(
@@ -57,7 +58,7 @@ describe('DriverService', () => {
});
it('completeOnboarding passes optional bio only when provided', async () => {
const completeDriverOnboardingUseCase = { execute: vi.fn(async () => {}) };
const completeDriverOnboardingUseCase = { execute: vi.fn(async () => Result.ok(undefined)) };
const service = new DriverService(
{ execute: vi.fn() } as any,
@@ -112,7 +113,7 @@ describe('DriverService', () => {
});
it('getDriverRegistrationStatus passes raceId and driverId and returns presenter model', async () => {
const isDriverRegisteredForRaceUseCase = { execute: vi.fn(async () => {}) };
const isDriverRegisteredForRaceUseCase = { execute: vi.fn(async () => Result.ok(undefined)) };
const driverRegistrationStatusPresenter = { getResponseModel: vi.fn(() => ({ isRegistered: true })) };
const service = new DriverService(
@@ -142,7 +143,7 @@ describe('DriverService', () => {
it('getCurrentDriver calls repository and returns presenter model', async () => {
const driverRepository = { findById: vi.fn(async () => null) };
const driverPresenter = { getResponseModel: vi.fn(() => null) };
const driverPresenter = { present: vi.fn(), getResponseModel: vi.fn(() => null) };
const service = new DriverService(
{ execute: vi.fn() } as any,
@@ -168,7 +169,7 @@ describe('DriverService', () => {
it('updateDriverProfile builds optional input and returns presenter model', async () => {
const updateDriverProfileUseCase = { execute: vi.fn(async () => {}) };
const driverPresenter = { getResponseModel: vi.fn(() => ({ driver: { id: 'd1' } })) };
const driverPresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ driver: { id: 'd1' } })) };
const service = new DriverService(
{ execute: vi.fn() } as any,
@@ -210,7 +211,7 @@ describe('DriverService', () => {
it('getDriver calls repository and returns presenter model', async () => {
const driverRepository = { findById: vi.fn(async () => null) };
const driverPresenter = { getResponseModel: vi.fn(() => ({ driver: null })) };
const driverPresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ driver: null })) };
const service = new DriverService(
{ execute: vi.fn() } as any,
@@ -235,7 +236,7 @@ describe('DriverService', () => {
});
it('getDriverProfile executes use case and returns presenter model', async () => {
const getProfileOverviewUseCase = { execute: vi.fn(async () => {}) };
const getProfileOverviewUseCase = { execute: vi.fn(async () => Result.ok(undefined)) };
const driverProfilePresenter = { getResponseModel: vi.fn(() => ({ profile: { id: 'd1' } })) };
const service = new DriverService(

View File

@@ -5,11 +5,13 @@
* Aggregate root for sponsor information.
*/
import type { IEntity } from '@core/shared/domain';
import { SponsorCreatedAt } from './SponsorCreatedAt';
import { SponsorEmail } from './SponsorEmail';
import { SponsorId } from './SponsorId';
import { SponsorName } from './SponsorName';
import { SponsorEmail } from './SponsorEmail';
import { Url } from './Url';
import { SponsorCreatedAt } from './SponsorCreatedAt';
// TODO sponsor is not actually the racing domain in my opinion
export class Sponsor implements IEntity<SponsorId> {
readonly id: SponsorId;