This commit is contained in:
2025-12-17 00:33:13 +01:00
parent 8c67081953
commit f01e01e50c
186 changed files with 9242 additions and 1342 deletions

View File

@@ -4,9 +4,14 @@
* Represents a league in the GridPilot platform.
* Immutable entity with factory methods and domain validation.
*/
import { RacingDomainValidationError } from '../errors/RacingDomainError';
import type { IEntity } from '@core/shared/domain';
import { LeagueId } from './LeagueId';
import { LeagueName } from './LeagueName';
import { LeagueDescription } from './LeagueDescription';
import { LeagueOwnerId } from './LeagueOwnerId';
import { LeagueCreatedAt } from './LeagueCreatedAt';
import { LeagueSocialLinks } from './LeagueSocialLinks';
/**
* Stewarding decision mode for protests
@@ -21,41 +26,41 @@ export type StewardingDecisionMode =
export interface StewardingSettings {
/**
* How protest decisions are made
*/
* How protest decisions are made
*/
decisionMode: StewardingDecisionMode;
/**
* Number of votes required to uphold/reject a protest
* Used with steward_vote, member_vote, steward_veto, member_veto modes
*/
* Number of votes required to uphold/reject a protest
* Used with steward_vote, member_vote, steward_veto, member_veto modes
*/
requiredVotes?: number;
/**
* Whether to require a defense from the accused before deciding
*/
* Whether to require a defense from the accused before deciding
*/
requireDefense?: boolean;
/**
* Time limit (hours) for accused to submit defense
*/
* Time limit (hours) for accused to submit defense
*/
defenseTimeLimit?: number;
/**
* Time limit (hours) for voting to complete
*/
* Time limit (hours) for voting to complete
*/
voteTimeLimit?: number;
/**
* Time limit (hours) after race ends when protests can be filed
*/
* Time limit (hours) after race ends when protests can be filed
*/
protestDeadlineHours?: number;
/**
* Time limit (hours) after race ends when stewarding is closed (no more decisions)
*/
* Time limit (hours) after race ends when stewarding is closed (no more decisions)
*/
stewardingClosesHours?: number;
/**
* Whether to notify the accused when a protest is filed
*/
* Whether to notify the accused when a protest is filed
*/
notifyAccusedOnProtest?: boolean;
/**
* Whether to notify eligible voters when a vote is required
*/
* Whether to notify eligible voters when a vote is required
*/
notifyOnVoteRequired?: boolean;
}
@@ -65,38 +70,32 @@ export interface LeagueSettings {
qualifyingFormat?: 'single-lap' | 'open';
customPoints?: Record<number, number>;
/**
* Maximum number of drivers allowed in the league.
* Used for simple capacity display on the website.
*/
* Maximum number of drivers allowed in the league.
* Used for simple capacity display on the website.
*/
maxDrivers?: number;
/**
* Stewarding settings for protest handling
*/
* Stewarding settings for protest handling
*/
stewarding?: StewardingSettings;
}
export interface LeagueSocialLinks {
discordUrl?: string;
youtubeUrl?: string;
websiteUrl?: string;
}
export class League implements IEntity<string> {
readonly id: string;
readonly name: string;
readonly description: string;
readonly ownerId: string;
export class League implements IEntity<LeagueId> {
readonly id: LeagueId;
readonly name: LeagueName;
readonly description: LeagueDescription;
readonly ownerId: LeagueOwnerId;
readonly settings: LeagueSettings;
readonly createdAt: Date;
readonly createdAt: LeagueCreatedAt;
readonly socialLinks: LeagueSocialLinks | undefined;
private constructor(props: {
id: string;
name: string;
description: string;
ownerId: string;
id: LeagueId;
name: LeagueName;
description: LeagueDescription;
ownerId: LeagueOwnerId;
settings: LeagueSettings;
createdAt: Date;
createdAt: LeagueCreatedAt;
socialLinks?: LeagueSocialLinks;
}) {
this.id = props.id;
@@ -118,9 +117,17 @@ export class League implements IEntity<string> {
ownerId: string;
settings?: Partial<LeagueSettings>;
createdAt?: Date;
socialLinks?: LeagueSocialLinks;
socialLinks?: {
discordUrl?: string;
youtubeUrl?: string;
websiteUrl?: string;
};
}): League {
this.validate(props);
const id = LeagueId.create(props.id);
const name = LeagueName.create(props.name);
const description = LeagueDescription.create(props.description);
const ownerId = LeagueOwnerId.create(props.ownerId);
const createdAt = LeagueCreatedAt.create(props.createdAt ?? new Date());
const defaultStewardingSettings: StewardingSettings = {
decisionMode: 'admin_only',
@@ -141,48 +148,19 @@ export class League implements IEntity<string> {
stewarding: defaultStewardingSettings,
};
const socialLinks = props.socialLinks;
const socialLinks = props.socialLinks ? LeagueSocialLinks.create(props.socialLinks) : undefined;
return new League({
id: props.id,
name: props.name,
description: props.description,
ownerId: props.ownerId,
id,
name,
description,
ownerId,
settings: { ...defaultSettings, ...props.settings },
createdAt: props.createdAt ?? new Date(),
createdAt,
...(socialLinks !== undefined ? { socialLinks } : {}),
});
}
/**
* Domain validation logic
*/
private static validate(props: {
id: string;
name: string;
description: string;
ownerId: string;
}): void {
if (!props.id || props.id.trim().length === 0) {
throw new RacingDomainValidationError('League ID is required');
}
if (!props.name || props.name.trim().length === 0) {
throw new RacingDomainValidationError('League name is required');
}
if (props.name.length > 100) {
throw new RacingDomainValidationError('League name must be 100 characters or less');
}
if (!props.description || props.description.trim().length === 0) {
throw new RacingDomainValidationError('League description is required');
}
if (!props.ownerId || props.ownerId.trim().length === 0) {
throw new RacingDomainValidationError('League owner ID is required');
}
}
/**
* Create a copy with updated properties
@@ -192,20 +170,25 @@ export class League implements IEntity<string> {
description: string;
ownerId: string;
settings: LeagueSettings;
socialLinks?: LeagueSocialLinks;
socialLinks?: {
discordUrl?: string;
youtubeUrl?: string;
websiteUrl?: string;
};
}>): League {
const name = props.name ? LeagueName.create(props.name) : this.name;
const description = props.description ? LeagueDescription.create(props.description) : this.description;
const ownerId = props.ownerId ? LeagueOwnerId.create(props.ownerId) : this.ownerId;
const socialLinks = props.socialLinks ? LeagueSocialLinks.create(props.socialLinks) : this.socialLinks;
return new League({
id: this.id,
name: props.name ?? this.name,
description: props.description ?? this.description,
ownerId: props.ownerId ?? this.ownerId,
name,
description,
ownerId,
settings: props.settings ?? this.settings,
createdAt: this.createdAt,
...(props.socialLinks !== undefined
? { socialLinks: props.socialLinks }
: this.socialLinks !== undefined
? { socialLinks: this.socialLinks }
: {}),
...(socialLinks !== undefined ? { socialLinks } : {}),
});
}
}