wip
This commit is contained in:
@@ -19,9 +19,9 @@ export class Car implements IEntity<string> {
|
||||
readonly carClass: CarClass;
|
||||
readonly license: CarLicense;
|
||||
readonly year: number;
|
||||
readonly horsepower?: number;
|
||||
readonly weight?: number;
|
||||
readonly imageUrl?: string;
|
||||
readonly horsepower: number | undefined;
|
||||
readonly weight: number | undefined;
|
||||
readonly imageUrl: string | undefined;
|
||||
readonly gameId: string;
|
||||
|
||||
private constructor(props: {
|
||||
|
||||
@@ -13,7 +13,7 @@ export class Driver implements IEntity<string> {
|
||||
readonly iracingId: string;
|
||||
readonly name: string;
|
||||
readonly country: string;
|
||||
readonly bio?: string;
|
||||
readonly bio: string | undefined;
|
||||
readonly joinedAt: Date;
|
||||
|
||||
private constructor(props: {
|
||||
@@ -92,14 +92,18 @@ export class Driver implements IEntity<string> {
|
||||
update(props: Partial<{
|
||||
name: string;
|
||||
country: string;
|
||||
bio: string;
|
||||
bio?: string;
|
||||
}>): Driver {
|
||||
const nextName = props.name ?? this.name;
|
||||
const nextCountry = props.country ?? this.country;
|
||||
const nextBio = props.bio ?? this.bio;
|
||||
|
||||
return new Driver({
|
||||
id: this.id,
|
||||
iracingId: this.iracingId,
|
||||
name: props.name ?? this.name,
|
||||
country: props.country ?? this.country,
|
||||
bio: props.bio ?? this.bio,
|
||||
name: nextName,
|
||||
country: nextCountry,
|
||||
...(nextBio !== undefined ? { bio: nextBio } : {}),
|
||||
joinedAt: this.joinedAt,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ export class League implements IEntity<string> {
|
||||
readonly ownerId: string;
|
||||
readonly settings: LeagueSettings;
|
||||
readonly createdAt: Date;
|
||||
readonly socialLinks?: LeagueSocialLinks;
|
||||
readonly socialLinks: LeagueSocialLinks | undefined;
|
||||
|
||||
private constructor(props: {
|
||||
id: string;
|
||||
@@ -140,6 +140,8 @@ export class League implements IEntity<string> {
|
||||
stewarding: defaultStewardingSettings,
|
||||
};
|
||||
|
||||
const socialLinks = props.socialLinks;
|
||||
|
||||
return new League({
|
||||
id: props.id,
|
||||
name: props.name,
|
||||
@@ -147,7 +149,7 @@ export class League implements IEntity<string> {
|
||||
ownerId: props.ownerId,
|
||||
settings: { ...defaultSettings, ...props.settings },
|
||||
createdAt: props.createdAt ?? new Date(),
|
||||
socialLinks: props.socialLinks,
|
||||
...(socialLinks !== undefined ? { socialLinks } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -189,7 +191,7 @@ export class League implements IEntity<string> {
|
||||
description: string;
|
||||
ownerId: string;
|
||||
settings: LeagueSettings;
|
||||
socialLinks: LeagueSocialLinks | undefined;
|
||||
socialLinks?: LeagueSocialLinks;
|
||||
}>): League {
|
||||
return new League({
|
||||
id: this.id,
|
||||
@@ -198,7 +200,11 @@ export class League implements IEntity<string> {
|
||||
ownerId: props.ownerId ?? this.ownerId,
|
||||
settings: props.settings ?? this.settings,
|
||||
createdAt: this.createdAt,
|
||||
socialLinks: props.socialLinks ?? this.socialLinks,
|
||||
...(props.socialLinks !== undefined
|
||||
? { socialLinks: props.socialLinks }
|
||||
: this.socialLinks !== undefined
|
||||
? { socialLinks: this.socialLinks }
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -102,12 +102,16 @@ export class Penalty implements IEntity<string> {
|
||||
if (this.props.status === 'overturned') {
|
||||
throw new RacingDomainInvariantError('Cannot apply an overturned penalty');
|
||||
}
|
||||
return new Penalty({
|
||||
const base: PenaltyProps = {
|
||||
...this.props,
|
||||
status: 'applied',
|
||||
appliedAt: new Date(),
|
||||
notes,
|
||||
});
|
||||
};
|
||||
|
||||
const next: PenaltyProps =
|
||||
notes !== undefined ? { ...base, notes } : base;
|
||||
|
||||
return Penalty.create(next);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -153,14 +153,18 @@ export class Protest implements IEntity<string> {
|
||||
if (!statement?.trim()) {
|
||||
throw new RacingDomainValidationError('Defense statement is required');
|
||||
}
|
||||
const defenseBase: ProtestDefense = {
|
||||
statement: statement.trim(),
|
||||
submittedAt: new Date(),
|
||||
};
|
||||
|
||||
const nextDefense: ProtestDefense =
|
||||
videoUrl !== undefined ? { ...defenseBase, videoUrl } : defenseBase;
|
||||
|
||||
return new Protest({
|
||||
...this.props,
|
||||
status: 'under_review',
|
||||
defense: {
|
||||
statement: statement.trim(),
|
||||
videoUrl,
|
||||
submittedAt: new Date(),
|
||||
},
|
||||
defense: nextDefense,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -16,14 +16,14 @@ export class Race implements IEntity<string> {
|
||||
readonly leagueId: string;
|
||||
readonly scheduledAt: Date;
|
||||
readonly track: string;
|
||||
readonly trackId?: string;
|
||||
readonly trackId: string | undefined;
|
||||
readonly car: string;
|
||||
readonly carId?: string;
|
||||
readonly carId: string | undefined;
|
||||
readonly sessionType: SessionType;
|
||||
readonly status: RaceStatus;
|
||||
readonly strengthOfField?: number;
|
||||
readonly registeredCount?: number;
|
||||
readonly maxParticipants?: number;
|
||||
readonly strengthOfField: number | undefined;
|
||||
readonly registeredCount: number | undefined;
|
||||
readonly maxParticipants: number | undefined;
|
||||
|
||||
private constructor(props: {
|
||||
id: string;
|
||||
@@ -127,10 +127,34 @@ export class Race implements IEntity<string> {
|
||||
throw new RacingDomainInvariantError('Only scheduled races can be started');
|
||||
}
|
||||
|
||||
return new Race({
|
||||
...this,
|
||||
status: 'running',
|
||||
});
|
||||
const base = {
|
||||
id: this.id,
|
||||
leagueId: this.leagueId,
|
||||
scheduledAt: this.scheduledAt,
|
||||
track: this.track,
|
||||
car: this.car,
|
||||
sessionType: this.sessionType,
|
||||
status: 'running' as RaceStatus,
|
||||
};
|
||||
|
||||
const withTrackId =
|
||||
this.trackId !== undefined ? { ...base, trackId: this.trackId } : base;
|
||||
const withCarId =
|
||||
this.carId !== undefined ? { ...withTrackId, carId: this.carId } : withTrackId;
|
||||
const withSof =
|
||||
this.strengthOfField !== undefined
|
||||
? { ...withCarId, strengthOfField: this.strengthOfField }
|
||||
: withCarId;
|
||||
const withRegistered =
|
||||
this.registeredCount !== undefined
|
||||
? { ...withSof, registeredCount: this.registeredCount }
|
||||
: withSof;
|
||||
const props =
|
||||
this.maxParticipants !== undefined
|
||||
? { ...withRegistered, maxParticipants: this.maxParticipants }
|
||||
: withRegistered;
|
||||
|
||||
return Race.create(props);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -145,10 +169,34 @@ export class Race implements IEntity<string> {
|
||||
throw new RacingDomainInvariantError('Cannot complete a cancelled race');
|
||||
}
|
||||
|
||||
return new Race({
|
||||
...this,
|
||||
status: 'completed',
|
||||
});
|
||||
const base = {
|
||||
id: this.id,
|
||||
leagueId: this.leagueId,
|
||||
scheduledAt: this.scheduledAt,
|
||||
track: this.track,
|
||||
car: this.car,
|
||||
sessionType: this.sessionType,
|
||||
status: 'completed' as RaceStatus,
|
||||
};
|
||||
|
||||
const withTrackId =
|
||||
this.trackId !== undefined ? { ...base, trackId: this.trackId } : base;
|
||||
const withCarId =
|
||||
this.carId !== undefined ? { ...withTrackId, carId: this.carId } : withTrackId;
|
||||
const withSof =
|
||||
this.strengthOfField !== undefined
|
||||
? { ...withCarId, strengthOfField: this.strengthOfField }
|
||||
: withCarId;
|
||||
const withRegistered =
|
||||
this.registeredCount !== undefined
|
||||
? { ...withSof, registeredCount: this.registeredCount }
|
||||
: withSof;
|
||||
const props =
|
||||
this.maxParticipants !== undefined
|
||||
? { ...withRegistered, maxParticipants: this.maxParticipants }
|
||||
: withRegistered;
|
||||
|
||||
return Race.create(props);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -163,21 +211,62 @@ export class Race implements IEntity<string> {
|
||||
throw new RacingDomainInvariantError('Race is already cancelled');
|
||||
}
|
||||
|
||||
return new Race({
|
||||
...this,
|
||||
status: 'cancelled',
|
||||
});
|
||||
const base = {
|
||||
id: this.id,
|
||||
leagueId: this.leagueId,
|
||||
scheduledAt: this.scheduledAt,
|
||||
track: this.track,
|
||||
car: this.car,
|
||||
sessionType: this.sessionType,
|
||||
status: 'cancelled' as RaceStatus,
|
||||
};
|
||||
|
||||
const withTrackId =
|
||||
this.trackId !== undefined ? { ...base, trackId: this.trackId } : base;
|
||||
const withCarId =
|
||||
this.carId !== undefined ? { ...withTrackId, carId: this.carId } : withTrackId;
|
||||
const withSof =
|
||||
this.strengthOfField !== undefined
|
||||
? { ...withCarId, strengthOfField: this.strengthOfField }
|
||||
: withCarId;
|
||||
const withRegistered =
|
||||
this.registeredCount !== undefined
|
||||
? { ...withSof, registeredCount: this.registeredCount }
|
||||
: withSof;
|
||||
const props =
|
||||
this.maxParticipants !== undefined
|
||||
? { ...withRegistered, maxParticipants: this.maxParticipants }
|
||||
: withRegistered;
|
||||
|
||||
return Race.create(props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update SOF and participant count
|
||||
*/
|
||||
updateField(strengthOfField: number, registeredCount: number): Race {
|
||||
return new Race({
|
||||
...this,
|
||||
const base = {
|
||||
id: this.id,
|
||||
leagueId: this.leagueId,
|
||||
scheduledAt: this.scheduledAt,
|
||||
track: this.track,
|
||||
car: this.car,
|
||||
sessionType: this.sessionType,
|
||||
status: this.status,
|
||||
strengthOfField,
|
||||
registeredCount,
|
||||
});
|
||||
};
|
||||
|
||||
const withTrackId =
|
||||
this.trackId !== undefined ? { ...base, trackId: this.trackId } : base;
|
||||
const withCarId =
|
||||
this.carId !== undefined ? { ...withTrackId, carId: this.carId } : withTrackId;
|
||||
const props =
|
||||
this.maxParticipants !== undefined
|
||||
? { ...withCarId, maxParticipants: this.maxParticipants }
|
||||
: withCarId;
|
||||
|
||||
return Race.create(props);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,11 +8,11 @@ export class Season implements IEntity<string> {
|
||||
readonly leagueId: string;
|
||||
readonly gameId: string;
|
||||
readonly name: string;
|
||||
readonly year?: number;
|
||||
readonly order?: number;
|
||||
readonly year: number | undefined;
|
||||
readonly order: number | undefined;
|
||||
readonly status: SeasonStatus;
|
||||
readonly startDate?: Date;
|
||||
readonly endDate?: Date;
|
||||
readonly startDate: Date | undefined;
|
||||
readonly endDate: Date | undefined;
|
||||
|
||||
private constructor(props: {
|
||||
id: string;
|
||||
|
||||
@@ -33,8 +33,8 @@ export class SeasonSponsorship implements IEntity<string> {
|
||||
readonly pricing: Money;
|
||||
readonly status: SponsorshipStatus;
|
||||
readonly createdAt: Date;
|
||||
readonly activatedAt?: Date;
|
||||
readonly description?: string;
|
||||
readonly activatedAt: Date | undefined;
|
||||
readonly description: string | undefined;
|
||||
|
||||
private constructor(props: SeasonSponsorshipProps) {
|
||||
this.id = props.id;
|
||||
@@ -105,11 +105,23 @@ export class SeasonSponsorship implements IEntity<string> {
|
||||
throw new RacingDomainInvariantError('Cannot activate a cancelled SeasonSponsorship');
|
||||
}
|
||||
|
||||
return new SeasonSponsorship({
|
||||
...this,
|
||||
const base: SeasonSponsorshipProps = {
|
||||
id: this.id,
|
||||
seasonId: this.seasonId,
|
||||
sponsorId: this.sponsorId,
|
||||
tier: this.tier,
|
||||
pricing: this.pricing,
|
||||
status: 'active',
|
||||
createdAt: this.createdAt,
|
||||
activatedAt: new Date(),
|
||||
});
|
||||
};
|
||||
|
||||
const next: SeasonSponsorshipProps =
|
||||
this.description !== undefined
|
||||
? { ...base, description: this.description }
|
||||
: base;
|
||||
|
||||
return new SeasonSponsorship(next);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -120,10 +132,27 @@ export class SeasonSponsorship implements IEntity<string> {
|
||||
throw new RacingDomainInvariantError('SeasonSponsorship is already cancelled');
|
||||
}
|
||||
|
||||
return new SeasonSponsorship({
|
||||
...this,
|
||||
const base: SeasonSponsorshipProps = {
|
||||
id: this.id,
|
||||
seasonId: this.seasonId,
|
||||
sponsorId: this.sponsorId,
|
||||
tier: this.tier,
|
||||
pricing: this.pricing,
|
||||
status: 'cancelled',
|
||||
});
|
||||
createdAt: this.createdAt,
|
||||
};
|
||||
|
||||
const withActivated =
|
||||
this.activatedAt !== undefined
|
||||
? { ...base, activatedAt: this.activatedAt }
|
||||
: base;
|
||||
|
||||
const next: SeasonSponsorshipProps =
|
||||
this.description !== undefined
|
||||
? { ...withActivated, description: this.description }
|
||||
: withActivated;
|
||||
|
||||
return new SeasonSponsorship(next);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,8 +20,8 @@ export class Sponsor implements IEntity<string> {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly contactEmail: string;
|
||||
readonly logoUrl?: string;
|
||||
readonly websiteUrl?: string;
|
||||
readonly logoUrl: string | undefined;
|
||||
readonly websiteUrl: string | undefined;
|
||||
readonly createdAt: Date;
|
||||
|
||||
private constructor(props: SponsorProps) {
|
||||
@@ -35,11 +35,23 @@ export class Sponsor implements IEntity<string> {
|
||||
|
||||
static create(props: Omit<SponsorProps, 'createdAt'> & { createdAt?: Date }): Sponsor {
|
||||
this.validate(props);
|
||||
|
||||
return new Sponsor({
|
||||
...props,
|
||||
createdAt: props.createdAt ?? new Date(),
|
||||
});
|
||||
|
||||
const { createdAt, ...rest } = props;
|
||||
const base = {
|
||||
id: rest.id,
|
||||
name: rest.name,
|
||||
contactEmail: rest.contactEmail,
|
||||
createdAt: createdAt ?? new Date(),
|
||||
};
|
||||
|
||||
const withLogo =
|
||||
rest.logoUrl !== undefined ? { ...base, logoUrl: rest.logoUrl } : base;
|
||||
const withWebsite =
|
||||
rest.websiteUrl !== undefined
|
||||
? { ...withLogo, websiteUrl: rest.websiteUrl }
|
||||
: withLogo;
|
||||
|
||||
return new Sponsor(withWebsite);
|
||||
}
|
||||
|
||||
private static validate(props: Omit<SponsorProps, 'createdAt'>): void {
|
||||
@@ -80,18 +92,30 @@ export class Sponsor implements IEntity<string> {
|
||||
update(props: Partial<{
|
||||
name: string;
|
||||
contactEmail: string;
|
||||
logoUrl: string | undefined;
|
||||
websiteUrl: string | undefined;
|
||||
logoUrl?: string;
|
||||
websiteUrl?: string;
|
||||
}>): Sponsor {
|
||||
const updated = {
|
||||
const updatedBase = {
|
||||
id: this.id,
|
||||
name: props.name ?? this.name,
|
||||
contactEmail: props.contactEmail ?? this.contactEmail,
|
||||
logoUrl: props.logoUrl !== undefined ? props.logoUrl : this.logoUrl,
|
||||
websiteUrl: props.websiteUrl !== undefined ? props.websiteUrl : this.websiteUrl,
|
||||
createdAt: this.createdAt,
|
||||
};
|
||||
|
||||
const withLogo =
|
||||
props.logoUrl !== undefined
|
||||
? { ...updatedBase, logoUrl: props.logoUrl }
|
||||
: this.logoUrl !== undefined
|
||||
? { ...updatedBase, logoUrl: this.logoUrl }
|
||||
: updatedBase;
|
||||
|
||||
const updated =
|
||||
props.websiteUrl !== undefined
|
||||
? { ...withLogo, websiteUrl: props.websiteUrl }
|
||||
: this.websiteUrl !== undefined
|
||||
? { ...withLogo, websiteUrl: this.websiteUrl }
|
||||
: withLogo;
|
||||
|
||||
Sponsor.validate(updated);
|
||||
return new Sponsor(updated);
|
||||
}
|
||||
|
||||
@@ -36,12 +36,12 @@ export class SponsorshipRequest implements IEntity<string> {
|
||||
readonly entityId: string;
|
||||
readonly tier: SponsorshipTier;
|
||||
readonly offeredAmount: Money;
|
||||
readonly message?: string;
|
||||
readonly message: string | undefined;
|
||||
readonly status: SponsorshipRequestStatus;
|
||||
readonly createdAt: Date;
|
||||
readonly respondedAt?: Date;
|
||||
readonly respondedBy?: string;
|
||||
readonly rejectionReason?: string;
|
||||
readonly respondedAt: Date | undefined;
|
||||
readonly respondedBy: string | undefined;
|
||||
readonly rejectionReason: string | undefined;
|
||||
|
||||
private constructor(props: SponsorshipRequestProps) {
|
||||
this.id = props.id;
|
||||
@@ -113,12 +113,28 @@ export class SponsorshipRequest implements IEntity<string> {
|
||||
throw new RacingDomainValidationError('respondedBy is required when accepting');
|
||||
}
|
||||
|
||||
return new SponsorshipRequest({
|
||||
...this,
|
||||
const base: SponsorshipRequestProps = {
|
||||
id: this.id,
|
||||
sponsorId: this.sponsorId,
|
||||
entityType: this.entityType,
|
||||
entityId: this.entityId,
|
||||
tier: this.tier,
|
||||
offeredAmount: this.offeredAmount,
|
||||
status: 'accepted',
|
||||
createdAt: this.createdAt,
|
||||
respondedAt: new Date(),
|
||||
respondedBy,
|
||||
});
|
||||
};
|
||||
|
||||
const withMessage =
|
||||
this.message !== undefined ? { ...base, message: this.message } : base;
|
||||
|
||||
const next: SponsorshipRequestProps =
|
||||
this.rejectionReason !== undefined
|
||||
? { ...withMessage, rejectionReason: this.rejectionReason }
|
||||
: withMessage;
|
||||
|
||||
return new SponsorshipRequest(next);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -133,13 +149,26 @@ export class SponsorshipRequest implements IEntity<string> {
|
||||
throw new RacingDomainValidationError('respondedBy is required when rejecting');
|
||||
}
|
||||
|
||||
return new SponsorshipRequest({
|
||||
...this,
|
||||
const base: SponsorshipRequestProps = {
|
||||
id: this.id,
|
||||
sponsorId: this.sponsorId,
|
||||
entityType: this.entityType,
|
||||
entityId: this.entityId,
|
||||
tier: this.tier,
|
||||
offeredAmount: this.offeredAmount,
|
||||
status: 'rejected',
|
||||
createdAt: this.createdAt,
|
||||
respondedAt: new Date(),
|
||||
respondedBy,
|
||||
rejectionReason: reason,
|
||||
});
|
||||
};
|
||||
|
||||
const withMessage =
|
||||
this.message !== undefined ? { ...base, message: this.message } : base;
|
||||
|
||||
const next: SponsorshipRequestProps =
|
||||
reason !== undefined ? { ...withMessage, rejectionReason: reason } : withMessage;
|
||||
|
||||
return new SponsorshipRequest(next);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -150,11 +179,34 @@ export class SponsorshipRequest implements IEntity<string> {
|
||||
throw new RacingDomainInvariantError(`Cannot withdraw a ${this.status} sponsorship request`);
|
||||
}
|
||||
|
||||
return new SponsorshipRequest({
|
||||
...this,
|
||||
const base: SponsorshipRequestProps = {
|
||||
id: this.id,
|
||||
sponsorId: this.sponsorId,
|
||||
entityType: this.entityType,
|
||||
entityId: this.entityId,
|
||||
tier: this.tier,
|
||||
offeredAmount: this.offeredAmount,
|
||||
status: 'withdrawn',
|
||||
createdAt: this.createdAt,
|
||||
respondedAt: new Date(),
|
||||
});
|
||||
};
|
||||
|
||||
const withRespondedBy =
|
||||
this.respondedBy !== undefined
|
||||
? { ...base, respondedBy: this.respondedBy }
|
||||
: base;
|
||||
|
||||
const withMessage =
|
||||
this.message !== undefined
|
||||
? { ...withRespondedBy, message: this.message }
|
||||
: withRespondedBy;
|
||||
|
||||
const next: SponsorshipRequestProps =
|
||||
this.rejectionReason !== undefined
|
||||
? { ...withMessage, rejectionReason: this.rejectionReason }
|
||||
: withMessage;
|
||||
|
||||
return new SponsorshipRequest(next);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* Immutable entity with factory methods and domain validation.
|
||||
*/
|
||||
|
||||
import { RacingDomainValidationError, RacingDomainError } from '../errors/RacingDomainError';
|
||||
import { RacingDomainError, RacingDomainValidationError } from '../errors/RacingDomainError';
|
||||
import type { IEntity } from '@gridpilot/shared/domain';
|
||||
|
||||
export class Standing implements IEntity<string> {
|
||||
@@ -104,12 +104,17 @@ export class Standing implements IEntity<string> {
|
||||
*/
|
||||
updatePosition(position: number): Standing {
|
||||
if (!Number.isInteger(position) || position < 1) {
|
||||
throw new RacingDomainError('Position must be a positive integer');
|
||||
throw new RacingDomainValidationError('Position must be a positive integer');
|
||||
}
|
||||
|
||||
return new Standing({
|
||||
...this,
|
||||
return Standing.create({
|
||||
id: this.id,
|
||||
leagueId: this.leagueId,
|
||||
driverId: this.driverId,
|
||||
points: this.points,
|
||||
wins: this.wins,
|
||||
position,
|
||||
racesCompleted: this.racesCompleted,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ export class Track implements IEntity<string> {
|
||||
readonly difficulty: TrackDifficulty;
|
||||
readonly lengthKm: number;
|
||||
readonly turns: number;
|
||||
readonly imageUrl?: string;
|
||||
readonly imageUrl: string | undefined;
|
||||
readonly gameId: string;
|
||||
|
||||
private constructor(props: {
|
||||
@@ -32,7 +32,7 @@ export class Track implements IEntity<string> {
|
||||
difficulty: TrackDifficulty;
|
||||
lengthKm: number;
|
||||
turns: number;
|
||||
imageUrl?: string;
|
||||
imageUrl?: string | undefined;
|
||||
gameId: string;
|
||||
}) {
|
||||
this.id = props.id;
|
||||
@@ -64,7 +64,7 @@ export class Track implements IEntity<string> {
|
||||
}): Track {
|
||||
this.validate(props);
|
||||
|
||||
return new Track({
|
||||
const base = {
|
||||
id: props.id,
|
||||
name: props.name,
|
||||
shortName: props.shortName ?? props.name.slice(0, 3).toUpperCase(),
|
||||
@@ -73,9 +73,13 @@ export class Track implements IEntity<string> {
|
||||
difficulty: props.difficulty ?? 'intermediate',
|
||||
lengthKm: props.lengthKm,
|
||||
turns: props.turns,
|
||||
imageUrl: props.imageUrl,
|
||||
gameId: props.gameId,
|
||||
});
|
||||
};
|
||||
|
||||
const withImage =
|
||||
props.imageUrl !== undefined ? { ...base, imageUrl: props.imageUrl } : base;
|
||||
|
||||
return new Track(withImage);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -36,6 +36,11 @@ export interface ITeamMembershipRepository {
|
||||
*/
|
||||
removeMembership(teamId: string, driverId: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Count active members for a team.
|
||||
*/
|
||||
countByTeamId(teamId: string): Promise<number>;
|
||||
|
||||
/**
|
||||
* Get all join requests for a team.
|
||||
*/
|
||||
|
||||
@@ -115,6 +115,10 @@ export class EventScoringService
|
||||
const sortedByLap = [...results].sort((a, b) => a.fastestLap - b.fastestLap);
|
||||
const best = sortedByLap[0];
|
||||
|
||||
if (!best) {
|
||||
return;
|
||||
}
|
||||
|
||||
const requiresTop = rule.requiresFinishInTopN;
|
||||
if (typeof requiresTop === 'number') {
|
||||
if (best.position <= 0 || best.position > requiresTop) {
|
||||
|
||||
13
packages/racing/domain/services/IDriverStatsService.ts
Normal file
13
packages/racing/domain/services/IDriverStatsService.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { IDomainService } from '@gridpilot/shared/domain';
|
||||
|
||||
export interface DriverStats {
|
||||
rating: number;
|
||||
wins: number;
|
||||
podiums: number;
|
||||
totalRaces: number;
|
||||
overallRank: number | null;
|
||||
}
|
||||
|
||||
export interface IDriverStatsService extends IDomainService {
|
||||
getDriverStats(driverId: string): DriverStats | null;
|
||||
}
|
||||
11
packages/racing/domain/services/IRankingService.ts
Normal file
11
packages/racing/domain/services/IRankingService.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { IDomainService } from '@gridpilot/shared/domain';
|
||||
|
||||
export interface DriverRanking {
|
||||
driverId: string;
|
||||
rating: number;
|
||||
overallRank: number | null;
|
||||
}
|
||||
|
||||
export interface IRankingService extends IDomainService {
|
||||
getAllDriverRankings(): DriverRanking[];
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { SeasonSchedule } from '../value-objects/SeasonSchedule';
|
||||
import { ScheduledRaceSlot } from '../value-objects/ScheduledRaceSlot';
|
||||
import type { RecurrenceStrategy } from '../value-objects/RecurrenceStrategy';
|
||||
import { RacingDomainValidationError, RacingDomainError } from '../errors/RacingDomainError';
|
||||
import { RacingDomainError, RacingDomainValidationError } from '../errors/RacingDomainError';
|
||||
import { RaceTimeOfDay } from '../value-objects/RaceTimeOfDay';
|
||||
import type { Weekday } from '../types/Weekday';
|
||||
import { weekdayToIndex } from '../types/Weekday';
|
||||
@@ -163,7 +163,7 @@ export class SeasonScheduleGenerator {
|
||||
|
||||
static generateSlotsUpTo(schedule: SeasonSchedule, maxRounds: number): ScheduledRaceSlot[] {
|
||||
if (!Number.isInteger(maxRounds) || maxRounds <= 0) {
|
||||
throw new RacingDomainError('maxRounds must be a positive integer');
|
||||
throw new RacingDomainValidationError('maxRounds must be a positive integer');
|
||||
}
|
||||
|
||||
const recurrence: RecurrenceStrategy = schedule.recurrence;
|
||||
|
||||
@@ -24,7 +24,7 @@ export interface GameConstraintsProps {
|
||||
/**
|
||||
* Game-specific constraints for popular sim racing games
|
||||
*/
|
||||
const GAME_CONSTRAINTS: Record<string, GameConstraintsData> = {
|
||||
const GAME_CONSTRAINTS: Record<string, GameConstraintsData> & { default: GameConstraintsData } = {
|
||||
iracing: {
|
||||
maxDrivers: 64,
|
||||
maxTeams: 32,
|
||||
@@ -76,6 +76,15 @@ const GAME_CONSTRAINTS: Record<string, GameConstraintsData> = {
|
||||
},
|
||||
};
|
||||
|
||||
function getConstraintsForId(gameId: string): GameConstraintsData {
|
||||
const lower = gameId.toLowerCase();
|
||||
const fromMap = GAME_CONSTRAINTS[lower];
|
||||
if (fromMap) {
|
||||
return fromMap;
|
||||
}
|
||||
return GAME_CONSTRAINTS.default;
|
||||
}
|
||||
|
||||
export class GameConstraints implements IValueObject<GameConstraintsProps> {
|
||||
readonly gameId: string;
|
||||
readonly constraints: GameConstraintsData;
|
||||
@@ -100,8 +109,8 @@ export class GameConstraints implements IValueObject<GameConstraintsProps> {
|
||||
* Get constraints for a specific game
|
||||
*/
|
||||
static forGame(gameId: string): GameConstraints {
|
||||
const constraints = getConstraintsForId(gameId);
|
||||
const lowerId = gameId.toLowerCase();
|
||||
const constraints = GAME_CONSTRAINTS[lowerId] ?? GAME_CONSTRAINTS.default;
|
||||
return new GameConstraints(lowerId, constraints);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,17 +17,17 @@ export interface SponsorshipSlotConfig {
|
||||
}
|
||||
|
||||
export interface SponsorshipPricingProps {
|
||||
mainSlot?: SponsorshipSlotConfig;
|
||||
secondarySlots?: SponsorshipSlotConfig;
|
||||
mainSlot?: SponsorshipSlotConfig | undefined;
|
||||
secondarySlots?: SponsorshipSlotConfig | undefined;
|
||||
acceptingApplications: boolean;
|
||||
customRequirements?: string;
|
||||
customRequirements?: string | undefined;
|
||||
}
|
||||
|
||||
export class SponsorshipPricing implements IValueObject<SponsorshipPricingProps> {
|
||||
readonly mainSlot?: SponsorshipSlotConfig;
|
||||
readonly secondarySlots?: SponsorshipSlotConfig;
|
||||
readonly mainSlot: SponsorshipSlotConfig | undefined;
|
||||
readonly secondarySlots: SponsorshipSlotConfig | undefined;
|
||||
readonly acceptingApplications: boolean;
|
||||
readonly customRequirements?: string;
|
||||
readonly customRequirements: string | undefined;
|
||||
|
||||
private constructor(props: SponsorshipPricingProps) {
|
||||
this.mainSlot = props.mainSlot;
|
||||
@@ -212,8 +212,10 @@ export class SponsorshipPricing implements IValueObject<SponsorshipPricingProps>
|
||||
maxSlots: 1,
|
||||
};
|
||||
|
||||
const base = this.props;
|
||||
|
||||
return new SponsorshipPricing({
|
||||
...this,
|
||||
...base,
|
||||
mainSlot: {
|
||||
...currentMain,
|
||||
...config,
|
||||
@@ -234,8 +236,10 @@ export class SponsorshipPricing implements IValueObject<SponsorshipPricingProps>
|
||||
maxSlots: 2,
|
||||
};
|
||||
|
||||
const base = this.props;
|
||||
|
||||
return new SponsorshipPricing({
|
||||
...this,
|
||||
...base,
|
||||
secondarySlots: {
|
||||
...currentSecondary,
|
||||
...config,
|
||||
@@ -248,8 +252,10 @@ export class SponsorshipPricing implements IValueObject<SponsorshipPricingProps>
|
||||
* Enable/disable accepting applications
|
||||
*/
|
||||
setAcceptingApplications(accepting: boolean): SponsorshipPricing {
|
||||
const base = this.props;
|
||||
|
||||
return new SponsorshipPricing({
|
||||
...this,
|
||||
...base,
|
||||
acceptingApplications: accepting,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user