harden business rules
This commit is contained in:
@@ -1,10 +1,3 @@
|
||||
export type SeasonStatus =
|
||||
| 'planned'
|
||||
| 'active'
|
||||
| 'completed'
|
||||
| 'archived'
|
||||
| 'cancelled';
|
||||
|
||||
import {
|
||||
RacingDomainInvariantError,
|
||||
RacingDomainValidationError,
|
||||
@@ -14,6 +7,9 @@ import type { SeasonSchedule } from '../../value-objects/SeasonSchedule';
|
||||
import type { SeasonScoringConfig } from '../../value-objects/SeasonScoringConfig';
|
||||
import type { SeasonDropPolicy } from '../../value-objects/SeasonDropPolicy';
|
||||
import type { SeasonStewardingConfig } from '../../value-objects/SeasonStewardingConfig';
|
||||
import { SeasonStatus, SeasonStatusValue } from '../../value-objects/SeasonStatus';
|
||||
import { ParticipantCount } from '../../value-objects/ParticipantCount';
|
||||
import { MaxParticipants } from '../../value-objects/MaxParticipants';
|
||||
|
||||
export class Season implements IEntity<string> {
|
||||
readonly id: string;
|
||||
@@ -31,6 +27,9 @@ export class Season implements IEntity<string> {
|
||||
readonly stewardingConfig: SeasonStewardingConfig | undefined;
|
||||
readonly maxDrivers: number | undefined;
|
||||
|
||||
// Domain state for business rule enforcement
|
||||
private readonly _participantCount: ParticipantCount;
|
||||
|
||||
private constructor(props: {
|
||||
id: string;
|
||||
leagueId: string;
|
||||
@@ -46,6 +45,7 @@ export class Season implements IEntity<string> {
|
||||
dropPolicy?: SeasonDropPolicy;
|
||||
stewardingConfig?: SeasonStewardingConfig;
|
||||
maxDrivers?: number;
|
||||
participantCount: ParticipantCount;
|
||||
}) {
|
||||
this.id = props.id;
|
||||
this.leagueId = props.leagueId;
|
||||
@@ -61,6 +61,7 @@ export class Season implements IEntity<string> {
|
||||
this.dropPolicy = props.dropPolicy;
|
||||
this.stewardingConfig = props.stewardingConfig;
|
||||
this.maxDrivers = props.maxDrivers;
|
||||
this._participantCount = props.participantCount;
|
||||
}
|
||||
|
||||
static create(props: {
|
||||
@@ -70,7 +71,7 @@ export class Season implements IEntity<string> {
|
||||
name: string;
|
||||
year?: number;
|
||||
order?: number;
|
||||
status?: SeasonStatus;
|
||||
status?: SeasonStatusValue;
|
||||
startDate?: Date;
|
||||
endDate?: Date | undefined;
|
||||
schedule?: SeasonSchedule;
|
||||
@@ -78,7 +79,9 @@ export class Season implements IEntity<string> {
|
||||
dropPolicy?: SeasonDropPolicy;
|
||||
stewardingConfig?: SeasonStewardingConfig;
|
||||
maxDrivers?: number;
|
||||
participantCount?: number;
|
||||
}): Season {
|
||||
// Validate required fields
|
||||
if (!props.id || props.id.trim().length === 0) {
|
||||
throw new RacingDomainValidationError('Season ID is required');
|
||||
}
|
||||
@@ -95,7 +98,58 @@ export class Season implements IEntity<string> {
|
||||
throw new RacingDomainValidationError('Season name is required');
|
||||
}
|
||||
|
||||
const status: SeasonStatus = props.status ?? 'planned';
|
||||
// Validate maxDrivers if provided
|
||||
if (props.maxDrivers !== undefined) {
|
||||
if (props.maxDrivers <= 0) {
|
||||
throw new RacingDomainValidationError('Season maxDrivers must be greater than 0 when provided');
|
||||
}
|
||||
if (props.maxDrivers > 100) {
|
||||
throw new RacingDomainValidationError('Season maxDrivers cannot exceed 100');
|
||||
}
|
||||
}
|
||||
|
||||
// Validate participant count
|
||||
const participantCount = ParticipantCount.create(props.participantCount ?? 0);
|
||||
if (props.maxDrivers !== undefined) {
|
||||
const maxParticipants = MaxParticipants.create(props.maxDrivers);
|
||||
if (!maxParticipants.canAccommodate(participantCount.toNumber())) {
|
||||
throw new RacingDomainValidationError(
|
||||
`Participant count (${participantCount.toNumber()}) exceeds season capacity (${maxParticipants.toNumber()})`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate status transitions if status is provided
|
||||
const status = SeasonStatus.create(props.status ?? 'planned');
|
||||
|
||||
// Validate schedule if provided
|
||||
if (props.schedule) {
|
||||
// Ensure schedule dates align with season dates
|
||||
if (props.startDate && props.schedule.startDate < props.startDate) {
|
||||
throw new RacingDomainValidationError('Schedule start date cannot be before season start date');
|
||||
}
|
||||
}
|
||||
|
||||
// Validate scoring config if provided
|
||||
if (props.scoringConfig) {
|
||||
if (!props.scoringConfig.scoringPresetId || props.scoringConfig.scoringPresetId.trim().length === 0) {
|
||||
throw new RacingDomainValidationError('Scoring preset ID is required');
|
||||
}
|
||||
}
|
||||
|
||||
// Validate drop policy if provided
|
||||
if (props.dropPolicy) {
|
||||
if (props.dropPolicy.strategy === 'bestNResults' || props.dropPolicy.strategy === 'dropWorstN') {
|
||||
if (!props.dropPolicy.n || props.dropPolicy.n <= 0) {
|
||||
throw new RacingDomainValidationError('Drop policy requires positive n value for this strategy');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate stewarding config if provided
|
||||
if (props.stewardingConfig) {
|
||||
Season.validateStewardingConfig(props.stewardingConfig);
|
||||
}
|
||||
|
||||
return new Season({
|
||||
id: props.id,
|
||||
@@ -108,55 +162,110 @@ export class Season implements IEntity<string> {
|
||||
...(props.startDate !== undefined ? { startDate: props.startDate } : {}),
|
||||
...(props.endDate !== undefined ? { endDate: props.endDate } : {}),
|
||||
...(props.schedule !== undefined ? { schedule: props.schedule } : {}),
|
||||
...(props.scoringConfig !== undefined
|
||||
? { scoringConfig: props.scoringConfig }
|
||||
: {}),
|
||||
...(props.scoringConfig !== undefined ? { scoringConfig: props.scoringConfig } : {}),
|
||||
...(props.dropPolicy !== undefined ? { dropPolicy: props.dropPolicy } : {}),
|
||||
...(props.stewardingConfig !== undefined
|
||||
? { stewardingConfig: props.stewardingConfig }
|
||||
: {}),
|
||||
...(props.stewardingConfig !== undefined ? { stewardingConfig: props.stewardingConfig } : {}),
|
||||
...(props.maxDrivers !== undefined ? { maxDrivers: props.maxDrivers } : {}),
|
||||
participantCount,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate stewarding configuration
|
||||
*/
|
||||
private static validateStewardingConfig(config: SeasonStewardingConfig): void {
|
||||
if (!config.decisionMode) {
|
||||
throw new RacingDomainValidationError('Stewarding decision mode is required');
|
||||
}
|
||||
|
||||
const votingModes = ['steward_vote', 'member_vote', 'steward_veto', 'member_veto'];
|
||||
if (votingModes.includes(config.decisionMode)) {
|
||||
if (!config.requiredVotes || config.requiredVotes <= 0) {
|
||||
throw new RacingDomainValidationError(
|
||||
'Stewarding settings with voting modes require a positive requiredVotes value'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (config.requiredVotes !== undefined && !votingModes.includes(config.decisionMode)) {
|
||||
throw new RacingDomainValidationError(
|
||||
'requiredVotes should only be provided for voting/veto modes'
|
||||
);
|
||||
}
|
||||
|
||||
// Validate time limits
|
||||
if (config.defenseTimeLimit !== undefined && config.defenseTimeLimit < 0) {
|
||||
throw new RacingDomainValidationError('Defense time limit must be non-negative');
|
||||
}
|
||||
|
||||
if (config.voteTimeLimit !== undefined && config.voteTimeLimit <= 0) {
|
||||
throw new RacingDomainValidationError('Vote time limit must be positive');
|
||||
}
|
||||
|
||||
if (config.protestDeadlineHours !== undefined && config.protestDeadlineHours <= 0) {
|
||||
throw new RacingDomainValidationError('Protest deadline must be positive');
|
||||
}
|
||||
|
||||
if (config.stewardingClosesHours !== undefined && config.stewardingClosesHours <= 0) {
|
||||
throw new RacingDomainValidationError('Stewarding close time must be positive');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Domain rule: Wallet withdrawals are only allowed when season is completed
|
||||
*/
|
||||
canWithdrawFromWallet(): boolean {
|
||||
return this.status === 'completed';
|
||||
return this.status.isCompleted();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if season is active
|
||||
*/
|
||||
isActive(): boolean {
|
||||
return this.status === 'active';
|
||||
return this.status.isActive();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if season is completed
|
||||
*/
|
||||
isCompleted(): boolean {
|
||||
return this.status === 'completed';
|
||||
return this.status.isCompleted();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if season is planned (not yet active)
|
||||
*/
|
||||
isPlanned(): boolean {
|
||||
return this.status === 'planned';
|
||||
return this.status.isPlanned();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if season can accept more participants
|
||||
*/
|
||||
canAcceptMore(): boolean {
|
||||
if (this.maxDrivers === undefined) return true;
|
||||
return this._participantCount.toNumber() < this.maxDrivers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current participant count
|
||||
*/
|
||||
getParticipantCount(): number {
|
||||
return this._participantCount.toNumber();
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate the season from planned state.
|
||||
*/
|
||||
activate(): Season {
|
||||
if (this.status !== 'planned') {
|
||||
throw new RacingDomainInvariantError(
|
||||
'Only planned seasons can be activated',
|
||||
);
|
||||
const transition = this.status.canTransitionTo('active');
|
||||
if (!transition.valid) {
|
||||
throw new RacingDomainInvariantError(transition.error!);
|
||||
}
|
||||
|
||||
// Ensure start date is set
|
||||
const startDate = this.startDate ?? new Date();
|
||||
|
||||
return Season.create({
|
||||
id: this.id,
|
||||
leagueId: this.leagueId,
|
||||
@@ -165,19 +274,14 @@ export class Season implements IEntity<string> {
|
||||
...(this.year !== undefined ? { year: this.year } : {}),
|
||||
...(this.order !== undefined ? { order: this.order } : {}),
|
||||
status: 'active',
|
||||
...(this.startDate !== undefined ? { startDate: this.startDate } : {
|
||||
startDate: new Date(),
|
||||
}),
|
||||
startDate,
|
||||
...(this.endDate !== undefined ? { endDate: this.endDate } : {}),
|
||||
...(this.schedule !== undefined ? { schedule: this.schedule } : {}),
|
||||
...(this.scoringConfig !== undefined
|
||||
? { scoringConfig: this.scoringConfig }
|
||||
: {}),
|
||||
...(this.scoringConfig !== undefined ? { scoringConfig: this.scoringConfig } : {}),
|
||||
...(this.dropPolicy !== undefined ? { dropPolicy: this.dropPolicy } : {}),
|
||||
...(this.stewardingConfig !== undefined
|
||||
? { stewardingConfig: this.stewardingConfig }
|
||||
: {}),
|
||||
...(this.stewardingConfig !== undefined ? { stewardingConfig: this.stewardingConfig } : {}),
|
||||
...(this.maxDrivers !== undefined ? { maxDrivers: this.maxDrivers } : {}),
|
||||
participantCount: this._participantCount.toNumber(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -185,12 +289,14 @@ export class Season implements IEntity<string> {
|
||||
* Mark the season as completed.
|
||||
*/
|
||||
complete(): Season {
|
||||
if (this.status !== 'active') {
|
||||
throw new RacingDomainInvariantError(
|
||||
'Only active seasons can be completed',
|
||||
);
|
||||
const transition = this.status.canTransitionTo('completed');
|
||||
if (!transition.valid) {
|
||||
throw new RacingDomainInvariantError(transition.error!);
|
||||
}
|
||||
|
||||
// Ensure end date is set
|
||||
const endDate = this.endDate ?? new Date();
|
||||
|
||||
return Season.create({
|
||||
id: this.id,
|
||||
leagueId: this.leagueId,
|
||||
@@ -200,18 +306,13 @@ export class Season implements IEntity<string> {
|
||||
...(this.order !== undefined ? { order: this.order } : {}),
|
||||
status: 'completed',
|
||||
...(this.startDate !== undefined ? { startDate: this.startDate } : {}),
|
||||
...(this.endDate !== undefined ? { endDate: this.endDate } : {
|
||||
endDate: new Date(),
|
||||
}),
|
||||
endDate,
|
||||
...(this.schedule !== undefined ? { schedule: this.schedule } : {}),
|
||||
...(this.scoringConfig !== undefined
|
||||
? { scoringConfig: this.scoringConfig }
|
||||
: {}),
|
||||
...(this.scoringConfig !== undefined ? { scoringConfig: this.scoringConfig } : {}),
|
||||
...(this.dropPolicy !== undefined ? { dropPolicy: this.dropPolicy } : {}),
|
||||
...(this.stewardingConfig !== undefined
|
||||
? { stewardingConfig: this.stewardingConfig }
|
||||
: {}),
|
||||
...(this.stewardingConfig !== undefined ? { stewardingConfig: this.stewardingConfig } : {}),
|
||||
...(this.maxDrivers !== undefined ? { maxDrivers: this.maxDrivers } : {}),
|
||||
participantCount: this._participantCount.toNumber(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -219,10 +320,9 @@ export class Season implements IEntity<string> {
|
||||
* Archive a completed season.
|
||||
*/
|
||||
archive(): Season {
|
||||
if (!this.isCompleted()) {
|
||||
throw new RacingDomainInvariantError(
|
||||
'Only completed seasons can be archived',
|
||||
);
|
||||
const transition = this.status.canTransitionTo('archived');
|
||||
if (!transition.valid) {
|
||||
throw new RacingDomainInvariantError(transition.error!);
|
||||
}
|
||||
|
||||
return Season.create({
|
||||
@@ -236,14 +336,11 @@ export class Season implements IEntity<string> {
|
||||
...(this.startDate !== undefined ? { startDate: this.startDate } : {}),
|
||||
...(this.endDate !== undefined ? { endDate: this.endDate } : {}),
|
||||
...(this.schedule !== undefined ? { schedule: this.schedule } : {}),
|
||||
...(this.scoringConfig !== undefined
|
||||
? { scoringConfig: this.scoringConfig }
|
||||
: {}),
|
||||
...(this.scoringConfig !== undefined ? { scoringConfig: this.scoringConfig } : {}),
|
||||
...(this.dropPolicy !== undefined ? { dropPolicy: this.dropPolicy } : {}),
|
||||
...(this.stewardingConfig !== undefined
|
||||
? { stewardingConfig: this.stewardingConfig }
|
||||
: {}),
|
||||
...(this.stewardingConfig !== undefined ? { stewardingConfig: this.stewardingConfig } : {}),
|
||||
...(this.maxDrivers !== undefined ? { maxDrivers: this.maxDrivers } : {}),
|
||||
participantCount: this._participantCount.toNumber(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -251,16 +348,19 @@ export class Season implements IEntity<string> {
|
||||
* Cancel a planned or active season.
|
||||
*/
|
||||
cancel(): Season {
|
||||
if (this.status === 'completed' || this.status === 'archived') {
|
||||
throw new RacingDomainInvariantError(
|
||||
'Cannot cancel a completed or archived season',
|
||||
);
|
||||
const transition = this.status.canTransitionTo('cancelled');
|
||||
if (!transition.valid) {
|
||||
throw new RacingDomainInvariantError(transition.error!);
|
||||
}
|
||||
|
||||
if (this.status === 'cancelled') {
|
||||
// If already cancelled, return this
|
||||
if (this.status.isCancelled()) {
|
||||
return this;
|
||||
}
|
||||
|
||||
// Ensure end date is set
|
||||
const endDate = this.endDate ?? new Date();
|
||||
|
||||
return Season.create({
|
||||
id: this.id,
|
||||
leagueId: this.leagueId,
|
||||
@@ -270,18 +370,13 @@ export class Season implements IEntity<string> {
|
||||
...(this.order !== undefined ? { order: this.order } : {}),
|
||||
status: 'cancelled',
|
||||
...(this.startDate !== undefined ? { startDate: this.startDate } : {}),
|
||||
...(this.endDate !== undefined ? { endDate: this.endDate } : {
|
||||
endDate: new Date(),
|
||||
}),
|
||||
endDate,
|
||||
...(this.schedule !== undefined ? { schedule: this.schedule } : {}),
|
||||
...(this.scoringConfig !== undefined
|
||||
? { scoringConfig: this.scoringConfig }
|
||||
: {}),
|
||||
...(this.scoringConfig !== undefined ? { scoringConfig: this.scoringConfig } : {}),
|
||||
...(this.dropPolicy !== undefined ? { dropPolicy: this.dropPolicy } : {}),
|
||||
...(this.stewardingConfig !== undefined
|
||||
? { stewardingConfig: this.stewardingConfig }
|
||||
: {}),
|
||||
...(this.stewardingConfig !== undefined ? { stewardingConfig: this.stewardingConfig } : {}),
|
||||
...(this.maxDrivers !== undefined ? { maxDrivers: this.maxDrivers } : {}),
|
||||
participantCount: this._participantCount.toNumber(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -289,110 +384,9 @@ export class Season implements IEntity<string> {
|
||||
* Update schedule while keeping other properties intact.
|
||||
*/
|
||||
withSchedule(schedule: SeasonSchedule): Season {
|
||||
return Season.create({
|
||||
id: this.id,
|
||||
leagueId: this.leagueId,
|
||||
gameId: this.gameId,
|
||||
name: this.name,
|
||||
...(this.year !== undefined ? { year: this.year } : {}),
|
||||
...(this.order !== undefined ? { order: this.order } : {}),
|
||||
status: this.status,
|
||||
...(this.startDate !== undefined ? { startDate: this.startDate } : {}),
|
||||
...(this.endDate !== undefined ? { endDate: this.endDate } : {}),
|
||||
schedule,
|
||||
...(this.scoringConfig !== undefined
|
||||
? { scoringConfig: this.scoringConfig }
|
||||
: {}),
|
||||
...(this.dropPolicy !== undefined ? { dropPolicy: this.dropPolicy } : {}),
|
||||
...(this.stewardingConfig !== undefined
|
||||
? { stewardingConfig: this.stewardingConfig }
|
||||
: {}),
|
||||
...(this.maxDrivers !== undefined ? { maxDrivers: this.maxDrivers } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update scoring configuration for the season.
|
||||
*/
|
||||
withScoringConfig(scoringConfig: SeasonScoringConfig): Season {
|
||||
return Season.create({
|
||||
id: this.id,
|
||||
leagueId: this.leagueId,
|
||||
gameId: this.gameId,
|
||||
name: this.name,
|
||||
...(this.year !== undefined ? { year: this.year } : {}),
|
||||
...(this.order !== undefined ? { order: this.order } : {}),
|
||||
status: this.status,
|
||||
...(this.startDate !== undefined ? { startDate: this.startDate } : {}),
|
||||
...(this.endDate !== undefined ? { endDate: this.endDate } : {}),
|
||||
...(this.schedule !== undefined ? { schedule: this.schedule } : {}),
|
||||
scoringConfig,
|
||||
...(this.dropPolicy !== undefined ? { dropPolicy: this.dropPolicy } : {}),
|
||||
...(this.stewardingConfig !== undefined
|
||||
? { stewardingConfig: this.stewardingConfig }
|
||||
: {}),
|
||||
...(this.maxDrivers !== undefined ? { maxDrivers: this.maxDrivers } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update drop policy for the season.
|
||||
*/
|
||||
withDropPolicy(dropPolicy: SeasonDropPolicy): Season {
|
||||
return Season.create({
|
||||
id: this.id,
|
||||
leagueId: this.leagueId,
|
||||
gameId: this.gameId,
|
||||
name: this.name,
|
||||
...(this.year !== undefined ? { year: this.year } : {}),
|
||||
...(this.order !== undefined ? { order: this.order } : {}),
|
||||
status: this.status,
|
||||
...(this.startDate !== undefined ? { startDate: this.startDate } : {}),
|
||||
...(this.endDate !== undefined ? { endDate: this.endDate } : {}),
|
||||
...(this.schedule !== undefined ? { schedule: this.schedule } : {}),
|
||||
...(this.scoringConfig !== undefined
|
||||
? { scoringConfig: this.scoringConfig }
|
||||
: {}),
|
||||
dropPolicy,
|
||||
...(this.stewardingConfig !== undefined
|
||||
? { stewardingConfig: this.stewardingConfig }
|
||||
: {}),
|
||||
...(this.maxDrivers !== undefined ? { maxDrivers: this.maxDrivers } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update stewarding configuration for the season.
|
||||
*/
|
||||
withStewardingConfig(stewardingConfig: SeasonStewardingConfig): Season {
|
||||
return Season.create({
|
||||
id: this.id,
|
||||
leagueId: this.leagueId,
|
||||
gameId: this.gameId,
|
||||
name: this.name,
|
||||
...(this.year !== undefined ? { year: this.year } : {}),
|
||||
...(this.order !== undefined ? { order: this.order } : {}),
|
||||
status: this.status,
|
||||
...(this.startDate !== undefined ? { startDate: this.startDate } : {}),
|
||||
...(this.endDate !== undefined ? { endDate: this.endDate } : {}),
|
||||
...(this.schedule !== undefined ? { schedule: this.schedule } : {}),
|
||||
...(this.scoringConfig !== undefined
|
||||
? { scoringConfig: this.scoringConfig }
|
||||
: {}),
|
||||
...(this.dropPolicy !== undefined ? { dropPolicy: this.dropPolicy } : {}),
|
||||
stewardingConfig,
|
||||
...(this.maxDrivers !== undefined ? { maxDrivers: this.maxDrivers } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update max driver capacity for the season.
|
||||
*/
|
||||
withMaxDrivers(maxDrivers: number | undefined): Season {
|
||||
if (maxDrivers !== undefined && maxDrivers <= 0) {
|
||||
throw new RacingDomainValidationError(
|
||||
'Season maxDrivers must be greater than 0 when provided',
|
||||
);
|
||||
// Validate schedule against season dates
|
||||
if (this.startDate && schedule.startDate < this.startDate) {
|
||||
throw new RacingDomainValidationError('Schedule start date cannot be before season start date');
|
||||
}
|
||||
|
||||
return Season.create({
|
||||
@@ -402,18 +396,194 @@ export class Season implements IEntity<string> {
|
||||
name: this.name,
|
||||
...(this.year !== undefined ? { year: this.year } : {}),
|
||||
...(this.order !== undefined ? { order: this.order } : {}),
|
||||
status: this.status,
|
||||
status: this.status.toString(),
|
||||
...(this.startDate !== undefined ? { startDate: this.startDate } : {}),
|
||||
...(this.endDate !== undefined ? { endDate: this.endDate } : {}),
|
||||
schedule,
|
||||
...(this.scoringConfig !== undefined ? { scoringConfig: this.scoringConfig } : {}),
|
||||
...(this.dropPolicy !== undefined ? { dropPolicy: this.dropPolicy } : {}),
|
||||
...(this.stewardingConfig !== undefined ? { stewardingConfig: this.stewardingConfig } : {}),
|
||||
...(this.maxDrivers !== undefined ? { maxDrivers: this.maxDrivers } : {}),
|
||||
participantCount: this._participantCount.toNumber(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update scoring configuration for the season.
|
||||
*/
|
||||
withScoringConfig(scoringConfig: SeasonScoringConfig): Season {
|
||||
if (!scoringConfig.scoringPresetId || scoringConfig.scoringPresetId.trim().length === 0) {
|
||||
throw new RacingDomainValidationError('Scoring preset ID is required');
|
||||
}
|
||||
|
||||
return Season.create({
|
||||
id: this.id,
|
||||
leagueId: this.leagueId,
|
||||
gameId: this.gameId,
|
||||
name: this.name,
|
||||
...(this.year !== undefined ? { year: this.year } : {}),
|
||||
...(this.order !== undefined ? { order: this.order } : {}),
|
||||
status: this.status.toString(),
|
||||
...(this.startDate !== undefined ? { startDate: this.startDate } : {}),
|
||||
...(this.endDate !== undefined ? { endDate: this.endDate } : {}),
|
||||
...(this.schedule !== undefined ? { schedule: this.schedule } : {}),
|
||||
...(this.scoringConfig !== undefined
|
||||
? { scoringConfig: this.scoringConfig }
|
||||
: {}),
|
||||
scoringConfig,
|
||||
...(this.dropPolicy !== undefined ? { dropPolicy: this.dropPolicy } : {}),
|
||||
...(this.stewardingConfig !== undefined
|
||||
? { stewardingConfig: this.stewardingConfig }
|
||||
: {}),
|
||||
...(this.stewardingConfig !== undefined ? { stewardingConfig: this.stewardingConfig } : {}),
|
||||
...(this.maxDrivers !== undefined ? { maxDrivers: this.maxDrivers } : {}),
|
||||
participantCount: this._participantCount.toNumber(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update drop policy for the season.
|
||||
*/
|
||||
withDropPolicy(dropPolicy: SeasonDropPolicy): Season {
|
||||
// Validate drop policy
|
||||
if (dropPolicy.strategy === 'bestNResults' || dropPolicy.strategy === 'dropWorstN') {
|
||||
if (!dropPolicy.n || dropPolicy.n <= 0) {
|
||||
throw new RacingDomainValidationError('Drop policy requires positive n value for this strategy');
|
||||
}
|
||||
}
|
||||
|
||||
return Season.create({
|
||||
id: this.id,
|
||||
leagueId: this.leagueId,
|
||||
gameId: this.gameId,
|
||||
name: this.name,
|
||||
...(this.year !== undefined ? { year: this.year } : {}),
|
||||
...(this.order !== undefined ? { order: this.order } : {}),
|
||||
status: this.status.toString(),
|
||||
...(this.startDate !== undefined ? { startDate: this.startDate } : {}),
|
||||
...(this.endDate !== undefined ? { endDate: this.endDate } : {}),
|
||||
...(this.schedule !== undefined ? { schedule: this.schedule } : {}),
|
||||
...(this.scoringConfig !== undefined ? { scoringConfig: this.scoringConfig } : {}),
|
||||
dropPolicy,
|
||||
...(this.stewardingConfig !== undefined ? { stewardingConfig: this.stewardingConfig } : {}),
|
||||
...(this.maxDrivers !== undefined ? { maxDrivers: this.maxDrivers } : {}),
|
||||
participantCount: this._participantCount.toNumber(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update stewarding configuration for the season.
|
||||
*/
|
||||
withStewardingConfig(stewardingConfig: SeasonStewardingConfig): Season {
|
||||
Season.validateStewardingConfig(stewardingConfig);
|
||||
|
||||
return Season.create({
|
||||
id: this.id,
|
||||
leagueId: this.leagueId,
|
||||
gameId: this.gameId,
|
||||
name: this.name,
|
||||
...(this.year !== undefined ? { year: this.year } : {}),
|
||||
...(this.order !== undefined ? { order: this.order } : {}),
|
||||
status: this.status.toString(),
|
||||
...(this.startDate !== undefined ? { startDate: this.startDate } : {}),
|
||||
...(this.endDate !== undefined ? { endDate: this.endDate } : {}),
|
||||
...(this.schedule !== undefined ? { schedule: this.schedule } : {}),
|
||||
...(this.scoringConfig !== undefined ? { scoringConfig: this.scoringConfig } : {}),
|
||||
...(this.dropPolicy !== undefined ? { dropPolicy: this.dropPolicy } : {}),
|
||||
stewardingConfig,
|
||||
...(this.maxDrivers !== undefined ? { maxDrivers: this.maxDrivers } : {}),
|
||||
participantCount: this._participantCount.toNumber(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update max driver capacity for the season.
|
||||
*/
|
||||
withMaxDrivers(maxDrivers: number | undefined): Season {
|
||||
if (maxDrivers !== undefined) {
|
||||
if (maxDrivers <= 0) {
|
||||
throw new RacingDomainValidationError('Season maxDrivers must be greater than 0 when provided');
|
||||
}
|
||||
if (maxDrivers > 100) {
|
||||
throw new RacingDomainValidationError('Season maxDrivers cannot exceed 100');
|
||||
}
|
||||
if (maxDrivers < this._participantCount.toNumber()) {
|
||||
throw new RacingDomainValidationError(
|
||||
`Cannot reduce max drivers (${maxDrivers}) below current participant count (${this._participantCount.toNumber()})`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return Season.create({
|
||||
id: this.id,
|
||||
leagueId: this.leagueId,
|
||||
gameId: this.gameId,
|
||||
name: this.name,
|
||||
...(this.year !== undefined ? { year: this.year } : {}),
|
||||
...(this.order !== undefined ? { order: this.order } : {}),
|
||||
status: this.status.toString(),
|
||||
...(this.startDate !== undefined ? { startDate: this.startDate } : {}),
|
||||
...(this.endDate !== undefined ? { endDate: this.endDate } : {}),
|
||||
...(this.schedule !== undefined ? { schedule: this.schedule } : {}),
|
||||
...(this.scoringConfig !== undefined ? { scoringConfig: this.scoringConfig } : {}),
|
||||
...(this.dropPolicy !== undefined ? { dropPolicy: this.dropPolicy } : {}),
|
||||
...(this.stewardingConfig !== undefined ? { stewardingConfig: this.stewardingConfig } : {}),
|
||||
...(maxDrivers !== undefined ? { maxDrivers } : {}),
|
||||
participantCount: this._participantCount.toNumber(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a participant to the season
|
||||
*/
|
||||
addParticipant(): Season {
|
||||
if (!this.canAcceptMore()) {
|
||||
throw new RacingDomainInvariantError(
|
||||
`Season capacity (${this.maxDrivers}) would be exceeded`
|
||||
);
|
||||
}
|
||||
|
||||
const newCount = this._participantCount.increment();
|
||||
|
||||
return Season.create({
|
||||
id: this.id,
|
||||
leagueId: this.leagueId,
|
||||
gameId: this.gameId,
|
||||
name: this.name,
|
||||
year: this.year,
|
||||
order: this.order,
|
||||
status: this.status.toString(),
|
||||
startDate: this.startDate,
|
||||
endDate: this.endDate,
|
||||
schedule: this.schedule,
|
||||
scoringConfig: this.scoringConfig,
|
||||
dropPolicy: this.dropPolicy,
|
||||
stewardingConfig: this.stewardingConfig,
|
||||
maxDrivers: this.maxDrivers,
|
||||
participantCount: newCount.toNumber(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a participant from the season
|
||||
*/
|
||||
removeParticipant(): Season {
|
||||
if (this._participantCount.isZero()) {
|
||||
throw new RacingDomainInvariantError('Cannot remove participant: season has no participants');
|
||||
}
|
||||
|
||||
const newCount = this._participantCount.decrement();
|
||||
|
||||
return Season.create({
|
||||
id: this.id,
|
||||
leagueId: this.leagueId,
|
||||
gameId: this.gameId,
|
||||
name: this.name,
|
||||
year: this.year,
|
||||
order: this.order,
|
||||
status: this.status.toString(),
|
||||
startDate: this.startDate,
|
||||
endDate: this.endDate,
|
||||
schedule: this.schedule,
|
||||
scoringConfig: this.scoringConfig,
|
||||
dropPolicy: this.dropPolicy,
|
||||
stewardingConfig: this.stewardingConfig,
|
||||
maxDrivers: this.maxDrivers,
|
||||
participantCount: newCount.toNumber(),
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user