/** * Use Case: QuickPenaltyUseCase * * Allows league admins to quickly issue common penalties without protest process. * Designed for fast, common penalty scenarios like track limits, warnings, etc. */ import { Penalty, type PenaltyType } from '../../domain/entities/Penalty'; import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository'; import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'; import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository'; import { randomUUID } from 'crypto'; import type { AsyncUseCase } from '@core/shared/application'; import type { Logger } from '@core/shared/application'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; type QuickPenaltyErrorCode = 'RACE_NOT_FOUND' | 'UNAUTHORIZED' | 'UNKNOWN_INFRACTION' | 'REPOSITORY_ERROR'; type QuickPenaltyApplicationError = ApplicationErrorCode; export interface QuickPenaltyCommand { raceId: string; driverId: string; adminId: string; infractionType: 'track_limits' | 'unsafe_rejoin' | 'aggressive_driving' | 'false_start' | 'other'; severity: 'warning' | 'minor' | 'major' | 'severe'; notes?: string; } export class QuickPenaltyUseCase implements AsyncUseCase { constructor( private readonly penaltyRepository: IPenaltyRepository, private readonly raceRepository: IRaceRepository, private readonly leagueMembershipRepository: ILeagueMembershipRepository, private readonly logger: Logger, ) {} async execute(command: QuickPenaltyCommand): Promise> { this.logger.debug('Executing QuickPenaltyUseCase', { command }); try { // Validate race exists const race = await this.raceRepository.findById(command.raceId); if (!race) { this.logger.warn('Race not found', { raceId: command.raceId }); return Result.err({ code: 'RACE_NOT_FOUND', details: { message: 'Race not found' } }); } // Validate admin has authority const memberships = await this.leagueMembershipRepository.getLeagueMembers(race.leagueId); const adminMembership = memberships.find( m => m.driverId === command.adminId && m.status === 'active' ); if (!adminMembership || (adminMembership.role !== 'owner' && adminMembership.role !== 'admin')) { this.logger.warn('Unauthorized admin attempting to issue penalty', { adminId: command.adminId, leagueId: race.leagueId }); return Result.err({ code: 'UNAUTHORIZED', details: { message: 'Only league owners and admins can issue penalties' } }); } // Map infraction + severity to penalty type and value const penaltyMapping = this.mapInfractionToPenalty( command.infractionType, command.severity ); if (!penaltyMapping) { this.logger.error('Unknown infraction type', { infractionType: command.infractionType, severity: command.severity }); return Result.err({ code: 'UNKNOWN_INFRACTION', details: { message: 'Unknown infraction type' } }); } const { type, value, reason } = penaltyMapping; // Create the penalty const penalty = Penalty.create({ id: randomUUID(), leagueId: race.leagueId, raceId: command.raceId, driverId: command.driverId, type, ...(value !== undefined ? { value } : {}), reason, issuedBy: command.adminId, status: 'applied', // Quick penalties are applied immediately issuedAt: new Date(), appliedAt: new Date(), ...(command.notes !== undefined ? { notes: command.notes } : {}), }); await this.penaltyRepository.create(penalty); this.logger.info('Quick penalty applied successfully', { penaltyId: penalty.id, raceId: command.raceId, driverId: command.driverId }); return Result.ok({ penaltyId: penalty.id }); } catch (error) { this.logger.error('Failed to apply quick penalty', { error: error instanceof Error ? error.message : 'Unknown error' }); return Result.err({ code: 'REPOSITORY_ERROR', details: { message: error instanceof Error ? error.message : 'Unknown error' } }); } } private mapInfractionToPenalty( infractionType: QuickPenaltyCommand['infractionType'], severity: QuickPenaltyCommand['severity'] ): { type: PenaltyType; value?: number; reason: string } | null { const severityMultipliers = { warning: 1, minor: 2, major: 3, severe: 4, }; const multiplier = severityMultipliers[severity]; switch (infractionType) { case 'track_limits': if (severity === 'warning') { return { type: 'warning', reason: 'Track limits violation - warning' }; } return { type: 'points_deduction', value: multiplier, reason: `Track limits violation - ${multiplier} point${multiplier > 1 ? 's' : ''} deducted` }; case 'unsafe_rejoin': return { type: 'time_penalty', value: 5 * multiplier, reason: `Unsafe rejoining to track - +${5 * multiplier}s time penalty` }; case 'aggressive_driving': if (severity === 'warning') { return { type: 'warning', reason: 'Aggressive driving - warning' }; } return { type: 'points_deduction', value: 2 * multiplier, reason: `Aggressive driving - ${2 * multiplier} point${multiplier > 1 ? 's' : ''} deducted` }; case 'false_start': return { type: 'grid_penalty', value: multiplier, reason: `False start - ${multiplier} grid position${multiplier > 1 ? 's' : ''} penalty` }; case 'other': if (severity === 'warning') { return { type: 'warning', reason: 'General infraction - warning' }; } return { type: 'points_deduction', value: 3 * multiplier, reason: `General infraction - ${3 * multiplier} point${multiplier > 1 ? 's' : ''} deducted` }; default: return null; } } }