/** * 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; /** * 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 { 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; 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 } : {}), }); } }