wip
This commit is contained in:
@@ -26,8 +26,8 @@ export interface DriverLiveryProps {
|
||||
userDecals: LiveryDecal[];
|
||||
leagueOverrides: DecalOverride[];
|
||||
createdAt: Date;
|
||||
updatedAt?: Date;
|
||||
validatedAt?: Date;
|
||||
updatedAt: Date | undefined;
|
||||
validatedAt: Date | undefined;
|
||||
}
|
||||
|
||||
export class DriverLivery implements IEntity<string> {
|
||||
@@ -39,8 +39,8 @@ export class DriverLivery implements IEntity<string> {
|
||||
readonly userDecals: LiveryDecal[];
|
||||
readonly leagueOverrides: DecalOverride[];
|
||||
readonly createdAt: Date;
|
||||
readonly updatedAt?: Date;
|
||||
readonly validatedAt?: Date;
|
||||
readonly updatedAt: Date | undefined;
|
||||
readonly validatedAt: Date | undefined;
|
||||
|
||||
private constructor(props: DriverLiveryProps) {
|
||||
this.id = props.id;
|
||||
@@ -50,7 +50,7 @@ export class DriverLivery implements IEntity<string> {
|
||||
this.uploadedImageUrl = props.uploadedImageUrl;
|
||||
this.userDecals = props.userDecals;
|
||||
this.leagueOverrides = props.leagueOverrides;
|
||||
this.createdAt = props.createdAt;
|
||||
this.createdAt = props.createdAt ?? new Date();
|
||||
this.updatedAt = props.updatedAt;
|
||||
this.validatedAt = props.validatedAt;
|
||||
}
|
||||
@@ -101,9 +101,16 @@ export class DriverLivery implements IEntity<string> {
|
||||
}
|
||||
|
||||
return new DriverLivery({
|
||||
...this,
|
||||
id: this.id,
|
||||
driverId: this.driverId,
|
||||
gameId: this.gameId,
|
||||
carId: this.carId,
|
||||
uploadedImageUrl: this.uploadedImageUrl,
|
||||
userDecals: [...this.userDecals, decal],
|
||||
leagueOverrides: this.leagueOverrides,
|
||||
createdAt: this.createdAt,
|
||||
updatedAt: new Date(),
|
||||
validatedAt: this.validatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -118,9 +125,16 @@ export class DriverLivery implements IEntity<string> {
|
||||
}
|
||||
|
||||
return new DriverLivery({
|
||||
...this,
|
||||
id: this.id,
|
||||
driverId: this.driverId,
|
||||
gameId: this.gameId,
|
||||
carId: this.carId,
|
||||
uploadedImageUrl: this.uploadedImageUrl,
|
||||
userDecals: updatedDecals,
|
||||
leagueOverrides: this.leagueOverrides,
|
||||
createdAt: this.createdAt,
|
||||
updatedAt: new Date(),
|
||||
validatedAt: this.validatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -131,16 +145,23 @@ export class DriverLivery implements IEntity<string> {
|
||||
const index = this.userDecals.findIndex(d => d.id === decalId);
|
||||
|
||||
if (index === -1) {
|
||||
throw new RacingDomainError('Decal not found in livery');
|
||||
throw new RacingDomainValidationError('Decal not found in livery');
|
||||
}
|
||||
|
||||
const updatedDecals = [...this.userDecals];
|
||||
updatedDecals[index] = updatedDecal;
|
||||
|
||||
return new DriverLivery({
|
||||
...this,
|
||||
id: this.id,
|
||||
driverId: this.driverId,
|
||||
gameId: this.gameId,
|
||||
carId: this.carId,
|
||||
uploadedImageUrl: this.uploadedImageUrl,
|
||||
userDecals: updatedDecals,
|
||||
leagueOverrides: this.leagueOverrides,
|
||||
createdAt: this.createdAt,
|
||||
updatedAt: new Date(),
|
||||
validatedAt: this.validatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -163,9 +184,16 @@ export class DriverLivery implements IEntity<string> {
|
||||
}
|
||||
|
||||
return new DriverLivery({
|
||||
...this,
|
||||
id: this.id,
|
||||
driverId: this.driverId,
|
||||
gameId: this.gameId,
|
||||
carId: this.carId,
|
||||
uploadedImageUrl: this.uploadedImageUrl,
|
||||
userDecals: this.userDecals,
|
||||
leagueOverrides: updatedOverrides,
|
||||
createdAt: this.createdAt,
|
||||
updatedAt: new Date(),
|
||||
validatedAt: this.validatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -178,9 +206,16 @@ export class DriverLivery implements IEntity<string> {
|
||||
);
|
||||
|
||||
return new DriverLivery({
|
||||
...this,
|
||||
id: this.id,
|
||||
driverId: this.driverId,
|
||||
gameId: this.gameId,
|
||||
carId: this.carId,
|
||||
uploadedImageUrl: this.uploadedImageUrl,
|
||||
userDecals: this.userDecals,
|
||||
leagueOverrides: updatedOverrides,
|
||||
createdAt: this.createdAt,
|
||||
updatedAt: new Date(),
|
||||
validatedAt: this.validatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -198,7 +233,15 @@ export class DriverLivery implements IEntity<string> {
|
||||
*/
|
||||
markAsValidated(): DriverLivery {
|
||||
return new DriverLivery({
|
||||
...this,
|
||||
id: this.id,
|
||||
driverId: this.driverId,
|
||||
gameId: this.gameId,
|
||||
carId: this.carId,
|
||||
uploadedImageUrl: this.uploadedImageUrl,
|
||||
userDecals: this.userDecals,
|
||||
leagueOverrides: this.leagueOverrides,
|
||||
createdAt: this.createdAt,
|
||||
updatedAt: this.updatedAt,
|
||||
validatedAt: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ export interface LiveryTemplateProps {
|
||||
baseImageUrl: string;
|
||||
adminDecals: LiveryDecal[];
|
||||
createdAt: Date;
|
||||
updatedAt?: Date;
|
||||
updatedAt: Date | undefined;
|
||||
}
|
||||
|
||||
export class LiveryTemplate implements IEntity<string> {
|
||||
@@ -28,7 +28,7 @@ export class LiveryTemplate implements IEntity<string> {
|
||||
readonly baseImageUrl: string;
|
||||
readonly adminDecals: LiveryDecal[];
|
||||
readonly createdAt: Date;
|
||||
readonly updatedAt?: Date;
|
||||
readonly updatedAt: Date | undefined;
|
||||
|
||||
private constructor(props: LiveryTemplateProps) {
|
||||
this.id = props.id;
|
||||
@@ -37,7 +37,7 @@ export class LiveryTemplate implements IEntity<string> {
|
||||
this.carId = props.carId;
|
||||
this.baseImageUrl = props.baseImageUrl;
|
||||
this.adminDecals = props.adminDecals;
|
||||
this.createdAt = props.createdAt;
|
||||
this.createdAt = props.createdAt ?? new Date();
|
||||
this.updatedAt = props.updatedAt;
|
||||
}
|
||||
|
||||
@@ -113,9 +113,9 @@ export class LiveryTemplate implements IEntity<string> {
|
||||
*/
|
||||
updateDecal(decalId: string, updatedDecal: LiveryDecal): LiveryTemplate {
|
||||
const index = this.adminDecals.findIndex(d => d.id === decalId);
|
||||
|
||||
|
||||
if (index === -1) {
|
||||
throw new RacingDomainError('Decal not found in template');
|
||||
throw new RacingDomainValidationError('Decal not found in template');
|
||||
}
|
||||
|
||||
const updatedDecals = [...this.adminDecals];
|
||||
|
||||
@@ -19,9 +19,9 @@ export interface PrizeProps {
|
||||
driverId?: string;
|
||||
status: PrizeStatus;
|
||||
createdAt: Date;
|
||||
awardedAt?: Date;
|
||||
paidAt?: Date;
|
||||
description?: string;
|
||||
awardedAt: Date | undefined;
|
||||
paidAt: Date | undefined;
|
||||
description: string | undefined;
|
||||
}
|
||||
|
||||
export class Prize implements IEntity<string> {
|
||||
@@ -29,12 +29,12 @@ export class Prize implements IEntity<string> {
|
||||
readonly seasonId: string;
|
||||
readonly position: number;
|
||||
readonly amount: Money;
|
||||
readonly driverId?: string;
|
||||
readonly driverId: string | undefined;
|
||||
readonly status: PrizeStatus;
|
||||
readonly createdAt: Date;
|
||||
readonly awardedAt?: Date;
|
||||
readonly paidAt?: Date;
|
||||
readonly description?: string;
|
||||
readonly awardedAt: Date | undefined;
|
||||
readonly paidAt: Date | undefined;
|
||||
readonly description: string | undefined;
|
||||
|
||||
private constructor(props: PrizeProps) {
|
||||
this.id = props.id;
|
||||
@@ -43,7 +43,7 @@ export class Prize implements IEntity<string> {
|
||||
this.amount = props.amount;
|
||||
this.driverId = props.driverId;
|
||||
this.status = props.status;
|
||||
this.createdAt = props.createdAt;
|
||||
this.createdAt = props.createdAt ?? new Date();
|
||||
this.awardedAt = props.awardedAt;
|
||||
this.paidAt = props.paidAt;
|
||||
this.description = props.description;
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
export type SeasonStatus = 'planned' | 'active' | 'completed';
|
||||
|
||||
import { RacingDomainValidationError } from '../errors/RacingDomainError';
|
||||
export type SeasonStatus =
|
||||
| 'planned'
|
||||
| 'active'
|
||||
| 'completed'
|
||||
| 'archived'
|
||||
| 'cancelled';
|
||||
|
||||
import {
|
||||
RacingDomainInvariantError,
|
||||
RacingDomainValidationError,
|
||||
} from '../errors/RacingDomainError';
|
||||
import type { IEntity } from '@gridpilot/shared/domain';
|
||||
|
||||
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';
|
||||
|
||||
export class Season implements IEntity<string> {
|
||||
readonly id: string;
|
||||
readonly leagueId: string;
|
||||
@@ -13,6 +25,11 @@ export class Season implements IEntity<string> {
|
||||
readonly status: SeasonStatus;
|
||||
readonly startDate: Date | undefined;
|
||||
readonly endDate: Date | undefined;
|
||||
readonly schedule: SeasonSchedule | undefined;
|
||||
readonly scoringConfig: SeasonScoringConfig | undefined;
|
||||
readonly dropPolicy: SeasonDropPolicy | undefined;
|
||||
readonly stewardingConfig: SeasonStewardingConfig | undefined;
|
||||
readonly maxDrivers: number | undefined;
|
||||
|
||||
private constructor(props: {
|
||||
id: string;
|
||||
@@ -24,6 +41,11 @@ export class Season implements IEntity<string> {
|
||||
status: SeasonStatus;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
schedule?: SeasonSchedule;
|
||||
scoringConfig?: SeasonScoringConfig;
|
||||
dropPolicy?: SeasonDropPolicy;
|
||||
stewardingConfig?: SeasonStewardingConfig;
|
||||
maxDrivers?: number;
|
||||
}) {
|
||||
this.id = props.id;
|
||||
this.leagueId = props.leagueId;
|
||||
@@ -34,6 +56,11 @@ export class Season implements IEntity<string> {
|
||||
this.status = props.status;
|
||||
this.startDate = props.startDate;
|
||||
this.endDate = props.endDate;
|
||||
this.schedule = props.schedule;
|
||||
this.scoringConfig = props.scoringConfig;
|
||||
this.dropPolicy = props.dropPolicy;
|
||||
this.stewardingConfig = props.stewardingConfig;
|
||||
this.maxDrivers = props.maxDrivers;
|
||||
}
|
||||
|
||||
static create(props: {
|
||||
@@ -46,6 +73,11 @@ export class Season implements IEntity<string> {
|
||||
status?: SeasonStatus;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
schedule?: SeasonSchedule;
|
||||
scoringConfig?: SeasonScoringConfig;
|
||||
dropPolicy?: SeasonDropPolicy;
|
||||
stewardingConfig?: SeasonStewardingConfig;
|
||||
maxDrivers?: number;
|
||||
}): Season {
|
||||
if (!props.id || props.id.trim().length === 0) {
|
||||
throw new RacingDomainValidationError('Season ID is required');
|
||||
@@ -75,6 +107,15 @@ export class Season implements IEntity<string> {
|
||||
status,
|
||||
...(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.dropPolicy !== undefined ? { dropPolicy: props.dropPolicy } : {}),
|
||||
...(props.stewardingConfig !== undefined
|
||||
? { stewardingConfig: props.stewardingConfig }
|
||||
: {}),
|
||||
...(props.maxDrivers !== undefined ? { maxDrivers: props.maxDrivers } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -98,4 +139,281 @@ export class Season implements IEntity<string> {
|
||||
isCompleted(): boolean {
|
||||
return this.status === 'completed';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if season is planned (not yet active)
|
||||
*/
|
||||
isPlanned(): boolean {
|
||||
return this.status === 'planned';
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate the season from planned state.
|
||||
*/
|
||||
activate(): Season {
|
||||
if (this.status !== 'planned') {
|
||||
throw new RacingDomainInvariantError(
|
||||
'Only planned seasons can be activated',
|
||||
);
|
||||
}
|
||||
|
||||
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: 'active',
|
||||
...(this.startDate !== undefined ? { startDate: this.startDate } : {
|
||||
startDate: new Date(),
|
||||
}),
|
||||
...(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 }
|
||||
: {}),
|
||||
...(this.maxDrivers !== undefined ? { maxDrivers: this.maxDrivers } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the season as completed.
|
||||
*/
|
||||
complete(): Season {
|
||||
if (this.status !== 'active') {
|
||||
throw new RacingDomainInvariantError(
|
||||
'Only active seasons can be completed',
|
||||
);
|
||||
}
|
||||
|
||||
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: 'completed',
|
||||
...(this.startDate !== undefined ? { startDate: this.startDate } : {}),
|
||||
...(this.endDate !== undefined ? { endDate: this.endDate } : {
|
||||
endDate: new Date(),
|
||||
}),
|
||||
...(this.schedule !== undefined ? { schedule: this.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 } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Archive a completed season.
|
||||
*/
|
||||
archive(): Season {
|
||||
if (!this.isCompleted()) {
|
||||
throw new RacingDomainInvariantError(
|
||||
'Only completed seasons can be archived',
|
||||
);
|
||||
}
|
||||
|
||||
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: 'archived',
|
||||
...(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 }
|
||||
: {}),
|
||||
...(this.maxDrivers !== undefined ? { maxDrivers: this.maxDrivers } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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',
|
||||
);
|
||||
}
|
||||
|
||||
if (this.status === 'cancelled') {
|
||||
return this;
|
||||
}
|
||||
|
||||
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: 'cancelled',
|
||||
...(this.startDate !== undefined ? { startDate: this.startDate } : {}),
|
||||
...(this.endDate !== undefined ? { endDate: this.endDate } : {
|
||||
endDate: new Date(),
|
||||
}),
|
||||
...(this.schedule !== undefined ? { schedule: this.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 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',
|
||||
);
|
||||
}
|
||||
|
||||
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 } : {}),
|
||||
...(this.stewardingConfig !== undefined
|
||||
? { stewardingConfig: this.stewardingConfig }
|
||||
: {}),
|
||||
...(maxDrivers !== undefined ? { maxDrivers } : {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -11,40 +11,53 @@ import type { IEntity } from '@gridpilot/shared/domain';
|
||||
import type { Money } from '../value-objects/Money';
|
||||
|
||||
export type SponsorshipTier = 'main' | 'secondary';
|
||||
export type SponsorshipStatus = 'pending' | 'active' | 'cancelled';
|
||||
export type SponsorshipStatus = 'pending' | 'active' | 'ended' | 'cancelled';
|
||||
|
||||
export interface SeasonSponsorshipProps {
|
||||
id: string;
|
||||
seasonId: string;
|
||||
/**
|
||||
* Optional denormalized leagueId for fast league-level aggregations.
|
||||
* Must always match the owning Season's leagueId when present.
|
||||
*/
|
||||
leagueId?: string;
|
||||
sponsorId: string;
|
||||
tier: SponsorshipTier;
|
||||
pricing: Money;
|
||||
status: SponsorshipStatus;
|
||||
createdAt: Date;
|
||||
activatedAt?: Date;
|
||||
endedAt?: Date;
|
||||
cancelledAt?: Date;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export class SeasonSponsorship implements IEntity<string> {
|
||||
readonly id: string;
|
||||
readonly seasonId: string;
|
||||
readonly leagueId: string | undefined;
|
||||
readonly sponsorId: string;
|
||||
readonly tier: SponsorshipTier;
|
||||
readonly pricing: Money;
|
||||
readonly status: SponsorshipStatus;
|
||||
readonly createdAt: Date;
|
||||
readonly activatedAt: Date | undefined;
|
||||
readonly endedAt: Date | undefined;
|
||||
readonly cancelledAt: Date | undefined;
|
||||
readonly description: string | undefined;
|
||||
|
||||
private constructor(props: SeasonSponsorshipProps) {
|
||||
this.id = props.id;
|
||||
this.seasonId = props.seasonId;
|
||||
this.leagueId = props.leagueId;
|
||||
this.sponsorId = props.sponsorId;
|
||||
this.tier = props.tier;
|
||||
this.pricing = props.pricing;
|
||||
this.status = props.status;
|
||||
this.createdAt = props.createdAt;
|
||||
this.activatedAt = props.activatedAt;
|
||||
this.endedAt = props.endedAt;
|
||||
this.cancelledAt = props.cancelledAt;
|
||||
this.description = props.description;
|
||||
}
|
||||
|
||||
@@ -57,12 +70,15 @@ export class SeasonSponsorship implements IEntity<string> {
|
||||
return new SeasonSponsorship({
|
||||
id: props.id,
|
||||
seasonId: props.seasonId,
|
||||
...(props.leagueId !== undefined ? { leagueId: props.leagueId } : {}),
|
||||
sponsorId: props.sponsorId,
|
||||
tier: props.tier,
|
||||
pricing: props.pricing,
|
||||
status: props.status ?? 'pending',
|
||||
createdAt: props.createdAt ?? new Date(),
|
||||
...(props.activatedAt !== undefined ? { activatedAt: props.activatedAt } : {}),
|
||||
...(props.endedAt !== undefined ? { endedAt: props.endedAt } : {}),
|
||||
...(props.cancelledAt !== undefined ? { cancelledAt: props.cancelledAt } : {}),
|
||||
...(props.description !== undefined ? { description: props.description } : {}),
|
||||
});
|
||||
}
|
||||
@@ -105,15 +121,56 @@ export class SeasonSponsorship implements IEntity<string> {
|
||||
throw new RacingDomainInvariantError('Cannot activate a cancelled SeasonSponsorship');
|
||||
}
|
||||
|
||||
if (this.status === 'ended') {
|
||||
throw new RacingDomainInvariantError('Cannot activate an ended SeasonSponsorship');
|
||||
}
|
||||
|
||||
const base: SeasonSponsorshipProps = {
|
||||
id: this.id,
|
||||
seasonId: this.seasonId,
|
||||
...(this.leagueId !== undefined ? { leagueId: this.leagueId } : {}),
|
||||
sponsorId: this.sponsorId,
|
||||
tier: this.tier,
|
||||
pricing: this.pricing,
|
||||
status: 'active',
|
||||
createdAt: this.createdAt,
|
||||
activatedAt: new Date(),
|
||||
...(this.endedAt !== undefined ? { endedAt: this.endedAt } : {}),
|
||||
...(this.cancelledAt !== undefined ? { cancelledAt: this.cancelledAt } : {}),
|
||||
};
|
||||
|
||||
const next: SeasonSponsorshipProps =
|
||||
this.description !== undefined
|
||||
? { ...base, description: this.description }
|
||||
: base;
|
||||
|
||||
return new SeasonSponsorship(next);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the sponsorship as ended (completed term)
|
||||
*/
|
||||
end(): SeasonSponsorship {
|
||||
if (this.status === 'cancelled') {
|
||||
throw new RacingDomainInvariantError('Cannot end a cancelled SeasonSponsorship');
|
||||
}
|
||||
|
||||
if (this.status === 'ended') {
|
||||
throw new RacingDomainInvariantError('SeasonSponsorship is already ended');
|
||||
}
|
||||
|
||||
const base: SeasonSponsorshipProps = {
|
||||
id: this.id,
|
||||
seasonId: this.seasonId,
|
||||
...(this.leagueId !== undefined ? { leagueId: this.leagueId } : {}),
|
||||
sponsorId: this.sponsorId,
|
||||
tier: this.tier,
|
||||
pricing: this.pricing,
|
||||
status: 'ended',
|
||||
createdAt: this.createdAt,
|
||||
...(this.activatedAt !== undefined ? { activatedAt: this.activatedAt } : {}),
|
||||
endedAt: new Date(),
|
||||
...(this.cancelledAt !== undefined ? { cancelledAt: this.cancelledAt } : {}),
|
||||
};
|
||||
|
||||
const next: SeasonSponsorshipProps =
|
||||
@@ -135,22 +192,55 @@ export class SeasonSponsorship implements IEntity<string> {
|
||||
const base: SeasonSponsorshipProps = {
|
||||
id: this.id,
|
||||
seasonId: this.seasonId,
|
||||
...(this.leagueId !== undefined ? { leagueId: this.leagueId } : {}),
|
||||
sponsorId: this.sponsorId,
|
||||
tier: this.tier,
|
||||
pricing: this.pricing,
|
||||
status: 'cancelled',
|
||||
createdAt: this.createdAt,
|
||||
...(this.activatedAt !== undefined ? { activatedAt: this.activatedAt } : {}),
|
||||
...(this.endedAt !== undefined ? { endedAt: this.endedAt } : {}),
|
||||
cancelledAt: new Date(),
|
||||
};
|
||||
|
||||
const withActivated =
|
||||
this.activatedAt !== undefined
|
||||
? { ...base, activatedAt: this.activatedAt }
|
||||
: base;
|
||||
|
||||
const next: SeasonSponsorshipProps =
|
||||
this.description !== undefined
|
||||
? { ...withActivated, description: this.description }
|
||||
: withActivated;
|
||||
? { ...base, description: this.description }
|
||||
: base;
|
||||
|
||||
return new SeasonSponsorship(next);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update pricing/terms when allowed
|
||||
*/
|
||||
withPricing(pricing: Money): SeasonSponsorship {
|
||||
if (pricing.amount <= 0) {
|
||||
throw new RacingDomainValidationError('SeasonSponsorship pricing must be greater than zero');
|
||||
}
|
||||
|
||||
if (this.status === 'cancelled' || this.status === 'ended') {
|
||||
throw new RacingDomainInvariantError('Cannot update pricing for ended or cancelled SeasonSponsorship');
|
||||
}
|
||||
|
||||
const base: SeasonSponsorshipProps = {
|
||||
id: this.id,
|
||||
seasonId: this.seasonId,
|
||||
...(this.leagueId !== undefined ? { leagueId: this.leagueId } : {}),
|
||||
sponsorId: this.sponsorId,
|
||||
tier: this.tier,
|
||||
pricing,
|
||||
status: this.status,
|
||||
createdAt: this.createdAt,
|
||||
...(this.activatedAt !== undefined ? { activatedAt: this.activatedAt } : {}),
|
||||
...(this.endedAt !== undefined ? { endedAt: this.endedAt } : {}),
|
||||
...(this.cancelledAt !== undefined ? { cancelledAt: this.cancelledAt } : {}),
|
||||
};
|
||||
|
||||
const next: SeasonSponsorshipProps =
|
||||
this.description !== undefined
|
||||
? { ...base, description: this.description }
|
||||
: base;
|
||||
|
||||
return new SeasonSponsorship(next);
|
||||
}
|
||||
|
||||
@@ -27,9 +27,9 @@ export interface TransactionProps {
|
||||
netAmount: Money;
|
||||
status: TransactionStatus;
|
||||
createdAt: Date;
|
||||
completedAt?: Date;
|
||||
description?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
completedAt: Date | undefined;
|
||||
description: string | undefined;
|
||||
metadata: Record<string, unknown> | undefined;
|
||||
}
|
||||
|
||||
export class Transaction implements IEntity<string> {
|
||||
@@ -41,9 +41,9 @@ export class Transaction implements IEntity<string> {
|
||||
readonly netAmount: Money;
|
||||
readonly status: TransactionStatus;
|
||||
readonly createdAt: Date;
|
||||
readonly completedAt?: Date;
|
||||
readonly description?: string;
|
||||
readonly metadata?: Record<string, unknown>;
|
||||
readonly completedAt: Date | undefined;
|
||||
readonly description: string | undefined;
|
||||
readonly metadata: Record<string, unknown> | undefined;
|
||||
|
||||
private constructor(props: TransactionProps) {
|
||||
this.id = props.id;
|
||||
|
||||
@@ -2,6 +2,34 @@ import type { Season } from '../entities/Season';
|
||||
|
||||
export interface ISeasonRepository {
|
||||
findById(id: string): Promise<Season | null>;
|
||||
/**
|
||||
* Backward-compatible alias retained for existing callers.
|
||||
* Prefer listByLeague for new usage.
|
||||
*/
|
||||
findByLeagueId(leagueId: string): Promise<Season[]>;
|
||||
/**
|
||||
* Backward-compatible alias retained for existing callers.
|
||||
* Prefer add for new usage.
|
||||
*/
|
||||
create(season: Season): Promise<Season>;
|
||||
|
||||
/**
|
||||
* Add a new Season aggregate.
|
||||
*/
|
||||
add(season: Season): Promise<void>;
|
||||
|
||||
/**
|
||||
* Persist changes to an existing Season aggregate.
|
||||
*/
|
||||
update(season: Season): Promise<void>;
|
||||
|
||||
/**
|
||||
* List all Seasons for a given League.
|
||||
*/
|
||||
listByLeague(leagueId: string): Promise<Season[]>;
|
||||
|
||||
/**
|
||||
* List Seasons for a League that are currently active.
|
||||
*/
|
||||
listActiveByLeague(leagueId: string): Promise<Season[]>;
|
||||
}
|
||||
@@ -9,6 +9,12 @@ import type { SeasonSponsorship, SponsorshipTier } from '../entities/SeasonSpons
|
||||
export interface ISeasonSponsorshipRepository {
|
||||
findById(id: string): Promise<SeasonSponsorship | null>;
|
||||
findBySeasonId(seasonId: string): Promise<SeasonSponsorship[]>;
|
||||
/**
|
||||
* Convenience lookup for aggregating sponsorships at league level.
|
||||
* Implementations should rely on the denormalized leagueId where present,
|
||||
* falling back to joining through Seasons if needed.
|
||||
*/
|
||||
findByLeagueId(leagueId: string): Promise<SeasonSponsorship[]>;
|
||||
findBySponsorId(sponsorId: string): Promise<SeasonSponsorship[]>;
|
||||
findBySeasonAndTier(seasonId: string, tier: SponsorshipTier): Promise<SeasonSponsorship[]>;
|
||||
create(sponsorship: SeasonSponsorship): Promise<SeasonSponsorship>;
|
||||
|
||||
@@ -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