module cleanup
This commit is contained in:
34
core/media/application/ports/MediaStoragePort.ts
Normal file
34
core/media/application/ports/MediaStoragePort.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Port: MediaStoragePort
|
||||
*
|
||||
* Defines the contract for media file storage operations.
|
||||
*/
|
||||
|
||||
export interface UploadOptions {
|
||||
filename: string;
|
||||
mimeType: string;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface UploadResult {
|
||||
success: boolean;
|
||||
url?: string;
|
||||
filename?: string;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export interface MediaStoragePort {
|
||||
/**
|
||||
* Upload a media file
|
||||
* @param buffer File buffer
|
||||
* @param options Upload options
|
||||
* @returns Upload result with URL
|
||||
*/
|
||||
uploadMedia(buffer: Buffer, options: UploadOptions): Promise<UploadResult>;
|
||||
|
||||
/**
|
||||
* Delete a media file by URL
|
||||
* @param url Media URL to delete
|
||||
*/
|
||||
deleteMedia(url: string): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export interface DeleteMediaResult {
|
||||
success: boolean;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export interface IDeleteMediaPresenter {
|
||||
present(result: DeleteMediaResult): void;
|
||||
}
|
||||
14
core/media/application/presenters/IGetAvatarPresenter.ts
Normal file
14
core/media/application/presenters/IGetAvatarPresenter.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export interface GetAvatarResult {
|
||||
success: boolean;
|
||||
avatar?: {
|
||||
id: string;
|
||||
driverId: string;
|
||||
mediaUrl: string;
|
||||
selectedAt: Date;
|
||||
};
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export interface IGetAvatarPresenter {
|
||||
present(result: GetAvatarResult): void;
|
||||
}
|
||||
20
core/media/application/presenters/IGetMediaPresenter.ts
Normal file
20
core/media/application/presenters/IGetMediaPresenter.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export interface GetMediaResult {
|
||||
success: boolean;
|
||||
media?: {
|
||||
id: string;
|
||||
filename: string;
|
||||
originalName: string;
|
||||
mimeType: string;
|
||||
size: number;
|
||||
url: string;
|
||||
type: string;
|
||||
uploadedBy: string;
|
||||
uploadedAt: Date;
|
||||
metadata?: Record<string, any>;
|
||||
};
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export interface IGetMediaPresenter {
|
||||
present(result: GetMediaResult): void;
|
||||
}
|
||||
@@ -8,6 +8,6 @@ export interface RequestAvatarGenerationResultDTO {
|
||||
export interface IRequestAvatarGenerationPresenter {
|
||||
reset(): void;
|
||||
present(dto: RequestAvatarGenerationResultDTO): void;
|
||||
get viewModel(): any;
|
||||
getViewModel(): any;
|
||||
get viewModel(): RequestAvatarGenerationResultDTO;
|
||||
getViewModel(): RequestAvatarGenerationResultDTO;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export interface SelectAvatarResult {
|
||||
success: boolean;
|
||||
selectedAvatarUrl?: string;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export interface ISelectAvatarPresenter {
|
||||
present(result: SelectAvatarResult): void;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export interface UpdateAvatarResult {
|
||||
success: boolean;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export interface IUpdateAvatarPresenter {
|
||||
present(result: UpdateAvatarResult): void;
|
||||
}
|
||||
10
core/media/application/presenters/IUploadMediaPresenter.ts
Normal file
10
core/media/application/presenters/IUploadMediaPresenter.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export interface UploadMediaResult {
|
||||
success: boolean;
|
||||
mediaId?: string;
|
||||
url?: string;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export interface IUploadMediaPresenter {
|
||||
present(result: UploadMediaResult): void;
|
||||
}
|
||||
77
core/media/application/use-cases/DeleteMediaUseCase.ts
Normal file
77
core/media/application/use-cases/DeleteMediaUseCase.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Use Case: DeleteMediaUseCase
|
||||
*
|
||||
* Handles the business logic for deleting media files.
|
||||
*/
|
||||
|
||||
import type { IMediaRepository } from '../../domain/repositories/IMediaRepository';
|
||||
import type { MediaStoragePort } from '../ports/MediaStoragePort';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { IDeleteMediaPresenter } from '../presenters/IDeleteMediaPresenter';
|
||||
|
||||
export interface DeleteMediaInput {
|
||||
mediaId: string;
|
||||
}
|
||||
|
||||
export interface DeleteMediaResult {
|
||||
success: boolean;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export interface IDeleteMediaPresenter {
|
||||
present(result: DeleteMediaResult): void;
|
||||
}
|
||||
|
||||
export class DeleteMediaUseCase {
|
||||
constructor(
|
||||
private readonly mediaRepo: IMediaRepository,
|
||||
private readonly mediaStorage: MediaStoragePort,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
input: DeleteMediaInput,
|
||||
presenter: IDeleteMediaPresenter,
|
||||
): Promise<void> {
|
||||
try {
|
||||
this.logger.info('[DeleteMediaUseCase] Deleting media', {
|
||||
mediaId: input.mediaId,
|
||||
});
|
||||
|
||||
const media = await this.mediaRepo.findById(input.mediaId);
|
||||
|
||||
if (!media) {
|
||||
presenter.present({
|
||||
success: false,
|
||||
errorMessage: 'Media not found',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete from storage
|
||||
await this.mediaStorage.deleteMedia(media.url.value);
|
||||
|
||||
// Delete from repository
|
||||
await this.mediaRepo.delete(input.mediaId);
|
||||
|
||||
presenter.present({
|
||||
success: true,
|
||||
});
|
||||
|
||||
this.logger.info('[DeleteMediaUseCase] Media deleted successfully', {
|
||||
mediaId: input.mediaId,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('[DeleteMediaUseCase] Error deleting media', {
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
mediaId: input.mediaId,
|
||||
});
|
||||
|
||||
presenter.present({
|
||||
success: false,
|
||||
errorMessage: 'Internal error occurred while deleting media',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
77
core/media/application/use-cases/GetAvatarUseCase.ts
Normal file
77
core/media/application/use-cases/GetAvatarUseCase.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Use Case: GetAvatarUseCase
|
||||
*
|
||||
* Handles the business logic for retrieving a driver's avatar.
|
||||
*/
|
||||
|
||||
import type { IAvatarRepository } from '../../domain/repositories/IAvatarRepository';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { IGetAvatarPresenter } from '../presenters/IGetAvatarPresenter';
|
||||
|
||||
export interface GetAvatarInput {
|
||||
driverId: string;
|
||||
}
|
||||
|
||||
export interface GetAvatarResult {
|
||||
success: boolean;
|
||||
avatar?: {
|
||||
id: string;
|
||||
driverId: string;
|
||||
mediaUrl: string;
|
||||
selectedAt: Date;
|
||||
};
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export interface IGetAvatarPresenter {
|
||||
present(result: GetAvatarResult): void;
|
||||
}
|
||||
|
||||
export class GetAvatarUseCase {
|
||||
constructor(
|
||||
private readonly avatarRepo: IAvatarRepository,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
input: GetAvatarInput,
|
||||
presenter: IGetAvatarPresenter,
|
||||
): Promise<void> {
|
||||
try {
|
||||
this.logger.info('[GetAvatarUseCase] Getting avatar', {
|
||||
driverId: input.driverId,
|
||||
});
|
||||
|
||||
const avatar = await this.avatarRepo.findActiveByDriverId(input.driverId);
|
||||
|
||||
if (!avatar) {
|
||||
presenter.present({
|
||||
success: false,
|
||||
errorMessage: 'Avatar not found',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
presenter.present({
|
||||
success: true,
|
||||
avatar: {
|
||||
id: avatar.id,
|
||||
driverId: avatar.driverId,
|
||||
mediaUrl: avatar.mediaUrl.value,
|
||||
selectedAt: avatar.selectedAt,
|
||||
},
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('[GetAvatarUseCase] Error getting avatar', {
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
driverId: input.driverId,
|
||||
});
|
||||
|
||||
presenter.present({
|
||||
success: false,
|
||||
errorMessage: 'Internal error occurred while retrieving avatar',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
89
core/media/application/use-cases/GetMediaUseCase.ts
Normal file
89
core/media/application/use-cases/GetMediaUseCase.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Use Case: GetMediaUseCase
|
||||
*
|
||||
* Handles the business logic for retrieving media information.
|
||||
*/
|
||||
|
||||
import type { IMediaRepository } from '../../domain/repositories/IMediaRepository';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { IGetMediaPresenter } from '../presenters/IGetMediaPresenter';
|
||||
|
||||
export interface GetMediaInput {
|
||||
mediaId: string;
|
||||
}
|
||||
|
||||
export interface GetMediaResult {
|
||||
success: boolean;
|
||||
media?: {
|
||||
id: string;
|
||||
filename: string;
|
||||
originalName: string;
|
||||
mimeType: string;
|
||||
size: number;
|
||||
url: string;
|
||||
type: string;
|
||||
uploadedBy: string;
|
||||
uploadedAt: Date;
|
||||
metadata?: Record<string, any>;
|
||||
};
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export interface IGetMediaPresenter {
|
||||
present(result: GetMediaResult): void;
|
||||
}
|
||||
|
||||
export class GetMediaUseCase {
|
||||
constructor(
|
||||
private readonly mediaRepo: IMediaRepository,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
input: GetMediaInput,
|
||||
presenter: IGetMediaPresenter,
|
||||
): Promise<void> {
|
||||
try {
|
||||
this.logger.info('[GetMediaUseCase] Getting media', {
|
||||
mediaId: input.mediaId,
|
||||
});
|
||||
|
||||
const media = await this.mediaRepo.findById(input.mediaId);
|
||||
|
||||
if (!media) {
|
||||
presenter.present({
|
||||
success: false,
|
||||
errorMessage: 'Media not found',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
presenter.present({
|
||||
success: true,
|
||||
media: {
|
||||
id: media.id,
|
||||
filename: media.filename,
|
||||
originalName: media.originalName,
|
||||
mimeType: media.mimeType,
|
||||
size: media.size,
|
||||
url: media.url.value,
|
||||
type: media.type,
|
||||
uploadedBy: media.uploadedBy,
|
||||
uploadedAt: media.uploadedAt,
|
||||
metadata: media.metadata,
|
||||
},
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('[GetMediaUseCase] Error getting media', {
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
mediaId: input.mediaId,
|
||||
});
|
||||
|
||||
presenter.present({
|
||||
success: false,
|
||||
errorMessage: 'Internal error occurred while retrieving media',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,155 +1,136 @@
|
||||
import type { UseCase, Logger } from '@core/shared/application';
|
||||
/**
|
||||
* Use Case: RequestAvatarGenerationUseCase
|
||||
*
|
||||
* Handles the business logic for requesting avatar generation from a face photo.
|
||||
*/
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import type { IAvatarGenerationRepository } from '../../domain/repositories/IAvatarGenerationRepository';
|
||||
import type { FaceValidationPort } from '../ports/FaceValidationPort';
|
||||
import type { AvatarGenerationPort } from '../ports/AvatarGenerationPort';
|
||||
import type { IRequestAvatarGenerationPresenter, RequestAvatarGenerationResultDTO } from '../presenters/IRequestAvatarGenerationPresenter';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import { AvatarGenerationRequest } from '../../domain/entities/AvatarGenerationRequest';
|
||||
import type { RacingSuitColor, AvatarStyle } from '../../domain/types/AvatarGenerationRequest';
|
||||
import type { IRequestAvatarGenerationPresenter } from '../presenters/IRequestAvatarGenerationPresenter';
|
||||
import type { RacingSuitColor } from '../../domain/types/AvatarGenerationRequest';
|
||||
|
||||
export interface RequestAvatarGenerationCommand {
|
||||
export interface RequestAvatarGenerationInput {
|
||||
userId: string;
|
||||
facePhotoData: string; // Base64 encoded image data
|
||||
facePhotoData: string;
|
||||
suitColor: RacingSuitColor;
|
||||
style?: AvatarStyle;
|
||||
style?: 'realistic' | 'cartoon' | 'pixel-art';
|
||||
}
|
||||
|
||||
export class RequestAvatarGenerationUseCase
|
||||
implements UseCase<RequestAvatarGenerationCommand, RequestAvatarGenerationResultDTO, any, IRequestAvatarGenerationPresenter> {
|
||||
export class RequestAvatarGenerationUseCase {
|
||||
constructor(
|
||||
private readonly avatarRepository: IAvatarGenerationRepository,
|
||||
private readonly avatarRepo: IAvatarGenerationRepository,
|
||||
private readonly faceValidation: FaceValidationPort,
|
||||
private readonly avatarGeneration: AvatarGenerationPort,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(command: RequestAvatarGenerationCommand, presenter: IRequestAvatarGenerationPresenter): Promise<void> {
|
||||
presenter.reset();
|
||||
this.logger.debug(
|
||||
`Executing RequestAvatarGenerationUseCase for userId: ${command.userId}`,
|
||||
command,
|
||||
);
|
||||
|
||||
async execute(
|
||||
input: RequestAvatarGenerationInput,
|
||||
presenter: IRequestAvatarGenerationPresenter,
|
||||
): Promise<void> {
|
||||
try {
|
||||
// Create the generation request
|
||||
const requestId = this.generateId();
|
||||
this.logger.info('[RequestAvatarGenerationUseCase] Starting avatar generation request', {
|
||||
userId: input.userId,
|
||||
suitColor: input.suitColor,
|
||||
});
|
||||
|
||||
// Create the avatar generation request entity
|
||||
const requestId = uuidv4();
|
||||
const request = AvatarGenerationRequest.create({
|
||||
id: requestId,
|
||||
userId: command.userId,
|
||||
facePhotoUrl: `data:image/jpeg;base64,${command.facePhotoData}`,
|
||||
suitColor: command.suitColor,
|
||||
...(command.style ? { style: command.style } : {}),
|
||||
userId: input.userId,
|
||||
facePhotoUrl: input.facePhotoData, // Assuming facePhotoData is a URL or base64
|
||||
suitColor: input.suitColor,
|
||||
style: input.style,
|
||||
});
|
||||
|
||||
this.logger.info(`Avatar generation request created with ID: ${requestId}`);
|
||||
// Save initial request
|
||||
await this.avatarRepo.save(request);
|
||||
|
||||
// Mark as validating
|
||||
// Present initial status
|
||||
presenter.present({
|
||||
requestId,
|
||||
status: 'validating',
|
||||
});
|
||||
|
||||
// Validate face photo
|
||||
request.markAsValidating();
|
||||
await this.avatarRepository.save(request);
|
||||
this.logger.debug(`Request ${requestId} marked as validating.`);
|
||||
await this.avatarRepo.save(request);
|
||||
|
||||
// Validate the face photo
|
||||
const validationResult = await this.faceValidation.validateFacePhoto(command.facePhotoData);
|
||||
this.logger.debug(
|
||||
`Face validation result for request ${requestId}:`,
|
||||
validationResult,
|
||||
);
|
||||
|
||||
if (!validationResult.isValid) {
|
||||
const errorMessage = validationResult.errorMessage || 'Face validation failed';
|
||||
const validationResult = await this.faceValidation.validateFacePhoto(input.facePhotoData);
|
||||
|
||||
if (!validationResult.isValid || !validationResult.hasFace || validationResult.faceCount !== 1) {
|
||||
const errorMessage = validationResult.errorMessage || 'Invalid face photo: must contain exactly one face';
|
||||
request.fail(errorMessage);
|
||||
await this.avatarRepository.save(request);
|
||||
this.logger.error(`Face validation failed for request ${requestId}: ${errorMessage}`);
|
||||
await this.avatarRepo.save(request);
|
||||
|
||||
presenter.present({
|
||||
requestId,
|
||||
status: 'failed',
|
||||
errorMessage: validationResult.errorMessage || 'Please upload a clear photo of your face',
|
||||
errorMessage,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validationResult.hasFace) {
|
||||
const errorMessage = 'No face detected in the image';
|
||||
request.fail(errorMessage);
|
||||
await this.avatarRepository.save(request);
|
||||
this.logger.error(`No face detected for request ${requestId}: ${errorMessage}`);
|
||||
presenter.present({
|
||||
requestId,
|
||||
status: 'failed',
|
||||
errorMessage: 'No face detected. Please upload a photo that clearly shows your face.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (validationResult.faceCount > 1) {
|
||||
const errorMessage = 'Multiple faces detected';
|
||||
request.fail(errorMessage);
|
||||
await this.avatarRepository.save(request);
|
||||
this.logger.error(`Multiple faces detected for request ${requestId}: ${errorMessage}`);
|
||||
presenter.present({
|
||||
requestId,
|
||||
status: 'failed',
|
||||
errorMessage: 'Multiple faces detected. Please upload a photo with only your face.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.logger.info(`Face validation successful for request ${requestId}.`);
|
||||
|
||||
// Mark as generating
|
||||
request.markAsGenerating();
|
||||
await this.avatarRepository.save(request);
|
||||
this.logger.debug(`Request ${requestId} marked as generating.`);
|
||||
|
||||
// Generate avatars
|
||||
const generationResult = await this.avatarGeneration.generateAvatars({
|
||||
facePhotoUrl: request.facePhotoUrl.value,
|
||||
request.markAsGenerating();
|
||||
await this.avatarRepo.save(request);
|
||||
|
||||
const generationOptions = {
|
||||
facePhotoUrl: input.facePhotoData,
|
||||
prompt: request.buildPrompt(),
|
||||
suitColor: request.suitColor,
|
||||
style: request.style,
|
||||
count: 3, // Generate 3 options
|
||||
});
|
||||
this.logger.debug(
|
||||
`Avatar generation service result for request ${requestId}:`,
|
||||
generationResult,
|
||||
);
|
||||
suitColor: input.suitColor,
|
||||
style: input.style || 'realistic',
|
||||
count: 3, // Generate 3 avatar options
|
||||
};
|
||||
|
||||
const generationResult = await this.avatarGeneration.generateAvatars(generationOptions);
|
||||
|
||||
if (!generationResult.success) {
|
||||
const errorMessage = generationResult.errorMessage || 'Avatar generation failed';
|
||||
const errorMessage = generationResult.errorMessage || 'Failed to generate avatars';
|
||||
request.fail(errorMessage);
|
||||
await this.avatarRepository.save(request);
|
||||
this.logger.error(`Avatar generation failed for request ${requestId}: ${errorMessage}`);
|
||||
await this.avatarRepo.save(request);
|
||||
|
||||
presenter.present({
|
||||
requestId,
|
||||
status: 'failed',
|
||||
errorMessage: generationResult.errorMessage || 'Failed to generate avatars. Please try again.',
|
||||
errorMessage,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Complete with generated avatars
|
||||
const avatarUrls = generationResult.avatars.map(a => a.url);
|
||||
// Complete the request
|
||||
const avatarUrls = generationResult.avatars.map(avatar => avatar.url);
|
||||
request.completeWithAvatars(avatarUrls);
|
||||
await this.avatarRepository.save(request);
|
||||
this.logger.info(`Avatar generation completed successfully for request ${requestId}.`);
|
||||
await this.avatarRepo.save(request);
|
||||
|
||||
presenter.present({
|
||||
requestId,
|
||||
status: 'completed',
|
||||
avatarUrls,
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`An unexpected error occurred during avatar generation for userId: ${command.userId}`,
|
||||
error as Error,
|
||||
);
|
||||
// Re-throw or return a generic error, depending on desired error handling strategy
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private generateId(): string {
|
||||
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
||||
return crypto.randomUUID();
|
||||
this.logger.info('[RequestAvatarGenerationUseCase] Avatar generation completed', {
|
||||
requestId,
|
||||
userId: input.userId,
|
||||
avatarCount: avatarUrls.length,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('[RequestAvatarGenerationUseCase] Error during avatar generation', {
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
userId: input.userId,
|
||||
});
|
||||
|
||||
presenter.present({
|
||||
requestId: uuidv4(), // Fallback ID
|
||||
status: 'failed',
|
||||
errorMessage: 'Internal error occurred during avatar generation',
|
||||
});
|
||||
}
|
||||
return 'avatar-' + Date.now().toString(36) + Math.random().toString(36).substr(2, 9);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
/**
|
||||
* Use Case: SelectAvatarUseCase
|
||||
*
|
||||
* Allows a user to select one of the generated avatars as their profile avatar.
|
||||
*
|
||||
* Handles the business logic for selecting a generated avatar from the options.
|
||||
*/
|
||||
|
||||
import type { AsyncUseCase, Logger } from '@core/shared/application';
|
||||
import type { IAvatarGenerationRepository } from '../../domain/repositories/IAvatarGenerationRepository';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { ISelectAvatarPresenter } from '../presenters/ISelectAvatarPresenter';
|
||||
|
||||
export interface SelectAvatarCommand {
|
||||
export interface SelectAvatarInput {
|
||||
requestId: string;
|
||||
userId: string;
|
||||
avatarIndex: number;
|
||||
selectedIndex: number;
|
||||
}
|
||||
|
||||
export interface SelectAvatarResult {
|
||||
@@ -19,60 +19,69 @@ export interface SelectAvatarResult {
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export class SelectAvatarUseCase
|
||||
implements AsyncUseCase<SelectAvatarCommand, SelectAvatarResult> {
|
||||
export interface ISelectAvatarPresenter {
|
||||
present(result: SelectAvatarResult): void;
|
||||
}
|
||||
|
||||
export class SelectAvatarUseCase {
|
||||
constructor(
|
||||
private readonly avatarRepository: IAvatarGenerationRepository,
|
||||
private readonly avatarRepo: IAvatarGenerationRepository,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(command: SelectAvatarCommand): Promise<SelectAvatarResult> {
|
||||
this.logger.debug(`Executing SelectAvatarUseCase for userId: ${command.userId}, requestId: ${command.requestId}, avatarIndex: ${command.avatarIndex}`);
|
||||
|
||||
const request = await this.avatarRepository.findById(command.requestId);
|
||||
|
||||
if (!request) {
|
||||
this.logger.info(`Avatar generation request not found for requestId: ${command.requestId}`);
|
||||
return {
|
||||
success: false,
|
||||
errorMessage: 'Avatar generation request not found',
|
||||
};
|
||||
}
|
||||
|
||||
if (request.userId !== command.userId) {
|
||||
this.logger.info(`Permission denied for userId: ${command.userId} to select avatar for requestId: ${command.requestId}`);
|
||||
return {
|
||||
success: false,
|
||||
errorMessage: 'You do not have permission to select this avatar',
|
||||
};
|
||||
}
|
||||
|
||||
if (request.status !== 'completed') {
|
||||
this.logger.info(`Avatar generation not completed for requestId: ${command.requestId}, current status: ${request.status}`);
|
||||
return {
|
||||
success: false,
|
||||
errorMessage: 'Avatar generation is not yet complete',
|
||||
};
|
||||
}
|
||||
|
||||
async execute(
|
||||
input: SelectAvatarInput,
|
||||
presenter: ISelectAvatarPresenter,
|
||||
): Promise<void> {
|
||||
try {
|
||||
request.selectAvatar(command.avatarIndex);
|
||||
await this.avatarRepository.save(request);
|
||||
this.logger.info('[SelectAvatarUseCase] Selecting avatar', {
|
||||
requestId: input.requestId,
|
||||
selectedIndex: input.selectedIndex,
|
||||
});
|
||||
|
||||
const request = await this.avatarRepo.findById(input.requestId);
|
||||
|
||||
if (!request) {
|
||||
presenter.present({
|
||||
success: false,
|
||||
errorMessage: 'Avatar generation request not found',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.status !== 'completed') {
|
||||
presenter.present({
|
||||
success: false,
|
||||
errorMessage: 'Avatar generation is not completed yet',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
request.selectAvatar(input.selectedIndex);
|
||||
await this.avatarRepo.save(request);
|
||||
|
||||
const selectedAvatarUrl = request.selectedAvatarUrl;
|
||||
const result: SelectAvatarResult =
|
||||
selectedAvatarUrl !== undefined
|
||||
? { success: true, selectedAvatarUrl }
|
||||
: { success: true };
|
||||
|
||||
this.logger.info(`Avatar selected successfully for userId: ${command.userId}, requestId: ${command.requestId}, selectedAvatarUrl: ${selectedAvatarUrl}`);
|
||||
return result;
|
||||
presenter.present({
|
||||
success: true,
|
||||
selectedAvatarUrl,
|
||||
});
|
||||
|
||||
this.logger.info('[SelectAvatarUseCase] Avatar selected successfully', {
|
||||
requestId: input.requestId,
|
||||
selectedAvatarUrl,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to select avatar for userId: ${command.userId}, requestId: ${command.requestId}: ${error instanceof Error ? error.message : 'Unknown error'}`, error);
|
||||
return {
|
||||
this.logger.error('[SelectAvatarUseCase] Error selecting avatar', {
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
requestId: input.requestId,
|
||||
});
|
||||
|
||||
presenter.present({
|
||||
success: false,
|
||||
errorMessage: error instanceof Error ? error.message : 'Failed to select avatar',
|
||||
};
|
||||
errorMessage: 'Internal error occurred while selecting avatar',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
81
core/media/application/use-cases/UpdateAvatarUseCase.ts
Normal file
81
core/media/application/use-cases/UpdateAvatarUseCase.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Use Case: UpdateAvatarUseCase
|
||||
*
|
||||
* Handles the business logic for updating a driver's avatar.
|
||||
*/
|
||||
|
||||
import type { IAvatarRepository } from '../../domain/repositories/IAvatarRepository';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import { Avatar } from '../../domain/entities/Avatar';
|
||||
import type { IUpdateAvatarPresenter } from '../presenters/IUpdateAvatarPresenter';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export interface UpdateAvatarInput {
|
||||
driverId: string;
|
||||
mediaUrl: string;
|
||||
}
|
||||
|
||||
export interface UpdateAvatarResult {
|
||||
success: boolean;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export interface IUpdateAvatarPresenter {
|
||||
present(result: UpdateAvatarResult): void;
|
||||
}
|
||||
|
||||
export class UpdateAvatarUseCase {
|
||||
constructor(
|
||||
private readonly avatarRepo: IAvatarRepository,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
input: UpdateAvatarInput,
|
||||
presenter: IUpdateAvatarPresenter,
|
||||
): Promise<void> {
|
||||
try {
|
||||
this.logger.info('[UpdateAvatarUseCase] Updating avatar', {
|
||||
driverId: input.driverId,
|
||||
mediaUrl: input.mediaUrl,
|
||||
});
|
||||
|
||||
// Deactivate current active avatar
|
||||
const currentAvatar = await this.avatarRepo.findActiveByDriverId(input.driverId);
|
||||
if (currentAvatar) {
|
||||
currentAvatar.deactivate();
|
||||
await this.avatarRepo.save(currentAvatar);
|
||||
}
|
||||
|
||||
// Create new avatar
|
||||
const avatarId = uuidv4();
|
||||
const newAvatar = Avatar.create({
|
||||
id: avatarId,
|
||||
driverId: input.driverId,
|
||||
mediaUrl: input.mediaUrl,
|
||||
});
|
||||
|
||||
await this.avatarRepo.save(newAvatar);
|
||||
|
||||
presenter.present({
|
||||
success: true,
|
||||
});
|
||||
|
||||
this.logger.info('[UpdateAvatarUseCase] Avatar updated successfully', {
|
||||
driverId: input.driverId,
|
||||
avatarId,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('[UpdateAvatarUseCase] Error updating avatar', {
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
driverId: input.driverId,
|
||||
});
|
||||
|
||||
presenter.present({
|
||||
success: false,
|
||||
errorMessage: 'Internal error occurred while updating avatar',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
111
core/media/application/use-cases/UploadMediaUseCase.ts
Normal file
111
core/media/application/use-cases/UploadMediaUseCase.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* Use Case: UploadMediaUseCase
|
||||
*
|
||||
* Handles the business logic for uploading media files.
|
||||
*/
|
||||
|
||||
import type { IMediaRepository } from '../../domain/repositories/IMediaRepository';
|
||||
import type { MediaStoragePort } from '../ports/MediaStoragePort';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import { Media } from '../../domain/entities/Media';
|
||||
import type { IUploadMediaPresenter } from '../presenters/IUploadMediaPresenter';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export interface UploadMediaInput {
|
||||
file: Express.Multer.File;
|
||||
uploadedBy: string;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface UploadMediaResult {
|
||||
success: boolean;
|
||||
mediaId?: string;
|
||||
url?: string;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export interface IUploadMediaPresenter {
|
||||
present(result: UploadMediaResult): void;
|
||||
}
|
||||
|
||||
export class UploadMediaUseCase {
|
||||
constructor(
|
||||
private readonly mediaRepo: IMediaRepository,
|
||||
private readonly mediaStorage: MediaStoragePort,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
input: UploadMediaInput,
|
||||
presenter: IUploadMediaPresenter,
|
||||
): Promise<void> {
|
||||
try {
|
||||
this.logger.info('[UploadMediaUseCase] Starting media upload', {
|
||||
filename: input.file.originalname,
|
||||
size: input.file.size,
|
||||
uploadedBy: input.uploadedBy,
|
||||
});
|
||||
|
||||
// Upload file to storage service
|
||||
const uploadResult = await this.mediaStorage.uploadMedia(input.file.buffer, {
|
||||
filename: input.file.originalname,
|
||||
mimeType: input.file.mimetype,
|
||||
metadata: input.metadata,
|
||||
});
|
||||
|
||||
if (!uploadResult.success) {
|
||||
presenter.present({
|
||||
success: false,
|
||||
errorMessage: uploadResult.errorMessage || 'Failed to upload media',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine media type
|
||||
const mediaType: 'image' | 'video' | 'document' = input.file.mimetype.startsWith('image/')
|
||||
? 'image'
|
||||
: input.file.mimetype.startsWith('video/')
|
||||
? 'video'
|
||||
: 'document';
|
||||
|
||||
// Create media entity
|
||||
const mediaId = uuidv4();
|
||||
const media = Media.create({
|
||||
id: mediaId,
|
||||
filename: uploadResult.filename || input.file.originalname,
|
||||
originalName: input.file.originalname,
|
||||
mimeType: input.file.mimetype,
|
||||
size: input.file.size,
|
||||
url: uploadResult.url,
|
||||
type: mediaType,
|
||||
uploadedBy: input.uploadedBy,
|
||||
metadata: input.metadata,
|
||||
});
|
||||
|
||||
// Save to repository
|
||||
await this.mediaRepo.save(media);
|
||||
|
||||
presenter.present({
|
||||
success: true,
|
||||
mediaId,
|
||||
url: uploadResult.url,
|
||||
});
|
||||
|
||||
this.logger.info('[UploadMediaUseCase] Media uploaded successfully', {
|
||||
mediaId,
|
||||
url: uploadResult.url,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('[UploadMediaUseCase] Error uploading media', {
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
filename: input.file.originalname,
|
||||
});
|
||||
|
||||
presenter.present({
|
||||
success: false,
|
||||
errorMessage: 'Internal error occurred during media upload',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user