/** * 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 { 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 & { 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): 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'; } }