tests cleanup
This commit is contained in:
@@ -359,6 +359,66 @@
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"no-restricted-syntax": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["tests/**/*.ts"],
|
||||
"rules": {
|
||||
"no-restricted-imports": ["error", {
|
||||
"paths": [
|
||||
{
|
||||
"name": "@core/*",
|
||||
"message": "Integration tests must use in-memory adapters, not core directly"
|
||||
},
|
||||
{
|
||||
"name": "@adapters/*",
|
||||
"message": "Integration tests must use in-memory adapters only"
|
||||
}
|
||||
]
|
||||
}]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["tests/e2e/**/*.ts"],
|
||||
"rules": {
|
||||
"no-restricted-imports": ["error", {
|
||||
"patterns": [
|
||||
{
|
||||
"group": ["**/inmemory/**"],
|
||||
"message": "E2E tests must use TypeORM/PostgreSQL, not in-memory adapters"
|
||||
}
|
||||
]
|
||||
}]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["core/**/*.ts"],
|
||||
"rules": {
|
||||
"no-restricted-imports": ["error", {
|
||||
"paths": [
|
||||
{
|
||||
"name": "testing",
|
||||
"message": "Use @testing/* from adapters/testing"
|
||||
},
|
||||
{
|
||||
"name": "@testing/*",
|
||||
"message": "Core layer should not depend on testing utilities"
|
||||
}
|
||||
]
|
||||
}]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["adapters/**/*.ts"],
|
||||
"rules": {
|
||||
"no-restricted-imports": ["error", {
|
||||
"paths": [
|
||||
{
|
||||
"name": "testing",
|
||||
"message": "Use @testing/* from adapters/testing"
|
||||
}
|
||||
]
|
||||
}]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -28,7 +28,7 @@
|
||||
"../../core/*"
|
||||
],
|
||||
"@testing/*": [
|
||||
"../../testing/*"
|
||||
"../../adapters/testing/*"
|
||||
]
|
||||
},
|
||||
"removeComments": true,
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
"./*"
|
||||
],
|
||||
"@testing/*": [
|
||||
"../../testing/*"
|
||||
"../../adapters/testing/*"
|
||||
],
|
||||
"@/lib/dtos": [
|
||||
"./lib/dtos"
|
||||
|
||||
@@ -10,11 +10,28 @@ import {
|
||||
SeasonDropPolicy,
|
||||
} from '@core/racing/domain/value-objects/SeasonDropPolicy';
|
||||
import { SeasonStewardingConfig } from '@core/racing/domain/value-objects/SeasonStewardingConfig';
|
||||
import {
|
||||
createMinimalSeason,
|
||||
createBaseSeason,
|
||||
} from '@core/testing/factories/racing/SeasonFactory';
|
||||
import { Season, SeasonStatus } from '@core/racing/domain/entities/season/Season';
|
||||
|
||||
const createMinimalSeason = (overrides?: { status?: SeasonStatus }) =>
|
||||
Season.create({
|
||||
id: 'season-1',
|
||||
leagueId: 'league-1',
|
||||
gameId: 'iracing',
|
||||
name: 'Test Season',
|
||||
status: overrides?.status ?? 'planned',
|
||||
});
|
||||
|
||||
const createBaseSeason = () =>
|
||||
Season.create({
|
||||
id: 'season-1',
|
||||
leagueId: 'league-1',
|
||||
gameId: 'iracing',
|
||||
name: 'Config Season',
|
||||
status: 'planned',
|
||||
startDate: new Date('2025-01-01T00:00:00Z'),
|
||||
endDate: undefined,
|
||||
maxDrivers: 24,
|
||||
});
|
||||
|
||||
describe('Season aggregate lifecycle', () => {
|
||||
it('transitions Planned → Active → Completed → Archived with timestamps', () => {
|
||||
@@ -205,5 +222,3 @@ describe('Season configuration updates', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -4,8 +4,63 @@ import { EventScoringService } from '@core/racing/domain/services/EventScoringSe
|
||||
import type { BonusRule } from '@core/racing/domain/types/BonusRule';
|
||||
import { Result } from '@core/racing/domain/entities/result/Result';
|
||||
import type { Penalty } from '@core/racing/domain/entities/Penalty';
|
||||
import { makeChampionshipConfig } from '../../../testing/factories/racing/ChampionshipConfigFactory';
|
||||
import type { ChampionshipConfig } from '@core/racing/domain/types/ChampionshipConfig';
|
||||
import type { SessionType } from '@core/racing/domain/types/SessionType';
|
||||
import { PointsTable } from '@core/racing/domain/value-objects/PointsTable';
|
||||
|
||||
const makeChampionshipConfig = (params: {
|
||||
id: string;
|
||||
name: string;
|
||||
sessionTypes: SessionType[];
|
||||
mainPoints: number[];
|
||||
sprintPoints?: number[];
|
||||
mainBonusRules?: BonusRule[];
|
||||
}): ChampionshipConfig => {
|
||||
const { id, name, sessionTypes, mainPoints, sprintPoints, mainBonusRules } = params;
|
||||
|
||||
const pointsTableBySessionType: Record<SessionType, PointsTable> = {} as Record<SessionType, PointsTable>;
|
||||
|
||||
sessionTypes.forEach((sessionType) => {
|
||||
if (sessionType === 'main') {
|
||||
pointsTableBySessionType[sessionType] = new PointsTable(
|
||||
mainPoints.reduce((acc, points, index) => {
|
||||
acc[index + 1] = points;
|
||||
return acc;
|
||||
}, {} as Record<number, number>),
|
||||
);
|
||||
} else if (sessionType === 'sprint' && sprintPoints) {
|
||||
pointsTableBySessionType[sessionType] = new PointsTable(
|
||||
sprintPoints.reduce((acc, points, index) => {
|
||||
acc[index + 1] = points;
|
||||
return acc;
|
||||
}, {} as Record<number, number>),
|
||||
);
|
||||
} else {
|
||||
pointsTableBySessionType[sessionType] = new PointsTable({});
|
||||
}
|
||||
});
|
||||
|
||||
const bonusRulesBySessionType: Record<SessionType, BonusRule[]> = {} as Record<SessionType, BonusRule[]>;
|
||||
sessionTypes.forEach((sessionType) => {
|
||||
if (sessionType === 'main' && mainBonusRules) {
|
||||
bonusRulesBySessionType[sessionType] = mainBonusRules;
|
||||
} else {
|
||||
bonusRulesBySessionType[sessionType] = [];
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
type: 'driver',
|
||||
sessionTypes,
|
||||
pointsTableBySessionType,
|
||||
bonusRulesBySessionType,
|
||||
dropScorePolicy: {
|
||||
strategy: 'none',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
describe('EventScoringService', () => {
|
||||
const seasonId = 'season-1';
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import type { ChampionshipConfig } from '../../../racing/domain/types/ChampionshipConfig';
|
||||
import type { ChampionshipType } from '../../../racing/domain/types/ChampionshipType';
|
||||
import type { SessionType } from '../../../racing/domain/types/SessionType';
|
||||
import type { BonusRule } from '../../../racing/domain/types/BonusRule';
|
||||
import type { DropScorePolicy, DropScoreStrategy } from '../../../racing/domain/types/DropScorePolicy';
|
||||
import { PointsTable } from '../../../racing/domain/value-objects/PointsTable';
|
||||
|
||||
interface ChampionshipConfigInput {
|
||||
id: string;
|
||||
name: string;
|
||||
sessionTypes: SessionType[];
|
||||
mainPoints?: number[];
|
||||
mainBonusRules?: BonusRule[];
|
||||
type?: ChampionshipType;
|
||||
dropScorePolicy?: DropScorePolicy;
|
||||
strategy?: DropScoreStrategy;
|
||||
}
|
||||
|
||||
export function makeChampionshipConfig(input: ChampionshipConfigInput): ChampionshipConfig {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
sessionTypes,
|
||||
mainPoints = [25, 18, 15, 12, 10, 8, 6, 4, 2, 1],
|
||||
mainBonusRules = [],
|
||||
type = 'driver',
|
||||
dropScorePolicy = { strategy: 'none' },
|
||||
} = input;
|
||||
|
||||
const pointsTableBySessionType: Record<SessionType, PointsTable> = {} as Record<SessionType, PointsTable>;
|
||||
|
||||
// Convert array format to PointsTable for each session type
|
||||
sessionTypes.forEach(sessionType => {
|
||||
const pointsArray = mainPoints;
|
||||
const pointsMap: Record<number, number> = {};
|
||||
pointsArray.forEach((points, index) => {
|
||||
pointsMap[index + 1] = points;
|
||||
});
|
||||
pointsTableBySessionType[sessionType] = new PointsTable(pointsMap);
|
||||
});
|
||||
|
||||
const bonusRulesBySessionType: Record<SessionType, BonusRule[]> = {} as Record<SessionType, BonusRule[]>;
|
||||
|
||||
// Add bonus rules for each session type
|
||||
sessionTypes.forEach(sessionType => {
|
||||
bonusRulesBySessionType[sessionType] = mainBonusRules;
|
||||
});
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
type,
|
||||
sessionTypes,
|
||||
pointsTableBySessionType,
|
||||
bonusRulesBySessionType,
|
||||
dropScorePolicy,
|
||||
};
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import type { ParticipantRef } from '../../../racing/domain/types/ParticipantRef';
|
||||
|
||||
export function makeDriverRef(driverId: string): ParticipantRef {
|
||||
return {
|
||||
id: driverId,
|
||||
type: 'driver',
|
||||
};
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { PointsTable } from '../../../racing/domain/value-objects/PointsTable';
|
||||
|
||||
export function makePointsTable(points: number[]): PointsTable {
|
||||
return new PointsTable(points);
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Season } from '@core/racing/domain/entities/season/Season';
|
||||
import type { SeasonStatusValue } from '@core/racing/domain/value-objects/SeasonStatus';
|
||||
|
||||
export const createMinimalSeason = (overrides?: { status?: SeasonStatusValue }) =>
|
||||
Season.create({
|
||||
id: 'season-1',
|
||||
leagueId: 'league-1',
|
||||
gameId: 'iracing',
|
||||
name: 'Test Season',
|
||||
status: overrides?.status ?? 'planned',
|
||||
});
|
||||
|
||||
export const createBaseSeason = () =>
|
||||
Season.create({
|
||||
id: 'season-1',
|
||||
leagueId: 'league-1',
|
||||
gameId: 'iracing',
|
||||
name: 'Config Season',
|
||||
status: 'planned',
|
||||
startDate: new Date('2025-01-01T00:00:00Z'),
|
||||
endDate: undefined,
|
||||
maxDrivers: 24,
|
||||
});
|
||||
1158
docs/TESTS.md
1158
docs/TESTS.md
File diff suppressed because it is too large
Load Diff
@@ -1,49 +0,0 @@
|
||||
import type { SessionType } from '@core/racing/domain/types/SessionType';
|
||||
import { PointsTable } from '@core/racing/domain/value-objects/PointsTable';
|
||||
import type { BonusRule } from '@core/racing/domain/types/BonusRule';
|
||||
import type { ChampionshipConfig } from '@core/racing/domain/types/ChampionshipConfig';
|
||||
import { makePointsTable } from './PointsTableFactory';
|
||||
|
||||
export const makeChampionshipConfig = (params: {
|
||||
id: string;
|
||||
name: string;
|
||||
sessionTypes: SessionType[];
|
||||
mainPoints: number[];
|
||||
sprintPoints?: number[];
|
||||
mainBonusRules?: BonusRule[];
|
||||
}): ChampionshipConfig => {
|
||||
const { id, name, sessionTypes, mainPoints, sprintPoints, mainBonusRules } = params;
|
||||
|
||||
const pointsTableBySessionType: Record<SessionType, PointsTable> = {} as Record<SessionType, PointsTable>;
|
||||
|
||||
sessionTypes.forEach((sessionType) => {
|
||||
if (sessionType === 'main') {
|
||||
pointsTableBySessionType[sessionType] = makePointsTable(mainPoints);
|
||||
} else if (sessionType === 'sprint' && sprintPoints) {
|
||||
pointsTableBySessionType[sessionType] = makePointsTable(sprintPoints);
|
||||
} else {
|
||||
pointsTableBySessionType[sessionType] = new PointsTable({});
|
||||
}
|
||||
});
|
||||
|
||||
const bonusRulesBySessionType: Record<SessionType, BonusRule[]> = {} as Record<SessionType, BonusRule[]>;
|
||||
sessionTypes.forEach((sessionType) => {
|
||||
if (sessionType === 'main' && mainBonusRules) {
|
||||
bonusRulesBySessionType[sessionType] = mainBonusRules;
|
||||
} else {
|
||||
bonusRulesBySessionType[sessionType] = [];
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
type: 'driver',
|
||||
sessionTypes,
|
||||
pointsTableBySessionType,
|
||||
bonusRulesBySessionType,
|
||||
dropScorePolicy: {
|
||||
strategy: 'none',
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
import type { ParticipantRef } from '@core/racing/domain/types/ParticipantRef';
|
||||
import type { ChampionshipType } from '@core/racing/domain/types/ChampionshipType';
|
||||
import { MediaReference } from '../../../core/domain/media/MediaReference';
|
||||
|
||||
export const makeDriverRef = (id: string): ParticipantRef => ({
|
||||
type: 'driver' as ChampionshipType,
|
||||
id,
|
||||
avatarRef: MediaReference.systemDefault('avatar'),
|
||||
});
|
||||
@@ -1,9 +0,0 @@
|
||||
import { PointsTable } from '@core/racing/domain/value-objects/PointsTable';
|
||||
|
||||
export const makePointsTable = (points: number[]): PointsTable => {
|
||||
const pointsByPosition: Record<number, number> = {};
|
||||
points.forEach((value, index) => {
|
||||
pointsByPosition[index + 1] = value;
|
||||
});
|
||||
return new PointsTable(pointsByPosition);
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Season } from '@core/racing/domain/entities/season/Season';
|
||||
import type { SeasonStatus } from '@core/racing/domain/entities/season/Season';
|
||||
|
||||
export const createMinimalSeason = (overrides?: { status?: SeasonStatus }) =>
|
||||
Season.create({
|
||||
id: 'season-1',
|
||||
leagueId: 'league-1',
|
||||
gameId: 'iracing',
|
||||
name: 'Test Season',
|
||||
status: overrides?.status ?? 'planned',
|
||||
});
|
||||
|
||||
export const createBaseSeason = () =>
|
||||
Season.create({
|
||||
id: 'season-1',
|
||||
leagueId: 'league-1',
|
||||
gameId: 'iracing',
|
||||
name: 'Config Season',
|
||||
status: 'planned',
|
||||
startDate: new Date('2025-01-01T00:00:00Z'),
|
||||
endDate: undefined,
|
||||
maxDrivers: 24,
|
||||
});
|
||||
@@ -1,50 +0,0 @@
|
||||
import { randomUUID } from 'crypto';
|
||||
import type {
|
||||
AuthCallbackCommand,
|
||||
AuthenticatedUser,
|
||||
IdentityProviderPort,
|
||||
StartAuthCommand,
|
||||
} from '@core/identity/application/ports/IdentityProviderPort';
|
||||
|
||||
export class IracingDemoIdentityProviderAdapter implements IdentityProviderPort {
|
||||
private readonly seedDriverId: string;
|
||||
|
||||
constructor() {
|
||||
this.seedDriverId = 'driver-1';
|
||||
}
|
||||
|
||||
async startAuth(command: StartAuthCommand): Promise<{ redirectUrl: string; state: string }> {
|
||||
const state = randomUUID();
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.set('code', 'dummy-code');
|
||||
params.set('state', state);
|
||||
if (command.returnTo) {
|
||||
params.set('returnTo', command.returnTo);
|
||||
}
|
||||
|
||||
return {
|
||||
redirectUrl: `/auth/iracing/callback?${params.toString()}`,
|
||||
state,
|
||||
};
|
||||
}
|
||||
|
||||
async completeAuth(command: AuthCallbackCommand): Promise<AuthenticatedUser> {
|
||||
if (!command.code) {
|
||||
throw new Error('Missing auth code');
|
||||
}
|
||||
if (!command.state) {
|
||||
throw new Error('Missing auth state');
|
||||
}
|
||||
|
||||
const user: AuthenticatedUser = {
|
||||
id: 'demo-user',
|
||||
displayName: 'GridPilot Demo Driver',
|
||||
iracingCustomerId: '000000',
|
||||
primaryDriverId: this.seedDriverId,
|
||||
avatarUrl: `/api/avatar/${this.seedDriverId}`,
|
||||
};
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
import { Car } from '@core/racing/domain/entities/Car';
|
||||
|
||||
/**
|
||||
* Demo car data for iRacing.
|
||||
* Extracted from the legacy DemoData module so that cars
|
||||
* live in their own focused file.
|
||||
*/
|
||||
export const DEMO_CARS: Car[] = [
|
||||
Car.create({
|
||||
id: 'car-porsche-992',
|
||||
name: '911 GT3 R',
|
||||
shortName: '992 GT3R',
|
||||
manufacturer: 'Porsche',
|
||||
carClass: 'gt',
|
||||
license: 'B',
|
||||
year: 2023,
|
||||
horsepower: 565,
|
||||
weight: 1300,
|
||||
gameId: 'iracing',
|
||||
}),
|
||||
Car.create({
|
||||
id: 'car-ferrari-296',
|
||||
name: '296 GT3',
|
||||
shortName: '296 GT3',
|
||||
manufacturer: 'Ferrari',
|
||||
carClass: 'gt',
|
||||
license: 'B',
|
||||
year: 2023,
|
||||
horsepower: 600,
|
||||
weight: 1270,
|
||||
gameId: 'iracing',
|
||||
}),
|
||||
Car.create({
|
||||
id: 'car-mclaren-720s',
|
||||
name: '720S GT3 Evo',
|
||||
shortName: '720S',
|
||||
manufacturer: 'McLaren',
|
||||
carClass: 'gt',
|
||||
license: 'B',
|
||||
year: 2023,
|
||||
horsepower: 552,
|
||||
weight: 1290,
|
||||
gameId: 'iracing',
|
||||
}),
|
||||
Car.create({
|
||||
id: 'car-mercedes-gt3',
|
||||
name: 'AMG GT3 2020',
|
||||
shortName: 'AMG GT3',
|
||||
manufacturer: 'Mercedes',
|
||||
carClass: 'gt',
|
||||
license: 'B',
|
||||
year: 2020,
|
||||
horsepower: 550,
|
||||
weight: 1285,
|
||||
gameId: 'iracing',
|
||||
}),
|
||||
Car.create({
|
||||
id: 'car-lmp2',
|
||||
name: 'Dallara P217 LMP2',
|
||||
shortName: 'LMP2',
|
||||
manufacturer: 'Dallara',
|
||||
carClass: 'prototype',
|
||||
license: 'A',
|
||||
year: 2021,
|
||||
horsepower: 600,
|
||||
weight: 930,
|
||||
gameId: 'iracing',
|
||||
}),
|
||||
Car.create({
|
||||
id: 'car-f4',
|
||||
name: 'Formula 4',
|
||||
shortName: 'F4',
|
||||
manufacturer: 'Tatuus',
|
||||
carClass: 'formula',
|
||||
license: 'D',
|
||||
year: 2022,
|
||||
horsepower: 160,
|
||||
weight: 570,
|
||||
gameId: 'iracing',
|
||||
}),
|
||||
Car.create({
|
||||
id: 'car-mx5',
|
||||
name: 'MX-5 Cup',
|
||||
shortName: 'MX5',
|
||||
manufacturer: 'Mazda',
|
||||
carClass: 'sports',
|
||||
license: 'D',
|
||||
year: 2023,
|
||||
horsepower: 181,
|
||||
weight: 1128,
|
||||
gameId: 'iracing',
|
||||
}),
|
||||
];
|
||||
@@ -1,75 +0,0 @@
|
||||
/**
|
||||
* Driver statistics and ranking data used for demo seeding.
|
||||
* Split out from the legacy DemoData module to keep responsibilities focused.
|
||||
*/
|
||||
export interface DriverStats {
|
||||
driverId: string;
|
||||
rating: number;
|
||||
totalRaces: number;
|
||||
wins: number;
|
||||
podiums: number;
|
||||
dnfs: number;
|
||||
avgFinish: number;
|
||||
bestFinish: number;
|
||||
worstFinish: number;
|
||||
consistency: number;
|
||||
overallRank: number;
|
||||
percentile: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create demo driver statistics based on seed data.
|
||||
* This is deterministic for a given driver ordering so it can be reused
|
||||
* by any in-memory repository wiring.
|
||||
*/
|
||||
export function createDemoDriverStats(drivers: Array<{ id: string }>): Record<string, DriverStats> {
|
||||
const stats: Record<string, DriverStats> = {};
|
||||
|
||||
drivers.forEach((driver, index) => {
|
||||
const totalRaces = 40 + index * 5;
|
||||
const wins = Math.max(0, Math.floor(totalRaces * 0.2) - index);
|
||||
const podiums = Math.max(wins * 2, 0);
|
||||
const dnfs = Math.max(0, Math.floor(index / 2));
|
||||
const rating = 1500 + index * 25;
|
||||
|
||||
stats[driver.id] = {
|
||||
driverId: driver.id,
|
||||
rating,
|
||||
totalRaces,
|
||||
wins,
|
||||
podiums,
|
||||
dnfs,
|
||||
avgFinish: 4,
|
||||
bestFinish: 1,
|
||||
worstFinish: 20,
|
||||
consistency: 80,
|
||||
overallRank: index + 1,
|
||||
percentile: Math.max(0, 100 - index),
|
||||
};
|
||||
});
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get league-specific rankings for a driver (demo implementation).
|
||||
* In production this would be calculated from actual league membership
|
||||
* and results; here we keep a very small static example for UI wiring.
|
||||
*/
|
||||
export function getDemoLeagueRankings(driverId: string, leagueId: string): {
|
||||
rank: number;
|
||||
totalDrivers: number;
|
||||
percentile: number;
|
||||
} {
|
||||
// Mock league rankings (in production, calculate from actual league membership)
|
||||
const mockLeagueRanks: Record<string, Record<string, any>> = {
|
||||
'league-1': {
|
||||
'driver-1': { rank: 1, totalDrivers: 12, percentile: 92 },
|
||||
'driver-2': { rank: 2, totalDrivers: 12, percentile: 84 },
|
||||
'driver-3': { rank: 4, totalDrivers: 12, percentile: 67 },
|
||||
'driver-4': { rank: 5, totalDrivers: 12, percentile: 58 },
|
||||
},
|
||||
};
|
||||
|
||||
return mockLeagueRanks[leagueId]?.[driverId] || { rank: 0, totalDrivers: 0, percentile: 0 };
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
import { Track } from '@core/racing/domain/entities/Track';
|
||||
|
||||
/**
|
||||
* Demo track data for iRacing.
|
||||
* Extracted from the legacy DemoData module so that tracks
|
||||
* live in their own focused file.
|
||||
*/
|
||||
export const DEMO_TRACKS: Track[] = [
|
||||
Track.create({
|
||||
id: 'track-spa',
|
||||
name: 'Spa-Francorchamps',
|
||||
shortName: 'SPA',
|
||||
country: 'Belgium',
|
||||
category: 'road',
|
||||
difficulty: 'advanced',
|
||||
lengthKm: 7.004,
|
||||
turns: 19,
|
||||
imageUrl: '/images/tracks/spa.jpg',
|
||||
gameId: 'iracing',
|
||||
}),
|
||||
Track.create({
|
||||
id: 'track-monza',
|
||||
name: 'Autodromo Nazionale Monza',
|
||||
shortName: 'MON',
|
||||
country: 'Italy',
|
||||
category: 'road',
|
||||
difficulty: 'intermediate',
|
||||
lengthKm: 5.793,
|
||||
turns: 11,
|
||||
imageUrl: '/images/tracks/monza.jpg',
|
||||
gameId: 'iracing',
|
||||
}),
|
||||
Track.create({
|
||||
id: 'track-nurburgring',
|
||||
name: 'Nürburgring Grand Prix',
|
||||
shortName: 'NUR',
|
||||
country: 'Germany',
|
||||
category: 'road',
|
||||
difficulty: 'advanced',
|
||||
lengthKm: 5.148,
|
||||
turns: 15,
|
||||
imageUrl: '/images/tracks/nurburgring.jpg',
|
||||
gameId: 'iracing',
|
||||
}),
|
||||
Track.create({
|
||||
id: 'track-silverstone',
|
||||
name: 'Silverstone Circuit',
|
||||
shortName: 'SIL',
|
||||
country: 'United Kingdom',
|
||||
category: 'road',
|
||||
difficulty: 'intermediate',
|
||||
lengthKm: 5.891,
|
||||
turns: 18,
|
||||
imageUrl: '/images/tracks/silverstone.jpg',
|
||||
gameId: 'iracing',
|
||||
}),
|
||||
Track.create({
|
||||
id: 'track-suzuka',
|
||||
name: 'Suzuka International Racing Course',
|
||||
shortName: 'SUZ',
|
||||
country: 'Japan',
|
||||
category: 'road',
|
||||
difficulty: 'expert',
|
||||
lengthKm: 5.807,
|
||||
turns: 18,
|
||||
imageUrl: '/images/tracks/suzuka.jpg',
|
||||
gameId: 'iracing',
|
||||
}),
|
||||
Track.create({
|
||||
id: 'track-daytona',
|
||||
name: 'Daytona International Speedway',
|
||||
shortName: 'DAY',
|
||||
country: 'United States',
|
||||
category: 'oval',
|
||||
difficulty: 'intermediate',
|
||||
lengthKm: 4.023,
|
||||
turns: 4,
|
||||
imageUrl: '/images/tracks/daytona.jpg',
|
||||
gameId: 'iracing',
|
||||
}),
|
||||
Track.create({
|
||||
id: 'track-laguna',
|
||||
name: 'WeatherTech Raceway Laguna Seca',
|
||||
shortName: 'LAG',
|
||||
country: 'United States',
|
||||
category: 'road',
|
||||
difficulty: 'advanced',
|
||||
lengthKm: 3.602,
|
||||
turns: 11,
|
||||
imageUrl: '/images/tracks/laguna.jpg',
|
||||
gameId: 'iracing',
|
||||
}),
|
||||
];
|
||||
@@ -1,292 +0,0 @@
|
||||
import { Driver } from '@core/racing/domain/entities/Driver';
|
||||
import { League } from '@core/racing/domain/entities/League';
|
||||
import { Race } from '@core/racing/domain/entities/Race';
|
||||
import type { Result } from '@core/racing/domain/entities/Result';
|
||||
import type { FeedItem } from '@core/social/domain/types/FeedItem';
|
||||
import type { SocialFriendSummary } from '@core/social/application/types/SocialUser';
|
||||
import { faker } from '../../helpers/faker/faker';
|
||||
import { getLeagueBanner, getDriverAvatar } from '../../helpers/images/images';
|
||||
import type { Friendship, RacingMembership } from './RacingSeedCore';
|
||||
|
||||
/**
|
||||
* Feed events and derived racing demo data.
|
||||
* Extracted from the legacy StaticRacingSeed module to keep files smaller.
|
||||
*/
|
||||
export function createFeedEvents(
|
||||
drivers: Driver[],
|
||||
leagues: League[],
|
||||
races: Race[],
|
||||
friendships: Friendship[],
|
||||
): FeedItem[] {
|
||||
const events: FeedItem[] = [];
|
||||
const now = new Date();
|
||||
const completedRaces = races.filter((race) => race.status === 'completed');
|
||||
|
||||
// Focus the global feed around a stable “core” of demo drivers
|
||||
const coreDrivers = faker.helpers.shuffle(drivers).slice(0, Math.min(16, drivers.length));
|
||||
|
||||
coreDrivers.forEach((driver, index) => {
|
||||
const league = pickOne(leagues);
|
||||
const raceSource = completedRaces.length > 0 ? completedRaces : races;
|
||||
const race = pickOne(raceSource);
|
||||
const minutesAgo = 10 + index * 5;
|
||||
const baseTimestamp = new Date(now.getTime() - minutesAgo * 60 * 1000);
|
||||
|
||||
const actorFriendId = driver.id;
|
||||
|
||||
// Joined league
|
||||
events.push({
|
||||
id: `friend-joined-league:${driver.id}:${minutesAgo}`,
|
||||
type: 'friend-joined-league',
|
||||
timestamp: baseTimestamp,
|
||||
actorDriverId: driver.id,
|
||||
actorFriendId,
|
||||
leagueId: league.id,
|
||||
headline: `${driver.name} joined ${league.name}`,
|
||||
body: 'They are now registered for the full season.',
|
||||
ctaLabel: 'View league',
|
||||
ctaHref: `/leagues/${league.id}`,
|
||||
});
|
||||
|
||||
// Finished race / podium highlight
|
||||
const finishingPosition = (index % 5) + 1;
|
||||
events.push({
|
||||
id: `friend-finished-race:${driver.id}:${minutesAgo}`,
|
||||
type: 'friend-finished-race',
|
||||
timestamp: new Date(baseTimestamp.getTime() - 8 * 60 * 1000),
|
||||
actorDriverId: driver.id,
|
||||
actorFriendId,
|
||||
leagueId: race.leagueId,
|
||||
raceId: race.id,
|
||||
position: finishingPosition,
|
||||
headline: `${driver.name} finished P${finishingPosition} at ${race.track}`,
|
||||
body:
|
||||
finishingPosition <= 3
|
||||
? `${driver.name} scored a podium in ${race.car}.`
|
||||
: `${driver.name} secured a strong result in ${race.car}.`,
|
||||
ctaLabel: 'View results',
|
||||
ctaHref: `/races/${race.id}/results`,
|
||||
});
|
||||
|
||||
// New personal best
|
||||
events.push({
|
||||
id: `friend-new-personal-best:${driver.id}:${minutesAgo}`,
|
||||
type: 'friend-new-personal-best',
|
||||
timestamp: new Date(baseTimestamp.getTime() - 20 * 60 * 1000),
|
||||
actorDriverId: driver.id,
|
||||
actorFriendId,
|
||||
leagueId: race.leagueId,
|
||||
raceId: race.id,
|
||||
headline: `${driver.name} set a new personal best at ${race.track}`,
|
||||
body: 'Consistency and pace are trending up this season.',
|
||||
ctaLabel: 'View lap chart',
|
||||
ctaHref: `/races/${race.id}/analysis`,
|
||||
});
|
||||
|
||||
// Joined team (where applicable)
|
||||
const driverFriendships = friendships.filter((f) => f.driverId === driver.id);
|
||||
if (driverFriendships.length > 0) {
|
||||
const friend = pickOne(driverFriendships);
|
||||
const teammate = drivers.find((d) => d.id === friend.friendId);
|
||||
if (teammate) {
|
||||
events.push({
|
||||
id: `friend-joined-team:${driver.id}:${minutesAgo}`,
|
||||
type: 'friend-joined-team',
|
||||
timestamp: new Date(baseTimestamp.getTime() - 30 * 60 * 1000),
|
||||
actorDriverId: driver.id,
|
||||
actorFriendId,
|
||||
headline: `${driver.name} and ${teammate.name} are now teammates`,
|
||||
body: 'They will be sharing strategy and setups this season.',
|
||||
ctaLabel: 'View team',
|
||||
ctaHref: '/teams',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// League highlight
|
||||
events.push({
|
||||
id: `league-highlight:${league.id}:${minutesAgo}`,
|
||||
type: 'league-highlight',
|
||||
timestamp: new Date(baseTimestamp.getTime() - 45 * 60 * 1000),
|
||||
leagueId: league.id,
|
||||
headline: `${league.name} active with ${drivers.length}+ drivers`,
|
||||
body: 'Participation is growing. Perfect time to join the grid.',
|
||||
ctaLabel: 'Explore league',
|
||||
ctaHref: `/leagues/${league.id}`,
|
||||
});
|
||||
});
|
||||
|
||||
// Global “system” events: new race scheduled and results posted
|
||||
const upcomingRaces = races.filter((race) => race.status === 'scheduled').slice(0, 8);
|
||||
upcomingRaces.forEach((race, index) => {
|
||||
const minutesAgo = 60 + index * 15;
|
||||
const timestamp = new Date(now.getTime() - minutesAgo * 60 * 1000);
|
||||
events.push({
|
||||
id: `new-race-scheduled:${race.id}`,
|
||||
type: 'new-race-scheduled',
|
||||
timestamp,
|
||||
leagueId: race.leagueId,
|
||||
raceId: race.id,
|
||||
headline: `New race scheduled at ${race.track}`,
|
||||
body: `${race.car} • ${race.scheduledAt.toLocaleString()}`,
|
||||
ctaLabel: 'View schedule',
|
||||
ctaHref: `/races/${race.id}`,
|
||||
});
|
||||
});
|
||||
|
||||
const completedForResults = completedRaces.slice(0, 8);
|
||||
completedForResults.forEach((race, index) => {
|
||||
const minutesAgo = 180 + index * 20;
|
||||
const timestamp = new Date(now.getTime() - minutesAgo * 60 * 1000);
|
||||
events.push({
|
||||
id: `new-result-posted:${race.id}`,
|
||||
type: 'new-result-posted',
|
||||
timestamp,
|
||||
leagueId: race.leagueId,
|
||||
raceId: race.id,
|
||||
headline: `Results posted for ${race.track}`,
|
||||
body: 'Standings and stats updated across the grid.',
|
||||
ctaLabel: 'View classification',
|
||||
ctaHref: `/races/${race.id}/results`,
|
||||
});
|
||||
});
|
||||
|
||||
const sorted = events
|
||||
.slice()
|
||||
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
|
||||
|
||||
return sorted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derived friend DTOs for UI consumption.
|
||||
* This preserves the previous demo-data `friends` shape while
|
||||
* keeping generation logic separate from the main seed.
|
||||
*/
|
||||
export function buildFriends(
|
||||
drivers: Driver[],
|
||||
memberships: RacingMembership[],
|
||||
): SocialFriendSummary[] {
|
||||
return drivers.map((driver) => {
|
||||
const membership = memberships.find((m) => m.driverId === driver.id);
|
||||
|
||||
const base: SocialFriendSummary = {
|
||||
driverId: driver.id,
|
||||
displayName: driver.name,
|
||||
avatarUrl: getDriverAvatar(driver.id),
|
||||
isOnline: true,
|
||||
lastSeen: new Date(),
|
||||
};
|
||||
|
||||
const withLeague =
|
||||
membership?.leagueId !== undefined
|
||||
? { ...base, primaryLeagueId: membership.leagueId }
|
||||
: base;
|
||||
|
||||
const withTeam =
|
||||
membership?.teamId !== undefined
|
||||
? { ...withLeague, primaryTeamId: membership.teamId }
|
||||
: withLeague;
|
||||
|
||||
return withTeam;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Build top leagues with banner URLs for UI.
|
||||
*/
|
||||
export type LeagueWithBannerDTO = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
ownerId: string;
|
||||
settings: League['settings'];
|
||||
createdAt: Date;
|
||||
socialLinks: League['socialLinks'];
|
||||
bannerUrl: string;
|
||||
};
|
||||
|
||||
export function buildTopLeagues(leagues: League[]): LeagueWithBannerDTO[] {
|
||||
return leagues.map((league) => ({
|
||||
id: league.id,
|
||||
name: league.name,
|
||||
description: league.description,
|
||||
ownerId: league.ownerId,
|
||||
settings: league.settings,
|
||||
createdAt: league.createdAt,
|
||||
socialLinks: league.socialLinks,
|
||||
bannerUrl: getLeagueBanner(league.id),
|
||||
}));
|
||||
}
|
||||
|
||||
export type RaceWithResultsDTO = {
|
||||
raceId: string;
|
||||
track: string;
|
||||
car: string;
|
||||
scheduledAt: Date;
|
||||
winnerDriverId: string;
|
||||
winnerName: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility to get upcoming races from a given race list.
|
||||
*/
|
||||
export function buildUpcomingRaces(
|
||||
races: Race[],
|
||||
limit?: number,
|
||||
): readonly Race[] {
|
||||
const upcoming = races.filter((race) => race.status === 'scheduled');
|
||||
const sorted = upcoming
|
||||
.slice()
|
||||
.sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime());
|
||||
return typeof limit === 'number' ? sorted.slice(0, limit) : sorted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to get latest race results from races + results + drivers.
|
||||
*/
|
||||
export function buildLatestResults(
|
||||
races: Race[],
|
||||
results: Result[],
|
||||
drivers: Driver[],
|
||||
limit?: number,
|
||||
): readonly RaceWithResultsDTO[] {
|
||||
const completedRaces = races.filter((race) => race.status === 'completed');
|
||||
|
||||
const joined = completedRaces.map((race) => {
|
||||
const raceResults = results
|
||||
.filter((result) => result.raceId === race.id)
|
||||
.slice()
|
||||
.sort((a, b) => a.position - b.position);
|
||||
const winner = raceResults[0];
|
||||
const winnerDriver =
|
||||
winner && drivers.find((driver) => driver.id === winner.driverId);
|
||||
|
||||
return {
|
||||
raceId: race.id,
|
||||
track: race.track,
|
||||
car: race.car,
|
||||
scheduledAt: race.scheduledAt,
|
||||
winnerDriverId: winner?.driverId ?? '',
|
||||
winnerName: winnerDriver?.name ?? 'Winner',
|
||||
};
|
||||
});
|
||||
|
||||
const sorted = joined
|
||||
.slice()
|
||||
.sort((a, b) => b.scheduledAt.getTime() - a.scheduledAt.getTime());
|
||||
|
||||
return typeof limit === 'number' ? sorted.slice(0, limit) : sorted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Local helper: random pick from an array.
|
||||
* Kept here to avoid importing from core in callers that only care about feed.
|
||||
*/
|
||||
function pickOne<T>(items: readonly T[]): T {
|
||||
if (items.length === 0) {
|
||||
throw new Error('pickOne: empty items array');
|
||||
}
|
||||
const index = faker.number.int({ min: 0, max: items.length - 1 });
|
||||
return items[index]!;
|
||||
}
|
||||
@@ -1,411 +0,0 @@
|
||||
import { Driver } from '@core/racing/domain/entities/Driver';
|
||||
import { League } from '@core/racing/domain/entities/League';
|
||||
import { Race } from '@core/racing/domain/entities/Race';
|
||||
import { Result } from '@core/racing/domain/entities/Result';
|
||||
import { Standing } from '@core/racing/domain/entities/Standing';
|
||||
import { SessionType } from '@core/racing/domain/value-objects/SessionType';
|
||||
import { faker } from '../../helpers/faker/faker';
|
||||
|
||||
/**
|
||||
* Core racing seed types and generators (drivers, leagues, teams, races, standings).
|
||||
* Extracted from the legacy StaticRacingSeed module to keep files smaller and focused.
|
||||
*/
|
||||
export type RacingMembership = {
|
||||
driverId: string;
|
||||
leagueId: string;
|
||||
teamId?: string;
|
||||
};
|
||||
|
||||
export type Friendship = {
|
||||
driverId: string;
|
||||
friendId: string;
|
||||
};
|
||||
|
||||
import { MediaReference } from '@core/domain/media/MediaReference';
|
||||
|
||||
export interface DemoTeamDTO {
|
||||
id: string;
|
||||
name: string;
|
||||
tag: string;
|
||||
description: string;
|
||||
logoRef: MediaReference;
|
||||
primaryLeagueId: string;
|
||||
memberCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Championship points table used when aggregating standings.
|
||||
*/
|
||||
export const POINTS_TABLE: Record<number, number> = {
|
||||
1: 25,
|
||||
2: 18,
|
||||
3: 15,
|
||||
4: 12,
|
||||
5: 10,
|
||||
6: 8,
|
||||
7: 6,
|
||||
8: 4,
|
||||
9: 2,
|
||||
10: 1,
|
||||
};
|
||||
|
||||
export function pickOne<T>(items: readonly T[]): T {
|
||||
if (items.length === 0) {
|
||||
throw new Error('pickOne: empty items array');
|
||||
}
|
||||
const index = faker.number.int({ min: 0, max: items.length - 1 });
|
||||
return items[index]!;
|
||||
}
|
||||
|
||||
export function createDrivers(count: number): Driver[] {
|
||||
const drivers: Driver[] = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const id = `driver-${i + 1}`;
|
||||
const name = faker.person.fullName();
|
||||
const country = faker.location.countryCode('alpha-2');
|
||||
const iracingId = faker.string.numeric(6);
|
||||
|
||||
drivers.push(
|
||||
Driver.create({
|
||||
id,
|
||||
iracingId,
|
||||
name,
|
||||
country,
|
||||
bio: faker.lorem.sentence(),
|
||||
joinedAt: faker.date.past(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return drivers;
|
||||
}
|
||||
|
||||
export function createLeagues(ownerIds: string[]): League[] {
|
||||
const leagueNames = [
|
||||
'GridPilot Sprint Series',
|
||||
'GridPilot Endurance Cup',
|
||||
'GridPilot Club Ladder',
|
||||
'Sprint Challenge League',
|
||||
'Club Racers Collective',
|
||||
'Sim Racing Alliance',
|
||||
'Pacific Time Attack',
|
||||
'Nordic Night Series',
|
||||
];
|
||||
|
||||
const leagues: League[] = [];
|
||||
const leagueCount = 6 + faker.number.int({ min: 0, max: 2 });
|
||||
|
||||
for (let i = 0; i < leagueCount; i++) {
|
||||
const id = `league-${i + 1}`;
|
||||
const name = leagueNames[i] ?? faker.company.name();
|
||||
// Ensure league-5 (demo league with running race) is owned by driver-1
|
||||
const ownerId = i === 4 ? 'driver-1' : pickOne(ownerIds);
|
||||
|
||||
const maxDriversOptions = [24, 32, 48, 64];
|
||||
let settings = {
|
||||
pointsSystem: faker.helpers.arrayElement(['f1-2024', 'indycar']),
|
||||
sessionDuration: faker.helpers.arrayElement([45, 60, 90, 120]),
|
||||
qualifyingFormat: faker.helpers.arrayElement(['open', 'single-lap']),
|
||||
maxDrivers: faker.helpers.arrayElement(maxDriversOptions),
|
||||
} as const;
|
||||
|
||||
if (i === 0) {
|
||||
settings = {
|
||||
...settings,
|
||||
maxDrivers: 24,
|
||||
};
|
||||
} else if (i === 1) {
|
||||
settings = {
|
||||
...settings,
|
||||
maxDrivers: 24,
|
||||
};
|
||||
} else if (i === 2) {
|
||||
settings = {
|
||||
...settings,
|
||||
maxDrivers: 40,
|
||||
};
|
||||
}
|
||||
|
||||
const socialLinks =
|
||||
i === 0
|
||||
? {
|
||||
discordUrl: 'https://discord.gg/gridpilot-demo',
|
||||
youtubeUrl: 'https://youtube.com/@gridpilot-demo',
|
||||
websiteUrl: 'https://gridpilot-demo.example.com',
|
||||
}
|
||||
: i === 1
|
||||
? {
|
||||
discordUrl: 'https://discord.gg/gridpilot-endurance',
|
||||
youtubeUrl: 'https://youtube.com/@gridpilot-endurance',
|
||||
}
|
||||
: i === 2
|
||||
? {
|
||||
websiteUrl: 'https://virtual-touring.example.com',
|
||||
}
|
||||
: undefined;
|
||||
|
||||
if (socialLinks) {
|
||||
leagues.push(
|
||||
League.create({
|
||||
id,
|
||||
name,
|
||||
description: faker.lorem.sentence(),
|
||||
ownerId,
|
||||
settings,
|
||||
createdAt: faker.date.past(),
|
||||
socialLinks,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
leagues.push(
|
||||
League.create({
|
||||
id,
|
||||
name,
|
||||
description: faker.lorem.sentence(),
|
||||
ownerId,
|
||||
settings,
|
||||
createdAt: faker.date.past(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return leagues;
|
||||
}
|
||||
|
||||
export function createTeams(leagues: League[]): DemoTeamDTO[] {
|
||||
const teams: DemoTeamDTO[] = [];
|
||||
const teamCount = 24 + faker.number.int({ min: 0, max: 12 });
|
||||
|
||||
for (let i = 0; i < teamCount; i++) {
|
||||
const id = `team-${i + 1}`;
|
||||
const primaryLeague = pickOne(leagues);
|
||||
const name = faker.company.name();
|
||||
const tag = faker.string.alpha({ length: 4 }).toUpperCase();
|
||||
const memberCount = faker.number.int({ min: 2, max: 8 });
|
||||
|
||||
teams.push({
|
||||
id,
|
||||
name,
|
||||
tag,
|
||||
description: faker.lorem.sentence(),
|
||||
logoRef: MediaReference.systemDefault('logo'),
|
||||
primaryLeagueId: primaryLeague.id,
|
||||
memberCount,
|
||||
});
|
||||
}
|
||||
|
||||
return teams;
|
||||
}
|
||||
|
||||
export function createMemberships(
|
||||
drivers: Driver[],
|
||||
leagues: League[],
|
||||
teams: DemoTeamDTO[],
|
||||
): RacingMembership[] {
|
||||
const memberships: RacingMembership[] = [];
|
||||
|
||||
const teamsByLeague = new Map<string, DemoTeamDTO[]>();
|
||||
teams.forEach((team) => {
|
||||
const list = teamsByLeague.get(team.primaryLeagueId) ?? [];
|
||||
list.push(team);
|
||||
teamsByLeague.set(team.primaryLeagueId, list);
|
||||
});
|
||||
|
||||
|
||||
drivers.forEach((driver) => {
|
||||
// Each driver participates in 1–3 leagues
|
||||
const leagueSampleSize = faker.number.int({ min: 1, max: Math.min(3, leagues.length) });
|
||||
const shuffledLeagues = faker.helpers.shuffle(leagues).slice(0, leagueSampleSize);
|
||||
|
||||
shuffledLeagues.forEach((league) => {
|
||||
const leagueTeams = teamsByLeague.get(league.id) ?? [];
|
||||
const team =
|
||||
leagueTeams.length > 0 && faker.datatype.boolean()
|
||||
? pickOne(leagueTeams)
|
||||
: undefined;
|
||||
|
||||
const membership: RacingMembership = {
|
||||
driverId: driver.id,
|
||||
leagueId: league.id,
|
||||
};
|
||||
|
||||
if (team) {
|
||||
membership.teamId = team.id;
|
||||
}
|
||||
|
||||
memberships.push(membership);
|
||||
});
|
||||
});
|
||||
|
||||
return memberships;
|
||||
}
|
||||
|
||||
export function createRaces(leagues: League[]): Race[] {
|
||||
const races: Race[] = [];
|
||||
const raceCount = 60 + faker.number.int({ min: 0, max: 20 });
|
||||
|
||||
const tracks = [
|
||||
'Monza GP',
|
||||
'Spa-Francorchamps',
|
||||
'Suzuka',
|
||||
'Mount Panorama',
|
||||
'Silverstone GP',
|
||||
'Interlagos',
|
||||
'Imola',
|
||||
'Laguna Seca',
|
||||
];
|
||||
|
||||
const cars = [
|
||||
'GT3 – Porsche 911',
|
||||
'GT3 – BMW M4',
|
||||
'LMP3 Prototype',
|
||||
'GT4 – Alpine',
|
||||
'Touring – Civic',
|
||||
];
|
||||
|
||||
const baseDate = new Date();
|
||||
|
||||
for (let i = 0; i < raceCount; i++) {
|
||||
const id = `race-${i + 1}`;
|
||||
let league = pickOne(leagues);
|
||||
const offsetDays = faker.number.int({ min: -30, max: 45 });
|
||||
const scheduledAt = new Date(baseDate.getTime() + offsetDays * 24 * 60 * 60 * 1000);
|
||||
let status: 'scheduled' | 'completed' | 'running' = scheduledAt.getTime() < baseDate.getTime() ? 'completed' : 'scheduled';
|
||||
let strengthOfField: number | undefined;
|
||||
|
||||
// Special case: Make race-1 a running race in league-5 (user's admin league)
|
||||
if (i === 0) {
|
||||
const league5 = leagues.find(l => l.id === 'league-5');
|
||||
if (league5) {
|
||||
league = league5;
|
||||
status = 'running';
|
||||
// Calculate SOF for the running race (simulate 12-20 drivers with average rating ~1500)
|
||||
const participantCount = faker.number.int({ min: 12, max: 20 });
|
||||
const averageRating = 1500 + faker.number.int({ min: -200, max: 300 });
|
||||
strengthOfField = Math.round(averageRating);
|
||||
}
|
||||
}
|
||||
|
||||
races.push(
|
||||
Race.create({
|
||||
id,
|
||||
leagueId: league.id,
|
||||
scheduledAt,
|
||||
track: faker.helpers.arrayElement(tracks),
|
||||
car: faker.helpers.arrayElement(cars),
|
||||
sessionType: SessionType.main(),
|
||||
status,
|
||||
...(strengthOfField !== undefined ? { strengthOfField } : {}),
|
||||
...(status === 'running' ? { registeredCount: faker.number.int({ min: 12, max: 20 }) } : {}),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return races;
|
||||
}
|
||||
|
||||
export function createResults(drivers: Driver[], races: Race[]): Result[] {
|
||||
const results: Result[] = [];
|
||||
|
||||
const completedRaces = races.filter((race) => race.status === 'completed');
|
||||
|
||||
completedRaces.forEach((race) => {
|
||||
const participantCount = faker.number.int({ min: 20, max: 32 });
|
||||
const shuffledDrivers = faker.helpers.shuffle(drivers).slice(0, participantCount);
|
||||
|
||||
shuffledDrivers.forEach((driver, index) => {
|
||||
const position = index + 1;
|
||||
const startPosition = faker.number.int({ min: 1, max: participantCount });
|
||||
const fastestLap = 90_000 + index * 250 + faker.number.int({ min: 0, max: 2_000 });
|
||||
const incidents = faker.number.int({ min: 0, max: 6 });
|
||||
|
||||
results.push(
|
||||
Result.create({
|
||||
id: `${race.id}-${driver.id}`,
|
||||
raceId: race.id,
|
||||
driverId: driver.id,
|
||||
position,
|
||||
startPosition,
|
||||
fastestLap,
|
||||
incidents,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
export function createStandings(leagues: League[], results: Result[]): Standing[] {
|
||||
const standingsByLeague = new Map<string, Standing[]>();
|
||||
|
||||
leagues.forEach((league) => {
|
||||
const leagueRaceIds = new Set(
|
||||
results
|
||||
.filter((result) => {
|
||||
return result.raceId.startsWith('race-');
|
||||
})
|
||||
.map((result) => result.raceId),
|
||||
);
|
||||
|
||||
const leagueResults = results.filter((result) => leagueRaceIds.has(result.raceId));
|
||||
|
||||
const standingsMap = new Map<string, Standing>();
|
||||
|
||||
leagueResults.forEach((result) => {
|
||||
const key = result.driverId;
|
||||
let standing = standingsMap.get(key);
|
||||
|
||||
if (!standing) {
|
||||
standing = Standing.create({
|
||||
leagueId: league.id,
|
||||
driverId: result.driverId,
|
||||
});
|
||||
}
|
||||
|
||||
standing = standing.addRaceResult(result.position, POINTS_TABLE);
|
||||
standingsMap.set(key, standing);
|
||||
});
|
||||
|
||||
const sortedStandings = Array.from(standingsMap.values()).sort((a, b) => {
|
||||
if (b.points !== a.points) {
|
||||
return b.points - a.points;
|
||||
}
|
||||
if (b.wins !== a.wins) {
|
||||
return b.wins - a.wins;
|
||||
}
|
||||
return b.racesCompleted - a.racesCompleted;
|
||||
});
|
||||
|
||||
const finalizedStandings = sortedStandings.map((standing, index) =>
|
||||
standing.updatePosition(index + 1),
|
||||
);
|
||||
|
||||
standingsByLeague.set(league.id, finalizedStandings);
|
||||
});
|
||||
|
||||
return Array.from(standingsByLeague.values()).flat();
|
||||
}
|
||||
|
||||
export function createFriendships(drivers: Driver[]): Friendship[] {
|
||||
const friendships: Friendship[] = [];
|
||||
|
||||
drivers.forEach((driver, index) => {
|
||||
const friendCount = faker.number.int({ min: 3, max: 8 });
|
||||
for (let offset = 1; offset <= friendCount; offset++) {
|
||||
const friendIndex = (index + offset) % drivers.length;
|
||||
const friend = drivers[friendIndex];
|
||||
if (!friend) continue;
|
||||
if (friend.id === driver.id) continue;
|
||||
|
||||
friendships.push({
|
||||
driverId: driver.id,
|
||||
friendId: friend.id,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return friendships;
|
||||
}
|
||||
@@ -1,451 +0,0 @@
|
||||
import { League } from '@core/racing/domain/entities/League';
|
||||
import { Race } from '@core/racing/domain/entities/Race';
|
||||
import { Driver } from '@core/racing/domain/entities/Driver';
|
||||
import { Money } from '@core/racing/domain/value-objects/Money';
|
||||
import { SponsorshipPricing } from '@core/racing/domain/value-objects/SponsorshipPricing';
|
||||
import {
|
||||
SponsorshipRequest,
|
||||
type SponsorableEntityType,
|
||||
} from '@core/racing/domain/entities/SponsorshipRequest';
|
||||
|
||||
import type { DemoTeamDTO } from './RacingSeedCore';
|
||||
|
||||
/**
|
||||
* Demo sponsor data for seeding.
|
||||
*/
|
||||
export interface DemoSponsorDTO {
|
||||
id: string;
|
||||
name: string;
|
||||
contactEmail: string;
|
||||
logoUrl: string;
|
||||
websiteUrl: string;
|
||||
tagline: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Demo season sponsorship data.
|
||||
* This remains a simple DTO since the SeasonSponsorship
|
||||
* domain entity is instantiated in the DI config.
|
||||
*/
|
||||
export interface DemoSeasonSponsorshipDTO {
|
||||
id: string;
|
||||
seasonId: string;
|
||||
sponsorId: string;
|
||||
tier: 'main' | 'secondary';
|
||||
pricingAmount: number;
|
||||
pricingCurrency: 'USD' | 'EUR' | 'GBP';
|
||||
status: 'pending' | 'active' | 'cancelled';
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Demo sponsorship request data for seeding.
|
||||
* Backed directly by the SponsorshipRequest domain entity.
|
||||
*/
|
||||
export type DemoSponsorshipRequestDTO = SponsorshipRequest;
|
||||
|
||||
/**
|
||||
* Demo sponsorship pricing configuration for entities, using the
|
||||
* SponsorshipPricing value object to keep pricing logic in the domain.
|
||||
*/
|
||||
export interface DemoSponsorshipPricingDTO {
|
||||
entityType: SponsorableEntityType;
|
||||
entityId: string;
|
||||
pricing: SponsorshipPricing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Demo sponsors data - realistic sim racing sponsors.
|
||||
*/
|
||||
export const DEMO_SPONSORS: DemoSponsorDTO[] = [
|
||||
{
|
||||
id: 'sponsor-fanatec',
|
||||
name: 'Fanatec',
|
||||
contactEmail: 'partnerships@fanatec.com',
|
||||
logoUrl: '/images/sponsors/fanatec.svg',
|
||||
websiteUrl: 'https://fanatec.com',
|
||||
tagline: "The world's leading sim racing hardware",
|
||||
},
|
||||
{
|
||||
id: 'sponsor-simucube',
|
||||
name: 'Simucube',
|
||||
contactEmail: 'sponsors@simucube.com',
|
||||
logoUrl: '/images/sponsors/simucube.svg',
|
||||
websiteUrl: 'https://simucube.com',
|
||||
tagline: 'Professional Direct Drive Wheels',
|
||||
},
|
||||
{
|
||||
id: 'sponsor-heusinkveld',
|
||||
name: 'Heusinkveld',
|
||||
contactEmail: 'info@heusinkveld.com',
|
||||
logoUrl: '/images/sponsors/heusinkveld.svg',
|
||||
websiteUrl: 'https://heusinkveld.com',
|
||||
tagline: 'Sim Racing Pedals & Hardware',
|
||||
},
|
||||
{
|
||||
id: 'sponsor-trak-racer',
|
||||
name: 'Trak Racer',
|
||||
contactEmail: 'partnerships@trakracer.com',
|
||||
logoUrl: '/images/sponsors/trak-racer.svg',
|
||||
websiteUrl: 'https://trakracer.com',
|
||||
tagline: 'Premium Racing Simulators & Cockpits',
|
||||
},
|
||||
{
|
||||
id: 'sponsor-simlab',
|
||||
name: 'Sim-Lab',
|
||||
contactEmail: 'sponsor@sim-lab.eu',
|
||||
logoUrl: '/images/sponsors/simlab.svg',
|
||||
websiteUrl: 'https://sim-lab.eu',
|
||||
tagline: 'Aluminum Profile Sim Racing Rigs',
|
||||
},
|
||||
{
|
||||
id: 'sponsor-motionrig',
|
||||
name: 'MotionRig Pro',
|
||||
contactEmail: 'business@motionrigpro.com',
|
||||
logoUrl: '/images/sponsors/motionrig.svg',
|
||||
websiteUrl: 'https://motionrigpro.com',
|
||||
tagline: 'Feel every turn, every bump',
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Create season sponsorships linking sponsors to leagues.
|
||||
*/
|
||||
export function createSeasonSponsorships(
|
||||
leagues: League[],
|
||||
sponsors: DemoSponsorDTO[],
|
||||
): DemoSeasonSponsorshipDTO[] {
|
||||
const sponsorships: DemoSeasonSponsorshipDTO[] = [];
|
||||
|
||||
const FANATEC_ID = sponsors.find((s) => s.id === 'sponsor-fanatec')?.id ?? 'sponsor-fanatec';
|
||||
const HEUSINKVELD_ID =
|
||||
sponsors.find((s) => s.id === 'sponsor-heusinkveld')?.id ?? 'sponsor-heusinkveld';
|
||||
const SIMUCUBE_ID = sponsors.find((s) => s.id === 'sponsor-simucube')?.id ?? 'sponsor-simucube';
|
||||
const TRAK_RACER_ID =
|
||||
sponsors.find((s) => s.id === 'sponsor-trak-racer')?.id ?? 'sponsor-trak-racer';
|
||||
const SIMLAB_ID = sponsors.find((s) => s.id === 'sponsor-simlab')?.id ?? 'sponsor-simlab';
|
||||
const MOTIONRIG_ID =
|
||||
sponsors.find((s) => s.id === 'sponsor-motionrig')?.id ?? 'sponsor-motionrig';
|
||||
|
||||
// GridPilot Sprint Series - sponsored by Fanatec (main) + Heusinkveld & Simucube (secondary)
|
||||
const sprintLeague = leagues.find((l) => l.name === 'GridPilot Sprint Series');
|
||||
if (sprintLeague) {
|
||||
sponsorships.push({
|
||||
id: `sponsorship-${sprintLeague.id}-fanatec`,
|
||||
seasonId: `season-${sprintLeague.id}-demo`,
|
||||
sponsorId: FANATEC_ID,
|
||||
tier: 'main',
|
||||
pricingAmount: 5000,
|
||||
pricingCurrency: 'USD',
|
||||
status: 'active',
|
||||
description: 'Main sponsor for the Sprint Series - premium wheel branding',
|
||||
});
|
||||
sponsorships.push({
|
||||
id: `sponsorship-${sprintLeague.id}-heusinkveld`,
|
||||
seasonId: `season-${sprintLeague.id}-demo`,
|
||||
sponsorId: HEUSINKVELD_ID,
|
||||
tier: 'secondary',
|
||||
pricingAmount: 2000,
|
||||
pricingCurrency: 'USD',
|
||||
status: 'active',
|
||||
});
|
||||
sponsorships.push({
|
||||
id: `sponsorship-${sprintLeague.id}-simucube`,
|
||||
seasonId: `season-${sprintLeague.id}-demo`,
|
||||
sponsorId: SIMUCUBE_ID,
|
||||
tier: 'secondary',
|
||||
pricingAmount: 2000,
|
||||
pricingCurrency: 'USD',
|
||||
status: 'active',
|
||||
});
|
||||
}
|
||||
|
||||
// GridPilot Endurance Cup - sponsored by Trak Racer (main) + Sim-Lab (secondary)
|
||||
const enduranceLeague = leagues.find((l) => l.name === 'GridPilot Endurance Cup');
|
||||
if (enduranceLeague) {
|
||||
sponsorships.push({
|
||||
id: `sponsorship-${enduranceLeague.id}-trakracer`,
|
||||
seasonId: `season-${enduranceLeague.id}-demo`,
|
||||
sponsorId: TRAK_RACER_ID,
|
||||
tier: 'main',
|
||||
pricingAmount: 7500,
|
||||
pricingCurrency: 'USD',
|
||||
status: 'active',
|
||||
description: 'Endurance series naming rights',
|
||||
});
|
||||
sponsorships.push({
|
||||
id: `sponsorship-${enduranceLeague.id}-simlab`,
|
||||
seasonId: `season-${enduranceLeague.id}-demo`,
|
||||
sponsorId: SIMLAB_ID,
|
||||
tier: 'secondary',
|
||||
pricingAmount: 3000,
|
||||
pricingCurrency: 'USD',
|
||||
status: 'active',
|
||||
});
|
||||
}
|
||||
|
||||
// GridPilot Club Ladder - sponsored by MotionRig Pro (main)
|
||||
const clubLeague = leagues.find((l) => l.name === 'GridPilot Club Ladder');
|
||||
if (clubLeague) {
|
||||
sponsorships.push({
|
||||
id: `sponsorship-${clubLeague.id}-motionrig`,
|
||||
seasonId: `season-${clubLeague.id}-demo`,
|
||||
sponsorId: MOTIONRIG_ID,
|
||||
tier: 'main',
|
||||
pricingAmount: 3500,
|
||||
pricingCurrency: 'USD',
|
||||
status: 'active',
|
||||
description: 'Club ladder motion platform showcase',
|
||||
});
|
||||
}
|
||||
|
||||
return sponsorships;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create sponsorship pricing configurations for demo entities.
|
||||
* Uses the SponsorshipPricing value object to ensure domain consistency
|
||||
* and to provide a mix of season, team, driver and race offerings.
|
||||
*/
|
||||
export function createSponsorshipPricings(
|
||||
leagues: League[],
|
||||
teams: DemoTeamDTO[],
|
||||
drivers: Driver[],
|
||||
races: Race[],
|
||||
): DemoSponsorshipPricingDTO[] {
|
||||
const pricings: DemoSponsorshipPricingDTO[] = [];
|
||||
|
||||
// League/Season pricing - all leagues can accept sponsorships, with varied configs
|
||||
leagues.forEach((league, index) => {
|
||||
let pricing = SponsorshipPricing.defaultLeague();
|
||||
|
||||
// Vary league pricing/availability for demo richness
|
||||
if (index % 3 === 1) {
|
||||
// Some leagues closed for applications
|
||||
pricing = pricing.setAcceptingApplications(false);
|
||||
} else if (index % 3 === 2) {
|
||||
// Some leagues with main-only sponsorship
|
||||
pricing = pricing.updateSecondarySlot({ available: false, maxSlots: 0 });
|
||||
} else {
|
||||
// Slightly higher price for featured leagues
|
||||
pricing = pricing.updateMainSlot({
|
||||
price: Money.create(1000 + index * 50, 'USD'),
|
||||
});
|
||||
}
|
||||
|
||||
pricings.push({
|
||||
entityType: 'season',
|
||||
entityId: `season-${league.id}-demo`,
|
||||
pricing,
|
||||
});
|
||||
});
|
||||
|
||||
// Team pricing - first 10 teams accept sponsorships using team defaults,
|
||||
// with some teams pausing applications.
|
||||
teams.slice(0, 10).forEach((team, index) => {
|
||||
let pricing = SponsorshipPricing.defaultTeam();
|
||||
|
||||
if (index % 4 === 1) {
|
||||
// Teams with main + secondary but not currently accepting
|
||||
pricing = pricing.setAcceptingApplications(false);
|
||||
} else if (index % 4 === 2) {
|
||||
// Teams with only secondary slots
|
||||
pricing = pricing.updateMainSlot({ available: false, maxSlots: 0 });
|
||||
} else if (index % 4 === 3) {
|
||||
// Teams with premium main slot pricing
|
||||
pricing = pricing.updateMainSlot({
|
||||
price: Money.create(750 + index * 25, 'USD'),
|
||||
});
|
||||
}
|
||||
|
||||
pricings.push({
|
||||
entityType: 'team',
|
||||
entityId: team.id,
|
||||
pricing,
|
||||
});
|
||||
});
|
||||
|
||||
// Driver pricing - first 20 drivers accept sponsorships with varied availability.
|
||||
drivers.slice(0, 20).forEach((driver, index) => {
|
||||
let pricing = SponsorshipPricing.defaultDriver();
|
||||
|
||||
if (index % 3 === 0) {
|
||||
// Higher profile drivers
|
||||
pricing = pricing.updateMainSlot({
|
||||
price: Money.create(250 + index * 10, 'USD'),
|
||||
});
|
||||
} else if (index % 3 === 1) {
|
||||
// Drivers temporarily not accepting sponsorships
|
||||
pricing = pricing.setAcceptingApplications(false);
|
||||
}
|
||||
|
||||
pricings.push({
|
||||
entityType: 'driver',
|
||||
entityId: driver.id,
|
||||
pricing,
|
||||
});
|
||||
});
|
||||
|
||||
// Race pricing - upcoming races can have title sponsors with different tiers
|
||||
const upcomingRaces = races.filter((r) => r.status === 'scheduled').slice(0, 10);
|
||||
upcomingRaces.forEach((race, index) => {
|
||||
let pricing = SponsorshipPricing.defaultRace();
|
||||
|
||||
if (index % 2 === 0) {
|
||||
// Premium events with higher pricing
|
||||
pricing = pricing.updateMainSlot({
|
||||
price: Money.create(350 + index * 30, 'USD'),
|
||||
});
|
||||
}
|
||||
|
||||
pricings.push({
|
||||
entityType: 'race',
|
||||
entityId: race.id,
|
||||
pricing,
|
||||
});
|
||||
});
|
||||
|
||||
return pricings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create demo sponsorship requests (some pending, some accepted/rejected).
|
||||
* Uses the SponsorshipRequest domain entity and Money value object so that
|
||||
* all downstream sponsor flows can rely on domain behavior.
|
||||
*/
|
||||
export function createSponsorshipRequests(
|
||||
sponsors: DemoSponsorDTO[],
|
||||
leagues: League[],
|
||||
teams: DemoTeamDTO[],
|
||||
drivers: Driver[],
|
||||
races: Race[],
|
||||
): DemoSponsorshipRequestDTO[] {
|
||||
const requests: DemoSponsorshipRequestDTO[] = [];
|
||||
const now = new Date();
|
||||
|
||||
const SIMUCUBE_ID = sponsors.find((s) => s.id === 'sponsor-simucube')?.id ?? 'sponsor-simucube';
|
||||
const HEUSINKVELD_ID =
|
||||
sponsors.find((s) => s.id === 'sponsor-heusinkveld')?.id ?? 'sponsor-heusinkveld';
|
||||
const TRAK_RACER_ID =
|
||||
sponsors.find((s) => s.id === 'sponsor-trak-racer')?.id ?? 'sponsor-trak-racer';
|
||||
const MOTIONRIG_ID =
|
||||
sponsors.find((s) => s.id === 'sponsor-motionrig')?.id ?? 'sponsor-motionrig';
|
||||
const SIMLAB_ID = sponsors.find((s) => s.id === 'sponsor-simlab')?.id ?? 'sponsor-simlab';
|
||||
|
||||
// Pending request: Simucube wants to sponsor a driver
|
||||
if (drivers.length > 6) {
|
||||
const targetDriver = drivers[5];
|
||||
if (targetDriver) {
|
||||
requests.push(
|
||||
SponsorshipRequest.create({
|
||||
id: 'req-simucube-driver-1',
|
||||
sponsorId: SIMUCUBE_ID,
|
||||
entityType: 'driver',
|
||||
entityId: targetDriver.id,
|
||||
tier: 'main',
|
||||
offeredAmount: Money.create(250, 'USD'),
|
||||
message:
|
||||
'We would love to sponsor your racing career! Simucube offers the best direct drive wheels in sim racing.',
|
||||
createdAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000), // 2 days ago
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Pending request: Heusinkveld wants to sponsor a team
|
||||
if (teams.length > 3) {
|
||||
const targetTeam = teams[2];
|
||||
if (targetTeam) {
|
||||
requests.push(
|
||||
SponsorshipRequest.create({
|
||||
id: 'req-heusinkveld-team-1',
|
||||
sponsorId: HEUSINKVELD_ID,
|
||||
entityType: 'team',
|
||||
entityId: targetTeam.id,
|
||||
tier: 'main',
|
||||
offeredAmount: Money.create(550, 'USD'),
|
||||
message:
|
||||
'Heusinkveld pedals are known for their precision. We believe your team embodies the same values.',
|
||||
createdAt: new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000), // 3 days ago
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Pending request: Trak Racer wants to sponsor a race
|
||||
const upcomingRace = races.find((r) => r.status === 'scheduled');
|
||||
if (upcomingRace) {
|
||||
requests.push(
|
||||
SponsorshipRequest.create({
|
||||
id: 'req-trakracer-race-1',
|
||||
sponsorId: TRAK_RACER_ID,
|
||||
entityType: 'race',
|
||||
entityId: upcomingRace.id,
|
||||
tier: 'main',
|
||||
offeredAmount: Money.create(350, 'USD'),
|
||||
message: 'We would like to be the title sponsor for this exciting race event!',
|
||||
createdAt: new Date(now.getTime() - 1 * 24 * 60 * 60 * 1000), // 1 day ago
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Pending request: MotionRig Pro wants secondary spot on a league season
|
||||
const clubLeague = leagues.find((l) => l.name === 'Sprint Challenge League');
|
||||
if (clubLeague) {
|
||||
requests.push(
|
||||
SponsorshipRequest.create({
|
||||
id: 'req-motionrig-league-1',
|
||||
sponsorId: MOTIONRIG_ID,
|
||||
entityType: 'season',
|
||||
entityId: `season-${clubLeague.id}-demo`,
|
||||
tier: 'secondary',
|
||||
offeredAmount: Money.create(1500, 'USD'),
|
||||
message:
|
||||
'MotionRig Pro would love to be a secondary sponsor. Our motion platforms are perfect for your competitive drivers.',
|
||||
createdAt: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000), // 5 days ago
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Already accepted request (for history)
|
||||
if (teams.length > 0) {
|
||||
const acceptedTeam = teams[0];
|
||||
if (acceptedTeam) {
|
||||
requests.push(
|
||||
SponsorshipRequest.create({
|
||||
id: 'req-simlab-team-accepted',
|
||||
sponsorId: SIMLAB_ID,
|
||||
entityType: 'team',
|
||||
entityId: acceptedTeam.id,
|
||||
tier: 'secondary',
|
||||
offeredAmount: Money.create(300, 'USD'),
|
||||
message: 'Sim-Lab rigs are the foundation of any competitive setup.',
|
||||
status: 'accepted',
|
||||
createdAt: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000), // 30 days ago
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Already rejected request (for history)
|
||||
if (drivers.length > 10) {
|
||||
const rejectedDriver = drivers[10];
|
||||
if (rejectedDriver) {
|
||||
requests.push(
|
||||
SponsorshipRequest.create({
|
||||
id: 'req-motionrig-driver-rejected',
|
||||
sponsorId: MOTIONRIG_ID,
|
||||
entityType: 'driver',
|
||||
entityId: rejectedDriver.id,
|
||||
tier: 'main',
|
||||
offeredAmount: Money.create(150, 'USD'),
|
||||
message: 'Would you like to represent MotionRig Pro?',
|
||||
status: 'rejected',
|
||||
createdAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000), // 20 days ago
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return requests;
|
||||
}
|
||||
@@ -1,202 +0,0 @@
|
||||
import { Driver } from '@core/racing/domain/entities/Driver';
|
||||
import { League } from '@core/racing/domain/entities/League';
|
||||
import { Race } from '@core/racing/domain/entities/Race';
|
||||
import { Result } from '@core/racing/domain/entities/Result';
|
||||
import { Standing } from '@core/racing/domain/entities/Standing';
|
||||
|
||||
import type { FeedItem } from '@core/social/domain/types/FeedItem';
|
||||
import type { SocialFriendSummary } from '@core/social/application/types/SocialUser';
|
||||
|
||||
import { faker } from '../../helpers/faker/faker';
|
||||
|
||||
import {
|
||||
createDrivers,
|
||||
createLeagues,
|
||||
createTeams,
|
||||
createMemberships,
|
||||
createRaces,
|
||||
createResults,
|
||||
createStandings,
|
||||
createFriendships,
|
||||
type RacingMembership,
|
||||
type Friendship,
|
||||
type DemoTeamDTO,
|
||||
} from './RacingSeedCore';
|
||||
import {
|
||||
DEMO_SPONSORS,
|
||||
createSeasonSponsorships,
|
||||
createSponsorshipPricings,
|
||||
createSponsorshipRequests,
|
||||
type DemoSponsorDTO,
|
||||
type DemoSeasonSponsorshipDTO,
|
||||
type DemoSponsorshipRequestDTO,
|
||||
type DemoSponsorshipPricingDTO,
|
||||
} from './RacingSponsorshipSeed';
|
||||
import {
|
||||
createFeedEvents,
|
||||
buildFriends,
|
||||
buildTopLeagues,
|
||||
buildUpcomingRaces,
|
||||
buildLatestResults,
|
||||
type RaceWithResultsDTO,
|
||||
} from './RacingFeedSeed';
|
||||
|
||||
/**
|
||||
* Aggregated racing seed data used by the website DI container
|
||||
* and other demo infrastructure.
|
||||
*/
|
||||
export type RacingSeedData = {
|
||||
drivers: Driver[];
|
||||
leagues: League[];
|
||||
races: Race[];
|
||||
results: Result[];
|
||||
standings: Standing[];
|
||||
memberships: RacingMembership[];
|
||||
friendships: Friendship[];
|
||||
feedEvents: FeedItem[];
|
||||
teams: DemoTeamDTO[];
|
||||
sponsors: DemoSponsorDTO[];
|
||||
seasonSponsorships: DemoSeasonSponsorshipDTO[];
|
||||
sponsorshipRequests: DemoSponsorshipRequestDTO[];
|
||||
sponsorshipPricings: DemoSponsorshipPricingDTO[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Create the full static racing seed from the smaller core/sponsorship/feed modules.
|
||||
*/
|
||||
export function createStaticRacingSeed(seed: number): RacingSeedData {
|
||||
faker.seed(seed);
|
||||
|
||||
const drivers = createDrivers(96);
|
||||
const leagues = createLeagues(drivers.slice(0, 12).map((d) => d.id));
|
||||
const teams = createTeams(leagues);
|
||||
const memberships = createMemberships(drivers, leagues, teams);
|
||||
const races = createRaces(leagues);
|
||||
const results = createResults(drivers, races);
|
||||
const friendships = createFriendships(drivers);
|
||||
const feedEvents = createFeedEvents(drivers, leagues, races, friendships);
|
||||
const standings = createStandings(leagues, results);
|
||||
const sponsors = DEMO_SPONSORS;
|
||||
const seasonSponsorships = createSeasonSponsorships(leagues, sponsors);
|
||||
const sponsorshipPricings = createSponsorshipPricings(leagues, teams, drivers, races);
|
||||
const sponsorshipRequests = createSponsorshipRequests(sponsors, leagues, teams, drivers, races);
|
||||
|
||||
return {
|
||||
drivers,
|
||||
leagues,
|
||||
races,
|
||||
results,
|
||||
standings,
|
||||
memberships,
|
||||
friendships,
|
||||
feedEvents,
|
||||
teams,
|
||||
sponsors,
|
||||
seasonSponsorships,
|
||||
sponsorshipRequests,
|
||||
sponsorshipPricings,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Singleton seed used by website demo helpers.
|
||||
*
|
||||
* Alpha demo dataset (deterministic, in-memory only):
|
||||
* - 90+ drivers across multiple leagues
|
||||
* - Leagues with precomputed races, results and standings
|
||||
* - Team memberships and friendships forming social “circles”
|
||||
* - Feed events referencing real driver, league, race and team IDs
|
||||
*/
|
||||
const staticSeed = createStaticRacingSeed(42);
|
||||
|
||||
export const drivers = staticSeed.drivers;
|
||||
export const leagues = staticSeed.leagues;
|
||||
export const races = staticSeed.races;
|
||||
export const results = staticSeed.results;
|
||||
export const standings = staticSeed.standings;
|
||||
export const teams = staticSeed.teams;
|
||||
export const memberships = staticSeed.memberships;
|
||||
export const friendships = staticSeed.friendships;
|
||||
export const feedEvents = staticSeed.feedEvents;
|
||||
export const sponsors = staticSeed.sponsors;
|
||||
export const seasonSponsorships = staticSeed.seasonSponsorships;
|
||||
export const sponsorshipRequests = staticSeed.sponsorshipRequests;
|
||||
export const sponsorshipPricings = staticSeed.sponsorshipPricings;
|
||||
|
||||
/**
|
||||
* Derived friend DTOs for UI consumption.
|
||||
* This preserves the previous demo-data `friends` shape.
|
||||
*/
|
||||
export const friends: SocialFriendSummary[] = buildFriends(staticSeed.drivers, staticSeed.memberships);
|
||||
|
||||
/**
|
||||
* Top leagues with banner URLs for UI.
|
||||
*/
|
||||
export const topLeagues = buildTopLeagues(leagues);
|
||||
|
||||
/**
|
||||
* Re-export RaceWithResultsDTO and helpers for latest/upcoming races.
|
||||
*/
|
||||
export type { RaceWithResultsDTO } from './RacingFeedSeed';
|
||||
|
||||
export function getUpcomingRaces(limit?: number): readonly Race[] {
|
||||
return buildUpcomingRaces(races, limit);
|
||||
}
|
||||
|
||||
export function getLatestResults(limit?: number): readonly RaceWithResultsDTO[] {
|
||||
return buildLatestResults(races, results, drivers, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Demo league archetype helper for seeding structure and scoring.
|
||||
* Kept here as the small, focused definition used by DI.
|
||||
*/
|
||||
export type DemoLeagueArchetype =
|
||||
| {
|
||||
id: 'sprint-series';
|
||||
name: 'GridPilot Sprint Series';
|
||||
structure: { mode: 'solo'; maxDrivers: 24 };
|
||||
scoringPresetId: 'sprint-main-driver';
|
||||
}
|
||||
| {
|
||||
id: 'endurance-cup';
|
||||
name: 'GridPilot Endurance Cup';
|
||||
structure: { mode: 'fixedTeams'; maxTeams: 12; driversPerTeam: 2 };
|
||||
scoringPresetId: 'endurance-main-double';
|
||||
}
|
||||
| {
|
||||
id: 'club-ladder';
|
||||
name: 'GridPilot Club Ladder';
|
||||
structure: { mode: 'solo'; maxDrivers: 40 };
|
||||
scoringPresetId: 'club-default';
|
||||
};
|
||||
|
||||
export function getDemoLeagueArchetypeByName(
|
||||
leagueName: string,
|
||||
): DemoLeagueArchetype | undefined {
|
||||
switch (leagueName) {
|
||||
case 'GridPilot Sprint Series':
|
||||
return {
|
||||
id: 'sprint-series',
|
||||
name: 'GridPilot Sprint Series',
|
||||
structure: { mode: 'solo', maxDrivers: 24 },
|
||||
scoringPresetId: 'sprint-main-driver',
|
||||
};
|
||||
case 'GridPilot Endurance Cup':
|
||||
return {
|
||||
id: 'endurance-cup',
|
||||
name: 'GridPilot Endurance Cup',
|
||||
structure: { mode: 'fixedTeams', maxTeams: 12, driversPerTeam: 2 },
|
||||
scoringPresetId: 'endurance-main-double',
|
||||
};
|
||||
case 'GridPilot Club Ladder':
|
||||
return {
|
||||
id: 'club-ladder',
|
||||
name: 'GridPilot Club Ladder',
|
||||
structure: { mode: 'solo', maxDrivers: 40 },
|
||||
scoringPresetId: 'club-default',
|
||||
};
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { faker as baseFaker } from '@faker-js/faker';
|
||||
|
||||
const faker = baseFaker;
|
||||
|
||||
// Fixed seed so demo data is stable across builds
|
||||
faker.seed(20240317);
|
||||
|
||||
export { faker };
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +0,0 @@
|
||||
export interface FriendDTO {
|
||||
driverId: string;
|
||||
displayName: string;
|
||||
avatarUrl: string;
|
||||
isOnline: boolean;
|
||||
lastSeen: Date;
|
||||
primaryLeagueId?: string;
|
||||
primaryTeamId?: string;
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
import type { Page, BrowserContext } from '@playwright/test';
|
||||
import type { RouteAccess } from './websiteRouteInventory';
|
||||
|
||||
export type WebsiteAuthContext = 'public' | 'auth' | 'admin' | 'sponsor';
|
||||
|
||||
export type WebsiteSessionDriftMode = 'invalid-cookie' | 'expired' | 'missing-sponsor-id';
|
||||
export type WebsiteFaultMode = 'null-array' | 'missing-field' | 'invalid-date';
|
||||
|
||||
export function authContextForAccess(access: RouteAccess): WebsiteAuthContext {
|
||||
if (access === 'public') return 'public';
|
||||
if (access === 'auth') return 'auth';
|
||||
if (access === 'admin') return 'admin';
|
||||
return 'sponsor';
|
||||
}
|
||||
|
||||
export async function setWebsiteAuthContext(
|
||||
context: BrowserContext,
|
||||
auth: WebsiteAuthContext,
|
||||
options: { sessionDrift?: WebsiteSessionDriftMode; faultMode?: WebsiteFaultMode } = {},
|
||||
): Promise<void> {
|
||||
const domain = 'localhost';
|
||||
const base = { domain, path: '/' };
|
||||
|
||||
// The website uses `gp_session` cookie for authentication
|
||||
// For smoke tests, we use normal login API with seeded demo user credentials
|
||||
// to get real session cookies
|
||||
|
||||
if (auth === 'public') {
|
||||
// No authentication needed
|
||||
await context.clearCookies();
|
||||
return;
|
||||
}
|
||||
|
||||
// For authenticated contexts, we need to perform a normal login
|
||||
// This ensures we get real session cookies with proper structure
|
||||
// Note: All auth contexts use the same seeded demo driver user for simplicity
|
||||
// Role-based access control is tested separately in integration tests
|
||||
|
||||
// Call the normal login API with seeded demo user credentials
|
||||
// Use demo.driver@example.com for all auth contexts (driver role)
|
||||
const response = await fetch('http://localhost:3101/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: 'demo.driver@example.com',
|
||||
password: 'Demo1234!',
|
||||
}),
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Normal login failed: ${response.status}`);
|
||||
}
|
||||
|
||||
// Extract cookies from the response
|
||||
const setCookieHeader = response.headers.get('set-cookie');
|
||||
if (!setCookieHeader) {
|
||||
throw new Error('No cookies set by normal login');
|
||||
}
|
||||
|
||||
// Parse the Set-Cookie headers
|
||||
const cookies = setCookieHeader.split(',').map(cookieStr => {
|
||||
const parts = cookieStr.split(';').map(p => p.trim());
|
||||
const [nameValue, ...attributes] = parts;
|
||||
const [name, value] = nameValue.split('=');
|
||||
|
||||
const cookie: any = {
|
||||
name,
|
||||
value: decodeURIComponent(value),
|
||||
domain: 'localhost',
|
||||
path: '/',
|
||||
expires: Math.floor(Date.now() / 1000) + 3600, // 1 hour
|
||||
httpOnly: false,
|
||||
secure: false,
|
||||
sameSite: 'Lax' as const
|
||||
};
|
||||
|
||||
for (const attr of attributes) {
|
||||
const [attrName, attrValue] = attr.split('=');
|
||||
const lowerName = attrName.toLowerCase();
|
||||
|
||||
if (lowerName === 'path') cookie.path = attrValue;
|
||||
else if (lowerName === 'httponly') cookie.httpOnly = true;
|
||||
else if (lowerName === 'secure') cookie.secure = true;
|
||||
else if (lowerName === 'samesite') cookie.sameSite = attrValue as any;
|
||||
else if (lowerName === 'domain') {
|
||||
// Skip domain from API - we'll use localhost
|
||||
}
|
||||
else if (lowerName === 'max-age') cookie.expires = Math.floor(Date.now() / 1000) + parseInt(attrValue);
|
||||
}
|
||||
|
||||
// For Docker/local testing, ensure cookies work with localhost
|
||||
// Playwright's context.addCookies requires specific settings for localhost
|
||||
if (cookie.domain === 'localhost') {
|
||||
cookie.secure = false; // Localhost doesn't need HTTPS
|
||||
// Keep sameSite as provided by API, but ensure it's compatible
|
||||
if (cookie.sameSite === 'None') {
|
||||
// For SameSite=None, we need Secure=true, but localhost doesn't support it
|
||||
// So we fall back to Lax for local testing
|
||||
cookie.sameSite = 'Lax';
|
||||
}
|
||||
}
|
||||
|
||||
return cookie;
|
||||
});
|
||||
|
||||
// Apply session drift or fault modes if specified
|
||||
if (options.sessionDrift || options.faultMode) {
|
||||
const sessionCookie = cookies.find(c => c.name === 'gp_session');
|
||||
|
||||
if (sessionCookie) {
|
||||
if (options.sessionDrift) {
|
||||
sessionCookie.value = `drift-${options.sessionDrift}-${sessionCookie.value}`;
|
||||
}
|
||||
|
||||
if (options.faultMode) {
|
||||
cookies.push({
|
||||
name: 'gridpilot_fault_mode',
|
||||
value: options.faultMode,
|
||||
domain,
|
||||
path: '/',
|
||||
expires: Math.floor(Date.now() / 1000) + 3600,
|
||||
httpOnly: false,
|
||||
secure: false,
|
||||
sameSite: 'Lax' as const
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear existing cookies and add the new ones
|
||||
await context.clearCookies();
|
||||
await context.addCookies(cookies);
|
||||
}
|
||||
|
||||
export type ConsoleCapture = {
|
||||
consoleErrors: string[];
|
||||
pageErrors: string[];
|
||||
};
|
||||
|
||||
export function attachConsoleErrorCapture(page: Page): ConsoleCapture {
|
||||
const consoleErrors: string[] = [];
|
||||
const pageErrors: string[] = [];
|
||||
|
||||
page.on('pageerror', (err) => {
|
||||
pageErrors.push(String(err));
|
||||
});
|
||||
|
||||
page.on('console', (msg) => {
|
||||
const type = msg.type();
|
||||
if (type !== 'error') return;
|
||||
|
||||
const text = msg.text();
|
||||
|
||||
// Filter known benign warnings (keep small + generic).
|
||||
if (text.includes('Download the React DevTools')) return;
|
||||
|
||||
// Next/Image accessibility warning (not a runtime failure for smoke coverage).
|
||||
if (text.includes('Image is missing required "alt" property')) return;
|
||||
|
||||
// React controlled <select> warning (still renders fine; treat as non-fatal for route coverage).
|
||||
if (text.includes('Use the `defaultValue` or `value` props on <select> instead of setting `selected` on <option>.')) return;
|
||||
|
||||
consoleErrors.push(`[${type}] ${text}`);
|
||||
});
|
||||
|
||||
return { consoleErrors, pageErrors };
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
export type RouteAccess = 'public' | 'auth' | 'admin' | 'sponsor';
|
||||
|
||||
export type RouteParams = Record<string, string>;
|
||||
|
||||
export type WebsiteRouteDefinition = {
|
||||
pathTemplate: string;
|
||||
params?: RouteParams;
|
||||
access: RouteAccess;
|
||||
expectedPathTemplate?: string;
|
||||
allowNotFound?: boolean;
|
||||
};
|
||||
|
||||
function walkDir(rootDir: string): string[] {
|
||||
const entries = fs.readdirSync(rootDir, { withFileTypes: true });
|
||||
const results: string[] = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.name.startsWith('.')) continue;
|
||||
|
||||
const fullPath = path.join(rootDir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
results.push(...walkDir(fullPath));
|
||||
continue;
|
||||
}
|
||||
|
||||
results.push(fullPath);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
function toPathTemplate(appDir: string, pageFilePath: string): string {
|
||||
const rel = path.relative(appDir, pageFilePath);
|
||||
const segments = rel.split(path.sep);
|
||||
|
||||
// drop trailing "page.tsx"
|
||||
segments.pop();
|
||||
|
||||
// root page.tsx
|
||||
if (segments.length === 0) return '/';
|
||||
|
||||
return `/${segments.join('/')}`;
|
||||
}
|
||||
|
||||
export function listNextAppPageTemplates(appDir?: string): string[] {
|
||||
const resolvedAppDir = appDir ?? path.join(process.cwd(), 'apps', 'website', 'app');
|
||||
|
||||
const files = walkDir(resolvedAppDir);
|
||||
const pages = files.filter((f) => path.basename(f) === 'page.tsx');
|
||||
|
||||
return pages.map((pagePath) => toPathTemplate(resolvedAppDir, pagePath));
|
||||
}
|
||||
|
||||
export function resolvePathTemplate(pathTemplate: string, params: RouteParams = {}): string {
|
||||
return pathTemplate.replace(/\[([^\]]+)\]/g, (_match, key: string) => {
|
||||
const replacement = params[key];
|
||||
if (!replacement) {
|
||||
throw new Error(`Missing route param "${key}" for template "${pathTemplate}"`);
|
||||
}
|
||||
return replacement;
|
||||
});
|
||||
}
|
||||
|
||||
// Default IDs used to resolve dynamic routes in smoke tests.
|
||||
// These values must be supported by the docker mock API in docker-compose.test.yml.
|
||||
const LEAGUE_ID = 'league-1';
|
||||
const DRIVER_ID = 'driver-1';
|
||||
const TEAM_ID = 'team-1';
|
||||
const RACE_ID = 'race-1';
|
||||
const PROTEST_ID = 'protest-1';
|
||||
|
||||
const ROUTE_META: Record<string, Omit<WebsiteRouteDefinition, 'pathTemplate'>> = {
|
||||
'/': { access: 'public' },
|
||||
|
||||
'/404': { access: 'public' },
|
||||
'/500': { access: 'public' },
|
||||
|
||||
'/admin': { access: 'admin' },
|
||||
'/admin/users': { access: 'admin' },
|
||||
|
||||
'/auth/forgot-password': { access: 'public' },
|
||||
'/auth/login': { access: 'public' },
|
||||
'/auth/reset-password': { access: 'public' },
|
||||
'/auth/signup': { access: 'public' },
|
||||
|
||||
'/dashboard': { access: 'auth' },
|
||||
|
||||
'/drivers': { access: 'public' },
|
||||
'/drivers/[id]': { access: 'public', params: { id: DRIVER_ID } },
|
||||
|
||||
'/leaderboards': { access: 'public' },
|
||||
'/leaderboards/drivers': { access: 'public' },
|
||||
|
||||
'/leagues': { access: 'public' },
|
||||
'/leagues/create': { access: 'auth' },
|
||||
'/leagues/[id]': { access: 'public', params: { id: LEAGUE_ID } },
|
||||
'/leagues/[id]/roster/admin': { access: 'admin', params: { id: LEAGUE_ID } },
|
||||
'/leagues/[id]/rulebook': { access: 'public', params: { id: LEAGUE_ID } },
|
||||
'/leagues/[id]/schedule': { access: 'public', params: { id: LEAGUE_ID } },
|
||||
'/leagues/[id]/schedule/admin': { access: 'admin', params: { id: LEAGUE_ID } },
|
||||
'/leagues/[id]/settings': { access: 'admin', params: { id: LEAGUE_ID } },
|
||||
'/leagues/[id]/sponsorships': { access: 'admin', params: { id: LEAGUE_ID } },
|
||||
'/leagues/[id]/standings': { access: 'public', params: { id: LEAGUE_ID } },
|
||||
'/leagues/[id]/stewarding': { access: 'admin', params: { id: LEAGUE_ID } },
|
||||
'/leagues/[id]/stewarding/protests/[protestId]': {
|
||||
access: 'admin',
|
||||
params: { id: LEAGUE_ID, protestId: PROTEST_ID },
|
||||
},
|
||||
'/leagues/[id]/wallet': { access: 'admin', params: { id: LEAGUE_ID } },
|
||||
|
||||
'/onboarding': { access: 'auth' },
|
||||
|
||||
'/profile': { access: 'auth' },
|
||||
'/profile/leagues': { access: 'auth' },
|
||||
'/profile/liveries': { access: 'auth' },
|
||||
'/profile/liveries/upload': { access: 'auth' },
|
||||
'/profile/settings': { access: 'auth' },
|
||||
'/profile/sponsorship-requests': { access: 'auth' },
|
||||
|
||||
'/races': { access: 'public' },
|
||||
'/races/all': { access: 'public' },
|
||||
'/races/[id]': { access: 'public', params: { id: RACE_ID } },
|
||||
'/races/[id]/results': { access: 'public', params: { id: RACE_ID } },
|
||||
'/races/[id]/stewarding': { access: 'admin', params: { id: RACE_ID } },
|
||||
|
||||
'/sponsor': { access: 'sponsor', expectedPathTemplate: '/sponsor/dashboard' },
|
||||
'/sponsor/billing': { access: 'sponsor' },
|
||||
'/sponsor/campaigns': { access: 'sponsor' },
|
||||
'/sponsor/dashboard': { access: 'sponsor' },
|
||||
'/sponsor/leagues': { access: 'sponsor' },
|
||||
'/sponsor/leagues/[id]': { access: 'sponsor', params: { id: LEAGUE_ID } },
|
||||
'/sponsor/settings': { access: 'sponsor' },
|
||||
'/sponsor/signup': { access: 'public' },
|
||||
|
||||
'/teams': { access: 'public' },
|
||||
'/teams/leaderboard': { access: 'public' },
|
||||
'/teams/[id]': { access: 'public', params: { id: TEAM_ID } },
|
||||
};
|
||||
|
||||
export function getWebsiteRouteInventory(): WebsiteRouteDefinition[] {
|
||||
const discovered = listNextAppPageTemplates();
|
||||
|
||||
const missingMeta = discovered.filter((template) => !ROUTE_META[template]);
|
||||
if (missingMeta.length > 0) {
|
||||
throw new Error(
|
||||
`Missing ROUTE_META entries for discovered pages:\n${missingMeta
|
||||
.slice()
|
||||
.sort()
|
||||
.map((t) => `- ${t}`)
|
||||
.join('\n')}`,
|
||||
);
|
||||
}
|
||||
|
||||
const extraMeta = Object.keys(ROUTE_META).filter((template) => !discovered.includes(template));
|
||||
if (extraMeta.length > 0) {
|
||||
throw new Error(
|
||||
`ROUTE_META contains templates that are not present as page.tsx routes:\n${extraMeta
|
||||
.slice()
|
||||
.sort()
|
||||
.map((t) => `- ${t}`)
|
||||
.join('\n')}`,
|
||||
);
|
||||
}
|
||||
|
||||
return discovered
|
||||
.slice()
|
||||
.sort()
|
||||
.map((pathTemplate) => ({ pathTemplate, ...ROUTE_META[pathTemplate] }));
|
||||
}
|
||||
|
||||
export function getWebsiteParamEdgeCases(): WebsiteRouteDefinition[] {
|
||||
return [
|
||||
{ pathTemplate: '/races/[id]', params: { id: 'does-not-exist' }, access: 'public', allowNotFound: true },
|
||||
{ pathTemplate: '/leagues/[id]', params: { id: 'does-not-exist' }, access: 'public', allowNotFound: true },
|
||||
];
|
||||
}
|
||||
|
||||
export function getWebsiteFaultInjectionRoutes(): WebsiteRouteDefinition[] {
|
||||
return [
|
||||
{ pathTemplate: '/leagues/[id]', params: { id: LEAGUE_ID }, access: 'public' },
|
||||
{ pathTemplate: '/leagues/[id]/schedule/admin', params: { id: LEAGUE_ID }, access: 'admin' },
|
||||
{ pathTemplate: '/sponsor/dashboard', access: 'sponsor' },
|
||||
{ pathTemplate: '/races/[id]', params: { id: RACE_ID }, access: 'public' },
|
||||
];
|
||||
}
|
||||
|
||||
export function getWebsiteAuthDriftRoutes(): WebsiteRouteDefinition[] {
|
||||
return [{ pathTemplate: '/sponsor/dashboard', access: 'sponsor' }];
|
||||
}
|
||||
@@ -13,7 +13,7 @@
|
||||
"@/*": ["apps/website/*"],
|
||||
"@core/*": ["core/*"],
|
||||
"@adapters/*": ["adapters/*"],
|
||||
"@testing/*": ["testing/*"]
|
||||
"@testing/*": ["adapters/testing/*"]
|
||||
},
|
||||
|
||||
"esModuleInterop": true,
|
||||
|
||||
@@ -37,7 +37,7 @@ export default defineConfig({
|
||||
alias: {
|
||||
'@core': path.resolve(__dirname, './core'),
|
||||
'@adapters': path.resolve(__dirname, './adapters'),
|
||||
'@testing': path.resolve(__dirname, './testing'),
|
||||
'@testing': path.resolve(__dirname, './adapters/testing'),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -29,7 +29,7 @@ export default defineConfig({
|
||||
'@': resolve(__dirname, './apps/website'),
|
||||
'@core': resolve(__dirname, './core'),
|
||||
'@adapters': resolve(__dirname, './adapters'),
|
||||
'@testing': resolve(__dirname, './testing'),
|
||||
'@testing': resolve(__dirname, './adapters/testing'),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -40,7 +40,7 @@ export default defineConfig({
|
||||
'@gridpilot/shared-result': path.resolve(__dirname, 'core/shared/result/Result.ts'),
|
||||
'@gridpilot/automation': path.resolve(__dirname, 'core/automation'),
|
||||
'@gridpilot/automation/*': path.resolve(__dirname, 'core/automation/*'),
|
||||
'@gridpilot/testing-support': path.resolve(__dirname, 'core/testing-support'),
|
||||
'@gridpilot/testing-support': path.resolve(__dirname, 'adapters/testing'),
|
||||
'@gridpilot/media': path.resolve(__dirname, 'core/media'),
|
||||
'@': path.resolve(__dirname, 'apps/website'),
|
||||
'@/*': path.resolve(__dirname, 'apps/website/*'),
|
||||
|
||||
Reference in New Issue
Block a user