This commit is contained in:
2025-12-04 23:31:55 +01:00
parent 9fa21a488a
commit fb509607c1
96 changed files with 5839 additions and 1609 deletions

View File

@@ -0,0 +1,41 @@
import type { ParticipantRef } from '../value-objects/ParticipantRef';
export class ChampionshipStanding {
readonly seasonId: string;
readonly championshipId: string;
readonly participant: ParticipantRef;
readonly totalPoints: number;
readonly resultsCounted: number;
readonly resultsDropped: number;
readonly position: number;
constructor(props: {
seasonId: string;
championshipId: string;
participant: ParticipantRef;
totalPoints: number;
resultsCounted: number;
resultsDropped: number;
position: number;
}) {
this.seasonId = props.seasonId;
this.championshipId = props.championshipId;
this.participant = props.participant;
this.totalPoints = props.totalPoints;
this.resultsCounted = props.resultsCounted;
this.resultsDropped = props.resultsDropped;
this.position = props.position;
}
withPosition(position: number): ChampionshipStanding {
return new ChampionshipStanding({
seasonId: this.seasonId,
championshipId: this.championshipId,
participant: this.participant,
totalPoints: this.totalPoints,
resultsCounted: this.resultsCounted,
resultsDropped: this.resultsDropped,
position,
});
}
}

View File

@@ -0,0 +1,24 @@
export class Game {
readonly id: string;
readonly name: string;
private constructor(props: { id: string; name: string }) {
this.id = props.id;
this.name = props.name;
}
static create(props: { id: string; name: string }): Game {
if (!props.id || props.id.trim().length === 0) {
throw new Error('Game ID is required');
}
if (!props.name || props.name.trim().length === 0) {
throw new Error('Game name is required');
}
return new Game({
id: props.id,
name: props.name,
});
}
}

View File

@@ -10,6 +10,17 @@ export interface LeagueSettings {
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;
}
export interface LeagueSocialLinks {
discordUrl?: string;
youtubeUrl?: string;
websiteUrl?: string;
}
export class League {
@@ -19,6 +30,7 @@ export class League {
readonly ownerId: string;
readonly settings: LeagueSettings;
readonly createdAt: Date;
readonly socialLinks?: LeagueSocialLinks;
private constructor(props: {
id: string;
@@ -27,6 +39,7 @@ export class League {
ownerId: string;
settings: LeagueSettings;
createdAt: Date;
socialLinks?: LeagueSocialLinks;
}) {
this.id = props.id;
this.name = props.name;
@@ -34,6 +47,7 @@ export class League {
this.ownerId = props.ownerId;
this.settings = props.settings;
this.createdAt = props.createdAt;
this.socialLinks = props.socialLinks;
}
/**
@@ -46,6 +60,7 @@ export class League {
ownerId: string;
settings?: Partial<LeagueSettings>;
createdAt?: Date;
socialLinks?: LeagueSocialLinks;
}): League {
this.validate(props);
@@ -53,6 +68,7 @@ export class League {
pointsSystem: 'f1-2024',
sessionDuration: 60,
qualifyingFormat: 'open',
maxDrivers: 32,
};
return new League({
@@ -62,6 +78,7 @@ export class League {
ownerId: props.ownerId,
settings: { ...defaultSettings, ...props.settings },
createdAt: props.createdAt ?? new Date(),
socialLinks: props.socialLinks,
});
}
@@ -102,6 +119,7 @@ export class League {
name: string;
description: string;
settings: LeagueSettings;
socialLinks: LeagueSocialLinks | undefined;
}>): League {
return new League({
id: this.id,
@@ -110,6 +128,7 @@ export class League {
ownerId: this.ownerId,
settings: props.settings ?? this.settings,
createdAt: this.createdAt,
socialLinks: props.socialLinks ?? this.socialLinks,
});
}
}

View File

@@ -0,0 +1,7 @@
import type { ChampionshipConfig } from '../value-objects/ChampionshipConfig';
export interface LeagueScoringConfig {
id: string;
seasonId: string;
championships: ChampionshipConfig[];
}

View File

@@ -0,0 +1,29 @@
/**
* Domain Entity: Penalty
*
* Represents a season-long penalty or bonus applied to a driver
* within a specific league. This is intentionally simple for the
* alpha demo and models points adjustments only.
*/
export type PenaltyType = 'points-deduction' | 'points-bonus';
export interface Penalty {
id: string;
leagueId: string;
driverId: string;
type: PenaltyType;
/**
* Signed integer representing points adjustment:
* - negative for deductions
* - positive for bonuses
*/
pointsDelta: number;
/**
* Optional short reason/label (e.g. "Incident penalty", "Fastest laps bonus").
*/
reason?: string;
/**
* When this penalty was applied.
*/
appliedAt: Date;
}

View File

@@ -0,0 +1,77 @@
export type SeasonStatus = 'planned' | 'active' | 'completed';
export class Season {
readonly id: string;
readonly leagueId: string;
readonly gameId: string;
readonly name: string;
readonly year?: number;
readonly order?: number;
readonly status: SeasonStatus;
readonly startDate?: Date;
readonly endDate?: Date;
private constructor(props: {
id: string;
leagueId: string;
gameId: string;
name: string;
year?: number;
order?: number;
status: SeasonStatus;
startDate?: Date;
endDate?: Date;
}) {
this.id = props.id;
this.leagueId = props.leagueId;
this.gameId = props.gameId;
this.name = props.name;
this.year = props.year;
this.order = props.order;
this.status = props.status;
this.startDate = props.startDate;
this.endDate = props.endDate;
}
static create(props: {
id: string;
leagueId: string;
gameId: string;
name: string;
year?: number;
order?: number;
status?: SeasonStatus;
startDate?: Date;
endDate?: Date;
}): Season {
if (!props.id || props.id.trim().length === 0) {
throw new Error('Season ID is required');
}
if (!props.leagueId || props.leagueId.trim().length === 0) {
throw new Error('Season leagueId is required');
}
if (!props.gameId || props.gameId.trim().length === 0) {
throw new Error('Season gameId is required');
}
if (!props.name || props.name.trim().length === 0) {
throw new Error('Season name is required');
}
const status: SeasonStatus = props.status ?? 'planned';
return new Season({
id: props.id,
leagueId: props.leagueId,
gameId: props.gameId,
name: props.name,
year: props.year,
order: props.order,
status,
startDate: props.startDate,
endDate: props.endDate,
});
}
}