/** * Domain Entity: Protest * * Represents a protest filed by a driver against another driver for an incident during a race. */ export type ProtestStatus = 'pending' | 'under_review' | 'upheld' | 'dismissed' | 'withdrawn'; export interface ProtestIncident { /** Lap number where the incident occurred */ lap: number; /** Time in the race (seconds from start, or timestamp) */ timeInRace?: number; /** Brief description of the incident */ description: string; } export interface ProtestProps { id: string; raceId: string; /** The driver filing the protest */ protestingDriverId: string; /** The driver being protested against */ accusedDriverId: string; /** Details of the incident */ incident: ProtestIncident; /** Optional comment/statement from the protesting driver */ comment?: string; /** URL to proof video clip */ proofVideoUrl?: string; /** Current status of the protest */ status: ProtestStatus; /** ID of the steward/admin who reviewed (if any) */ reviewedBy?: string; /** Decision notes from the steward */ decisionNotes?: string; /** Timestamp when the protest was filed */ filedAt: Date; /** Timestamp when the protest was reviewed */ reviewedAt?: Date; } export class Protest { private constructor(private readonly props: ProtestProps) {} static create(props: ProtestProps): Protest { if (!props.id) throw new Error('Protest ID is required'); if (!props.raceId) throw new Error('Race ID is required'); if (!props.protestingDriverId) throw new Error('Protesting driver ID is required'); if (!props.accusedDriverId) throw new Error('Accused driver ID is required'); if (!props.incident) throw new Error('Incident details are required'); if (props.incident.lap < 0) throw new Error('Lap number must be non-negative'); if (!props.incident.description?.trim()) throw new Error('Incident description is required'); return new Protest({ ...props, status: props.status || 'pending', filedAt: props.filedAt || new Date(), }); } get id(): string { return this.props.id; } get raceId(): string { return this.props.raceId; } get protestingDriverId(): string { return this.props.protestingDriverId; } get accusedDriverId(): string { return this.props.accusedDriverId; } get incident(): ProtestIncident { return { ...this.props.incident }; } get comment(): string | undefined { return this.props.comment; } get proofVideoUrl(): string | undefined { return this.props.proofVideoUrl; } get status(): ProtestStatus { return this.props.status; } get reviewedBy(): string | undefined { return this.props.reviewedBy; } get decisionNotes(): string | undefined { return this.props.decisionNotes; } get filedAt(): Date { return this.props.filedAt; } get reviewedAt(): Date | undefined { return this.props.reviewedAt; } isPending(): boolean { return this.props.status === 'pending'; } isUnderReview(): boolean { return this.props.status === 'under_review'; } isResolved(): boolean { return ['upheld', 'dismissed', 'withdrawn'].includes(this.props.status); } /** * Start reviewing the protest */ startReview(stewardId: string): Protest { if (!this.isPending()) { throw new Error('Only pending protests can be put under review'); } return new Protest({ ...this.props, status: 'under_review', reviewedBy: stewardId, }); } /** * Uphold the protest (finding the accused guilty) */ uphold(stewardId: string, decisionNotes: string): Protest { if (!this.isPending() && !this.isUnderReview()) { throw new Error('Only pending or under-review protests can be upheld'); } return new Protest({ ...this.props, status: 'upheld', reviewedBy: stewardId, decisionNotes, reviewedAt: new Date(), }); } /** * Dismiss the protest (finding no fault) */ dismiss(stewardId: string, decisionNotes: string): Protest { if (!this.isPending() && !this.isUnderReview()) { throw new Error('Only pending or under-review protests can be dismissed'); } return new Protest({ ...this.props, status: 'dismissed', reviewedBy: stewardId, decisionNotes, reviewedAt: new Date(), }); } /** * Withdraw the protest (by the protesting driver) */ withdraw(): Protest { if (this.isResolved()) { throw new Error('Cannot withdraw a resolved protest'); } return new Protest({ ...this.props, status: 'withdrawn', reviewedAt: new Date(), }); } }