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(); 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); } }