294 lines
12 KiB
TypeScript
294 lines
12 KiB
TypeScript
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';
|
|
import type { SeasonStatusValue } from '@core/racing/domain/value-objects/SeasonStatus';
|
|
import { faker } from '@faker-js/faker';
|
|
import { seedId } from './SeedIdHelper';
|
|
|
|
export class RacingSeasonSponsorshipFactory {
|
|
constructor(
|
|
private readonly baseDate: Date,
|
|
private readonly persistence: 'postgres' | 'inmemory' = 'inmemory',
|
|
) {}
|
|
|
|
createSeasons(leagues: League[]): Season[] {
|
|
const seasons: Season[] = [];
|
|
|
|
for (const league of leagues) {
|
|
const leagueId = league.id.toString();
|
|
const leagueIndex = parseInt(leagueId.split('-')[1] || '0');
|
|
|
|
// Create 2-4 seasons per league to reach ~100 total seasons
|
|
const seasonCount = faker.number.int({ min: 2, max: 4 });
|
|
|
|
for (let i = 0; i < seasonCount; i++) {
|
|
const id = seedId(`${leagueId}-season-${i + 1}`, this.persistence);
|
|
|
|
// Systematically cover all 5 statuses across all leagues
|
|
// planned: 20%, active: 20%, completed: 30%, archived: 20%, cancelled: 10%
|
|
let status: SeasonStatusValue;
|
|
if (i === 0 && leagueIndex % 5 === 0) {
|
|
status = 'planned';
|
|
} else if (i === 0 && leagueIndex % 5 === 1) {
|
|
status = 'active';
|
|
} else if (i === 1 && leagueIndex % 5 === 2) {
|
|
status = 'completed';
|
|
} else if (i === 2 && leagueIndex % 5 === 3) {
|
|
status = 'archived';
|
|
} else if (i === 3 && leagueIndex % 5 === 4) {
|
|
status = 'cancelled';
|
|
} else {
|
|
// Weighted random distribution
|
|
const statusWeights = [
|
|
{ weight: 2, value: 'planned' as const },
|
|
{ weight: 2, value: 'active' as const },
|
|
{ weight: 3, value: 'completed' as const },
|
|
{ weight: 2, value: 'archived' as const },
|
|
{ weight: 1, value: 'cancelled' as const },
|
|
];
|
|
const totalWeight = statusWeights.reduce((sum, item) => sum + item.weight, 0);
|
|
const random = faker.number.int({ min: 1, max: totalWeight });
|
|
let cumulative = 0;
|
|
status = 'planned';
|
|
for (const item of statusWeights) {
|
|
cumulative += item.weight;
|
|
if (random <= cumulative) {
|
|
status = item.value;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
const baseYear = this.baseDate.getUTCFullYear() + faker.number.int({ min: -1, max: 1 });
|
|
|
|
// Calculate dates based on status
|
|
let startDate: Date | undefined;
|
|
let endDate: Date | undefined;
|
|
let schedulePublished: boolean | undefined;
|
|
let participantCount: number | undefined;
|
|
let maxDrivers: number | undefined;
|
|
|
|
// Special case: ensure league-5-season-1 starts unpublished for test compatibility
|
|
const isTestSeason = id === seedId('league-5-season-1', this.persistence);
|
|
|
|
switch (status) {
|
|
case 'planned':
|
|
startDate = this.daysFromBase(faker.number.int({ min: 7, max: 90 }));
|
|
schedulePublished = isTestSeason ? false : faker.datatype.boolean({ probability: 0.6 });
|
|
participantCount = 0;
|
|
break;
|
|
|
|
case 'active':
|
|
startDate = this.daysFromBase(faker.number.int({ min: -60, max: -1 }));
|
|
schedulePublished = true;
|
|
maxDrivers = faker.number.int({
|
|
min: 10,
|
|
max: Math.max(10, Math.min(league.settings.maxDrivers || 32, 100))
|
|
});
|
|
participantCount = faker.number.int({ min: 5, max: maxDrivers });
|
|
break;
|
|
|
|
case 'completed':
|
|
startDate = this.daysFromBase(faker.number.int({ min: -180, max: -60 }));
|
|
endDate = this.daysFromBase(faker.number.int({ min: -59, max: -7 }));
|
|
schedulePublished = true;
|
|
maxDrivers = faker.number.int({
|
|
min: 10,
|
|
max: Math.max(10, Math.min(league.settings.maxDrivers || 32, 100))
|
|
});
|
|
participantCount = faker.number.int({ min: 10, max: maxDrivers });
|
|
break;
|
|
|
|
case 'archived':
|
|
startDate = this.daysFromBase(faker.number.int({ min: -365, max: -200 }));
|
|
endDate = this.daysFromBase(faker.number.int({ min: -199, max: -150 }));
|
|
schedulePublished = true;
|
|
maxDrivers = faker.number.int({
|
|
min: 10,
|
|
max: Math.max(10, Math.min(league.settings.maxDrivers || 32, 100))
|
|
});
|
|
participantCount = faker.number.int({ min: 8, max: maxDrivers });
|
|
break;
|
|
|
|
case 'cancelled':
|
|
startDate = this.daysFromBase(faker.number.int({ min: -30, max: -1 }));
|
|
endDate = this.daysFromBase(faker.number.int({ min: -1, max: 1 })); // Cancelled early
|
|
schedulePublished = isTestSeason ? false : faker.datatype.boolean({ probability: 0.3 });
|
|
// Cancelled seasons can have maxDrivers but participantCount should be low
|
|
maxDrivers = faker.number.int({
|
|
min: 5,
|
|
max: Math.max(5, Math.min(league.settings.maxDrivers || 32, 100))
|
|
});
|
|
participantCount = faker.number.int({ min: 0, max: Math.min(5, maxDrivers) }); // Minimal participants
|
|
break;
|
|
}
|
|
|
|
// Build season data with proper undefined handling
|
|
const seasonData: {
|
|
id: string;
|
|
leagueId: string;
|
|
gameId: string;
|
|
name: string;
|
|
year?: number;
|
|
order?: number;
|
|
status: SeasonStatusValue;
|
|
startDate?: Date;
|
|
endDate?: Date;
|
|
schedulePublished?: boolean;
|
|
participantCount?: number;
|
|
maxDrivers?: number;
|
|
} = {
|
|
id,
|
|
leagueId,
|
|
gameId: 'iracing',
|
|
name: `${faker.word.adjective()} ${faker.word.noun()} Season`,
|
|
status,
|
|
};
|
|
|
|
// Add optional fields only if they have values
|
|
if (baseYear !== undefined) seasonData.year = baseYear;
|
|
if (i + 1 !== undefined) seasonData.order = i + 1;
|
|
if (startDate !== undefined) seasonData.startDate = startDate;
|
|
if (endDate !== undefined) seasonData.endDate = endDate;
|
|
if (schedulePublished !== undefined) seasonData.schedulePublished = schedulePublished;
|
|
if (participantCount !== undefined) seasonData.participantCount = participantCount;
|
|
if (maxDrivers !== undefined) seasonData.maxDrivers = maxDrivers;
|
|
|
|
const season = Season.create(seasonData);
|
|
seasons.push(season);
|
|
}
|
|
}
|
|
|
|
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 expectedSeasonId = seedId('season-1', this.persistence);
|
|
const sponsorshipCount =
|
|
season.id === expectedSeasonId
|
|
? 2
|
|
: season.status.isActive()
|
|
? 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: seedId(`season-sponsorship-${season.id}-${i + 1}`, this.persistence),
|
|
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.isActive()
|
|
? {
|
|
status: faker.helpers.arrayElement(['active', 'pending'] as const),
|
|
activatedAt: faker.date.recent({ days: 30, refDate: this.baseDate }),
|
|
}
|
|
: season.status.isCompleted() || season.status.isArchived()
|
|
? {
|
|
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 expectedSeasonId = seedId('season-1', this.persistence);
|
|
const isHighTrafficDemo = season.id === expectedSeasonId;
|
|
const maxRequests =
|
|
isHighTrafficDemo
|
|
? 8
|
|
: season.status.isActive()
|
|
? faker.number.int({ min: 0, max: 4 })
|
|
: faker.number.int({ min: 0, max: 1 });
|
|
|
|
for (let i = 0; i < maxRequests; i++) {
|
|
const tier: SponsorshipRequest['tier'] =
|
|
i === 0 ? 'main' : faker.helpers.arrayElement(['secondary', 'secondary', 'main'] as const);
|
|
|
|
const sponsorId =
|
|
isHighTrafficDemo && i === 0
|
|
? seedId('demo-sponsor-1', this.persistence)
|
|
: 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.isActive()
|
|
? faker.helpers.arrayElement(['pending', 'pending', 'pending', 'rejected', 'withdrawn'] as const)
|
|
: faker.helpers.arrayElement(['pending', 'rejected'] as const);
|
|
|
|
requests.push(
|
|
SponsorshipRequest.create({
|
|
id: seedId(`sponsorship-request-${season.id}-${i + 1}`, this.persistence),
|
|
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);
|
|
}
|
|
} |