/** * Application Use Case: SendNotificationUseCase * * Sends a notification to a recipient through the appropriate channels * based on their preferences. */ import { v4 as uuid } from 'uuid'; import type { AsyncUseCase } from '@core/shared/application'; import type { Logger } from '@core/shared/application'; import { Notification } from '../../domain/entities/Notification'; import type { NotificationData } from '../../domain/entities/Notification'; import type { INotificationRepository } from '../../domain/repositories/INotificationRepository'; import type { INotificationPreferenceRepository } from '../../domain/repositories/INotificationPreferenceRepository'; import type { INotificationGatewayRegistry, NotificationDeliveryResult } from '../ports/INotificationGateway'; import type { NotificationType, NotificationChannel } from '../../domain/types/NotificationTypes'; 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 class SendNotificationUseCase implements AsyncUseCase { constructor( private readonly notificationRepository: INotificationRepository, private readonly preferenceRepository: INotificationPreferenceRepository, private readonly gatewayRegistry: INotificationGatewayRegistry, 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); } } return { notification: primaryNotification!, deliveryResults, }; } catch (error) { this.logger.error('Error sending notification', error as Error); throw error; } } }