import type { MediaReference } from '@core/domain/media/MediaReference'; import type { RacingSuitColor } from '@core/media/domain/types/AvatarGenerationRequest'; import { Inject, Injectable } from '@nestjs/common'; import type { DeleteMediaOutputDTO } from './dtos/DeleteMediaOutputDTO'; import type { GetAvatarOutputDTO } from './dtos/GetAvatarOutputDTO'; import type { GetMediaOutputDTO } from './dtos/GetMediaOutputDTO'; import type { RequestAvatarGenerationInputDTO } from './dtos/RequestAvatarGenerationInputDTO'; import type { RequestAvatarGenerationOutputDTO } from './dtos/RequestAvatarGenerationOutputDTO'; import type { UpdateAvatarInputDTO } from './dtos/UpdateAvatarInputDTO'; import type { UpdateAvatarOutputDTO } from './dtos/UpdateAvatarOutputDTO'; import type { UploadMediaInputDTO } from './dtos/UploadMediaInputDTO'; import type { UploadMediaOutputDTO } from './dtos/UploadMediaOutputDTO'; import type { ValidateFaceInputDTO } from './dtos/ValidateFaceInputDTO'; import type { ValidateFaceOutputDTO } from './dtos/ValidateFaceOutputDTO'; import type { MulterFile } from './types/MulterFile'; type RequestAvatarGenerationInput = RequestAvatarGenerationInputDTO; type UploadMediaInput = UploadMediaInputDTO; type UpdateAvatarInput = UpdateAvatarInputDTO; // Use cases import { DeleteMediaUseCase } from '@core/media/application/use-cases/DeleteMediaUseCase'; import { GetAvatarUseCase } from '@core/media/application/use-cases/GetAvatarUseCase'; import { GetMediaUseCase } from '@core/media/application/use-cases/GetMediaUseCase'; import { GetUploadedMediaUseCase, type GetUploadedMediaResult } from '@core/media/application/use-cases/GetUploadedMediaUseCase'; import { RequestAvatarGenerationUseCase } from '@core/media/application/use-cases/RequestAvatarGenerationUseCase'; import { ResolveMediaReferenceUseCase } from '@core/media/application/use-cases/ResolveMediaReferenceUseCase'; import { UpdateAvatarUseCase } from '@core/media/application/use-cases/UpdateAvatarUseCase'; import { UploadMediaUseCase } from '@core/media/application/use-cases/UploadMediaUseCase'; // Presenters (now transformers) import { DeleteMediaPresenter } from './presenters/DeleteMediaPresenter'; import { GetAvatarPresenter } from './presenters/GetAvatarPresenter'; import { GetMediaPresenter } from './presenters/GetMediaPresenter'; import { RequestAvatarGenerationPresenter } from './presenters/RequestAvatarGenerationPresenter'; import { UpdateAvatarPresenter } from './presenters/UpdateAvatarPresenter'; import { UploadMediaPresenter } from './presenters/UploadMediaPresenter'; import type { Logger } from '@core/shared/domain/Logger'; import { DELETE_MEDIA_USE_CASE_TOKEN, GET_AVATAR_USE_CASE_TOKEN, GET_MEDIA_USE_CASE_TOKEN, GET_UPLOADED_MEDIA_USE_CASE_TOKEN, LOGGER_TOKEN, REQUEST_AVATAR_GENERATION_USE_CASE_TOKEN, RESOLVE_MEDIA_REFERENCE_USE_CASE_TOKEN, UPDATE_AVATAR_USE_CASE_TOKEN, UPLOAD_MEDIA_USE_CASE_TOKEN, } from './MediaTokens'; @Injectable() export class MediaService { constructor( @Inject(REQUEST_AVATAR_GENERATION_USE_CASE_TOKEN) private readonly requestAvatarGenerationUseCase: RequestAvatarGenerationUseCase, @Inject(UPLOAD_MEDIA_USE_CASE_TOKEN) private readonly uploadMediaUseCase: UploadMediaUseCase, @Inject(GET_MEDIA_USE_CASE_TOKEN) private readonly getMediaUseCase: GetMediaUseCase, @Inject(DELETE_MEDIA_USE_CASE_TOKEN) private readonly deleteMediaUseCase: DeleteMediaUseCase, @Inject(GET_AVATAR_USE_CASE_TOKEN) private readonly getAvatarUseCase: GetAvatarUseCase, @Inject(UPDATE_AVATAR_USE_CASE_TOKEN) private readonly updateAvatarUseCase: UpdateAvatarUseCase, @Inject(RESOLVE_MEDIA_REFERENCE_USE_CASE_TOKEN) private readonly resolveMediaReferenceUseCase: ResolveMediaReferenceUseCase, @Inject(GET_UPLOADED_MEDIA_USE_CASE_TOKEN) private readonly getUploadedMediaUseCase: GetUploadedMediaUseCase, @Inject(LOGGER_TOKEN) private readonly logger: Logger, private readonly requestAvatarGenerationPresenter: RequestAvatarGenerationPresenter, private readonly uploadMediaPresenter: UploadMediaPresenter, private readonly getMediaPresenter: GetMediaPresenter, private readonly deleteMediaPresenter: DeleteMediaPresenter, private readonly getAvatarPresenter: GetAvatarPresenter, private readonly updateAvatarPresenter: UpdateAvatarPresenter, ) {} async requestAvatarGeneration( input: RequestAvatarGenerationInput, ): Promise { this.logger.debug('[MediaService] Requesting avatar generation.'); const result = await this.requestAvatarGenerationUseCase.execute({ userId: input.userId, facePhotoData: input.facePhotoData, suitColor: input.suitColor as RacingSuitColor, }); if (result.isErr()) { const error = result.unwrapErr(); return { success: false, requestId: '', avatarUrls: [], errorMessage: error.details?.message ?? 'Failed to request avatar generation', }; } return this.requestAvatarGenerationPresenter.transform(result.unwrap()); } async uploadMedia( input: UploadMediaInput & { file: MulterFile } & { userId?: string; metadata?: Record }, ): Promise { this.logger.debug('[MediaService] Uploading media.'); const result = await this.uploadMediaUseCase.execute({ file: input.file, uploadedBy: input.userId ?? '', metadata: input.metadata || {}, }); if (result.isErr()) { const error = result.unwrapErr(); return { success: false, error: error.details?.message ?? 'Upload failed', }; } return this.uploadMediaPresenter.transform(result.unwrap()); } async getMedia(mediaId: string): Promise { this.logger.debug(`[MediaService] Getting media: ${mediaId}`); const result = await this.getMediaUseCase.execute({ mediaId }); if (result.isErr()) { const error = result.unwrapErr(); if (error.code === 'MEDIA_NOT_FOUND') { return null; } throw new Error(error.details?.message ?? 'Failed to get media'); } return this.getMediaPresenter.transform(result.unwrap()); } async deleteMedia(mediaId: string): Promise { this.logger.debug(`[MediaService] Deleting media: ${mediaId}`); const result = await this.deleteMediaUseCase.execute({ mediaId }); if (result.isErr()) { const error = result.unwrapErr(); return { success: false, error: error.details?.message ?? 'Failed to delete media', }; } return this.deleteMediaPresenter.transform(result.unwrap()); } async getAvatar(driverId: string): Promise { this.logger.debug(`[MediaService] Getting avatar for driver: ${driverId}`); const result = await this.getAvatarUseCase.execute({ driverId }); if (result.isErr()) { const error = result.unwrapErr(); if (error.code === 'AVATAR_NOT_FOUND') { return null; } throw new Error(error.details?.message ?? 'Failed to get avatar'); } return this.getAvatarPresenter.transform(result.unwrap()); } async updateAvatar(driverId: string, input: UpdateAvatarInput): Promise { this.logger.debug(`[MediaService] Updating avatar for driver: ${driverId}`); // Handle null avatarUrl - this would mean removing the avatar if (input.avatarUrl === null) { // For now, we'll treat null as an error since the use case requires a URL // In a complete implementation, this would trigger avatar removal return { success: false, error: 'Avatar URL cannot be null', }; } const result = await this.updateAvatarUseCase.execute({ driverId, mediaUrl: input.avatarUrl, }); if (result.isErr()) { const error = result.unwrapErr(); return { success: false, error: error.details?.message ?? 'Failed to update avatar', }; } return this.updateAvatarPresenter.transform(result.unwrap()); } async validateFacePhoto(input: ValidateFaceInputDTO): Promise { this.logger.debug('[MediaService] Validating face photo.'); // Simple validation: check if it's a valid base64 image if (!input.imageData || !input.imageData.startsWith('data:image/')) { return { isValid: false, errorMessage: 'Invalid image data' }; } // Check file size (rough estimate from base64 length) const base64Length = input.imageData.length; const fileSizeInBytes = (base64Length * 3) / 4; // Rough estimate const maxSize = 5 * 1024 * 1024; // 5MB if (fileSizeInBytes > maxSize) { return { isValid: false, errorMessage: 'Image too large (max 5MB)' }; } return { isValid: true }; } async resolveMediaReference(reference: MediaReference): Promise { const result = await this.resolveMediaReferenceUseCase.execute({ reference }); if (result.isErr()) { throw new Error(result.unwrapErr().message); } return result.unwrap(); } async getUploadedMedia(storageKey: string): Promise { const result = await this.getUploadedMediaUseCase.execute({ storageKey }); if (result.isErr()) { throw new Error(result.unwrapErr().message); } return result.unwrap(); } }