import type { Logger } from '@core/shared/domain/Logger'; import { Result } from '@core/shared/domain/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { NotificationService } from '../../../notifications/application/ports/NotificationService'; import type { NotificationType } from '../../../notifications/domain/types/NotificationTypes'; import type { RaceEvent } from '../../domain/entities/RaceEvent'; import { IncidentCount } from '../../domain/entities/result/IncidentCount'; import { Position } from '../../domain/entities/result/Position'; import type { Result as RaceResult } from '../../domain/entities/result/Result'; import { DriverRepository } from '../../domain/repositories/DriverRepository'; import { LeagueMembershipRepository } from '../../domain/repositories/LeagueMembershipRepository'; import { LeagueRepository } from '../../domain/repositories/LeagueRepository'; import { RaceEventRepository } from '../../domain/repositories/RaceEventRepository'; import { ResultRepository } from '../../domain/repositories/ResultRepository'; import { isLeagueStewardOrHigherRole } from '../../domain/types/LeagueRoles'; export type SendPerformanceSummaryInput = { leagueId: string; raceId: string; driverId: string; triggeredById: string; }; export type SendPerformanceSummaryResult = { leagueId: string; raceId: string; driverId: string; notificationsSent: number; }; export type SendPerformanceSummaryErrorCode = | 'LEAGUE_NOT_FOUND' | 'RACE_NOT_FOUND' | 'DRIVER_NOT_FOUND' | 'INSUFFICIENT_PERMISSIONS' | 'SUMMARY_NOT_AVAILABLE' | 'REPOSITORY_ERROR'; /** * Use Case: SendPerformanceSummaryUseCase * * Sends an immediate performance summary notification to a driver * for a specific race event. */ export class SendPerformanceSummaryUseCase { constructor(private readonly notificationService: NotificationService, private readonly raceEventRepository: RaceEventRepository, private readonly resultRepository: ResultRepository, private readonly leagueRepository: LeagueRepository, private readonly membershipRepository: LeagueMembershipRepository, private readonly driverRepository: DriverRepository, private readonly logger: Logger) {} async execute( input: SendPerformanceSummaryInput, ): Promise>> { try { const league = await this.leagueRepository.findById(input.leagueId); if (!league) { return Result.err({ code: 'LEAGUE_NOT_FOUND', details: { message: 'League not found' } }); } const raceEvent = await this.raceEventRepository.findById(input.raceId); if (!raceEvent) { return Result.err({ code: 'RACE_NOT_FOUND', details: { message: 'Race event not found' } }); } const driver = await this.driverRepository.findById(input.driverId); if (!driver) { return Result.err({ code: 'DRIVER_NOT_FOUND', details: { message: 'Driver not found' } }); } if (input.triggeredById !== input.driverId) { const membership = await this.membershipRepository.getMembership( league.id.toString(), input.triggeredById, ); if (!membership || !isLeagueStewardOrHigherRole(membership.role.toString())) { return Result.err({ code: 'INSUFFICIENT_PERMISSIONS', details: { message: 'Insufficient permissions to send performance summary' }, }); } } const mainRaceSession = raceEvent.getMainRaceSession(); if (!mainRaceSession || mainRaceSession.status !== 'completed') { return Result.err({ code: 'SUMMARY_NOT_AVAILABLE', details: { message: 'Performance summary is not available for this race' }, }); } const results = await this.resultRepository.findByRaceId(mainRaceSession.id); const driverResult = results.find((r: RaceResult) => r.driverId.toString() === input.driverId); if (!driverResult) { return Result.err({ code: 'SUMMARY_NOT_AVAILABLE', details: { message: 'Performance summary is not available for this driver' }, }); } let notificationsSent = 0; await this.sendPerformanceSummaryNotification(input.driverId, raceEvent, driverResult, league.id.toString()); notificationsSent += 1; const result: SendPerformanceSummaryResult = { leagueId: league.id.toString(), raceId: raceEvent.id, driverId: input.driverId, notificationsSent, }; return Result.ok(result); } catch (error: unknown) { const message = error instanceof Error ? error.message : 'Failed to send performance summary'; this.logger.error('SendPerformanceSummaryUseCase.execute failed', error instanceof Error ? error : undefined); return Result.err({ code: 'REPOSITORY_ERROR', details: { message } }); } } private async sendPerformanceSummaryNotification( driverId: string, raceEvent: RaceEvent, driverResult: RaceResult | undefined, leagueId: string, ): Promise { const position = driverResult?.position ?? 'DNF'; const positionChange = driverResult?.getPositionChange() ?? 0; const incidents = driverResult?.incidents ?? 0; const provisionalRatingChange = this.calculateProvisionalRatingChange(driverResult?.position?.toNumber(), driverResult?.incidents?.toNumber()); const title = `Race Complete: ${raceEvent.name}`; const positionValue = position instanceof Position ? position.toNumber() : position; const incidentValue = incidents instanceof IncidentCount ? incidents.toNumber() : incidents; const body = this.buildPerformanceSummaryBody(positionValue, positionChange, incidentValue, provisionalRatingChange); await this.notificationService.sendNotification({ recipientId: driverId, type: 'race_performance_summary' as NotificationType, title, body, channel: 'in_app', urgency: 'modal', data: { raceEventId: raceEvent.id, sessionId: raceEvent.getMainRaceSession()?.id ?? '', leagueId, position: positionValue, positionChange, incidents: incidentValue, provisionalRatingChange, }, actions: [ { label: 'View Full Results', type: 'primary', href: `/leagues/${leagueId}/races/${raceEvent.id}`, }, ], requiresResponse: false, }); } private buildPerformanceSummaryBody( position: number | 'DNF', positionChange: number, incidents: number, provisionalRatingChange: number, ): string { const positionText = position === 'DNF' ? 'DNF' : `P${position}`; const positionChangeText = positionChange > 0 ? `+${positionChange}` : positionChange < 0 ? `${positionChange}` : '±0'; const incidentsText = incidents === 0 ? 'Clean race!' : `${incidents} incident${incidents > 1 ? 's' : ''}`; const ratingText = provisionalRatingChange >= 0 ? `+${provisionalRatingChange} rating` : `${provisionalRatingChange} rating`; return `You finished ${positionText} (${positionChangeText} positions). ${incidentsText} Provisional ${ratingText}.`; } private calculateProvisionalRatingChange(position?: number, incidents?: number): number { if (!position) return -10; const baseChange = position <= 3 ? 25 : position <= 10 ? 10 : -5; const positionBonus = Math.max(0, (20 - position) * 2); const incidentPenalty = (incidents ?? 0) * -5; return baseChange + positionBonus + incidentPenalty; } }