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