194 lines
5.6 KiB
TypeScript
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 } : {}),
|
|
});
|
|
}
|
|
} |