/** * Get Team Rankings Use Case * * Orchestrates the retrieval of team rankings data. * Aggregates data from repositories and returns teams with search, filter, and sort capabilities. */ import { LeaderboardsRepository } from '../ports/LeaderboardsRepository'; import { LeaderboardsEventPublisher } from '../ports/LeaderboardsEventPublisher'; import { TeamRankingsQuery, TeamRankingsResult, TeamRankingEntry, PaginationMetadata, } from '../ports/TeamRankingsQuery'; import { ValidationError } from '../../../shared/errors/ValidationError'; export interface GetTeamRankingsUseCasePorts { leaderboardsRepository: LeaderboardsRepository; eventPublisher: LeaderboardsEventPublisher; } export class GetTeamRankingsUseCase { constructor(private readonly ports: GetTeamRankingsUseCasePorts) {} async execute(query: TeamRankingsQuery = {}): Promise { try { // Validate query parameters this.validateQuery(query); const page = query.page ?? 1; const limit = query.limit ?? 20; // Fetch all teams and drivers for member count aggregation const [allTeams, allDrivers] = await Promise.all([ this.ports.leaderboardsRepository.findAllTeams(), this.ports.leaderboardsRepository.findAllDrivers(), ]); // Count members from drivers const driverCounts = new Map(); allDrivers.forEach(driver => { if (driver.teamId) { driverCounts.set(driver.teamId, (driverCounts.get(driver.teamId) || 0) + 1); } }); // Map teams from repository const teamsWithAggregatedData = allTeams.map(team => { const countFromDrivers = driverCounts.get(team.id); return { ...team, // If drivers exist in repository for this team, use that count as source of truth. // Otherwise, fall back to the memberCount property on the team itself. memberCount: countFromDrivers !== undefined ? countFromDrivers : (team.memberCount || 0) }; }); // Discover teams that only exist in the drivers repository const discoveredTeams: any[] = []; driverCounts.forEach((count, teamId) => { if (!allTeams.some(t => t.id === teamId)) { const driverWithTeam = allDrivers.find(d => d.teamId === teamId); discoveredTeams.push({ id: teamId, name: driverWithTeam?.teamName || `Team ${teamId}`, rating: 0, memberCount: count, raceCount: 0 }); } }); const finalTeams = [...teamsWithAggregatedData, ...discoveredTeams]; // Apply search filter let filteredTeams = finalTeams; if (query.search) { const searchLower = query.search.toLowerCase(); filteredTeams = filteredTeams.filter((team) => team.name.toLowerCase().includes(searchLower), ); } // Apply rating filter if (query.minRating !== undefined) { filteredTeams = filteredTeams.filter( (team) => team.rating >= query.minRating!, ); } // Apply member count filter if (query.minMemberCount !== undefined) { filteredTeams = filteredTeams.filter( (team) => team.memberCount >= query.minMemberCount!, ); } // Sort teams const sortBy = query.sortBy ?? 'rating'; const sortOrder = query.sortOrder ?? 'desc'; filteredTeams.sort((a, b) => { let comparison = 0; switch (sortBy) { case 'rating': comparison = a.rating - b.rating; break; case 'name': comparison = a.name.localeCompare(b.name); break; case 'rank': comparison = 0; break; case 'memberCount': comparison = a.memberCount - b.memberCount; break; } // If primary sort is equal, always use name ASC as secondary sort if (comparison === 0 && sortBy !== 'name') { return a.name.localeCompare(b.name); } return sortOrder === 'asc' ? comparison : -comparison; }); // Calculate pagination const total = filteredTeams.length; const totalPages = Math.ceil(total / limit); const startIndex = (page - 1) * limit; const endIndex = Math.min(startIndex + limit, total); // Get paginated teams const paginatedTeams = filteredTeams.slice(startIndex, endIndex); // Map to ranking entries with rank const teamEntries: TeamRankingEntry[] = paginatedTeams.map( (team, index): TeamRankingEntry => ({ rank: startIndex + index + 1, id: team.id, name: team.name, rating: team.rating, memberCount: team.memberCount, raceCount: team.raceCount, }), ); // Publish event await this.ports.eventPublisher.publishTeamRankingsAccessed({ type: 'team_rankings_accessed', timestamp: new Date(), }); return { teams: teamEntries, pagination: { total, page, limit, totalPages, }, }; } catch (error) { // Publish error event await this.ports.eventPublisher.publishLeaderboardsError({ type: 'leaderboards_error', error: error instanceof Error ? error.message : String(error), timestamp: new Date(), }); throw error; } } private validateQuery(query: TeamRankingsQuery): void { if (query.page !== undefined && query.page < 1) { throw new ValidationError('Page must be a positive integer'); } if (query.limit !== undefined && query.limit < 1) { throw new ValidationError('Limit must be a positive integer'); } if (query.minRating !== undefined && query.minRating < 0) { throw new ValidationError('Min rating must be a non-negative number'); } if (query.minMemberCount !== undefined && query.minMemberCount < 0) { throw new ValidationError('Min member count must be a non-negative number'); } if (query.sortBy && !['rating', 'name', 'rank', 'memberCount'].includes(query.sortBy)) { throw new ValidationError('Invalid sort field'); } if (query.sortOrder && !['asc', 'desc'].includes(query.sortOrder)) { throw new ValidationError('Sort order must be "asc" or "desc"'); } } }