wip
This commit is contained in:
@@ -9,10 +9,20 @@ export interface MonthlyRecurrencePatternProps {
|
||||
export class MonthlyRecurrencePattern implements IValueObject<MonthlyRecurrencePatternProps> {
|
||||
readonly ordinal: 1 | 2 | 3 | 4;
|
||||
readonly weekday: Weekday;
|
||||
|
||||
constructor(ordinal: 1 | 2 | 3 | 4, weekday: Weekday) {
|
||||
this.ordinal = ordinal;
|
||||
this.weekday = weekday;
|
||||
|
||||
constructor(ordinal: 1 | 2 | 3 | 4, weekday: Weekday);
|
||||
constructor(props: MonthlyRecurrencePatternProps);
|
||||
constructor(
|
||||
ordinalOrProps: 1 | 2 | 3 | 4 | MonthlyRecurrencePatternProps,
|
||||
weekday?: Weekday,
|
||||
) {
|
||||
if (typeof ordinalOrProps === 'object') {
|
||||
this.ordinal = ordinalOrProps.ordinal;
|
||||
this.weekday = ordinalOrProps.weekday;
|
||||
} else {
|
||||
this.ordinal = ordinalOrProps;
|
||||
this.weekday = weekday as Weekday;
|
||||
}
|
||||
}
|
||||
|
||||
get props(): MonthlyRecurrencePatternProps {
|
||||
|
||||
59
packages/racing/domain/value-objects/SeasonDropPolicy.ts
Normal file
59
packages/racing/domain/value-objects/SeasonDropPolicy.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { RacingDomainValidationError } from '../errors/RacingDomainError';
|
||||
import type { IValueObject } from '@gridpilot/shared/domain';
|
||||
|
||||
export type SeasonDropStrategy = 'none' | 'bestNResults' | 'dropWorstN';
|
||||
|
||||
export interface SeasonDropPolicyProps {
|
||||
strategy: SeasonDropStrategy;
|
||||
/**
|
||||
* Number of results to consider for strategies that require a count.
|
||||
* - bestNResults: keep best N
|
||||
* - dropWorstN: drop worst N
|
||||
*/
|
||||
n?: number;
|
||||
}
|
||||
|
||||
export class SeasonDropPolicy implements IValueObject<SeasonDropPolicyProps> {
|
||||
readonly strategy: SeasonDropStrategy;
|
||||
readonly n?: number;
|
||||
|
||||
constructor(props: SeasonDropPolicyProps) {
|
||||
if (!props.strategy) {
|
||||
throw new RacingDomainValidationError(
|
||||
'SeasonDropPolicy.strategy is required',
|
||||
);
|
||||
}
|
||||
|
||||
if (props.strategy === 'bestNResults' || props.strategy === 'dropWorstN') {
|
||||
if (props.n === undefined || !Number.isInteger(props.n) || props.n <= 0) {
|
||||
throw new RacingDomainValidationError(
|
||||
'SeasonDropPolicy.n must be a positive integer when using bestNResults or dropWorstN',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (props.strategy === 'none' && props.n !== undefined) {
|
||||
throw new RacingDomainValidationError(
|
||||
'SeasonDropPolicy.n must be undefined when strategy is none',
|
||||
);
|
||||
}
|
||||
|
||||
this.strategy = props.strategy;
|
||||
if (props.n !== undefined) {
|
||||
this.n = props.n;
|
||||
}
|
||||
}
|
||||
|
||||
get props(): SeasonDropPolicyProps {
|
||||
return {
|
||||
strategy: this.strategy,
|
||||
...(this.n !== undefined ? { n: this.n } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
equals(other: IValueObject<SeasonDropPolicyProps>): boolean {
|
||||
const a = this.props;
|
||||
const b = other.props;
|
||||
return a.strategy === b.strategy && a.n === b.n;
|
||||
}
|
||||
}
|
||||
66
packages/racing/domain/value-objects/SeasonScoringConfig.ts
Normal file
66
packages/racing/domain/value-objects/SeasonScoringConfig.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { RacingDomainValidationError } from '../errors/RacingDomainError';
|
||||
import type { IValueObject } from '@gridpilot/shared/domain';
|
||||
|
||||
/**
|
||||
* Value Object: SeasonScoringConfig
|
||||
*
|
||||
* Represents the scoring configuration owned by a Season.
|
||||
* It is intentionally lightweight and primarily captures which
|
||||
* preset (or custom mode) is applied for this Season.
|
||||
*
|
||||
* Detailed championship scoring rules are still modeled via
|
||||
* `LeagueScoringConfig` and related types.
|
||||
*/
|
||||
export interface SeasonScoringConfigProps {
|
||||
/**
|
||||
* Identifier of the scoring preset applied to this Season.
|
||||
* Examples:
|
||||
* - 'sprint-main-driver'
|
||||
* - 'club-default'
|
||||
* - 'endurance-main-double'
|
||||
* - 'custom'
|
||||
*/
|
||||
scoringPresetId: string;
|
||||
|
||||
/**
|
||||
* Whether the Season uses custom scoring rather than a pure preset.
|
||||
* When true, `scoringPresetId` acts as a label rather than a strict preset key.
|
||||
*/
|
||||
customScoringEnabled?: boolean;
|
||||
}
|
||||
|
||||
export class SeasonScoringConfig
|
||||
implements IValueObject<SeasonScoringConfigProps>
|
||||
{
|
||||
readonly scoringPresetId: string;
|
||||
readonly customScoringEnabled: boolean;
|
||||
|
||||
constructor(params: SeasonScoringConfigProps) {
|
||||
if (!params.scoringPresetId || params.scoringPresetId.trim().length === 0) {
|
||||
throw new RacingDomainValidationError(
|
||||
'SeasonScoringConfig.scoringPresetId must be a non-empty string',
|
||||
);
|
||||
}
|
||||
|
||||
this.scoringPresetId = params.scoringPresetId.trim();
|
||||
this.customScoringEnabled = Boolean(params.customScoringEnabled);
|
||||
}
|
||||
|
||||
get props(): SeasonScoringConfigProps {
|
||||
return {
|
||||
scoringPresetId: this.scoringPresetId,
|
||||
...(this.customScoringEnabled
|
||||
? { customScoringEnabled: this.customScoringEnabled }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
equals(other: IValueObject<SeasonScoringConfigProps>): boolean {
|
||||
const a = this.props;
|
||||
const b = other.props;
|
||||
return (
|
||||
a.scoringPresetId === b.scoringPresetId &&
|
||||
Boolean(a.customScoringEnabled) === Boolean(b.customScoringEnabled)
|
||||
);
|
||||
}
|
||||
}
|
||||
131
packages/racing/domain/value-objects/SeasonStewardingConfig.ts
Normal file
131
packages/racing/domain/value-objects/SeasonStewardingConfig.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { RacingDomainValidationError } from '../errors/RacingDomainError';
|
||||
import type { IValueObject } from '@gridpilot/shared/domain';
|
||||
import type { StewardingDecisionMode } from '../entities/League';
|
||||
|
||||
export interface SeasonStewardingConfigProps {
|
||||
decisionMode: StewardingDecisionMode;
|
||||
requiredVotes?: number;
|
||||
requireDefense: boolean;
|
||||
defenseTimeLimit: number;
|
||||
voteTimeLimit: number;
|
||||
protestDeadlineHours: number;
|
||||
stewardingClosesHours: number;
|
||||
notifyAccusedOnProtest: boolean;
|
||||
notifyOnVoteRequired: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Value Object: SeasonStewardingConfig
|
||||
*
|
||||
* Encapsulates stewarding configuration owned by a Season.
|
||||
* Shape intentionally mirrors LeagueStewardingFormDTO used by the wizard.
|
||||
*/
|
||||
export class SeasonStewardingConfig
|
||||
implements IValueObject<SeasonStewardingConfigProps>
|
||||
{
|
||||
readonly decisionMode: StewardingDecisionMode;
|
||||
readonly requiredVotes?: number;
|
||||
readonly requireDefense: boolean;
|
||||
readonly defenseTimeLimit: number;
|
||||
readonly voteTimeLimit: number;
|
||||
readonly protestDeadlineHours: number;
|
||||
readonly stewardingClosesHours: number;
|
||||
readonly notifyAccusedOnProtest: boolean;
|
||||
readonly notifyOnVoteRequired: boolean;
|
||||
|
||||
constructor(props: SeasonStewardingConfigProps) {
|
||||
if (!props.decisionMode) {
|
||||
throw new RacingDomainValidationError(
|
||||
'SeasonStewardingConfig.decisionMode is required',
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
(props.decisionMode === 'steward_vote' ||
|
||||
props.decisionMode === 'member_vote' ||
|
||||
props.decisionMode === 'steward_veto' ||
|
||||
props.decisionMode === 'member_veto') &&
|
||||
(props.requiredVotes === undefined ||
|
||||
!Number.isInteger(props.requiredVotes) ||
|
||||
props.requiredVotes <= 0)
|
||||
) {
|
||||
throw new RacingDomainValidationError(
|
||||
'SeasonStewardingConfig.requiredVotes must be a positive integer for voting/veto modes',
|
||||
);
|
||||
}
|
||||
|
||||
if (!Number.isInteger(props.defenseTimeLimit) || props.defenseTimeLimit < 0) {
|
||||
throw new RacingDomainValidationError(
|
||||
'SeasonStewardingConfig.defenseTimeLimit must be a non-negative integer (hours)',
|
||||
);
|
||||
}
|
||||
|
||||
if (!Number.isInteger(props.voteTimeLimit) || props.voteTimeLimit <= 0) {
|
||||
throw new RacingDomainValidationError(
|
||||
'SeasonStewardingConfig.voteTimeLimit must be a positive integer (hours)',
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!Number.isInteger(props.protestDeadlineHours) ||
|
||||
props.protestDeadlineHours <= 0
|
||||
) {
|
||||
throw new RacingDomainValidationError(
|
||||
'SeasonStewardingConfig.protestDeadlineHours must be a positive integer (hours)',
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!Number.isInteger(props.stewardingClosesHours) ||
|
||||
props.stewardingClosesHours <= 0
|
||||
) {
|
||||
throw new RacingDomainValidationError(
|
||||
'SeasonStewardingConfig.stewardingClosesHours must be a positive integer (hours)',
|
||||
);
|
||||
}
|
||||
|
||||
this.decisionMode = props.decisionMode;
|
||||
if (props.requiredVotes !== undefined) {
|
||||
this.requiredVotes = props.requiredVotes;
|
||||
}
|
||||
this.requireDefense = props.requireDefense;
|
||||
this.defenseTimeLimit = props.defenseTimeLimit;
|
||||
this.voteTimeLimit = props.voteTimeLimit;
|
||||
this.protestDeadlineHours = props.protestDeadlineHours;
|
||||
this.stewardingClosesHours = props.stewardingClosesHours;
|
||||
this.notifyAccusedOnProtest = props.notifyAccusedOnProtest;
|
||||
this.notifyOnVoteRequired = props.notifyOnVoteRequired;
|
||||
}
|
||||
|
||||
get props(): SeasonStewardingConfigProps {
|
||||
return {
|
||||
decisionMode: this.decisionMode,
|
||||
...(this.requiredVotes !== undefined
|
||||
? { requiredVotes: this.requiredVotes }
|
||||
: {}),
|
||||
requireDefense: this.requireDefense,
|
||||
defenseTimeLimit: this.defenseTimeLimit,
|
||||
voteTimeLimit: this.voteTimeLimit,
|
||||
protestDeadlineHours: this.protestDeadlineHours,
|
||||
stewardingClosesHours: this.stewardingClosesHours,
|
||||
notifyAccusedOnProtest: this.notifyAccusedOnProtest,
|
||||
notifyOnVoteRequired: this.notifyOnVoteRequired,
|
||||
};
|
||||
}
|
||||
|
||||
equals(other: IValueObject<SeasonStewardingConfigProps>): boolean {
|
||||
const a = this.props;
|
||||
const b = other.props;
|
||||
return (
|
||||
a.decisionMode === b.decisionMode &&
|
||||
a.requiredVotes === b.requiredVotes &&
|
||||
a.requireDefense === b.requireDefense &&
|
||||
a.defenseTimeLimit === b.defenseTimeLimit &&
|
||||
a.voteTimeLimit === b.voteTimeLimit &&
|
||||
a.protestDeadlineHours === b.protestDeadlineHours &&
|
||||
a.stewardingClosesHours === b.stewardingClosesHours &&
|
||||
a.notifyAccusedOnProtest === b.notifyAccusedOnProtest &&
|
||||
a.notifyOnVoteRequired === b.notifyOnVoteRequired
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,10 @@ export interface WeekdaySetProps {
|
||||
export class WeekdaySet implements IValueObject<WeekdaySetProps> {
|
||||
private readonly days: Weekday[];
|
||||
|
||||
static fromArray(days: Weekday[]): WeekdaySet {
|
||||
return new WeekdaySet(days);
|
||||
}
|
||||
|
||||
constructor(days: Weekday[]) {
|
||||
if (!Array.isArray(days) || days.length === 0) {
|
||||
throw new RacingDomainValidationError('WeekdaySet requires at least one weekday');
|
||||
|
||||
Reference in New Issue
Block a user