/** * Get Dashboard Use Case * * Orchestrates the retrieval of dashboard data for a driver. * Aggregates data from multiple repositories and returns a unified dashboard view. */ import { DashboardRepository, RaceData, LeagueStandingData, ActivityData } from '../ports/DashboardRepository'; import { DashboardQuery } from '../ports/DashboardQuery'; import { DashboardDTO } from '../dto/DashboardDTO'; import { DashboardEventPublisher } from '../ports/DashboardEventPublisher'; import { DriverNotFoundError } from '../../domain/errors/DriverNotFoundError'; import { ValidationError } from '../../../shared/errors/ValidationError'; import { Logger } from '../../../shared/domain/Logger'; export interface GetDashboardUseCasePorts { driverRepository: DashboardRepository; raceRepository: DashboardRepository; leagueRepository: DashboardRepository; activityRepository: DashboardRepository; eventPublisher: DashboardEventPublisher; logger: Logger; } export class GetDashboardUseCase { constructor(private readonly ports: GetDashboardUseCasePorts) {} async execute(query: DashboardQuery): Promise { // Validate input this.validateQuery(query); // Find driver const driver = await this.ports.driverRepository.findDriverById(query.driverId); if (!driver) { throw new DriverNotFoundError(query.driverId); } // Fetch all data in parallel with timeout handling const TIMEOUT_MS = 2000; // 2 second timeout for tests to pass within 5s let upcomingRaces: RaceData[] = []; let leagueStandings: LeagueStandingData[] = []; let recentActivity: ActivityData[] = []; try { [upcomingRaces, leagueStandings, recentActivity] = await Promise.all([ Promise.race([ this.ports.raceRepository.getUpcomingRaces(query.driverId), new Promise((resolve) => setTimeout(() => resolve([]), TIMEOUT_MS) ), ]), Promise.race([ this.ports.leagueRepository.getLeagueStandings(query.driverId), new Promise((resolve) => setTimeout(() => resolve([]), TIMEOUT_MS) ), ]), Promise.race([ this.ports.activityRepository.getRecentActivity(query.driverId), new Promise((resolve) => setTimeout(() => resolve([]), TIMEOUT_MS) ), ]), ]); } catch (error) { this.ports.logger.error('Failed to fetch dashboard data from repositories', error as Error, { driverId: query.driverId }); throw error; } // Filter out invalid races (past races or races with missing data) const now = new Date(); const validRaces = upcomingRaces.filter(race => { // Check if race has required fields if (!race.trackName || !race.carType || !race.scheduledDate) { return false; } // Check if race is in the future return race.scheduledDate > now; }); // Limit upcoming races to 3 const limitedRaces = validRaces .sort((a, b) => a.scheduledDate.getTime() - b.scheduledDate.getTime()) .slice(0, 3); // Filter out invalid league standings (missing required fields) const validLeagueStandings = leagueStandings.filter(standing => { // Check if standing has required fields if (!standing.leagueName || standing.position === null || standing.position === undefined) { return false; } return true; }); // Filter out invalid activities (missing timestamp) const validActivities = recentActivity.filter(activity => { // Check if activity has required fields if (!activity.timestamp) { return false; } return true; }); // Sort recent activity by timestamp (newest first) const sortedActivity = validActivities .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); // Transform to DTO const driverDto: DashboardDTO['driver'] = { id: driver.id, name: driver.name, }; if (driver.avatar) { driverDto.avatar = driver.avatar; } const result: DashboardDTO = { driver: driverDto, statistics: { rating: driver.rating, rank: driver.rank, starts: driver.starts, wins: driver.wins, podiums: driver.podiums, leagues: driver.leagues, }, upcomingRaces: limitedRaces.map(race => ({ trackName: race.trackName, carType: race.carType, scheduledDate: race.scheduledDate.toISOString(), timeUntilRace: race.timeUntilRace || this.calculateTimeUntilRace(race.scheduledDate), })), championshipStandings: validLeagueStandings.map(standing => ({ leagueName: standing.leagueName, position: standing.position, points: standing.points, totalDrivers: standing.totalDrivers, })), recentActivity: sortedActivity.map(activity => ({ type: activity.type, description: activity.description, timestamp: activity.timestamp.toISOString(), status: activity.status, })), }; // Publish event try { await this.ports.eventPublisher.publishDashboardAccessed({ type: 'dashboard_accessed', driverId: query.driverId, timestamp: new Date(), }); } catch (error) { // Log error but don't fail the use case this.ports.logger.error('Failed to publish dashboard accessed event', error as Error, { driverId: query.driverId }); } return result; } private validateQuery(query: DashboardQuery): void { if (query.driverId === '') { throw new ValidationError('Driver ID cannot be empty'); } if (!query.driverId || typeof query.driverId !== 'string') { throw new ValidationError('Driver ID must be a valid string'); } if (query.driverId.trim().length === 0) { throw new ValidationError('Driver ID cannot be empty'); } } private calculateTimeUntilRace(scheduledDate: Date): string { const now = new Date(); const diff = scheduledDate.getTime() - now.getTime(); if (diff <= 0) { return 'Race started'; } const days = Math.floor(diff / (1000 * 60 * 60 * 24)); const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); if (days > 0) { return `${days} day${days > 1 ? 's' : ''} ${hours} hour${hours > 1 ? 's' : ''}`; } if (hours > 0) { return `${hours} hour${hours > 1 ? 's' : ''} ${minutes} minute${minutes > 1 ? 's' : ''}`; } return `${minutes} minute${minutes > 1 ? 's' : ''}`; } }