wip
This commit is contained in:
@@ -1,10 +1,18 @@
|
||||
/**
|
||||
* Domain Entity: Protest
|
||||
*
|
||||
*
|
||||
* Represents a protest filed by a driver against another driver for an incident during a race.
|
||||
*
|
||||
* Workflow states:
|
||||
* - pending: Initial state when protest is filed
|
||||
* - awaiting_defense: Defense has been requested from the accused driver
|
||||
* - under_review: Steward is actively reviewing the protest
|
||||
* - upheld: Protest was upheld (penalty will be applied)
|
||||
* - dismissed: Protest was dismissed (no action taken)
|
||||
* - withdrawn: Protesting driver withdrew the protest
|
||||
*/
|
||||
|
||||
export type ProtestStatus = 'pending' | 'under_review' | 'upheld' | 'dismissed' | 'withdrawn';
|
||||
export type ProtestStatus = 'pending' | 'awaiting_defense' | 'under_review' | 'upheld' | 'dismissed' | 'withdrawn';
|
||||
|
||||
export interface ProtestIncident {
|
||||
/** Lap number where the incident occurred */
|
||||
@@ -15,6 +23,15 @@ export interface ProtestIncident {
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface ProtestDefense {
|
||||
/** The accused driver's statement/defense */
|
||||
statement: string;
|
||||
/** URL to defense video clip (optional) */
|
||||
videoUrl?: string;
|
||||
/** Timestamp when defense was submitted */
|
||||
submittedAt: Date;
|
||||
}
|
||||
|
||||
export interface ProtestProps {
|
||||
id: string;
|
||||
raceId: string;
|
||||
@@ -38,6 +55,12 @@ export interface ProtestProps {
|
||||
filedAt: Date;
|
||||
/** Timestamp when the protest was reviewed */
|
||||
reviewedAt?: Date;
|
||||
/** Defense from the accused driver (if requested and submitted) */
|
||||
defense?: ProtestDefense;
|
||||
/** Timestamp when defense was requested */
|
||||
defenseRequestedAt?: Date;
|
||||
/** ID of the steward who requested defense */
|
||||
defenseRequestedBy?: string;
|
||||
}
|
||||
|
||||
export class Protest {
|
||||
@@ -71,11 +94,18 @@ export class Protest {
|
||||
get decisionNotes(): string | undefined { return this.props.decisionNotes; }
|
||||
get filedAt(): Date { return this.props.filedAt; }
|
||||
get reviewedAt(): Date | undefined { return this.props.reviewedAt; }
|
||||
get defense(): ProtestDefense | undefined { return this.props.defense ? { ...this.props.defense } : undefined; }
|
||||
get defenseRequestedAt(): Date | undefined { return this.props.defenseRequestedAt; }
|
||||
get defenseRequestedBy(): string | undefined { return this.props.defenseRequestedBy; }
|
||||
|
||||
isPending(): boolean {
|
||||
return this.props.status === 'pending';
|
||||
}
|
||||
|
||||
isAwaitingDefense(): boolean {
|
||||
return this.props.status === 'awaiting_defense';
|
||||
}
|
||||
|
||||
isUnderReview(): boolean {
|
||||
return this.props.status === 'under_review';
|
||||
}
|
||||
@@ -84,12 +114,60 @@ export class Protest {
|
||||
return ['upheld', 'dismissed', 'withdrawn'].includes(this.props.status);
|
||||
}
|
||||
|
||||
hasDefense(): boolean {
|
||||
return this.props.defense !== undefined;
|
||||
}
|
||||
|
||||
canRequestDefense(): boolean {
|
||||
return this.isPending() && !this.hasDefense() && !this.props.defenseRequestedAt;
|
||||
}
|
||||
|
||||
canSubmitDefense(): boolean {
|
||||
return this.isAwaitingDefense() && !this.hasDefense();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start reviewing the protest
|
||||
* Request defense from the accused driver
|
||||
*/
|
||||
requestDefense(stewardId: string): Protest {
|
||||
if (!this.canRequestDefense()) {
|
||||
throw new Error('Defense can only be requested for pending protests without existing defense');
|
||||
}
|
||||
return new Protest({
|
||||
...this.props,
|
||||
status: 'awaiting_defense',
|
||||
defenseRequestedAt: new Date(),
|
||||
defenseRequestedBy: stewardId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit defense from the accused driver
|
||||
*/
|
||||
submitDefense(statement: string, videoUrl?: string): Protest {
|
||||
if (!this.canSubmitDefense()) {
|
||||
throw new Error('Defense can only be submitted when protest is awaiting defense');
|
||||
}
|
||||
if (!statement?.trim()) {
|
||||
throw new Error('Defense statement is required');
|
||||
}
|
||||
return new Protest({
|
||||
...this.props,
|
||||
status: 'under_review',
|
||||
defense: {
|
||||
statement: statement.trim(),
|
||||
videoUrl,
|
||||
submittedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start reviewing the protest (without requiring defense)
|
||||
*/
|
||||
startReview(stewardId: string): Protest {
|
||||
if (!this.isPending()) {
|
||||
throw new Error('Only pending protests can be put under review');
|
||||
if (!this.isPending() && !this.isAwaitingDefense()) {
|
||||
throw new Error('Only pending or awaiting-defense protests can be put under review');
|
||||
}
|
||||
return new Protest({
|
||||
...this.props,
|
||||
@@ -102,8 +180,8 @@ export class Protest {
|
||||
* 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');
|
||||
if (!this.isPending() && !this.isUnderReview() && !this.isAwaitingDefense()) {
|
||||
throw new Error('Only pending, awaiting-defense, or under-review protests can be upheld');
|
||||
}
|
||||
return new Protest({
|
||||
...this.props,
|
||||
@@ -118,8 +196,8 @@ export class Protest {
|
||||
* 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');
|
||||
if (!this.isPending() && !this.isUnderReview() && !this.isAwaitingDefense()) {
|
||||
throw new Error('Only pending, awaiting-defense, or under-review protests can be dismissed');
|
||||
}
|
||||
return new Protest({
|
||||
...this.props,
|
||||
|
||||
Reference in New Issue
Block a user