Files
gridpilot.gg/core/racing/domain/entities/League.ts
2025-12-17 00:33:13 +01:00

194 lines
5.6 KiB
TypeScript

/**
* Domain Entity: League
*
* Represents a league in the GridPilot platform.
* Immutable entity with factory methods and domain validation.
*/
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
*/
export type StewardingDecisionMode =
| 'admin_only' // Only admins can decide
| 'steward_decides' // Single steward makes decision
| 'steward_vote' // X stewards must vote to uphold
| 'member_vote' // X members must vote to uphold
| 'steward_veto' // Upheld unless X stewards vote against
| 'member_veto'; // Upheld unless X members vote against
export interface StewardingSettings {
/**
* 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
*/
requiredVotes?: number;
/**
* Whether to require a defense from the accused before deciding
*/
requireDefense?: boolean;
/**
* Time limit (hours) for accused to submit defense
*/
defenseTimeLimit?: number;
/**
* Time limit (hours) for voting to complete
*/
voteTimeLimit?: number;
/**
* 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)
*/
stewardingClosesHours?: number;
/**
* Whether to notify the accused when a protest is filed
*/
notifyAccusedOnProtest?: boolean;
/**
* Whether to notify eligible voters when a vote is required
*/
notifyOnVoteRequired?: boolean;
}
export interface LeagueSettings {
pointsSystem: 'f1-2024' | 'indycar' | 'custom';
sessionDuration?: number;
qualifyingFormat?: 'single-lap' | 'open';
customPoints?: Record<number, number>;
/**
* Maximum number of drivers allowed in the league.
* Used for simple capacity display on the website.
*/
maxDrivers?: number;
/**
* Stewarding settings for protest handling
*/
stewarding?: StewardingSettings;
}
export class League implements IEntity<LeagueId> {
readonly id: LeagueId;
readonly name: LeagueName;
readonly description: LeagueDescription;
readonly ownerId: LeagueOwnerId;
readonly settings: LeagueSettings;
readonly createdAt: LeagueCreatedAt;
readonly socialLinks: LeagueSocialLinks | undefined;
private constructor(props: {
id: LeagueId;
name: LeagueName;
description: LeagueDescription;
ownerId: LeagueOwnerId;
settings: LeagueSettings;
createdAt: LeagueCreatedAt;
socialLinks?: LeagueSocialLinks;
}) {
this.id = props.id;
this.name = props.name;
this.description = props.description;
this.ownerId = props.ownerId;
this.settings = props.settings;
this.createdAt = props.createdAt;
this.socialLinks = props.socialLinks;
}
/**
* Factory method to create a new League entity
*/
static create(props: {
id: string;
name: string;
description: string;
ownerId: string;
settings?: Partial<LeagueSettings>;
createdAt?: Date;
socialLinks?: {
discordUrl?: string;
youtubeUrl?: string;
websiteUrl?: string;
};
}): League {
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',
requireDefense: false,
defenseTimeLimit: 48,
voteTimeLimit: 72,
protestDeadlineHours: 48,
stewardingClosesHours: 168, // 7 days
notifyAccusedOnProtest: true,
notifyOnVoteRequired: true,
};
const defaultSettings: LeagueSettings = {
pointsSystem: 'f1-2024',
sessionDuration: 60,
qualifyingFormat: 'open',
maxDrivers: 32,
stewarding: defaultStewardingSettings,
};
const socialLinks = props.socialLinks ? LeagueSocialLinks.create(props.socialLinks) : undefined;
return new League({
id,
name,
description,
ownerId,
settings: { ...defaultSettings, ...props.settings },
createdAt,
...(socialLinks !== undefined ? { socialLinks } : {}),
});
}
/**
* Create a copy with updated properties
*/
update(props: Partial<{
name: string;
description: string;
ownerId: string;
settings: LeagueSettings;
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,
description,
ownerId,
settings: props.settings ?? this.settings,
createdAt: this.createdAt,
...(socialLinks !== undefined ? { socialLinks } : {}),
});
}
}