This commit is contained in:
2025-12-11 13:50:38 +01:00
parent e4c1be628d
commit c7e5de40d6
212 changed files with 2965 additions and 763 deletions

View File

@@ -1,17 +1,17 @@
/**
* Domain Entity: Car
*/
import { RacingDomainValidationError } from '../errors/RacingDomainError';
*
*
* Represents a racing car/vehicle in the GridPilot platform.
* Immutable entity with factory methods and domain validation.
*/
import type { IEntity } from '@gridpilot/shared/domain';
import { RacingDomainValidationError } from '../errors/RacingDomainError';
export type CarClass = 'formula' | 'gt' | 'prototype' | 'touring' | 'sports' | 'oval' | 'dirt';
export type CarLicense = 'R' | 'D' | 'C' | 'B' | 'A' | 'Pro';
export class Car {
export class Car implements IEntity<string> {
readonly id: string;
readonly name: string;
readonly shortName: string;

View File

@@ -1,4 +1,4 @@
import type { ParticipantRef } from '../value-objects/ParticipantRef';
import type { ParticipantRef } from '../types/ParticipantRef';
export class ChampionshipStanding {
readonly seasonId: string;

View File

@@ -4,10 +4,11 @@
* Represents a driver profile in the GridPilot platform.
* Immutable entity with factory methods and domain validation.
*/
import { RacingDomainValidationError } from '../errors/RacingDomainError';
export class Driver {
import type { IEntity } from '@gridpilot/shared/domain';
export class Driver implements IEntity<string> {
readonly id: string;
readonly iracingId: string;
readonly name: string;

View File

@@ -1,8 +1,9 @@
/**
* Domain Entity: DriverLivery
*/
import { RacingDomainValidationError, RacingDomainInvariantError } from '../errors/RacingDomainError';
import type { IEntity } from '@gridpilot/shared/domain';
*
* Represents a driver's custom livery for a specific car.
* Includes user-placed decals and league-specific overrides.
@@ -31,7 +32,7 @@ export interface DriverLiveryProps {
validatedAt?: Date;
}
export class DriverLivery {
export class DriverLivery implements IEntity<string> {
readonly id: string;
readonly driverId: string;
readonly gameId: string;

View File

@@ -1,6 +1,7 @@
import { RacingDomainValidationError } from '../errors/RacingDomainError';
export class Game {
import type { IEntity } from '@gridpilot/shared/domain';
export class Game implements IEntity<string> {
readonly id: string;
readonly name: string;

View File

@@ -4,8 +4,9 @@
* Represents a league in the GridPilot platform.
* Immutable entity with factory methods and domain validation.
*/
import { RacingDomainValidationError } from '../errors/RacingDomainError';
import type { IEntity } from '@gridpilot/shared/domain';
/**
* Stewarding decision mode for protests
@@ -78,8 +79,8 @@ export interface LeagueSocialLinks {
youtubeUrl?: string;
websiteUrl?: string;
}
export class League {
export class League implements IEntity<string> {
readonly id: string;
readonly name: string;
readonly description: string;

View File

@@ -1,19 +1,75 @@
/**
* Domain Entity: LeagueMembership and JoinRequest
*
* Extracted from racing-application memberships module so that
* membership-related types live in the racing-domain package.
* Represents a driver's membership in a league and join requests.
*/
import type { IEntity } from '@gridpilot/shared/domain';
import { RacingDomainValidationError } from '../errors/RacingDomainError';
export type MembershipRole = 'owner' | 'admin' | 'steward' | 'member';
export type MembershipStatus = 'active' | 'pending' | 'none';
export interface LeagueMembership {
export interface LeagueMembershipProps {
id?: string;
leagueId: string;
driverId: string;
role: MembershipRole;
status: MembershipStatus;
joinedAt: Date;
status?: MembershipStatus;
joinedAt?: Date;
}
export class LeagueMembership implements IEntity<string> {
readonly id: string;
readonly leagueId: string;
readonly driverId: string;
readonly role: MembershipRole;
readonly status: MembershipStatus;
readonly joinedAt: Date;
private constructor(props: Required<LeagueMembershipProps>) {
this.id = props.id;
this.leagueId = props.leagueId;
this.driverId = props.driverId;
this.role = props.role;
this.status = props.status;
this.joinedAt = props.joinedAt;
}
static create(props: LeagueMembershipProps): LeagueMembership {
this.validate(props);
const id =
props.id && props.id.trim().length > 0
? props.id
: `${props.leagueId}:${props.driverId}`;
const status = props.status ?? 'pending';
const joinedAt = props.joinedAt ?? new Date();
return new LeagueMembership({
id,
leagueId: props.leagueId,
driverId: props.driverId,
role: props.role,
status,
joinedAt,
});
}
private static validate(props: LeagueMembershipProps): void {
if (!props.leagueId || props.leagueId.trim().length === 0) {
throw new RacingDomainValidationError('League ID is required');
}
if (!props.driverId || props.driverId.trim().length === 0) {
throw new RacingDomainValidationError('Driver ID is required');
}
if (!props.role) {
throw new RacingDomainValidationError('Membership role is required');
}
}
}
export interface JoinRequest {

View File

@@ -1,4 +1,4 @@
import type { ChampionshipConfig } from '../value-objects/ChampionshipConfig';
import type { ChampionshipConfig } from '../types/ChampionshipConfig';
export interface LeagueScoringConfig {
id: string;

View File

@@ -4,8 +4,9 @@
* Represents a league's financial wallet.
* Aggregate root for managing league finances and transactions.
*/
import { RacingDomainValidationError, RacingDomainInvariantError } from '../errors/RacingDomainError';
import type { IEntity } from '@gridpilot/shared/domain';
import type { Money } from '../value-objects/Money';
import type { Transaction } from './Transaction';
@@ -18,7 +19,7 @@ export interface LeagueWalletProps {
createdAt: Date;
}
export class LeagueWallet {
export class LeagueWallet implements IEntity<string> {
readonly id: string;
readonly leagueId: string;
readonly balance: Money;

View File

@@ -1,8 +1,9 @@
/**
* Domain Entity: LiveryTemplate
*/
import { RacingDomainValidationError, RacingDomainInvariantError } from '../errors/RacingDomainError';
import type { IEntity } from '@gridpilot/shared/domain';
*
* Represents an admin-defined livery template for a specific car.
* Contains base image and sponsor decal placements.
@@ -21,7 +22,7 @@ export interface LiveryTemplateProps {
updatedAt?: Date;
}
export class LiveryTemplate {
export class LiveryTemplate implements IEntity<string> {
readonly id: string;
readonly leagueId: string;
readonly seasonId: string;

View File

@@ -4,8 +4,9 @@
* Represents a penalty applied to a driver for an incident during a race.
* Penalties can be applied as a result of an upheld protest or directly by stewards.
*/
import { RacingDomainValidationError, RacingDomainInvariantError } from '../errors/RacingDomainError';
import type { IEntity } from '@gridpilot/shared/domain';
export type PenaltyType =
| 'time_penalty' // Add time to race result (e.g., +5 seconds)
@@ -45,7 +46,7 @@ export interface PenaltyProps {
notes?: string;
}
export class Penalty {
export class Penalty implements IEntity<string> {
private constructor(private readonly props: PenaltyProps) {}
static create(props: PenaltyProps): Penalty {

View File

@@ -3,8 +3,9 @@
*
* Represents a prize awarded to a driver for a specific position in a season.
*/
import { RacingDomainValidationError, RacingDomainInvariantError } from '../errors/RacingDomainError';
import type { IEntity } from '@gridpilot/shared/domain';
import type { Money } from '../value-objects/Money';
@@ -23,7 +24,7 @@ export interface PrizeProps {
description?: string;
}
export class Prize {
export class Prize implements IEntity<string> {
readonly id: string;
readonly seasonId: string;
readonly position: number;

View File

@@ -1,8 +1,9 @@
/**
* Domain Entity: Protest
*/
import { RacingDomainValidationError, RacingDomainInvariantError } from '../errors/RacingDomainError';
import type { IEntity } from '@gridpilot/shared/domain';
*
* Represents a protest filed by a driver against another driver for an incident during a race.
*
@@ -66,7 +67,7 @@ export interface ProtestProps {
defenseRequestedBy?: string;
}
export class Protest {
export class Protest implements IEntity<string> {
private constructor(private readonly props: ProtestProps) {}
static create(props: ProtestProps): Protest {

View File

@@ -4,13 +4,14 @@
* Represents a race/session in the GridPilot platform.
* Immutable entity with factory methods and domain validation.
*/
import { RacingDomainValidationError, RacingDomainInvariantError } from '../errors/RacingDomainError';
import type { IEntity } from '@gridpilot/shared/domain';
export type SessionType = 'practice' | 'qualifying' | 'race';
export type RaceStatus = 'scheduled' | 'running' | 'completed' | 'cancelled';
export class Race {
export class Race implements IEntity<string> {
readonly id: string;
readonly leagueId: string;
readonly scheduledAt: Date;

View File

@@ -1,12 +1,57 @@
/**
* Domain Entity: RaceRegistration
*
* Extracted from racing-application registrations module so that
* registration-related types live in the racing-domain package.
* Represents a registration of a driver for a specific race.
*/
export interface RaceRegistration {
import type { IEntity } from '@gridpilot/shared/domain';
import { RacingDomainValidationError } from '../errors/RacingDomainError';
export interface RaceRegistrationProps {
id?: string;
raceId: string;
driverId: string;
registeredAt: Date;
registeredAt?: Date;
}
export class RaceRegistration implements IEntity<string> {
readonly id: string;
readonly raceId: string;
readonly driverId: string;
readonly registeredAt: Date;
private constructor(props: Required<RaceRegistrationProps>) {
this.id = props.id;
this.raceId = props.raceId;
this.driverId = props.driverId;
this.registeredAt = props.registeredAt;
}
static create(props: RaceRegistrationProps): RaceRegistration {
this.validate(props);
const id =
props.id && props.id.trim().length > 0
? props.id
: `${props.raceId}:${props.driverId}`;
const registeredAt = props.registeredAt ?? new Date();
return new RaceRegistration({
id,
raceId: props.raceId,
driverId: props.driverId,
registeredAt,
});
}
private static validate(props: RaceRegistrationProps): void {
if (!props.raceId || props.raceId.trim().length === 0) {
throw new RacingDomainValidationError('Race ID is required');
}
if (!props.driverId || props.driverId.trim().length === 0) {
throw new RacingDomainValidationError('Driver ID is required');
}
}
}

View File

@@ -4,10 +4,11 @@
* Represents a race result in the GridPilot platform.
* Immutable entity with factory methods and domain validation.
*/
import { RacingDomainValidationError } from '../errors/RacingDomainError';
import type { IEntity } from '@gridpilot/shared/domain';
export class Result {
export class Result implements IEntity<string> {
readonly id: string;
readonly raceId: string;
readonly driverId: string;

View File

@@ -1,8 +1,9 @@
export type SeasonStatus = 'planned' | 'active' | 'completed';
import { RacingDomainValidationError } from '../errors/RacingDomainError';
export class Season {
import type { IEntity } from '@gridpilot/shared/domain';
export class Season implements IEntity<string> {
readonly id: string;
readonly leagueId: string;
readonly gameId: string;

View File

@@ -4,8 +4,9 @@
* Represents a sponsorship relationship between a Sponsor and a Season.
* Aggregate root for managing sponsorship slots and pricing.
*/
import { RacingDomainValidationError, RacingDomainInvariantError } from '../errors/RacingDomainError';
import type { IEntity } from '@gridpilot/shared/domain';
import type { Money } from '../value-objects/Money';
@@ -24,7 +25,7 @@ export interface SeasonSponsorshipProps {
description?: string;
}
export class SeasonSponsorship {
export class SeasonSponsorship implements IEntity<string> {
readonly id: string;
readonly seasonId: string;
readonly sponsorId: string;

View File

@@ -1,8 +1,9 @@
/**
* Domain Entity: Sponsor
*/
import { RacingDomainValidationError } from '../errors/RacingDomainError';
import type { IEntity } from '@gridpilot/shared/domain';
*
* Represents a sponsor that can sponsor leagues/seasons.
* Aggregate root for sponsor information.
@@ -17,7 +18,7 @@ export interface SponsorProps {
createdAt: Date;
}
export class Sponsor {
export class Sponsor implements IEntity<string> {
readonly id: string;
readonly name: string;
readonly contactEmail: string;

View File

@@ -4,8 +4,9 @@
* Represents a sponsorship application from a Sponsor to any sponsorable entity
* (driver, team, race, or league/season). The entity owner must approve/reject.
*/
import { RacingDomainValidationError, RacingDomainInvariantError } from '../errors/RacingDomainError';
import type { IEntity } from '@gridpilot/shared/domain';
import type { Money } from '../value-objects/Money';
import type { SponsorshipTier } from './SeasonSponsorship';
@@ -28,7 +29,7 @@ export interface SponsorshipRequestProps {
rejectionReason?: string;
}
export class SponsorshipRequest {
export class SponsorshipRequest implements IEntity<string> {
readonly id: string;
readonly sponsorId: string;
readonly entityType: SponsorableEntityType;

View File

@@ -1,21 +1,24 @@
/**
* Domain Entity: Standing
*
*
* Represents a championship standing in the GridPilot platform.
* Immutable entity with factory methods and domain validation.
*/
import { RacingDomainValidationError } from '../errors/RacingDomainError';
import type { IEntity } from '@gridpilot/shared/domain';
export class Standing {
export class Standing implements IEntity<string> {
readonly id: string;
readonly leagueId: string;
readonly driverId: string;
readonly points: number;
readonly wins: number;
readonly position: number;
readonly racesCompleted: number;
private constructor(props: {
id: string;
leagueId: string;
driverId: string;
points: number;
@@ -23,6 +26,7 @@ export class Standing {
position: number;
racesCompleted: number;
}) {
this.id = props.id;
this.leagueId = props.leagueId;
this.driverId = props.driverId;
this.points = props.points;
@@ -35,6 +39,7 @@ export class Standing {
* Factory method to create a new Standing entity
*/
static create(props: {
id?: string;
leagueId: string;
driverId: string;
points?: number;
@@ -44,7 +49,12 @@ export class Standing {
}): Standing {
this.validate(props);
const id = props.id && props.id.trim().length > 0
? props.id
: `${props.leagueId}:${props.driverId}`;
return new Standing({
id,
leagueId: props.leagueId,
driverId: props.driverId,
points: props.points ?? 0,
@@ -58,15 +68,16 @@ export class Standing {
* Domain validation logic
*/
private static validate(props: {
id?: string;
leagueId: string;
driverId: string;
}): void {
if (!props.leagueId || props.leagueId.trim().length === 0) {
throw new RacingDomainError('League ID is required');
throw new RacingDomainValidationError('League ID is required');
}
if (!props.driverId || props.driverId.trim().length === 0) {
throw new RacingDomainError('Driver ID is required');
throw new RacingDomainValidationError('Driver ID is required');
}
}
@@ -78,6 +89,7 @@ export class Standing {
const isWin = position === 1;
return new Standing({
id: this.id,
leagueId: this.leagueId,
driverId: this.driverId,
points: this.points + racePoints,

View File

@@ -1,35 +1,128 @@
/**
* Domain Entities: Team, TeamMembership, TeamJoinRequest
* Domain Entity: Team
*
* Extracted from racing-application teams module so that
* team-related types live in the racing-domain package.
* Represents a racing team in the GridPilot platform.
* Implements the shared IEntity<string> contract and encapsulates
* basic invariants around identity and core properties.
*/
export type TeamRole = 'owner' | 'manager' | 'driver';
export type TeamMembershipStatus = 'active' | 'pending' | 'none';
import type { IEntity } from '@gridpilot/shared/domain';
import { RacingDomainValidationError } from '../errors/RacingDomainError';
export interface Team {
id: string;
name: string;
tag: string;
description: string;
ownerId: string;
leagues: string[];
createdAt: Date;
}
export class Team implements IEntity<string> {
readonly id: string;
readonly name: string;
readonly tag: string;
readonly description: string;
readonly ownerId: string;
readonly leagues: string[];
readonly createdAt: Date;
export interface TeamMembership {
teamId: string;
driverId: string;
role: TeamRole;
status: TeamMembershipStatus;
joinedAt: Date;
}
private constructor(props: {
id: string;
name: string;
tag: string;
description: string;
ownerId: string;
leagues: string[];
createdAt: Date;
}) {
this.id = props.id;
this.name = props.name;
this.tag = props.tag;
this.description = props.description;
this.ownerId = props.ownerId;
this.leagues = props.leagues;
this.createdAt = props.createdAt;
}
export interface TeamJoinRequest {
id: string;
teamId: string;
driverId: string;
requestedAt: Date;
message?: string;
/**
* Factory method to create a new Team entity.
*/
static create(props: {
id: string;
name: string;
tag: string;
description: string;
ownerId: string;
leagues: string[];
createdAt?: Date;
}): Team {
this.validate(props);
return new Team({
id: props.id,
name: props.name,
tag: props.tag,
description: props.description,
ownerId: props.ownerId,
leagues: [...props.leagues],
createdAt: props.createdAt ?? new Date(),
});
}
/**
* Create a copy with updated properties.
*/
update(props: Partial<{
name: string;
tag: string;
description: string;
ownerId: string;
leagues: string[];
}>): Team {
const next: Team = new Team({
id: this.id,
name: props.name ?? this.name,
tag: props.tag ?? this.tag,
description: props.description ?? this.description,
ownerId: props.ownerId ?? this.ownerId,
leagues: props.leagues ? [...props.leagues] : [...this.leagues],
createdAt: this.createdAt,
});
// Re-validate updated aggregate
Team.validate({
id: next.id,
name: next.name,
tag: next.tag,
description: next.description,
ownerId: next.ownerId,
leagues: next.leagues,
});
return next;
}
/**
* Domain validation logic for core invariants.
*/
private static validate(props: {
id: string;
name: string;
tag: string;
description: string;
ownerId: string;
leagues: string[];
}): void {
if (!props.id || props.id.trim().length === 0) {
throw new RacingDomainValidationError('Team ID is required');
}
if (!props.name || props.name.trim().length === 0) {
throw new RacingDomainValidationError('Team name is required');
}
if (!props.tag || props.tag.trim().length === 0) {
throw new RacingDomainValidationError('Team tag is required');
}
if (!props.ownerId || props.ownerId.trim().length === 0) {
throw new RacingDomainValidationError('Team owner ID is required');
}
if (!Array.isArray(props.leagues)) {
throw new RacingDomainValidationError('Team leagues must be an array');
}
}
}

View File

@@ -4,13 +4,14 @@
* Represents a racing track/circuit in the GridPilot platform.
* Immutable entity with factory methods and domain validation.
*/
import { RacingDomainValidationError } from '../errors/RacingDomainError';
import type { IEntity } from '@gridpilot/shared/domain';
export type TrackCategory = 'oval' | 'road' | 'street' | 'dirt';
export type TrackDifficulty = 'beginner' | 'intermediate' | 'advanced' | 'expert';
export class Track {
export class Track implements IEntity<string> {
readonly id: string;
readonly name: string;
readonly shortName: string;

View File

@@ -3,10 +3,11 @@
*
* Represents a financial transaction in the league wallet system.
*/
import { RacingDomainValidationError, RacingDomainInvariantError } from '../errors/RacingDomainError';
import type { Money } from '../value-objects/Money';
import type { IEntity } from '@gridpilot/shared/domain';
export type TransactionType =
| 'sponsorship_payment'
@@ -30,8 +31,8 @@ export interface TransactionProps {
description?: string;
metadata?: Record<string, unknown>;
}
export class Transaction {
export class Transaction implements IEntity<string> {
readonly id: string;
readonly walletId: string;
readonly type: TransactionType;