refactor racing use cases

This commit is contained in:
2025-12-21 00:43:42 +01:00
parent e9d6f90bb2
commit c12656d671
308 changed files with 14401 additions and 7419 deletions

View File

@@ -5,67 +5,76 @@
* Designed for fast, common penalty scenarios like track limits, warnings, etc.
*/
import { Penalty, type PenaltyType } from '../../domain/entities/Penalty';
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 { AsyncUseCase , Logger } from '@core/shared/application';
import type { Logger, UseCaseOutputPort } 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';
export type QuickPenaltyErrorCode = 'RACE_NOT_FOUND' | 'UNAUTHORIZED' | 'UNKNOWN_INFRACTION' | 'REPOSITORY_ERROR';
type QuickPenaltyApplicationError = ApplicationErrorCode<QuickPenaltyErrorCode, { message: string }>;
export type QuickPenaltyApplicationError = ApplicationErrorCode<QuickPenaltyErrorCode, { message: string }>;
export interface QuickPenaltyCommand {
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 class QuickPenaltyUseCase
implements AsyncUseCase<QuickPenaltyCommand, { penaltyId: string }, QuickPenaltyErrorCode> {
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(command: QuickPenaltyCommand): Promise<Result<{ penaltyId: string }, QuickPenaltyApplicationError>> {
this.logger.debug('Executing QuickPenaltyUseCase', { command });
async execute(input: QuickPenaltyInput): Promise<Result<void, QuickPenaltyApplicationError>> {
this.logger.debug('Executing QuickPenaltyUseCase', { input });
try {
// Validate race exists
const race = await this.raceRepository.findById(command.raceId);
const race = await this.raceRepository.findById(input.raceId);
if (!race) {
this.logger.warn('Race not found', { raceId: command.raceId });
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 === command.adminId && m.status === 'active'
m => m.driverId.toString() === input.adminId && m.status.toString() === 'active'
);
if (!adminMembership || (adminMembership.role !== 'owner' && adminMembership.role !== 'admin')) {
this.logger.warn('Unauthorized admin attempting to issue penalty', { adminId: command.adminId, leagueId: race.leagueId });
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(
command.infractionType,
command.severity
input.infractionType,
input.severity
);
if (!penaltyMapping) {
this.logger.error('Unknown infraction type', { infractionType: command.infractionType, severity: command.severity });
this.logger.error('Unknown infraction type', { infractionType: input.infractionType, severity: input.severity });
return Result.err({ code: 'UNKNOWN_INFRACTION', details: { message: 'Unknown infraction type' } });
}
@@ -75,22 +84,33 @@ export class QuickPenaltyUseCase
const penalty = Penalty.create({
id: randomUUID(),
leagueId: race.leagueId,
raceId: command.raceId,
driverId: command.driverId,
raceId: input.raceId,
driverId: input.driverId,
type,
...(value !== undefined ? { value } : {}),
reason,
issuedBy: command.adminId,
issuedBy: input.adminId,
status: 'applied', // Quick penalties are applied immediately
issuedAt: new Date(),
appliedAt: new Date(),
...(command.notes !== undefined ? { notes: command.notes } : {}),
...(input.notes !== undefined ? { notes: input.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 });
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' } });
@@ -98,10 +118,10 @@ export class QuickPenaltyUseCase
}
private mapInfractionToPenalty(
infractionType: QuickPenaltyCommand['infractionType'],
severity: QuickPenaltyCommand['severity']
): { type: PenaltyType; value?: number; reason: string } | null {
const severityMultipliers = {
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,