/** * 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 { Result } from '@core/shared/domain/Result'; import type { Logger } from '@core/shared/domain/Logger'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import { randomUUID } from 'crypto'; import { Penalty } from '../../domain/entities/penalty/Penalty'; import { PenaltyRepository } from '../../domain/repositories/PenaltyRepository'; import { RaceRepository } from '../../domain/repositories/RaceRepository'; import { LeagueMembershipRepository } from '../../domain/repositories/LeagueMembershipRepository'; export type QuickPenaltyErrorCode = 'RACE_NOT_FOUND' | 'UNAUTHORIZED' | 'UNKNOWN_INFRACTION' | 'REPOSITORY_ERROR'; export type QuickPenaltyApplicationError = ApplicationErrorCode; export type QuickPenaltyInput = { raceId: string; driverId: string; adminId: string; infractionType: 'track_limits' | 'unsafe_rejoin' | 'aggressive_driving' | 'false_start' | 'other'; severity: 'warning' | 'minor' | 'major' | 'severe'; notes?: string; }; export type QuickPenaltyResult = { penaltyId: string; raceId: string; driverId: string; type: string; value?: number; reason: string; }; export class QuickPenaltyUseCase { constructor( private readonly penaltyRepository: PenaltyRepository, private readonly raceRepository: RaceRepository, private readonly leagueMembershipRepository: LeagueMembershipRepository, private readonly logger: Logger, ) {} async execute(input: QuickPenaltyInput): Promise> { this.logger.debug('Executing QuickPenaltyUseCase', { input }); try { // Validate race exists const race = await this.raceRepository.findById(input.raceId); if (!race) { this.logger.warn('Race not found', { raceId: input.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.toString() === input.adminId && m.status.toString() === 'active' ); if (!adminMembership || (adminMembership.role.toString() !== 'owner' && adminMembership.role.toString() !== 'admin')) { this.logger.warn('Unauthorized admin attempting to issue penalty', { adminId: input.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( input.infractionType, input.severity ); if (!penaltyMapping) { this.logger.error( 'Unknown infraction type', undefined, { infractionType: input.infractionType, severity: input.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: input.raceId, driverId: input.driverId, type, ...(value !== undefined ? { value } : {}), reason, issuedBy: input.adminId, status: 'applied', // Quick penalties are applied immediately issuedAt: new Date(), appliedAt: new Date(), ...(input.notes !== undefined ? { notes: input.notes } : {}), }); await this.penaltyRepository.create(penalty); const result: QuickPenaltyResult = { penaltyId: penalty.id.toString(), raceId: input.raceId, driverId: input.driverId, type, ...(value !== undefined ? { value } : {}), reason, }; this.logger.info('Quick penalty applied successfully', { penaltyId: penalty.id, raceId: input.raceId, driverId: input.driverId }); return Result.ok(result); } catch (error: unknown) { const err = error instanceof Error ? error : new Error('Failed to apply quick penalty'); this.logger.error('Failed to apply quick penalty', err); return Result.err({ code: 'REPOSITORY_ERROR', details: { message: err.message }, }); } } private mapInfractionToPenalty( infractionType: QuickPenaltyInput['infractionType'], severity: QuickPenaltyInput['severity'] ): { type: string; value?: number; reason: string } | null { const severityMultipliers: Record = { 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; } } }