This commit is contained in:
2025-12-17 00:33:13 +01:00
parent 8c67081953
commit f01e01e50c
186 changed files with 9242 additions and 1342 deletions

View File

@@ -0,0 +1,25 @@
import { RacingDomainValidationError } from '../errors/RacingDomainError';
import type { IValueObject } from '@core/shared/domain';
export class CarId implements IValueObject<string> {
private constructor(private readonly value: string) {}
static create(value: string): CarId {
if (!value || value.trim().length === 0) {
throw new RacingDomainValidationError('Car ID cannot be empty');
}
return new CarId(value.trim());
}
toString(): string {
return this.value;
}
equals(other: IValueObject<string>): boolean {
return this.value === other.props;
}
get props(): string {
return this.value;
}
}

View File

@@ -0,0 +1,47 @@
import { describe, it, expect } from 'vitest';
import { CountryCode } from './CountryCode';
describe('CountryCode', () => {
it('should create a country code', () => {
const code = CountryCode.create('US');
expect(code.toString()).toBe('US');
});
it('should uppercase the code', () => {
const code = CountryCode.create('us');
expect(code.toString()).toBe('US');
});
it('should accept 3 letter code', () => {
const code = CountryCode.create('USA');
expect(code.toString()).toBe('USA');
});
it('should throw on empty code', () => {
expect(() => CountryCode.create('')).toThrow('Country code is required');
});
it('should throw on invalid length', () => {
expect(() => CountryCode.create('U')).toThrow('Country must be a valid ISO code (2-3 letters)');
});
it('should throw on 4 letters', () => {
expect(() => CountryCode.create('USAA')).toThrow('Country must be a valid ISO code (2-3 letters)');
});
it('should throw on non-letters', () => {
expect(() => CountryCode.create('U1')).toThrow('Country must be a valid ISO code (2-3 letters)');
});
it('should equal same code', () => {
const c1 = CountryCode.create('US');
const c2 = CountryCode.create('US');
expect(c1.equals(c2)).toBe(true);
});
it('should not equal different code', () => {
const c1 = CountryCode.create('US');
const c2 = CountryCode.create('CA');
expect(c1.equals(c2)).toBe(false);
});
});

View File

@@ -0,0 +1,24 @@
import { RacingDomainValidationError } from '../errors/RacingDomainError';
export class CountryCode {
private constructor(private readonly value: string) {}
static create(value: string): CountryCode {
if (!value || value.trim().length === 0) {
throw new RacingDomainValidationError('Country code is required');
}
const trimmed = value.trim().toUpperCase();
if (!/^[A-Z]{2,3}$/.test(trimmed)) {
throw new RacingDomainValidationError('Country must be a valid ISO code (2-3 letters)');
}
return new CountryCode(trimmed);
}
toString(): string {
return this.value;
}
equals(other: CountryCode): boolean {
return this.value === other.value;
}
}

View File

@@ -0,0 +1,71 @@
import { RacingDomainValidationError } from '../errors/RacingDomainError';
import type { IValueObject } from '@core/shared/domain';
export interface DecalOverrideProps {
leagueId: string;
seasonId: string;
decalId: string;
newX: number;
newY: number;
}
export class DecalOverride implements IValueObject<DecalOverrideProps> {
readonly leagueId: string;
readonly seasonId: string;
readonly decalId: string;
readonly newX: number;
readonly newY: number;
private constructor(props: DecalOverrideProps) {
this.leagueId = props.leagueId;
this.seasonId = props.seasonId;
this.decalId = props.decalId;
this.newX = props.newX;
this.newY = props.newY;
}
static create(props: DecalOverrideProps): DecalOverride {
this.validate(props);
return new DecalOverride(props);
}
private static validate(props: DecalOverrideProps): void {
if (!props.leagueId || props.leagueId.trim().length === 0) {
throw new RacingDomainValidationError('DecalOverride leagueId is required');
}
if (!props.seasonId || props.seasonId.trim().length === 0) {
throw new RacingDomainValidationError('DecalOverride seasonId is required');
}
if (!props.decalId || props.decalId.trim().length === 0) {
throw new RacingDomainValidationError('DecalOverride decalId is required');
}
if (props.newX < 0 || props.newX > 1) {
throw new RacingDomainValidationError('DecalOverride newX must be between 0 and 1');
}
if (props.newY < 0 || props.newY > 1) {
throw new RacingDomainValidationError('DecalOverride newY must be between 0 and 1');
}
}
equals(other: IValueObject<DecalOverrideProps>): boolean {
const a = this.props;
const b = other.props;
return (
a.leagueId === b.leagueId &&
a.seasonId === b.seasonId &&
a.decalId === b.decalId &&
a.newX === b.newX &&
a.newY === b.newY
);
}
get props(): DecalOverrideProps {
return {
leagueId: this.leagueId,
seasonId: this.seasonId,
decalId: this.decalId,
newX: this.newX,
newY: this.newY,
};
}
}

View File

@@ -0,0 +1,31 @@
import { describe, it, expect } from 'vitest';
import { DriverBio } from './DriverBio';
describe('DriverBio', () => {
it('should create a driver bio', () => {
const bio = DriverBio.create('A passionate racer.');
expect(bio.toString()).toBe('A passionate racer.');
});
it('should allow empty string', () => {
const bio = DriverBio.create('');
expect(bio.toString()).toBe('');
});
it('should throw on bio too long', () => {
const longBio = 'a'.repeat(501);
expect(() => DriverBio.create(longBio)).toThrow('Driver bio cannot exceed 500 characters');
});
it('should equal same bio', () => {
const b1 = DriverBio.create('Racer');
const b2 = DriverBio.create('Racer');
expect(b1.equals(b2)).toBe(true);
});
it('should not equal different bio', () => {
const b1 = DriverBio.create('Racer');
const b2 = DriverBio.create('Driver');
expect(b1.equals(b2)).toBe(false);
});
});

View File

@@ -0,0 +1,20 @@
import { RacingDomainValidationError } from '../errors/RacingDomainError';
export class DriverBio {
private constructor(private readonly value: string) {}
static create(value: string): DriverBio {
if (value.length > 500) {
throw new RacingDomainValidationError('Driver bio cannot exceed 500 characters');
}
return new DriverBio(value);
}
toString(): string {
return this.value;
}
equals(other: DriverBio): boolean {
return this.value === other.value;
}
}

View File

@@ -0,0 +1,29 @@
import { describe, it, expect } from 'vitest';
import { DriverId } from './DriverId';
describe('DriverId', () => {
it('should create a driver id', () => {
const id = DriverId.create('driver1');
expect(id.toString()).toBe('driver1');
});
it('should throw on empty id', () => {
expect(() => DriverId.create('')).toThrow('Driver ID is required');
});
it('should throw on whitespace id', () => {
expect(() => DriverId.create(' ')).toThrow('Driver ID is required');
});
it('should equal same id', () => {
const id1 = DriverId.create('driver1');
const id2 = DriverId.create('driver1');
expect(id1.equals(id2)).toBe(true);
});
it('should not equal different id', () => {
const id1 = DriverId.create('driver1');
const id2 = DriverId.create('driver2');
expect(id1.equals(id2)).toBe(false);
});
});

View File

@@ -0,0 +1,25 @@
import { RacingDomainValidationError } from '../errors/RacingDomainError';
import type { IValueObject } from '@core/shared/domain';
export class DriverId implements IValueObject<string> {
private constructor(private readonly value: string) {}
static create(value: string): DriverId {
if (!value || value.trim().length === 0) {
throw new RacingDomainValidationError('Driver ID cannot be empty');
}
return new DriverId(value.trim());
}
toString(): string {
return this.value;
}
equals(other: IValueObject<string>): boolean {
return this.value === other.props;
}
get props(): string {
return this.value;
}
}

View File

@@ -0,0 +1,34 @@
import { describe, it, expect } from 'vitest';
import { DriverName } from './DriverName';
describe('DriverName', () => {
it('should create a driver name', () => {
const name = DriverName.create('John Doe');
expect(name.toString()).toBe('John Doe');
});
it('should trim whitespace', () => {
const name = DriverName.create(' John Doe ');
expect(name.toString()).toBe('John Doe');
});
it('should throw on empty name', () => {
expect(() => DriverName.create('')).toThrow('Driver name is required');
});
it('should throw on whitespace only', () => {
expect(() => DriverName.create(' ')).toThrow('Driver name is required');
});
it('should equal same name', () => {
const n1 = DriverName.create('John Doe');
const n2 = DriverName.create('John Doe');
expect(n1.equals(n2)).toBe(true);
});
it('should not equal different name', () => {
const n1 = DriverName.create('John Doe');
const n2 = DriverName.create('Jane Doe');
expect(n1.equals(n2)).toBe(false);
});
});

View File

@@ -0,0 +1,20 @@
import { RacingDomainValidationError } from '../errors/RacingDomainError';
export class DriverName {
private constructor(private readonly value: string) {}
static create(value: string): DriverName {
if (!value || value.trim().length === 0) {
throw new RacingDomainValidationError('Driver name is required');
}
return new DriverName(value.trim());
}
toString(): string {
return this.value;
}
equals(other: DriverName): boolean {
return this.value === other.value;
}
}

View File

@@ -0,0 +1,29 @@
import { describe, it, expect } from 'vitest';
import { IRacingId } from './IRacingId';
describe('IRacingId', () => {
it('should create an iRacing id', () => {
const id = IRacingId.create('12345');
expect(id.toString()).toBe('12345');
});
it('should throw on empty id', () => {
expect(() => IRacingId.create('')).toThrow('iRacing ID is required');
});
it('should throw on whitespace id', () => {
expect(() => IRacingId.create(' ')).toThrow('iRacing ID is required');
});
it('should equal same id', () => {
const id1 = IRacingId.create('12345');
const id2 = IRacingId.create('12345');
expect(id1.equals(id2)).toBe(true);
});
it('should not equal different id', () => {
const id1 = IRacingId.create('12345');
const id2 = IRacingId.create('67890');
expect(id1.equals(id2)).toBe(false);
});
});

View File

@@ -0,0 +1,20 @@
import { RacingDomainValidationError } from '../errors/RacingDomainError';
export class IRacingId {
private constructor(private readonly value: string) {}
static create(value: string): IRacingId {
if (!value || value.trim().length === 0) {
throw new RacingDomainValidationError('iRacing ID is required');
}
return new IRacingId(value);
}
toString(): string {
return this.value;
}
equals(other: IRacingId): boolean {
return this.value === other.value;
}
}

View File

@@ -0,0 +1,31 @@
import { RacingDomainValidationError } from '../errors/RacingDomainError';
import type { IValueObject } from '@core/shared/domain';
export class ImageUrl implements IValueObject<string> {
private constructor(private readonly value: string) {}
static create(value: string): ImageUrl {
if (!value || value.trim().length === 0) {
throw new RacingDomainValidationError('Image URL cannot be empty');
}
// Basic URL validation
try {
new URL(value);
} catch {
throw new RacingDomainValidationError('Invalid image URL format');
}
return new ImageUrl(value.trim());
}
toString(): string {
return this.value;
}
equals(other: IValueObject<string>): boolean {
return this.value === other.props;
}
get props(): string {
return this.value;
}
}

View File

@@ -0,0 +1,38 @@
import { describe, it, expect } from 'vitest';
import { JoinedAt } from './JoinedAt';
describe('JoinedAt', () => {
it('should create a joined date', () => {
const past = new Date('2020-01-01');
const joined = JoinedAt.create(past);
expect(joined.toDate()).toEqual(past);
});
it('should throw on future date', () => {
const future = new Date();
future.setFullYear(future.getFullYear() + 1);
expect(() => JoinedAt.create(future)).toThrow('Joined date cannot be in the future');
});
it('should allow current date', () => {
const now = new Date();
const joined = JoinedAt.create(now);
expect(joined.toDate().getTime()).toBeCloseTo(now.getTime(), -3); // close enough
});
it('should equal same date', () => {
const d1 = new Date('2020-01-01');
const d2 = new Date('2020-01-01');
const j1 = JoinedAt.create(d1);
const j2 = JoinedAt.create(d2);
expect(j1.equals(j2)).toBe(true);
});
it('should not equal different date', () => {
const d1 = new Date('2020-01-01');
const d2 = new Date('2020-01-02');
const j1 = JoinedAt.create(d1);
const j2 = JoinedAt.create(d2);
expect(j1.equals(j2)).toBe(false);
});
});

View File

@@ -0,0 +1,21 @@
import { RacingDomainValidationError } from '../errors/RacingDomainError';
export class JoinedAt {
private constructor(private readonly value: Date) {}
static create(value: Date): JoinedAt {
const now = new Date();
if (value > now) {
throw new RacingDomainValidationError('Joined date cannot be in the future');
}
return new JoinedAt(new Date(value));
}
toDate(): Date {
return new Date(this.value);
}
equals(other: JoinedAt): boolean {
return this.value.getTime() === other.value.getTime();
}
}

View File

@@ -0,0 +1,30 @@
import { describe, it, expect } from 'vitest';
import { Points } from './Points';
describe('Points', () => {
it('should create points', () => {
const points = Points.create(100);
expect(points.toNumber()).toBe(100);
});
it('should create zero points', () => {
const points = Points.create(0);
expect(points.toNumber()).toBe(0);
});
it('should not create negative points', () => {
expect(() => Points.create(-1)).toThrow('Points cannot be negative');
});
it('should equal same points', () => {
const p1 = Points.create(50);
const p2 = Points.create(50);
expect(p1.equals(p2)).toBe(true);
});
it('should not equal different points', () => {
const p1 = Points.create(50);
const p2 = Points.create(51);
expect(p1.equals(p2)).toBe(false);
});
});

View File

@@ -0,0 +1,20 @@
import { RacingDomainValidationError } from '../errors/RacingDomainError';
export class Points {
private constructor(private readonly value: number) {}
static create(value: number): Points {
if (value < 0) {
throw new RacingDomainValidationError('Points cannot be negative');
}
return new Points(value);
}
toNumber(): number {
return this.value;
}
equals(other: Points): boolean {
return this.value === other.value;
}
}

View File

@@ -0,0 +1,72 @@
import { describe, it, expect } from 'vitest';
import {
RacingDomainValidationError,
} from '../errors/RacingDomainError';
import {
SeasonDropPolicy,
type SeasonDropStrategy,
} from './SeasonDropPolicy';
describe('SeasonDropPolicy', () => {
it('allows strategy "none" with undefined n', () => {
const policy = new SeasonDropPolicy({ strategy: 'none' });
expect(policy.strategy).toBe('none');
expect(policy.n).toBeUndefined();
});
it('throws when strategy "none" has n defined', () => {
expect(
() =>
new SeasonDropPolicy({
strategy: 'none',
n: 1,
}),
).toThrow(RacingDomainValidationError);
});
it('requires positive integer n for "bestNResults" and "dropWorstN"', () => {
const strategies: SeasonDropStrategy[] = ['bestNResults', 'dropWorstN'];
for (const strategy of strategies) {
expect(
() =>
new SeasonDropPolicy({
strategy,
n: 0,
}),
).toThrow(RacingDomainValidationError);
expect(
() =>
new SeasonDropPolicy({
strategy,
n: -1,
}),
).toThrow(RacingDomainValidationError);
}
const okBest = new SeasonDropPolicy({
strategy: 'bestNResults',
n: 3,
});
const okDrop = new SeasonDropPolicy({
strategy: 'dropWorstN',
n: 2,
});
expect(okBest.n).toBe(3);
expect(okDrop.n).toBe(2);
});
it('equals compares strategy and n', () => {
const a = new SeasonDropPolicy({ strategy: 'none' });
const b = new SeasonDropPolicy({ strategy: 'none' });
const c = new SeasonDropPolicy({ strategy: 'bestNResults', n: 3 });
expect(a.equals(b)).toBe(true);
expect(a.equals(c)).toBe(false);
});
});

View File

@@ -0,0 +1,57 @@
import { describe, it, expect } from 'vitest';
import {
RacingDomainValidationError,
} from '../errors/RacingDomainError';
import { SeasonScoringConfig } from './SeasonScoringConfig';
describe('SeasonScoringConfig', () => {
it('constructs from preset id and customScoringEnabled', () => {
const config = new SeasonScoringConfig({
scoringPresetId: 'club-default',
customScoringEnabled: true,
});
expect(config.scoringPresetId).toBe('club-default');
expect(config.customScoringEnabled).toBe(true);
expect(config.props.scoringPresetId).toBe('club-default');
expect(config.props.customScoringEnabled).toBe(true);
});
it('normalizes customScoringEnabled to false when omitted', () => {
const config = new SeasonScoringConfig({
scoringPresetId: 'sprint-main-driver',
});
expect(config.customScoringEnabled).toBe(false);
expect(config.props.customScoringEnabled).toBeUndefined();
});
it('throws when scoringPresetId is empty', () => {
expect(
() =>
new SeasonScoringConfig({
scoringPresetId: ' ',
}),
).toThrow(RacingDomainValidationError);
});
it('equals compares by preset id and customScoringEnabled', () => {
const a = new SeasonScoringConfig({
scoringPresetId: 'club-default',
customScoringEnabled: false,
});
const b = new SeasonScoringConfig({
scoringPresetId: 'club-default',
customScoringEnabled: false,
});
const c = new SeasonScoringConfig({
scoringPresetId: 'club-default',
customScoringEnabled: true,
});
expect(a.equals(b)).toBe(true);
expect(a.equals(c)).toBe(false);
});
});

View File

@@ -0,0 +1,173 @@
import { describe, it, expect } from 'vitest';
import {
RacingDomainValidationError,
} from '../errors/RacingDomainError';
import { SeasonStewardingConfig } from './SeasonStewardingConfig';
describe('SeasonStewardingConfig', () => {
it('creates a valid config with voting mode and requiredVotes', () => {
const config = new SeasonStewardingConfig({
decisionMode: 'steward_vote',
requiredVotes: 3,
requireDefense: true,
defenseTimeLimit: 24,
voteTimeLimit: 24,
protestDeadlineHours: 48,
stewardingClosesHours: 72,
notifyAccusedOnProtest: true,
notifyOnVoteRequired: true,
});
expect(config.decisionMode).toBe('steward_vote');
expect(config.requiredVotes).toBe(3);
expect(config.requireDefense).toBe(true);
expect(config.defenseTimeLimit).toBe(24);
expect(config.voteTimeLimit).toBe(24);
expect(config.protestDeadlineHours).toBe(48);
expect(config.stewardingClosesHours).toBe(72);
expect(config.notifyAccusedOnProtest).toBe(true);
expect(config.notifyOnVoteRequired).toBe(true);
});
it('throws when decisionMode is missing', () => {
expect(
() =>
new SeasonStewardingConfig({
// @ts-expect-error intentional invalid
decisionMode: undefined,
requireDefense: true,
defenseTimeLimit: 24,
voteTimeLimit: 24,
protestDeadlineHours: 48,
stewardingClosesHours: 72,
notifyAccusedOnProtest: true,
notifyOnVoteRequired: true,
}),
).toThrow(RacingDomainValidationError);
});
it('requires requiredVotes for voting/veto modes', () => {
const votingModes = [
'steward_vote',
'member_vote',
'steward_veto',
'member_veto',
] as const;
for (const mode of votingModes) {
expect(
() =>
new SeasonStewardingConfig({
decisionMode: mode,
requireDefense: true,
defenseTimeLimit: 24,
voteTimeLimit: 24,
protestDeadlineHours: 48,
stewardingClosesHours: 72,
notifyAccusedOnProtest: true,
notifyOnVoteRequired: true,
}),
).toThrow(RacingDomainValidationError);
}
});
it('validates numeric limits as non-negative / positive integers', () => {
expect(
() =>
new SeasonStewardingConfig({
decisionMode: 'steward_decides',
requireDefense: true,
defenseTimeLimit: -1,
voteTimeLimit: 24,
protestDeadlineHours: 48,
stewardingClosesHours: 72,
notifyAccusedOnProtest: true,
notifyOnVoteRequired: true,
}),
).toThrow(RacingDomainValidationError);
expect(
() =>
new SeasonStewardingConfig({
decisionMode: 'steward_decides',
requireDefense: true,
defenseTimeLimit: 24,
voteTimeLimit: 0,
protestDeadlineHours: 48,
stewardingClosesHours: 72,
notifyAccusedOnProtest: true,
notifyOnVoteRequired: true,
}),
).toThrow(RacingDomainValidationError);
expect(
() =>
new SeasonStewardingConfig({
decisionMode: 'steward_decides',
requireDefense: true,
defenseTimeLimit: 24,
voteTimeLimit: 24,
protestDeadlineHours: 0,
stewardingClosesHours: 72,
notifyAccusedOnProtest: true,
notifyOnVoteRequired: true,
}),
).toThrow(RacingDomainValidationError);
expect(
() =>
new SeasonStewardingConfig({
decisionMode: 'steward_decides',
requireDefense: true,
defenseTimeLimit: 24,
voteTimeLimit: 24,
protestDeadlineHours: 48,
stewardingClosesHours: 0,
notifyAccusedOnProtest: true,
notifyOnVoteRequired: true,
}),
).toThrow(RacingDomainValidationError);
});
it('equals compares all props', () => {
const a = new SeasonStewardingConfig({
decisionMode: 'steward_vote',
requiredVotes: 3,
requireDefense: true,
defenseTimeLimit: 24,
voteTimeLimit: 24,
protestDeadlineHours: 48,
stewardingClosesHours: 72,
notifyAccusedOnProtest: true,
notifyOnVoteRequired: true,
});
const b = new SeasonStewardingConfig({
decisionMode: 'steward_vote',
requiredVotes: 3,
requireDefense: true,
defenseTimeLimit: 24,
voteTimeLimit: 24,
protestDeadlineHours: 48,
stewardingClosesHours: 72,
notifyAccusedOnProtest: true,
notifyOnVoteRequired: true,
});
const c = new SeasonStewardingConfig({
decisionMode: 'steward_decides',
requireDefense: false,
defenseTimeLimit: 0,
voteTimeLimit: 24,
protestDeadlineHours: 24,
stewardingClosesHours: 48,
notifyAccusedOnProtest: false,
notifyOnVoteRequired: false,
});
expect(a.equals(b)).toBe(true);
expect(a.equals(c)).toBe(false);
});
});

View File

@@ -0,0 +1,21 @@
import { RacingDomainValidationError } from '../errors/RacingDomainError';
export class TeamCreatedAt {
private constructor(private readonly value: Date) {}
static create(value: Date): TeamCreatedAt {
const now = new Date();
if (value > now) {
throw new RacingDomainValidationError('Created date cannot be in the future');
}
return new TeamCreatedAt(new Date(value));
}
toDate(): Date {
return new Date(this.value);
}
equals(other: TeamCreatedAt): boolean {
return this.value.getTime() === other.value.getTime();
}
}

View File

@@ -0,0 +1,20 @@
import { RacingDomainValidationError } from '../errors/RacingDomainError';
export class TeamDescription {
private constructor(private readonly value: string) {}
static create(value: string): TeamDescription {
if (!value || value.trim().length === 0) {
throw new RacingDomainValidationError('Team description is required');
}
return new TeamDescription(value.trim());
}
toString(): string {
return this.value;
}
equals(other: TeamDescription): boolean {
return this.value === other.value;
}
}

View File

@@ -0,0 +1,20 @@
import { RacingDomainValidationError } from '../errors/RacingDomainError';
export class TeamName {
private constructor(private readonly value: string) {}
static create(value: string): TeamName {
if (!value || value.trim().length === 0) {
throw new RacingDomainValidationError('Team name is required');
}
return new TeamName(value.trim());
}
toString(): string {
return this.value;
}
equals(other: TeamName): boolean {
return this.value === other.value;
}
}

View File

@@ -0,0 +1,20 @@
import { RacingDomainValidationError } from '../errors/RacingDomainError';
export class TeamTag {
private constructor(private readonly value: string) {}
static create(value: string): TeamTag {
if (!value || value.trim().length === 0) {
throw new RacingDomainValidationError('Team tag is required');
}
return new TeamTag(value.trim());
}
toString(): string {
return this.value;
}
equals(other: TeamTag): boolean {
return this.value === other.value;
}
}

View File

@@ -0,0 +1,20 @@
import { RacingDomainValidationError } from '../errors/RacingDomainError';
export class TrackCountry {
private constructor(private readonly value: string) {}
static create(value: string): TrackCountry {
if (!value || value.trim().length === 0) {
throw new RacingDomainValidationError('Track country is required');
}
return new TrackCountry(value.trim());
}
toString(): string {
return this.value;
}
equals(other: TrackCountry): boolean {
return this.value === other.value;
}
}

View File

@@ -0,0 +1,20 @@
import { RacingDomainValidationError } from '../errors/RacingDomainError';
export class TrackGameId {
private constructor(private readonly value: string) {}
static create(value: string): TrackGameId {
if (!value || value.trim().length === 0) {
throw new RacingDomainValidationError('Track game ID cannot be empty');
}
return new TrackGameId(value.trim());
}
toString(): string {
return this.value;
}
equals(other: TrackGameId): boolean {
return this.value === other.value;
}
}

View File

@@ -0,0 +1,20 @@
import { RacingDomainValidationError } from '../errors/RacingDomainError';
export class TrackId {
private constructor(private readonly value: string) {}
static create(value: string): TrackId {
if (!value || value.trim().length === 0) {
throw new RacingDomainValidationError('Track ID cannot be empty');
}
return new TrackId(value.trim());
}
toString(): string {
return this.value;
}
equals(other: TrackId): boolean {
return this.value === other.value;
}
}

View File

@@ -0,0 +1,21 @@
import { RacingDomainValidationError } from '../errors/RacingDomainError';
export class TrackImageUrl {
private constructor(private readonly value: string | undefined) {}
static create(value: string | undefined): TrackImageUrl {
// Allow undefined or valid URL, but for simplicity, just check if string is not empty if provided
if (value !== undefined && value.trim().length === 0) {
throw new RacingDomainValidationError('Track image URL cannot be empty string');
}
return new TrackImageUrl(value);
}
toString(): string | undefined {
return this.value;
}
equals(other: TrackImageUrl): boolean {
return this.value === other.value;
}
}

View File

@@ -0,0 +1,20 @@
import { RacingDomainValidationError } from '../errors/RacingDomainError';
export class TrackLength {
private constructor(private readonly value: number) {}
static create(value: number): TrackLength {
if (value <= 0) {
throw new RacingDomainValidationError('Track length must be positive');
}
return new TrackLength(value);
}
toNumber(): number {
return this.value;
}
equals(other: TrackLength): boolean {
return this.value === other.value;
}
}

View File

@@ -0,0 +1,20 @@
import { RacingDomainValidationError } from '../errors/RacingDomainError';
export class TrackName {
private constructor(private readonly value: string) {}
static create(value: string): TrackName {
if (!value || value.trim().length === 0) {
throw new RacingDomainValidationError('Track name is required');
}
return new TrackName(value.trim());
}
toString(): string {
return this.value;
}
equals(other: TrackName): boolean {
return this.value === other.value;
}
}

View File

@@ -0,0 +1,20 @@
import { RacingDomainValidationError } from '../errors/RacingDomainError';
export class TrackShortName {
private constructor(private readonly value: string) {}
static create(value: string): TrackShortName {
if (!value || value.trim().length === 0) {
throw new RacingDomainValidationError('Track short name is required');
}
return new TrackShortName(value.trim());
}
toString(): string {
return this.value;
}
equals(other: TrackShortName): boolean {
return this.value === other.value;
}
}

View File

@@ -0,0 +1,20 @@
import { RacingDomainValidationError } from '../errors/RacingDomainError';
export class TrackTurns {
private constructor(private readonly value: number) {}
static create(value: number): TrackTurns {
if (value < 0) {
throw new RacingDomainValidationError('Track turns cannot be negative');
}
return new TrackTurns(value);
}
toNumber(): number {
return this.value;
}
equals(other: TrackTurns): boolean {
return this.value === other.value;
}
}