harden business rules
This commit is contained in:
@@ -1,14 +1,10 @@
|
||||
/**
|
||||
* Domain Value Object: LeagueVisibility
|
||||
*
|
||||
*
|
||||
* Represents the visibility and ranking status of a league.
|
||||
*
|
||||
* - 'ranked' (public): Competitive leagues visible to everyone, affects driver ratings.
|
||||
* Requires minimum 10 players to ensure competitive integrity.
|
||||
* - 'unranked' (private): Casual leagues for friends/private groups, no rating impact.
|
||||
* Can have any number of players.
|
||||
* This is a hardened version that enforces strict business rules.
|
||||
*/
|
||||
|
||||
|
||||
import type { IValueObject } from '@core/shared/domain';
|
||||
import { RacingDomainValidationError } from '../errors/RacingDomainError';
|
||||
|
||||
@@ -16,6 +12,7 @@ export type LeagueVisibilityType = 'ranked' | 'unranked';
|
||||
|
||||
export interface LeagueVisibilityConstraints {
|
||||
readonly minDrivers: number;
|
||||
readonly maxDrivers: number;
|
||||
readonly isPubliclyVisible: boolean;
|
||||
readonly affectsRatings: boolean;
|
||||
readonly requiresApproval: boolean;
|
||||
@@ -24,22 +21,24 @@ export interface LeagueVisibilityConstraints {
|
||||
const VISIBILITY_CONSTRAINTS: Record<LeagueVisibilityType, LeagueVisibilityConstraints> = {
|
||||
ranked: {
|
||||
minDrivers: 10,
|
||||
maxDrivers: 100,
|
||||
isPubliclyVisible: true,
|
||||
affectsRatings: true,
|
||||
requiresApproval: false, // Anyone can join public leagues
|
||||
requiresApproval: false,
|
||||
},
|
||||
unranked: {
|
||||
minDrivers: 2,
|
||||
maxDrivers: 50,
|
||||
isPubliclyVisible: false,
|
||||
affectsRatings: false,
|
||||
requiresApproval: true, // Private leagues require invite/approval
|
||||
requiresApproval: true,
|
||||
},
|
||||
};
|
||||
|
||||
export interface LeagueVisibilityProps {
|
||||
type: LeagueVisibilityType;
|
||||
}
|
||||
|
||||
|
||||
export class LeagueVisibility implements IValueObject<LeagueVisibilityProps> {
|
||||
readonly type: LeagueVisibilityType;
|
||||
readonly constraints: LeagueVisibilityConstraints;
|
||||
@@ -58,7 +57,6 @@ export class LeagueVisibility implements IValueObject<LeagueVisibilityProps> {
|
||||
}
|
||||
|
||||
static fromString(value: string): LeagueVisibility {
|
||||
// Support both old ('public'/'private') and new ('ranked'/'unranked') terminology
|
||||
if (value === 'ranked' || value === 'public') {
|
||||
return LeagueVisibility.ranked();
|
||||
}
|
||||
@@ -70,32 +68,76 @@ export class LeagueVisibility implements IValueObject<LeagueVisibilityProps> {
|
||||
|
||||
/**
|
||||
* Validates that the given driver count meets the minimum requirement
|
||||
* for this visibility type.
|
||||
*/
|
||||
validateDriverCount(driverCount: number): { valid: boolean; error?: string } {
|
||||
if (driverCount < this.constraints.minDrivers) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `${this.type === 'ranked' ? 'Ranked' : 'Unranked'} leagues require at least ${this.constraints.minDrivers} drivers`,
|
||||
error: `${this.type === 'ranked' ? 'Ranked' : 'Unranked'} leagues require at least ${this.constraints.minDrivers} drivers`
|
||||
};
|
||||
}
|
||||
if (driverCount > this.constraints.maxDrivers) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `${this.type === 'ranked' ? 'Ranked' : 'Unranked'} leagues cannot exceed ${this.constraints.maxDrivers} drivers`
|
||||
};
|
||||
}
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this is a ranked/public league
|
||||
* Validates that the given max participants is appropriate for this visibility
|
||||
*/
|
||||
validateMaxParticipants(maxParticipants: number): { valid: boolean; error?: string } {
|
||||
if (maxParticipants < this.constraints.minDrivers) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Max participants must be at least ${this.constraints.minDrivers} for ${this.type} leagues`
|
||||
};
|
||||
}
|
||||
if (maxParticipants > this.constraints.maxDrivers) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Max participants cannot exceed ${this.constraints.maxDrivers} for ${this.type} leagues`
|
||||
};
|
||||
}
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is a ranked/public league
|
||||
*/
|
||||
isRanked(): boolean {
|
||||
return this.type === 'ranked';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this is an unranked/private league
|
||||
* Check if this is an unranked/private league
|
||||
*/
|
||||
isUnranked(): boolean {
|
||||
return this.type === 'unranked';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get minimum required drivers
|
||||
*/
|
||||
getMinDrivers(): number {
|
||||
return this.constraints.minDrivers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get maximum allowed drivers
|
||||
*/
|
||||
getMaxDrivers(): number {
|
||||
return this.constraints.maxDrivers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given driver count meets minimum requirements
|
||||
*/
|
||||
meetsMinimumForVisibility(driverCount: number): boolean {
|
||||
return driverCount >= this.constraints.minDrivers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to string for serialization
|
||||
@@ -104,10 +146,6 @@ export class LeagueVisibility implements IValueObject<LeagueVisibilityProps> {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
get props(): LeagueVisibilityProps {
|
||||
return { type: this.type };
|
||||
}
|
||||
|
||||
/**
|
||||
* For backward compatibility with existing 'public'/'private' terminology
|
||||
*/
|
||||
@@ -115,6 +153,10 @@ export class LeagueVisibility implements IValueObject<LeagueVisibilityProps> {
|
||||
return this.type === 'ranked' ? 'public' : 'private';
|
||||
}
|
||||
|
||||
get props(): LeagueVisibilityProps {
|
||||
return { type: this.type };
|
||||
}
|
||||
|
||||
equals(other: IValueObject<LeagueVisibilityProps>): boolean {
|
||||
return this.props.type === other.props.type;
|
||||
}
|
||||
@@ -122,4 +164,6 @@ export class LeagueVisibility implements IValueObject<LeagueVisibilityProps> {
|
||||
|
||||
// Export constants for validation
|
||||
export const MIN_RANKED_LEAGUE_DRIVERS = VISIBILITY_CONSTRAINTS.ranked.minDrivers;
|
||||
export const MIN_UNRANKED_LEAGUE_DRIVERS = VISIBILITY_CONSTRAINTS.unranked.minDrivers;
|
||||
export const MAX_RANKED_LEAGUE_DRIVERS = VISIBILITY_CONSTRAINTS.ranked.maxDrivers;
|
||||
export const MIN_UNRANKED_LEAGUE_DRIVERS = VISIBILITY_CONSTRAINTS.unranked.minDrivers;
|
||||
export const MAX_UNRANKED_LEAGUE_DRIVERS = VISIBILITY_CONSTRAINTS.unranked.maxDrivers;
|
||||
97
core/racing/domain/value-objects/MaxParticipants.ts
Normal file
97
core/racing/domain/value-objects/MaxParticipants.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Domain Value Object: MaxParticipants
|
||||
*
|
||||
* Represents the maximum number of participants allowed in a league or race.
|
||||
* Enforces reasonable limits and constraints.
|
||||
*/
|
||||
|
||||
import type { IValueObject } from '@core/shared/domain';
|
||||
import { RacingDomainValidationError } from '../errors/RacingDomainError';
|
||||
|
||||
export interface MaxParticipantsProps {
|
||||
value: number;
|
||||
}
|
||||
|
||||
export class MaxParticipants implements IValueObject<MaxParticipantsProps> {
|
||||
readonly value: number;
|
||||
|
||||
private constructor(value: number) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
static create(value: number): MaxParticipants {
|
||||
if (!Number.isInteger(value) || value <= 0) {
|
||||
throw new RacingDomainValidationError('Max participants must be a positive integer');
|
||||
}
|
||||
|
||||
// Enforce reasonable upper limit to prevent system abuse
|
||||
if (value > 100) {
|
||||
throw new RacingDomainValidationError('Max participants cannot exceed 100');
|
||||
}
|
||||
|
||||
return new MaxParticipants(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that max participants meets minimum requirements for ranked leagues
|
||||
*/
|
||||
validateForRankedLeague(): { valid: boolean; error?: string } {
|
||||
if (this.value < 10) {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Ranked leagues must allow at least 10 participants'
|
||||
};
|
||||
}
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that max participants meets minimum requirements for unranked leagues
|
||||
*/
|
||||
validateForUnrankedLeague(): { valid: boolean; error?: string } {
|
||||
if (this.value < 2) {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Unranked leagues must allow at least 2 participants'
|
||||
};
|
||||
}
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if max participants is sufficient for the given participant count
|
||||
*/
|
||||
canAccommodate(participantCount: number): boolean {
|
||||
return participantCount <= this.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if max participants is at least the given minimum
|
||||
*/
|
||||
isAtLeast(min: number): boolean {
|
||||
return this.value >= min;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if max participants is exactly the given value
|
||||
*/
|
||||
isExactly(value: number): boolean {
|
||||
return this.value === value;
|
||||
}
|
||||
|
||||
get props(): MaxParticipantsProps {
|
||||
return { value: this.value };
|
||||
}
|
||||
|
||||
equals(other: IValueObject<MaxParticipantsProps>): boolean {
|
||||
return this.value === other.props.value;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.value.toString();
|
||||
}
|
||||
|
||||
toNumber(): number {
|
||||
return this.value;
|
||||
}
|
||||
}
|
||||
121
core/racing/domain/value-objects/ParticipantCount.ts
Normal file
121
core/racing/domain/value-objects/ParticipantCount.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Domain Value Object: ParticipantCount
|
||||
*
|
||||
* Represents the number of participants in a league or race.
|
||||
* Enforces constraints based on league visibility and other business rules.
|
||||
*/
|
||||
|
||||
import type { IValueObject } from '@core/shared/domain';
|
||||
import { RacingDomainValidationError } from '../errors/RacingDomainError';
|
||||
|
||||
export interface ParticipantCountProps {
|
||||
value: number;
|
||||
}
|
||||
|
||||
export class ParticipantCount implements IValueObject<ParticipantCountProps> {
|
||||
readonly value: number;
|
||||
|
||||
private constructor(value: number) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
static create(value: number): ParticipantCount {
|
||||
if (!Number.isInteger(value) || value < 0) {
|
||||
throw new RacingDomainValidationError('Participant count must be a non-negative integer');
|
||||
}
|
||||
return new ParticipantCount(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate against minimum requirements for ranked leagues
|
||||
*/
|
||||
validateForRankedLeague(): { valid: boolean; error?: string } {
|
||||
if (this.value < 10) {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Ranked leagues require at least 10 participants'
|
||||
};
|
||||
}
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate against minimum requirements for unranked leagues
|
||||
*/
|
||||
validateForUnrankedLeague(): { valid: boolean; error?: string } {
|
||||
if (this.value < 2) {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Unranked leagues require at least 2 participants'
|
||||
};
|
||||
}
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate against maximum capacity
|
||||
*/
|
||||
validateAgainstMax(maxParticipants: number): { valid: boolean; error?: string } {
|
||||
if (this.value > maxParticipants) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Participant count (${this.value}) exceeds maximum capacity (${maxParticipants})`
|
||||
};
|
||||
}
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if count meets minimum for given visibility type
|
||||
*/
|
||||
meetsMinimumForVisibility(isRanked: boolean): boolean {
|
||||
return isRanked ? this.value >= 10 : this.value >= 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment count by 1
|
||||
*/
|
||||
increment(): ParticipantCount {
|
||||
return new ParticipantCount(this.value + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrement count by 1 (if > 0)
|
||||
*/
|
||||
decrement(): ParticipantCount {
|
||||
if (this.value === 0) {
|
||||
throw new RacingDomainValidationError('Cannot decrement below zero');
|
||||
}
|
||||
return new ParticipantCount(this.value - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if count is zero
|
||||
*/
|
||||
isZero(): boolean {
|
||||
return this.value === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if count is at least the given minimum
|
||||
*/
|
||||
isAtLeast(min: number): boolean {
|
||||
return this.value >= min;
|
||||
}
|
||||
|
||||
get props(): ParticipantCountProps {
|
||||
return { value: this.value };
|
||||
}
|
||||
|
||||
equals(other: IValueObject<ParticipantCountProps>): boolean {
|
||||
return this.value === other.props.value;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.value.toString();
|
||||
}
|
||||
|
||||
toNumber(): number {
|
||||
return this.value;
|
||||
}
|
||||
}
|
||||
128
core/racing/domain/value-objects/RaceStatus.ts
Normal file
128
core/racing/domain/value-objects/RaceStatus.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* Domain Value Object: RaceStatus
|
||||
*
|
||||
* Represents the status of a race with strict lifecycle rules.
|
||||
*/
|
||||
|
||||
import type { IValueObject } from '@core/shared/domain';
|
||||
import { RacingDomainValidationError } from '../errors/RacingDomainError';
|
||||
|
||||
export type RaceStatusValue = 'scheduled' | 'running' | 'completed' | 'cancelled';
|
||||
|
||||
export interface RaceStatusProps {
|
||||
value: RaceStatusValue;
|
||||
}
|
||||
|
||||
export class RaceStatus implements IValueObject<RaceStatusProps> {
|
||||
readonly value: RaceStatusValue;
|
||||
|
||||
private constructor(value: RaceStatusValue) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
static create(value: RaceStatusValue): RaceStatus {
|
||||
if (!value) {
|
||||
throw new RacingDomainValidationError('Race status is required');
|
||||
}
|
||||
return new RaceStatus(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if race can be started
|
||||
*/
|
||||
canStart(): boolean {
|
||||
return this.value === 'scheduled';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if race can be completed
|
||||
*/
|
||||
canComplete(): boolean {
|
||||
return this.value === 'running';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if race can be cancelled
|
||||
*/
|
||||
canCancel(): boolean {
|
||||
return this.value === 'scheduled' || this.value === 'running';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if race can be reopened
|
||||
*/
|
||||
canReopen(): boolean {
|
||||
return this.value === 'completed' || this.value === 'cancelled';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if race is in a terminal state
|
||||
*/
|
||||
isTerminal(): boolean {
|
||||
return this.value === 'completed' || this.value === 'cancelled';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if race is running
|
||||
*/
|
||||
isRunning(): boolean {
|
||||
return this.value === 'running';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if race is completed
|
||||
*/
|
||||
isCompleted(): boolean {
|
||||
return this.value === 'completed';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if race is scheduled
|
||||
*/
|
||||
isScheduled(): boolean {
|
||||
return this.value === 'scheduled';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if race is cancelled
|
||||
*/
|
||||
isCancelled(): boolean {
|
||||
return this.value === 'cancelled';
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate transition from current status to target status
|
||||
*/
|
||||
canTransitionTo(target: RaceStatusValue): { valid: boolean; error?: string } {
|
||||
const current = this.value;
|
||||
|
||||
// Define allowed transitions
|
||||
const allowedTransitions: Record<RaceStatusValue, RaceStatusValue[]> = {
|
||||
scheduled: ['running', 'cancelled'],
|
||||
running: ['completed', 'cancelled'],
|
||||
completed: [],
|
||||
cancelled: [],
|
||||
};
|
||||
|
||||
if (!allowedTransitions[current].includes(target)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Cannot transition from ${current} to ${target}`
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
get props(): RaceStatusProps {
|
||||
return { value: this.value };
|
||||
}
|
||||
|
||||
equals(other: IValueObject<RaceStatusProps>): boolean {
|
||||
return this.value === other.props.value;
|
||||
}
|
||||
|
||||
toString(): RaceStatusValue {
|
||||
return this.value;
|
||||
}
|
||||
}
|
||||
136
core/racing/domain/value-objects/SeasonStatus.ts
Normal file
136
core/racing/domain/value-objects/SeasonStatus.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* Domain Value Object: SeasonStatus
|
||||
*
|
||||
* Represents the status of a season with strict lifecycle rules.
|
||||
*/
|
||||
|
||||
import type { IValueObject } from '@core/shared/domain';
|
||||
import { RacingDomainValidationError } from '../errors/RacingDomainError';
|
||||
|
||||
export type SeasonStatusValue = 'planned' | 'active' | 'completed' | 'archived' | 'cancelled';
|
||||
|
||||
export interface SeasonStatusProps {
|
||||
value: SeasonStatusValue;
|
||||
}
|
||||
|
||||
export class SeasonStatus implements IValueObject<SeasonStatusProps> {
|
||||
readonly value: SeasonStatusValue;
|
||||
|
||||
private constructor(value: SeasonStatusValue) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
static create(value: SeasonStatusValue): SeasonStatus {
|
||||
if (!value) {
|
||||
throw new RacingDomainValidationError('Season status is required');
|
||||
}
|
||||
return new SeasonStatus(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if season can be activated
|
||||
*/
|
||||
canActivate(): boolean {
|
||||
return this.value === 'planned';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if season can be completed
|
||||
*/
|
||||
canComplete(): boolean {
|
||||
return this.value === 'active';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if season can be archived
|
||||
*/
|
||||
canArchive(): boolean {
|
||||
return this.value === 'completed';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if season can be cancelled
|
||||
*/
|
||||
canCancel(): boolean {
|
||||
return this.value === 'planned' || this.value === 'active';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if season is in a terminal state
|
||||
*/
|
||||
isTerminal(): boolean {
|
||||
return this.value === 'completed' || this.value === 'archived' || this.value === 'cancelled';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if season is active
|
||||
*/
|
||||
isActive(): boolean {
|
||||
return this.value === 'active';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if season is completed
|
||||
*/
|
||||
isCompleted(): boolean {
|
||||
return this.value === 'completed';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if season is planned
|
||||
*/
|
||||
isPlanned(): boolean {
|
||||
return this.value === 'planned';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if season is archived
|
||||
*/
|
||||
isArchived(): boolean {
|
||||
return this.value === 'archived';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if season is cancelled
|
||||
*/
|
||||
isCancelled(): boolean {
|
||||
return this.value === 'cancelled';
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate transition from current status to target status
|
||||
*/
|
||||
canTransitionTo(target: SeasonStatusValue): { valid: boolean; error?: string } {
|
||||
const current = this.value;
|
||||
|
||||
// Define allowed transitions
|
||||
const allowedTransitions: Record<SeasonStatusValue, SeasonStatusValue[]> = {
|
||||
planned: ['active', 'cancelled'],
|
||||
active: ['completed', 'cancelled'],
|
||||
completed: ['archived'],
|
||||
archived: [],
|
||||
cancelled: [],
|
||||
};
|
||||
|
||||
if (!allowedTransitions[current].includes(target)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Cannot transition from ${current} to ${target}`
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
get props(): SeasonStatusProps {
|
||||
return { value: this.value };
|
||||
}
|
||||
|
||||
equals(other: IValueObject<SeasonStatusProps>): boolean {
|
||||
return this.value === other.props.value;
|
||||
}
|
||||
|
||||
toString(): SeasonStatusValue {
|
||||
return this.value;
|
||||
}
|
||||
}
|
||||
98
core/racing/domain/value-objects/SessionDuration.ts
Normal file
98
core/racing/domain/value-objects/SessionDuration.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* Domain Value Object: SessionDuration
|
||||
*
|
||||
* Represents the duration of a racing session in minutes.
|
||||
* Enforces reasonable limits for different session types.
|
||||
*/
|
||||
|
||||
import type { IValueObject } from '@core/shared/domain';
|
||||
import { RacingDomainValidationError } from '../errors/RacingDomainError';
|
||||
|
||||
export interface SessionDurationProps {
|
||||
value: number;
|
||||
}
|
||||
|
||||
export class SessionDuration implements IValueObject<SessionDurationProps> {
|
||||
readonly value: number;
|
||||
|
||||
private constructor(value: number) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
static create(value: number): SessionDuration {
|
||||
if (!Number.isInteger(value) || value <= 0) {
|
||||
throw new RacingDomainValidationError('Session duration must be a positive integer');
|
||||
}
|
||||
|
||||
// Enforce reasonable limits
|
||||
if (value < 15) {
|
||||
throw new RacingDomainValidationError('Session duration must be at least 15 minutes');
|
||||
}
|
||||
|
||||
if (value > 240) {
|
||||
throw new RacingDomainValidationError('Session duration cannot exceed 240 minutes (4 hours)');
|
||||
}
|
||||
|
||||
return new SessionDuration(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if duration is suitable for sprint racing
|
||||
*/
|
||||
isSprint(): boolean {
|
||||
return this.value >= 15 && this.value <= 45;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if duration is suitable for standard racing
|
||||
*/
|
||||
isStandard(): boolean {
|
||||
return this.value > 45 && this.value <= 90;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if duration is suitable for endurance racing
|
||||
*/
|
||||
isEndurance(): boolean {
|
||||
return this.value > 90;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get duration classification
|
||||
*/
|
||||
getClassification(): 'sprint' | 'standard' | 'endurance' {
|
||||
if (this.isSprint()) return 'sprint';
|
||||
if (this.isStandard()) return 'standard';
|
||||
return 'endurance';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if duration is within specified range
|
||||
*/
|
||||
isWithinRange(min: number, max: number): boolean {
|
||||
return this.value >= min && this.value <= max;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get duration in hours
|
||||
*/
|
||||
inHours(): number {
|
||||
return this.value / 60;
|
||||
}
|
||||
|
||||
get props(): SessionDurationProps {
|
||||
return { value: this.value };
|
||||
}
|
||||
|
||||
equals(other: IValueObject<SessionDurationProps>): boolean {
|
||||
return this.value === other.props.value;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `${this.value} minutes`;
|
||||
}
|
||||
|
||||
toNumber(): number {
|
||||
return this.value;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user