more seeds
This commit is contained in:
@@ -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);
|
||||
|
||||
197
adapters/bootstrap/racing/RacingLeagueWalletFactory.ts
Normal file
197
adapters/bootstrap/racing/RacingLeagueWalletFactory.ts
Normal 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';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
}
|
||||
248
adapters/bootstrap/racing/RacingSeasonSponsorshipFactory.ts
Normal file
248
adapters/bootstrap/racing/RacingSeasonSponsorshipFactory.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
125
adapters/bootstrap/racing/RacingSponsorFactory.ts
Normal file
125
adapters/bootstrap/racing/RacingSponsorFactory.ts
Normal 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];
|
||||
}
|
||||
}
|
||||
260
adapters/bootstrap/racing/RacingStewardingFactory.ts
Normal file
260
adapters/bootstrap/racing/RacingStewardingFactory.ts
Normal 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 };
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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',
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
],
|
||||
|
||||
224
apps/api/src/domain/bootstrap/RacingSeed.test.ts
Normal file
224
apps/api/src/domain/bootstrap/RacingSeed.test.ts
Normal 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()));
|
||||
});
|
||||
});
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user