/** * Application Use Case: SendNotificationUseCase * * Sends a notification to a recipient through the appropriate channels * based on their preferences. */ import type { Logger, UseCaseOutputPort } from '@core/shared/application'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import { v4 as uuid } from 'uuid'; import type { NotificationData } from '../../domain/entities/Notification'; import { Notification } from '../../domain/entities/Notification'; import type { INotificationPreferenceRepository } from '../../domain/repositories/INotificationPreferenceRepository'; import type { INotificationRepository } from '../../domain/repositories/INotificationRepository'; import type { NotificationChannel, NotificationType } from '../../domain/types/NotificationTypes'; import type { NotificationDeliveryResult, NotificationGatewayRegistry } from '../ports/NotificationGateway'; export interface SendNotificationCommand { recipientId: string; type: NotificationType; title: string; body: string; data?: NotificationData; actionUrl?: string; /** How urgently to display this notification (default: 'silent') */ urgency?: 'silent' | 'toast' | 'modal'; /** Whether this notification requires a response before dismissal */ requiresResponse?: boolean; /** Action buttons for modal notifications */ actions?: Array<{ label: string; type: 'primary' | 'secondary' | 'danger'; href?: string; actionId?: string; }>; /** Override channels (skip preference check) */ forceChannels?: NotificationChannel[]; } export interface SendNotificationResult { /** The created notification */ notification: Notification; /** Delivery results for each channel */ deliveryResults: NotificationDeliveryResult[]; } export type SendNotificationErrorCode = 'REPOSITORY_ERROR'; export class SendNotificationUseCase { constructor( private readonly notificationRepository: INotificationRepository, private readonly preferenceRepository: INotificationPreferenceRepository, private readonly gatewayRegistry: NotificationGatewayRegistry, private readonly output: UseCaseOutputPort, private readonly logger: Logger, ) { this.logger.debug('SendNotificationUseCase initialized.'); } async execute( command: SendNotificationCommand, ): Promise>> { this.logger.debug('Executing SendNotificationUseCase', { command }); try { // Get recipient's preferences this.logger.debug('Checking notification preferences.', { type: command.type, recipientId: command.recipientId }); const preferences = await this.preferenceRepository.getOrCreateDefault(command.recipientId); // Check if this notification type is enabled if (!preferences.isTypeEnabled(command.type)) { // User has disabled this type - create but don't deliver const notification = Notification.create({ id: uuid(), recipientId: command.recipientId, type: command.type, title: command.title, body: command.body, channel: 'in_app', status: 'dismissed', // Auto-dismiss since user doesn't want these ...(command.data ? { data: command.data } : {}), ...(command.actionUrl ? { actionUrl: command.actionUrl } : {}), }); await this.notificationRepository.create(notification); return { notification, deliveryResults: [], }; } // Determine which channels to use const channels = command.forceChannels ?? preferences.getEnabledChannelsForType(command.type); // Check quiet hours (skip external channels during quiet hours) const effectiveChannels = preferences.isInQuietHours() ? channels.filter(ch => ch === 'in_app') : channels; // Ensure at least in_app is used if (!effectiveChannels.includes('in_app')) { effectiveChannels.unshift('in_app'); } const deliveryResults: NotificationDeliveryResult[] = []; let primaryNotification: Notification | null = null; // Send through each channel for (const channel of effectiveChannels) { const notification = Notification.create({ id: uuid(), recipientId: command.recipientId, type: command.type, title: command.title, body: command.body, channel, ...(command.urgency ? { urgency: command.urgency } : {}), ...(command.data ? { data: command.data } : {}), ...(command.actionUrl ? { actionUrl: command.actionUrl } : {}), ...(command.actions ? { actions: command.actions } : {}), ...(command.requiresResponse !== undefined ? { requiresResponse: command.requiresResponse } : {}), }); // Save to repository (in_app channel) or attempt delivery (external channels) if (channel === 'in_app') { await this.notificationRepository.create(notification); primaryNotification = notification; deliveryResults.push({ success: true, channel, attemptedAt: new Date(), }); } else { // Attempt external delivery const result = await this.gatewayRegistry.send(notification); deliveryResults.push(result); } } this.output.present({ notification: primaryNotification!, deliveryResults, }); return Result.ok(undefined); } catch (error) { const err = error instanceof Error ? error : new Error(String(error)); this.logger.error('Error sending notification', err); return Result.err({ code: 'REPOSITORY_ERROR', details: { message: err.message }, }); } } }