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