Files
gridpilot.gg/packages/racing/domain/entities/Prize.ts
2025-12-12 14:23:40 +01:00

160 lines
3.9 KiB
TypeScript

/**
* Domain Entity: Prize
*
* Represents a prize awarded to a driver for a specific position in a season.
*/
import { RacingDomainValidationError, RacingDomainInvariantError } from '../errors/RacingDomainError';
import type { IEntity } from '@gridpilot/shared/domain';
import type { Money } from '../value-objects/Money';
export type PrizeStatus = 'pending' | 'awarded' | 'paid' | 'cancelled';
export interface PrizeProps {
id: string;
seasonId: string;
position: number;
amount: Money;
driverId?: string;
status: PrizeStatus;
createdAt: Date;
awardedAt: Date | undefined;
paidAt: Date | undefined;
description: string | undefined;
}
export class Prize implements IEntity<string> {
readonly id: string;
readonly seasonId: string;
readonly position: number;
readonly amount: Money;
readonly driverId: string | undefined;
readonly status: PrizeStatus;
readonly createdAt: Date;
readonly awardedAt: Date | undefined;
readonly paidAt: Date | undefined;
readonly description: string | undefined;
private constructor(props: PrizeProps) {
this.id = props.id;
this.seasonId = props.seasonId;
this.position = props.position;
this.amount = props.amount;
this.driverId = props.driverId;
this.status = props.status;
this.createdAt = props.createdAt ?? new Date();
this.awardedAt = props.awardedAt;
this.paidAt = props.paidAt;
this.description = props.description;
}
static create(props: Omit<PrizeProps, 'createdAt' | 'status'> & {
createdAt?: Date;
status?: PrizeStatus;
}): Prize {
this.validate(props);
return new Prize({
...props,
createdAt: props.createdAt ?? new Date(),
status: props.status ?? 'pending',
});
}
private static validate(props: Omit<PrizeProps, 'createdAt' | 'status'>): void {
if (!props.id || props.id.trim().length === 0) {
throw new RacingDomainValidationError('Prize ID is required');
}
if (!props.seasonId || props.seasonId.trim().length === 0) {
throw new RacingDomainValidationError('Prize seasonId is required');
}
if (!Number.isInteger(props.position) || props.position < 1) {
throw new RacingDomainValidationError('Prize position must be a positive integer');
}
if (!props.amount) {
throw new RacingDomainValidationError('Prize amount is required');
}
if (props.amount.amount <= 0) {
throw new RacingDomainValidationError('Prize amount must be greater than zero');
}
}
/**
* Award prize to a driver
*/
awardTo(driverId: string): Prize {
if (!driverId || driverId.trim().length === 0) {
throw new RacingDomainValidationError('Driver ID is required to award prize');
}
if (this.status !== 'pending') {
throw new RacingDomainInvariantError('Only pending prizes can be awarded');
}
return new Prize({
...this,
driverId,
status: 'awarded',
awardedAt: new Date(),
});
}
/**
* Mark prize as paid
*/
markAsPaid(): Prize {
if (this.status !== 'awarded') {
throw new RacingDomainInvariantError('Only awarded prizes can be marked as paid');
}
if (!this.driverId) {
throw new RacingDomainInvariantError('Prize must have a driver to be paid');
}
return new Prize({
...this,
status: 'paid',
paidAt: new Date(),
});
}
/**
* Cancel prize
*/
cancel(): Prize {
if (this.status === 'paid') {
throw new RacingDomainInvariantError('Cannot cancel a paid prize');
}
return new Prize({
...this,
status: 'cancelled',
});
}
/**
* Check if prize is pending
*/
isPending(): boolean {
return this.status === 'pending';
}
/**
* Check if prize is awarded
*/
isAwarded(): boolean {
return this.status === 'awarded';
}
/**
* Check if prize is paid
*/
isPaid(): boolean {
return this.status === 'paid';
}
}