182 lines
6.5 KiB
TypeScript
182 lines
6.5 KiB
TypeScript
/**
|
|
* 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 } 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 { Logger, UseCaseOutputPort } from '@core/shared/application';
|
|
import { Result } from '@core/shared/application/Result';
|
|
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
|
|
|
export type QuickPenaltyErrorCode = 'RACE_NOT_FOUND' | 'UNAUTHORIZED' | 'UNKNOWN_INFRACTION' | 'REPOSITORY_ERROR';
|
|
|
|
export type QuickPenaltyApplicationError = ApplicationErrorCode<QuickPenaltyErrorCode, { message: string }>;
|
|
|
|
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: IPenaltyRepository,
|
|
private readonly raceRepository: IRaceRepository,
|
|
private readonly leagueMembershipRepository: ILeagueMembershipRepository,
|
|
private readonly logger: Logger,
|
|
private readonly output: UseCaseOutputPort<QuickPenaltyResult>,
|
|
) {}
|
|
|
|
async execute(input: QuickPenaltyInput): Promise<Result<void, QuickPenaltyApplicationError>> {
|
|
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', { 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,
|
|
raceId: input.raceId,
|
|
driverId: input.driverId,
|
|
type,
|
|
...(value !== undefined ? { value } : {}),
|
|
reason,
|
|
};
|
|
|
|
this.output.present(result);
|
|
|
|
this.logger.info('Quick penalty applied successfully', { penaltyId: penalty.id, raceId: input.raceId, driverId: input.driverId });
|
|
return Result.ok(undefined);
|
|
} 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: QuickPenaltyInput['infractionType'],
|
|
severity: QuickPenaltyInput['severity']
|
|
): { type: string; value?: number; reason: string } | null {
|
|
const severityMultipliers: Record<QuickPenaltyInput['severity'], number> = {
|
|
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;
|
|
}
|
|
}
|
|
} |