Some checks failed
CI / lint-typecheck (pull_request) Failing after 12s
CI / tests (pull_request) Has been skipped
CI / contract-tests (pull_request) Has been skipped
CI / e2e-tests (pull_request) Has been skipped
CI / comment-pr (pull_request) Has been skipped
CI / commit-types (pull_request) Has been skipped
209 lines
6.4 KiB
TypeScript
209 lines
6.4 KiB
TypeScript
/**
|
|
* 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,
|
|
} from '../ports/TeamRankingsQuery';
|
|
import { ValidationError } from '../../../shared/errors/ValidationError';
|
|
|
|
export interface GetTeamRankingsUseCasePorts {
|
|
leaderboardsRepository: LeaderboardsRepository;
|
|
eventPublisher: LeaderboardsEventPublisher;
|
|
}
|
|
|
|
interface DiscoveredTeam {
|
|
id: string;
|
|
name: string;
|
|
rating: number;
|
|
memberCount: number;
|
|
raceCount: number;
|
|
}
|
|
|
|
export class GetTeamRankingsUseCase {
|
|
constructor(private readonly ports: GetTeamRankingsUseCasePorts) {}
|
|
|
|
async execute(query: TeamRankingsQuery = {}): Promise<TeamRankingsResult> {
|
|
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<string, number>();
|
|
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: DiscoveredTeam[] = [];
|
|
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"');
|
|
}
|
|
}
|
|
}
|