146 lines
4.5 KiB
TypeScript
146 lines
4.5 KiB
TypeScript
/**
|
|
* 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(),
|
|
});
|
|
}
|
|
} |