/** * Domain Entity: AvatarGenerationRequest * * Represents a request to generate a racing avatar from a face photo. */ import type { IEntity } from '@core/shared/domain'; import type { AvatarGenerationRequestProps, AvatarGenerationStatus, AvatarStyle, RacingSuitColor, } from '../types/AvatarGenerationRequest'; import { MediaUrl } from '../value-objects/MediaUrl'; export class AvatarGenerationRequest implements IEntity { readonly id: string; readonly userId: string; readonly facePhotoUrl: MediaUrl; readonly suitColor: RacingSuitColor; readonly style: AvatarStyle; private _status: AvatarGenerationStatus; private _generatedAvatarUrls: MediaUrl[]; private _selectedAvatarIndex?: number; private _errorMessage?: string; readonly createdAt: Date; private _updatedAt: Date; private constructor(props: AvatarGenerationRequestProps) { this.id = props.id; this.userId = props.userId; this.facePhotoUrl = MediaUrl.create(props.facePhotoUrl); this.suitColor = props.suitColor; this.style = props.style; this._status = props.status; this._generatedAvatarUrls = props.generatedAvatarUrls.map(url => MediaUrl.create(url)); if (props.selectedAvatarIndex !== undefined) { this._selectedAvatarIndex = props.selectedAvatarIndex; } if (props.errorMessage !== undefined) { this._errorMessage = props.errorMessage; } this.createdAt = props.createdAt; this._updatedAt = props.updatedAt; } static create(props: { id: string; userId: string; facePhotoUrl: string; suitColor: RacingSuitColor; style?: AvatarStyle; }): AvatarGenerationRequest { if (!props.userId) { throw new Error('User ID is required'); } if (!props.facePhotoUrl) { throw new Error('Face photo URL is required'); } const now = new Date(); return new AvatarGenerationRequest({ id: props.id, userId: props.userId, facePhotoUrl: props.facePhotoUrl, suitColor: props.suitColor, style: props.style ?? 'realistic', status: 'pending', generatedAvatarUrls: [], createdAt: now, updatedAt: now, }); } static reconstitute(props: AvatarGenerationRequestProps): AvatarGenerationRequest { return new AvatarGenerationRequest(props); } get status(): AvatarGenerationStatus { return this._status; } get generatedAvatarUrls(): string[] { return this._generatedAvatarUrls.map(url => url.value); } get selectedAvatarIndex(): number | undefined { return this._selectedAvatarIndex; } get selectedAvatarUrl(): string | undefined { const index = this._selectedAvatarIndex; if (index === undefined) { return undefined; } const avatar = this._generatedAvatarUrls[index]; if (!avatar) { return undefined; } return avatar.value; } get errorMessage(): string | undefined { return this._errorMessage; } get updatedAt(): Date { return this._updatedAt; } markAsValidating(): void { if (this._status !== 'pending') { throw new Error('Can only start validation from pending status'); } this._status = 'validating'; this._updatedAt = new Date(); } markAsGenerating(): void { if (this._status !== 'validating') { throw new Error('Can only start generation from validating status'); } this._status = 'generating'; this._updatedAt = new Date(); } completeWithAvatars(avatarUrls: string[]): void { if (avatarUrls.length === 0) { throw new Error('At least one avatar URL is required'); } this._status = 'completed'; this._generatedAvatarUrls = avatarUrls.map(url => MediaUrl.create(url)); this._updatedAt = new Date(); } fail(errorMessage: string): void { this._status = 'failed'; this._errorMessage = errorMessage; this._updatedAt = new Date(); } selectAvatar(index: number): void { if (this._status !== 'completed') { throw new Error('Can only select avatar when generation is completed'); } if (index < 0 || index >= this._generatedAvatarUrls.length) { throw new Error('Invalid avatar index'); } this._selectedAvatarIndex = index; this._updatedAt = new Date(); } /** * Build the AI prompt for avatar generation. * We control the prompt completely - users cannot enter free text. */ buildPrompt(): string { const colorDescriptions: Record = { red: 'vibrant racing red', blue: 'deep motorsport blue', green: 'racing green', yellow: 'bright championship yellow', orange: 'McLaren-style papaya orange', purple: 'royal purple', black: 'stealth black', white: 'clean white', pink: 'hot pink', cyan: 'electric cyan', }; const styleDescriptions: Record = { realistic: 'photorealistic, professional motorsport portrait', cartoon: 'stylized cartoon racing character', 'pixel-art': '8-bit pixel art retro racing avatar', }; const suitColorDesc = colorDescriptions[this.suitColor]; const styleDesc = styleDescriptions[this.style]; return `Create a ${styleDesc} of a racing driver wearing a ${suitColorDesc} racing suit with matching helmet. The driver should look professional and confident, as if posing for a team photo. Background should be a subtle racing paddock or garage setting. High quality, well-lit, professional motorsport photography style.`; } toProps(): AvatarGenerationRequestProps { const base: AvatarGenerationRequestProps = { id: this.id, userId: this.userId, facePhotoUrl: this.facePhotoUrl.value, suitColor: this.suitColor, style: this.style, status: this._status, generatedAvatarUrls: this._generatedAvatarUrls.map(url => url.value), createdAt: this.createdAt, updatedAt: this._updatedAt, }; return { ...base, ...(this._selectedAvatarIndex !== undefined && { selectedAvatarIndex: this._selectedAvatarIndex, }), ...(this._errorMessage !== undefined && { errorMessage: this._errorMessage, }), }; } }