refactor
This commit is contained in:
25
core/racing/domain/value-objects/CarId.ts
Normal file
25
core/racing/domain/value-objects/CarId.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
47
core/racing/domain/value-objects/CountryCode.test.ts
Normal file
47
core/racing/domain/value-objects/CountryCode.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
24
core/racing/domain/value-objects/CountryCode.ts
Normal file
24
core/racing/domain/value-objects/CountryCode.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
71
core/racing/domain/value-objects/DecalOverride.ts
Normal file
71
core/racing/domain/value-objects/DecalOverride.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
31
core/racing/domain/value-objects/DriverBio.test.ts
Normal file
31
core/racing/domain/value-objects/DriverBio.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
20
core/racing/domain/value-objects/DriverBio.ts
Normal file
20
core/racing/domain/value-objects/DriverBio.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
29
core/racing/domain/value-objects/DriverId.test.ts
Normal file
29
core/racing/domain/value-objects/DriverId.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
25
core/racing/domain/value-objects/DriverId.ts
Normal file
25
core/racing/domain/value-objects/DriverId.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
34
core/racing/domain/value-objects/DriverName.test.ts
Normal file
34
core/racing/domain/value-objects/DriverName.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
20
core/racing/domain/value-objects/DriverName.ts
Normal file
20
core/racing/domain/value-objects/DriverName.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
29
core/racing/domain/value-objects/IRacingId.test.ts
Normal file
29
core/racing/domain/value-objects/IRacingId.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
20
core/racing/domain/value-objects/IRacingId.ts
Normal file
20
core/racing/domain/value-objects/IRacingId.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
31
core/racing/domain/value-objects/ImageUrl.ts
Normal file
31
core/racing/domain/value-objects/ImageUrl.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
38
core/racing/domain/value-objects/JoinedAt.test.ts
Normal file
38
core/racing/domain/value-objects/JoinedAt.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
21
core/racing/domain/value-objects/JoinedAt.ts
Normal file
21
core/racing/domain/value-objects/JoinedAt.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
30
core/racing/domain/value-objects/Points.test.ts
Normal file
30
core/racing/domain/value-objects/Points.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
20
core/racing/domain/value-objects/Points.ts
Normal file
20
core/racing/domain/value-objects/Points.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
72
core/racing/domain/value-objects/SeasonDropPolicy.test.ts
Normal file
72
core/racing/domain/value-objects/SeasonDropPolicy.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
57
core/racing/domain/value-objects/SeasonScoringConfig.test.ts
Normal file
57
core/racing/domain/value-objects/SeasonScoringConfig.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
173
core/racing/domain/value-objects/SeasonStewardingConfig.test.ts
Normal file
173
core/racing/domain/value-objects/SeasonStewardingConfig.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
21
core/racing/domain/value-objects/TeamCreatedAt.ts
Normal file
21
core/racing/domain/value-objects/TeamCreatedAt.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
20
core/racing/domain/value-objects/TeamDescription.ts
Normal file
20
core/racing/domain/value-objects/TeamDescription.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
20
core/racing/domain/value-objects/TeamName.ts
Normal file
20
core/racing/domain/value-objects/TeamName.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
20
core/racing/domain/value-objects/TeamTag.ts
Normal file
20
core/racing/domain/value-objects/TeamTag.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
20
core/racing/domain/value-objects/TrackCountry.ts
Normal file
20
core/racing/domain/value-objects/TrackCountry.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
20
core/racing/domain/value-objects/TrackGameId.ts
Normal file
20
core/racing/domain/value-objects/TrackGameId.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
20
core/racing/domain/value-objects/TrackId.ts
Normal file
20
core/racing/domain/value-objects/TrackId.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
21
core/racing/domain/value-objects/TrackImageUrl.ts
Normal file
21
core/racing/domain/value-objects/TrackImageUrl.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
20
core/racing/domain/value-objects/TrackLength.ts
Normal file
20
core/racing/domain/value-objects/TrackLength.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
20
core/racing/domain/value-objects/TrackName.ts
Normal file
20
core/racing/domain/value-objects/TrackName.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
20
core/racing/domain/value-objects/TrackShortName.ts
Normal file
20
core/racing/domain/value-objects/TrackShortName.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
20
core/racing/domain/value-objects/TrackTurns.ts
Normal file
20
core/racing/domain/value-objects/TrackTurns.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user