/** * Application Use Case: ApplyPenaltyUseCase * * Allows a steward to apply a penalty to a driver for an incident during a race. * The penalty can be standalone or linked to an upheld protest. */ import { Penalty, type PenaltyType } from '../../domain/entities/Penalty'; import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository'; import type { IProtestRepository } from '../../domain/repositories/IProtestRepository'; import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'; import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository'; import { randomUUID } from 'crypto'; import type { AsyncUseCase } from '@gridpilot/shared/application'; export interface ApplyPenaltyCommand { raceId: string; driverId: string; stewardId: string; type: PenaltyType; value?: number; reason: string; protestId?: string; notes?: string; } export class ApplyPenaltyUseCase implements AsyncUseCase { constructor( private readonly penaltyRepository: IPenaltyRepository, private readonly protestRepository: IProtestRepository, private readonly raceRepository: IRaceRepository, private readonly leagueMembershipRepository: ILeagueMembershipRepository, ) {} async execute(command: ApplyPenaltyCommand): Promise<{ penaltyId: string }> { // Validate race exists const race = await this.raceRepository.findById(command.raceId); if (!race) { throw new Error('Race not found'); } // Validate steward has authority (owner or admin of the league) const memberships = await this.leagueMembershipRepository.getLeagueMembers(race.leagueId); const stewardMembership = memberships.find( m => m.driverId === command.stewardId && m.status === 'active' ); if (!stewardMembership || (stewardMembership.role !== 'owner' && stewardMembership.role !== 'admin')) { throw new Error('Only league owners and admins can apply penalties'); } // If linked to a protest, validate the protest exists and is upheld if (command.protestId) { const protest = await this.protestRepository.findById(command.protestId); if (!protest) { throw new Error('Protest not found'); } if (protest.status !== 'upheld') { throw new Error('Can only create penalties for upheld protests'); } if (protest.raceId !== command.raceId) { throw new Error('Protest is not for this race'); } } // Create the penalty const penalty = Penalty.create({ id: randomUUID(), raceId: command.raceId, driverId: command.driverId, type: command.type, ...(command.value !== undefined ? { value: command.value } : {}), reason: command.reason, ...(command.protestId !== undefined ? { protestId: command.protestId } : {}), issuedBy: command.stewardId, status: 'pending', issuedAt: new Date(), ...(command.notes !== undefined ? { notes: command.notes } : {}), }); await this.penaltyRepository.create(penalty); return { penaltyId: penalty.id }; } }