Files
gridpilot.gg/core/notifications/application/use-cases/SendNotificationUseCase.ts
2026-01-16 19:46:49 +01:00

157 lines
6.0 KiB
TypeScript

/**
* Application Use Case: SendNotificationUseCase
*
* Sends a notification to a recipient through the appropriate channels
* based on their preferences.
*/
import type { Logger } from '@core/shared/domain/Logger';
import { Result } from '@core/shared/domain/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 { NotificationPreferenceRepository } from '../../domain/repositories/NotificationPreferenceRepository';
import { NotificationRepository } from '../../domain/repositories/NotificationRepository';
import { 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: NotificationRepository,
private readonly preferenceRepository: NotificationPreferenceRepository,
private readonly gatewayRegistry: NotificationGatewayRegistry,
private readonly logger: Logger,
) {
this.logger.debug('SendNotificationUseCase initialized.');
}
async execute(
command: SendNotificationCommand,
): Promise<Result<SendNotificationResult, ApplicationErrorCode<SendNotificationErrorCode, { message: string }>>> {
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 Result.ok<SendNotificationResult, ApplicationErrorCode<SendNotificationErrorCode, { message: string }>>({
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: NotificationChannel) => 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 Result.ok<SendNotificationResult, ApplicationErrorCode<SendNotificationErrorCode, { message: string }>>({
notification: primaryNotification!,
deliveryResults,
});
} catch (error) {
const err = error instanceof Error ? error : new Error(String(error));
this.logger.error('Error sending notification', err);
return Result.err<SendNotificationResult, ApplicationErrorCode<SendNotificationErrorCode, { message: string }>>({
code: 'REPOSITORY_ERROR',
details: { message: err.message },
});
}
}
}