/** * Domain Entity: DriverLivery * * Represents a driver's custom livery for a specific car. * Includes user-placed decals and league-specific overrides. */ import { Entity } from '@core/shared/domain/Entity'; import { RacingDomainInvariantError, RacingDomainValidationError } from '../errors/RacingDomainError'; import { CarId } from '../value-objects/CarId'; import { DecalOverride } from '../value-objects/DecalOverride'; import { DriverId } from '../value-objects/driver/DriverId'; import { ImageUrl } from '../value-objects/ImageUrl'; import { LiveryDecal } from '../value-objects/LiveryDecal'; import { GameId } from './GameId'; export interface DriverLiveryProps { id: string; driverId: DriverId; gameId: GameId; carId: CarId; uploadedImageUrl: ImageUrl; userDecals: LiveryDecal[]; leagueOverrides: DecalOverride[]; createdAt: Date; updatedAt: Date | undefined; validatedAt: Date | undefined; } export class DriverLivery extends Entity { readonly driverId: DriverId; readonly gameId: GameId; readonly carId: CarId; readonly uploadedImageUrl: ImageUrl; readonly userDecals: LiveryDecal[]; readonly leagueOverrides: DecalOverride[]; readonly createdAt: Date; readonly updatedAt: Date | undefined; readonly validatedAt: Date | undefined; private constructor(props: DriverLiveryProps) { super(props.id); this.driverId = props.driverId; this.gameId = props.gameId; this.carId = props.carId; this.uploadedImageUrl = props.uploadedImageUrl; this.userDecals = props.userDecals; this.leagueOverrides = props.leagueOverrides; this.createdAt = props.createdAt ?? new Date(); this.updatedAt = props.updatedAt; this.validatedAt = props.validatedAt; } static create(props: { id: string; driverId: string; gameId: string; carId: string; uploadedImageUrl: string; createdAt?: Date; userDecals?: LiveryDecal[]; leagueOverrides?: DecalOverride[]; }): DriverLivery { this.validate(props); return new DriverLivery({ id: props.id, driverId: DriverId.create(props.driverId), gameId: GameId.create(props.gameId), carId: CarId.create(props.carId), uploadedImageUrl: ImageUrl.create(props.uploadedImageUrl), userDecals: props.userDecals ?? [], leagueOverrides: props.leagueOverrides ?? [], createdAt: props.createdAt ?? new Date(), updatedAt: undefined, validatedAt: undefined, }); } private static validate(props: { id: string; driverId: string; gameId: string; carId: string; uploadedImageUrl: string; }): void { if (!props.id || props.id.trim().length === 0) { throw new RacingDomainValidationError('DriverLivery ID is required'); } if (!props.driverId || props.driverId.trim().length === 0) { throw new RacingDomainValidationError('DriverLivery driverId is required'); } if (!props.gameId || props.gameId.trim().length === 0) { throw new RacingDomainValidationError('DriverLivery gameId is required'); } if (!props.carId || props.carId.trim().length === 0) { throw new RacingDomainValidationError('DriverLivery carId is required'); } if (!props.uploadedImageUrl || props.uploadedImageUrl.trim().length === 0) { throw new RacingDomainValidationError('DriverLivery uploadedImageUrl is required'); } } /** * Add a user decal */ addDecal(decal: LiveryDecal): DriverLivery { if (decal.type !== 'user') { throw new RacingDomainInvariantError('Only user decals can be added to driver livery'); } return new DriverLivery({ id: this.id, driverId: this.driverId, gameId: this.gameId, carId: this.carId, uploadedImageUrl: this.uploadedImageUrl, userDecals: [...this.userDecals, decal], leagueOverrides: this.leagueOverrides, createdAt: this.createdAt, updatedAt: new Date(), validatedAt: this.validatedAt, }); } /** * Remove a user decal */ removeDecal(decalId: string): DriverLivery { const updatedDecals = this.userDecals.filter(d => d.id !== decalId); if (updatedDecals.length === this.userDecals.length) { throw new RacingDomainValidationError('Decal not found in livery'); } return new DriverLivery({ id: this.id, driverId: this.driverId, gameId: this.gameId, carId: this.carId, uploadedImageUrl: this.uploadedImageUrl, userDecals: updatedDecals, leagueOverrides: this.leagueOverrides, createdAt: this.createdAt, updatedAt: new Date(), validatedAt: this.validatedAt, }); } /** * Update a user decal */ updateDecal(decalId: string, updatedDecal: LiveryDecal): DriverLivery { const index = this.userDecals.findIndex(d => d.id === decalId); if (index === -1) { throw new RacingDomainValidationError('Decal not found in livery'); } const updatedDecals = [...this.userDecals]; updatedDecals[index] = updatedDecal; return new DriverLivery({ id: this.id, driverId: this.driverId, gameId: this.gameId, carId: this.carId, uploadedImageUrl: this.uploadedImageUrl, userDecals: updatedDecals, leagueOverrides: this.leagueOverrides, createdAt: this.createdAt, updatedAt: new Date(), validatedAt: this.validatedAt, }); } /** * Add or update a league-specific decal override */ setLeagueOverride(leagueId: string, seasonId: string, decalId: string, newX: number, newY: number): DriverLivery { const existingIndex = this.leagueOverrides.findIndex( o => o.leagueId === leagueId && o.seasonId === seasonId && o.decalId === decalId ); const override = DecalOverride.create({ leagueId, seasonId, decalId, newX, newY }); let updatedOverrides: DecalOverride[]; if (existingIndex >= 0) { updatedOverrides = [...this.leagueOverrides]; updatedOverrides[existingIndex] = override; } else { updatedOverrides = [...this.leagueOverrides, override]; } return new DriverLivery({ id: this.id, driverId: this.driverId, gameId: this.gameId, carId: this.carId, uploadedImageUrl: this.uploadedImageUrl, userDecals: this.userDecals, leagueOverrides: updatedOverrides, createdAt: this.createdAt, updatedAt: new Date(), validatedAt: this.validatedAt, }); } /** * Remove a league-specific override */ removeLeagueOverride(leagueId: string, seasonId: string, decalId: string): DriverLivery { const updatedOverrides = this.leagueOverrides.filter( o => !(o.leagueId === leagueId && o.seasonId === seasonId && o.decalId === decalId) ); return new DriverLivery({ id: this.id, driverId: this.driverId, gameId: this.gameId, carId: this.carId, uploadedImageUrl: this.uploadedImageUrl, userDecals: this.userDecals, leagueOverrides: updatedOverrides, createdAt: this.createdAt, updatedAt: new Date(), validatedAt: this.validatedAt, }); } /** * Get overrides for a specific league/season */ getOverridesFor(leagueId: string, seasonId: string): DecalOverride[] { return this.leagueOverrides.filter( o => o.leagueId === leagueId && o.seasonId === seasonId ); } /** * Mark livery as validated (no logos/text detected) */ markAsValidated(): DriverLivery { return new DriverLivery({ id: this.id, driverId: this.driverId, gameId: this.gameId, carId: this.carId, uploadedImageUrl: this.uploadedImageUrl, userDecals: this.userDecals, leagueOverrides: this.leagueOverrides, createdAt: this.createdAt, updatedAt: this.updatedAt, validatedAt: new Date(), }); } /** * Check if livery is validated */ isValidated(): boolean { return this.validatedAt !== undefined; } }