import webpush from 'web-push'; import logger, { logNotificationEvent } from '@utils/logger'; import { Subscription } from '@models/Subscription'; import { Notification } from '@models/Notification'; import { shouldSendEmail, shouldSendEmailWithOverride, shouldSendInAppNotification, EmailNotificationType } from '../emailtemplates/emailPreferences.helper'; type PushSubscription = any; // Web Push protocol JSON interface NotificationPayload { title: string; body: string; requestId?: string; requestNumber?: string; url?: string; type?: string; priority?: 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT'; actionRequired?: boolean; metadata?: any; } class NotificationService { private userIdToSubscriptions: Map = new Map(); configure(vapidPublicKey?: string, vapidPrivateKey?: string, mailto?: string) { const pub = vapidPublicKey || process.env.VAPID_PUBLIC_KEY || ''; const priv = vapidPrivateKey || process.env.VAPID_PRIVATE_KEY || ''; const contact = mailto || process.env.VAPID_CONTACT || 'mailto:admin@example.com'; if (!pub || !priv) { logger.warn('VAPID keys are not configured. Push notifications are disabled.'); return; } webpush.setVapidDetails(contact, pub, priv); logger.info('Web Push configured'); } async addSubscription(userId: string, subscription: PushSubscription, userAgent?: string) { // Persist to DB (upsert by endpoint) try { const endpoint: string = subscription?.endpoint || ''; const keys = subscription?.keys || {}; if (!endpoint || !keys?.p256dh || !keys?.auth) throw new Error('Invalid subscription payload'); await Subscription.upsert({ userId, endpoint, p256dh: keys.p256dh, auth: keys.auth, userAgent: userAgent || null, } as any); } catch (e) { logger.error('Failed to persist subscription', e); } const list = this.userIdToSubscriptions.get(userId) || []; const already = list.find((s) => JSON.stringify(s) === JSON.stringify(subscription)); if (!already) { list.push(subscription); this.userIdToSubscriptions.set(userId, list); } logger.info(`Subscription stored for user ${userId}. Total: ${list.length}`); } /** * Get all subscriptions for a user */ async getUserSubscriptions(userId: string) { try { const subscriptions = await Subscription.findAll({ where: { userId }, attributes: ['subscriptionId', 'endpoint', 'userAgent', 'createdAt'] }); return subscriptions; } catch (error) { logger.error(`[Notification] Failed to get subscriptions for user ${userId}:`, error); return []; } } /** * Remove expired/invalid subscription from database and memory cache */ private async removeExpiredSubscription(userId: string, endpoint: string) { try { // Remove from database await Subscription.destroy({ where: { endpoint } }); logger.info(`[Notification] Removed expired subscription from DB for user ${userId}, endpoint: ${endpoint.substring(0, 50)}...`); // Remove from memory cache const list = this.userIdToSubscriptions.get(userId) || []; const filtered = list.filter((s) => s.endpoint !== endpoint); if (filtered.length !== list.length) { this.userIdToSubscriptions.set(userId, filtered); logger.info(`[Notification] Removed expired subscription from memory cache for user ${userId}`); } } catch (error) { logger.error(`[Notification] Failed to remove expired subscription for user ${userId}:`, error); } } /** * Check if error indicates expired/invalid subscription * webpush returns status codes: 410 (Gone), 404 (Not Found), 403 (Forbidden) */ private isExpiredSubscriptionError(err: any): boolean { const statusCode = err?.statusCode || err?.status || err?.response?.statusCode; // 410 Gone = subscription expired // 404 Not Found = subscription doesn't exist // 403 Forbidden = subscription invalid return statusCode === 410 || statusCode === 404 || statusCode === 403; } /** * Send notification to users - saves to DB, sends via push/socket, and emails * Respects user notification preferences for all channels * Automatically sends email for applicable notification types */ async sendToUsers(userIds: string[], payload: NotificationPayload) { const message = JSON.stringify(payload); const { User } = require('@models/User'); for (const userId of userIds) { try { // Fetch user preferences and email data const user = await User.findByPk(userId, { attributes: [ 'userId', 'email', 'displayName', 'emailNotificationsEnabled', 'pushNotificationsEnabled', 'inAppNotificationsEnabled' ] }); if (!user) { logger.warn(`[Notification] User ${userId} not found, skipping notification`); continue; } const sentVia: string[] = []; // 1. Check admin + user preferences for in-app notifications const canSendInApp = await shouldSendInAppNotification(userId, payload.type || 'general'); logger.info(`[Notification] In-app notification check for user ${userId}:`, { canSendInApp, inAppNotificationsEnabled: user.inAppNotificationsEnabled, notificationType: payload.type, willCreate: canSendInApp && user.inAppNotificationsEnabled }); let notification: any = null; if (canSendInApp && user.inAppNotificationsEnabled) { try { notification = await Notification.create({ userId, requestId: payload.requestId, notificationType: payload.type || 'general', title: payload.title, message: payload.body, isRead: false, priority: payload.priority || 'MEDIUM', actionUrl: payload.url, actionRequired: payload.actionRequired || false, metadata: { requestNumber: payload.requestNumber, ...payload.metadata }, sentVia: ['IN_APP'], emailSent: false, smsSent: false, pushSent: false } as any); sentVia.push('IN_APP'); logger.info(`[Notification] ✅ Created in-app notification for user ${userId}: ${payload.title} (ID: ${(notification as any).notificationId})`); // 2. Emit real-time socket event for immediate delivery try { const { emitToUser } = require('../realtime/socket'); if (emitToUser) { emitToUser(userId, 'notification:new', { notification: notification.toJSON(), ...payload }); logger.info(`[Notification] ✅ Emitted socket event to user ${userId}`); } else { logger.warn(`[Notification] emitToUser function not available`); } } catch (socketError) { logger.warn(`[Notification] Socket emit failed (not critical):`, socketError); } } catch (notificationError) { logger.error(`[Notification] ❌ Failed to create in-app notification for user ${userId}:`, notificationError); // Continue - don't block other notification channels } // 3. Send push notification (if enabled and user has subscriptions) if (user.pushNotificationsEnabled && canSendInApp && notification) { let subs = this.userIdToSubscriptions.get(userId) || []; // Load from DB if memory empty if (subs.length === 0) { try { const rows = await Subscription.findAll({ where: { userId } }); subs = rows.map((r: any) => ({ endpoint: r.endpoint, keys: { p256dh: r.p256dh, auth: r.auth } })); } catch {} } if (subs.length > 0) { for (const sub of subs) { try { await webpush.sendNotification(sub, message); await notification.update({ pushSent: true }); sentVia.push('PUSH'); logNotificationEvent('sent', { userId, channel: 'push', type: payload.type, requestId: payload.requestId, }); } catch (err: any) { // Check if subscription is expired/invalid if (this.isExpiredSubscriptionError(err)) { logger.warn(`[Notification] Expired subscription detected for user ${userId}, removing...`); await this.removeExpiredSubscription(userId, sub.endpoint); } else { logNotificationEvent('failed', { userId, channel: 'push', type: payload.type, requestId: payload.requestId, error: err, }); } } } } } else { logger.info(`[Notification] Push notifications disabled for user ${userId}, skipping push`); } } else { if (!canSendInApp) { logger.info(`[Notification] In-app notifications disabled by admin/user for user ${userId}, type: ${payload.type}`); } else { logger.info(`[Notification] In-app notifications disabled for user ${userId}`); } } // 4. Send email notification for applicable types (async, don't wait) console.log(`[DEBUG] Checking email for notification type: ${payload.type}`); this.sendEmailNotification(userId, user, payload).catch(emailError => { console.error(`[Notification] Email sending failed for user ${userId}:`, emailError); logger.error(`[Notification] Email sending failed for user ${userId}:`, emailError); // Don't throw - email failure shouldn't block notification }); } catch (error) { logger.error(`[Notification] Failed to create notification for user ${userId}:`, error); // Continue to next user even if one fails } } } /** * Send email notification based on notification type * Only sends for notification types that warrant email */ private async sendEmailNotification(userId: string, user: any, payload: NotificationPayload): Promise { console.log(`[DEBUG Email] Notification type: ${payload.type}, userId: ${userId}`); // Import email service (lazy load to avoid circular dependencies) const { emailNotificationService } = await import('./emailNotification.service'); const { EmailNotificationType } = await import('../emailtemplates/emailPreferences.helper'); // Map notification type to email type and check if email should be sent const emailTypeMap: Record = { 'request_submitted': EmailNotificationType.REQUEST_CREATED, 'assignment': EmailNotificationType.APPROVAL_REQUEST, 'approval': EmailNotificationType.REQUEST_APPROVED, 'rejection': EmailNotificationType.REQUEST_REJECTED, 'tat_reminder': EmailNotificationType.TAT_REMINDER, 'tat_breach': EmailNotificationType.TAT_BREACHED, 'threshold1': EmailNotificationType.TAT_REMINDER, // 50% TAT reminder 'threshold2': EmailNotificationType.TAT_REMINDER, // 75% TAT reminder 'breach': EmailNotificationType.TAT_BREACHED, // 100% TAT breach 'tat_breach_initiator': EmailNotificationType.TAT_BREACHED, // Breach notification to initiator 'workflow_resumed': EmailNotificationType.WORKFLOW_RESUMED, 'closed': EmailNotificationType.REQUEST_CLOSED, // These don't get emails (in-app only) 'mention': null, 'comment': null, 'document_added': null, 'status_change': null, 'ai_conclusion_generated': null, 'summary_generated': null, 'workflow_paused': EmailNotificationType.WORKFLOW_PAUSED, 'approver_skipped': EmailNotificationType.APPROVER_SKIPPED, 'pause_retrigger_request': EmailNotificationType.WORKFLOW_PAUSED, // Use same template as pause 'pause_retriggered': null }; const emailType = emailTypeMap[payload.type || '']; console.log(`[DEBUG Email] Email type mapped: ${emailType}`); if (!emailType) { // This notification type doesn't warrant email console.log(`[DEBUG Email] No email for notification type: ${payload.type}`); return; } // Check if email should be sent (admin + user preferences) // Critical emails: rejection, tat_breach, breach const isCriticalEmail = payload.type === 'rejection' || payload.type === 'tat_breach' || payload.type === 'breach'; const shouldSend = isCriticalEmail ? await shouldSendEmailWithOverride(userId, emailType) // Critical emails : payload.type === 'assignment' ? await shouldSendEmailWithOverride(userId, emailType) // Assignment emails - use override to ensure delivery : await shouldSendEmail(userId, emailType); // Regular emails console.log(`[DEBUG Email] Should send email: ${shouldSend} for type: ${payload.type}, userId: ${userId}`); if (!shouldSend) { console.log(`[DEBUG Email] Email skipped for user ${userId}, type: ${payload.type} (preferences)`); logger.warn(`[Email] Email skipped for user ${userId}, type: ${payload.type} (preferences or admin disabled)`); return; } logger.info(`[Email] Sending email notification to user ${userId} for type: ${payload.type}, requestId: ${payload.requestId}`); // Trigger email based on notification type // Email service will fetch additional data as needed console.log(`[DEBUG Email] Triggering email for type: ${payload.type}`); try { await this.triggerEmailByType(payload.type || '', userId, payload, user); } catch (error) { console.error(`[DEBUG Email] Error triggering email:`, error); logger.error(`[Email] Failed to trigger email for type ${payload.type}:`, error); } } /** * Trigger appropriate email based on notification type */ private async triggerEmailByType( notificationType: string, userId: string, payload: NotificationPayload, user: any ): Promise { const { emailNotificationService } = await import('./emailNotification.service'); const { WorkflowRequest, User, ApprovalLevel } = await import('@models/index'); // Fetch request data if requestId is provided if (!payload.requestId) { logger.warn(`[Email] No requestId in payload for type ${notificationType}`); return; } const request = await WorkflowRequest.findByPk(payload.requestId); if (!request) { logger.warn(`[Email] Request ${payload.requestId} not found`); return; } const requestData = request.toJSON(); // Fetch initiator user const initiator = await User.findByPk(requestData.initiatorId); if (!initiator) { logger.warn(`[Email] Initiator not found for request ${payload.requestId}`); return; } const initiatorData = initiator.toJSON(); switch (notificationType) { case 'request_submitted': { const firstLevel = await ApprovalLevel.findOne({ where: { requestId: payload.requestId, levelNumber: 1 } }); const firstApprover = firstLevel ? await User.findByPk((firstLevel as any).approverId) : null; // Get first approver's TAT hours (not total TAT) const firstApproverTatHours = firstLevel ? (firstLevel as any).tatHours : null; // Add first approver's TAT to requestData for the email const requestDataWithFirstTat = { ...requestData, tatHours: firstApproverTatHours || (requestData as any).totalTatHours || 24 }; await emailNotificationService.sendRequestCreated( requestDataWithFirstTat, initiatorData, firstApprover ? firstApprover.toJSON() : null ); } break; case 'assignment': { // Fetch the approver user (the one being assigned) const approverUser = await User.findByPk(userId); if (!approverUser) { logger.warn(`[Email] Approver user ${userId} not found`); return; } const allLevels = await ApprovalLevel.findAll({ where: { requestId: payload.requestId }, order: [['levelNumber', 'ASC']] }); const isMultiLevel = allLevels.length > 1; const approverData = approverUser.toJSON(); // Add level number if available const currentLevel = allLevels.find((l: any) => l.approverId === userId); if (currentLevel) { (approverData as any).levelNumber = (currentLevel as any).levelNumber; } await emailNotificationService.sendApprovalRequest( requestData, approverData, initiatorData, isMultiLevel, isMultiLevel ? allLevels.map((l: any) => l.toJSON()) : undefined ); } break; case 'approval': { const approvedLevel = await ApprovalLevel.findOne({ where: { requestId: payload.requestId, status: 'APPROVED' }, order: [['actionDate', 'DESC'], ['levelEndTime', 'DESC']] }); const allLevels = await ApprovalLevel.findAll({ where: { requestId: payload.requestId }, order: [['levelNumber', 'ASC']] }); const approvedCount = allLevels.filter((l: any) => l.status === 'APPROVED').length; const isFinalApproval = approvedCount === allLevels.length; const nextLevel = isFinalApproval ? null : allLevels.find((l: any) => l.status === 'PENDING'); const nextApprover = nextLevel ? await User.findByPk((nextLevel as any).approverId) : null; // Get the approver who just approved from the approved level let approverData = user; // Fallback to user if we can't find the approver if (approvedLevel) { const approverUser = await User.findByPk((approvedLevel as any).approverId); if (approverUser) { approverData = approverUser.toJSON(); // Add approval metadata (approverData as any).approvedAt = (approvedLevel as any).actionDate; (approverData as any).comments = (approvedLevel as any).comments; } } await emailNotificationService.sendApprovalConfirmation( requestData, approverData, // Approver who just approved initiatorData, isFinalApproval, nextApprover ? nextApprover.toJSON() : undefined ); } break; case 'rejection': { const rejectedLevel = await ApprovalLevel.findOne({ where: { requestId: payload.requestId, status: 'REJECTED' }, order: [['actionDate', 'DESC'], ['levelEndTime', 'DESC']] }); // Get the approver who rejected from the rejected level let approverData = user; // Fallback to user if we can't find the approver if (rejectedLevel) { const approverUser = await User.findByPk((rejectedLevel as any).approverId); if (approverUser) { approverData = approverUser.toJSON(); // Add rejection metadata (approverData as any).rejectedAt = (rejectedLevel as any).actionDate; (approverData as any).comments = (rejectedLevel as any).comments; } else { // If user not found, use approver info from the level itself approverData = { userId: (rejectedLevel as any).approverId, displayName: (rejectedLevel as any).approverName || 'Unknown Approver', email: (rejectedLevel as any).approverEmail || 'unknown@royalenfield.com', rejectedAt: (rejectedLevel as any).actionDate, comments: (rejectedLevel as any).comments }; } } await emailNotificationService.sendRejectionNotification( requestData, approverData, // Approver who rejected initiatorData, (rejectedLevel as any)?.comments || payload.metadata?.rejectionReason || 'No reason provided' ); } break; case 'tat_reminder': case 'threshold1': case 'threshold2': case 'tat_breach': case 'breach': case 'tat_breach_initiator': { // Get the approver from the current level (the one who needs to take action) const currentLevel = await ApprovalLevel.findOne({ where: { requestId: payload.requestId, status: 'PENDING' }, order: [['levelNumber', 'ASC']] }); // Get approver data - prefer from level, fallback to user let approverData = user; // Fallback if (currentLevel) { const approverUser = await User.findByPk((currentLevel as any).approverId); if (approverUser) { approverData = approverUser.toJSON(); } else { // If user not found, use approver info from the level itself approverData = { userId: (currentLevel as any).approverId, displayName: (currentLevel as any).approverName || 'Unknown Approver', email: (currentLevel as any).approverEmail || 'unknown@royalenfield.com' }; } } // Determine threshold percentage based on notification type let thresholdPercentage = 75; // Default if (notificationType === 'threshold1') { thresholdPercentage = 50; } else if (notificationType === 'threshold2') { thresholdPercentage = 75; } else if (notificationType === 'breach' || notificationType === 'tat_breach' || notificationType === 'tat_breach_initiator') { thresholdPercentage = 100; } else if (payload.metadata?.thresholdPercentage) { thresholdPercentage = payload.metadata.thresholdPercentage; } // Extract TAT info from metadata or payload const tatInfo = payload.metadata?.tatInfo || { thresholdPercentage: thresholdPercentage, timeRemaining: payload.metadata?.timeRemaining || 'Unknown', tatDeadline: payload.metadata?.tatDeadline || new Date(), assignedDate: payload.metadata?.assignedDate || requestData.createdAt }; // Update threshold percentage if not in tatInfo if (!payload.metadata?.tatInfo) { tatInfo.thresholdPercentage = thresholdPercentage; } // Handle breach notifications (to approver or initiator) if (notificationType === 'breach' || notificationType === 'tat_breach') { // Breach notification to approver if (approverData && approverData.email) { await emailNotificationService.sendTATBreached( requestData, approverData, { timeOverdue: tatInfo.timeOverdue || tatInfo.timeRemaining || 'Exceeded', tatDeadline: tatInfo.tatDeadline, assignedDate: tatInfo.assignedDate } ); } } else if (notificationType === 'tat_breach_initiator') { // Breach notification to initiator if (initiatorData && initiatorData.email) { // For initiator, we can use a simpler notification or the same breach template // For now, skip email to initiator on breach (they get in-app notification) // Or we could create a separate initiator breach email template logger.info(`[Email] Breach notification to initiator - in-app only for now`); } } else { // TAT reminder (threshold1, threshold2, or tat_reminder) if (approverData && approverData.email) { await emailNotificationService.sendTATReminder( requestData, approverData, tatInfo ); } } } break; case 'workflow_resumed': { // Get current level to determine approver const currentLevel = await ApprovalLevel.findOne({ where: { requestId: payload.requestId, status: 'PENDING' }, order: [['levelNumber', 'ASC']] }); // Get approver data from current level let approverData = null; if (currentLevel) { const approverUser = await User.findByPk((currentLevel as any).approverId); if (approverUser) { approverData = approverUser.toJSON(); } else { // Use approver info from level approverData = { userId: (currentLevel as any).approverId, displayName: (currentLevel as any).approverName || 'Unknown Approver', email: (currentLevel as any).approverEmail || 'unknown@royalenfield.com' }; } } const resumedBy = payload.metadata?.resumedBy; const pauseDuration = payload.metadata?.pauseDuration || 'Unknown'; // Convert user to plain object if needed const userData = user.toJSON ? user.toJSON() : user; // Determine if the recipient is the approver or initiator const isApprover = approverData && userData.userId === approverData.userId; const isInitiator = userData.userId === initiatorData.userId; // Ensure user has email if (!userData.email) { logger.warn(`[Email] Cannot send Workflow Resumed email: user email missing`, { userId: userData.userId, displayName: userData.displayName, requestNumber: requestData.requestNumber }); return; } // Send appropriate email based on recipient role if (isApprover) { // Recipient is the approver - send approver email await emailNotificationService.sendWorkflowResumed( requestData, userData, initiatorData, resumedBy, pauseDuration ); } else if (isInitiator) { // Recipient is the initiator - send initiator email await emailNotificationService.sendWorkflowResumedToInitiator( requestData, userData, approverData, resumedBy, pauseDuration ); } else { // Recipient is neither approver nor initiator (spectator) - send initiator-style email await emailNotificationService.sendWorkflowResumedToInitiator( requestData, userData, approverData, resumedBy, pauseDuration ); } } break; case 'closed': { const closureData = { conclusionRemark: payload.metadata?.conclusionRemark, workNotesCount: payload.metadata?.workNotesCount || 0, documentsCount: payload.metadata?.documentsCount || 0 }; await emailNotificationService.sendRequestClosed( requestData, user, closureData ); } break; case 'approver_skipped': { const skippedLevel = await ApprovalLevel.findOne({ where: { requestId: payload.requestId, status: 'SKIPPED' }, order: [['levelEndTime', 'DESC'], ['actionDate', 'DESC']] }); const nextLevel = await ApprovalLevel.findOne({ where: { requestId: payload.requestId, status: 'PENDING' }, order: [['levelNumber', 'ASC']] }); const nextApprover = nextLevel ? await User.findByPk((nextLevel as any).approverId) : null; const skippedBy = payload.metadata?.skippedBy ? await User.findByPk(payload.metadata.skippedBy) : null; const skippedApprover = skippedLevel ? await User.findByPk((skippedLevel as any).approverId) : null; if (skippedApprover) { await emailNotificationService.sendApproverSkipped( requestData, skippedApprover.toJSON(), skippedBy ? skippedBy.toJSON() : { userId: null, displayName: 'System', email: 'system' }, nextApprover ? nextApprover.toJSON() : null, payload.metadata?.skipReason || (skippedLevel as any)?.skipReason || 'Not provided' ); } } break; case 'pause_retrigger_request': { // This is when initiator requests approver to resume a paused workflow // Treat it similar to workflow_paused but with different messaging const pausedBy = payload.metadata?.pausedBy ? await User.findByPk(payload.metadata.pausedBy) : null; const resumeDate = payload.metadata?.resumeDate || new Date(); // Get recipient data (the approver who paused it) let recipientData = user; if (!recipientData || !recipientData.email) { // Try to get from paused level const pausedLevel = await ApprovalLevel.findOne({ where: { requestId: payload.requestId, isPaused: true }, order: [['levelNumber', 'ASC']] }); if (pausedLevel) { const approverUser = await User.findByPk((pausedLevel as any).approverId); if (approverUser) { recipientData = approverUser.toJSON(); } else { recipientData = { userId: (pausedLevel as any).approverId, displayName: (pausedLevel as any).approverName || 'Unknown Approver', email: (pausedLevel as any).approverEmail || 'unknown@royalenfield.com' }; } } } // Ensure email exists before sending if (!recipientData || !recipientData.email) { logger.warn(`[Email] Cannot send Pause Retrigger Request email: recipient email missing`, { recipientData: recipientData ? { userId: recipientData.userId, displayName: recipientData.displayName } : null, requestNumber: requestData.requestNumber }); return; } // Use workflow paused email template but with retrigger context await emailNotificationService.sendWorkflowPaused( requestData, recipientData, pausedBy ? pausedBy.toJSON() : { userId: null, displayName: 'System', email: 'system' }, `Initiator has requested to resume this workflow. Please review and resume if appropriate.`, resumeDate ); } break; case 'workflow_paused': { const pausedBy = payload.metadata?.pausedBy ? await User.findByPk(payload.metadata.pausedBy) : null; const resumeDate = payload.metadata?.resumeDate || new Date(); // Get recipient data - prefer from user, ensure it has email let recipientData = user; if (!recipientData || !recipientData.email) { // If user object doesn't have email, try to get from current level const currentLevel = await ApprovalLevel.findOne({ where: { requestId: payload.requestId, status: 'PENDING' }, order: [['levelNumber', 'ASC']] }); if (currentLevel) { const approverUser = await User.findByPk((currentLevel as any).approverId); if (approverUser) { recipientData = approverUser.toJSON(); } else { // Use approver info from level recipientData = { userId: (currentLevel as any).approverId, displayName: (currentLevel as any).approverName || 'Unknown User', email: (currentLevel as any).approverEmail || 'unknown@royalenfield.com' }; } } else { // If no current level, try to get from initiator const initiatorUser = await User.findByPk(requestData.initiatorId); if (initiatorUser) { recipientData = initiatorUser.toJSON(); } else { logger.warn(`[Email] Cannot send Workflow Paused email: no recipient found for request ${payload.requestId}`); return; } } } // Ensure email exists before sending if (!recipientData.email) { logger.warn(`[Email] Cannot send Workflow Paused email: recipient email missing`, { recipientData: { userId: recipientData.userId, displayName: recipientData.displayName }, requestNumber: requestData.requestNumber }); return; } await emailNotificationService.sendWorkflowPaused( requestData, recipientData, pausedBy ? pausedBy.toJSON() : { userId: null, displayName: 'System', email: 'system' }, payload.metadata?.pauseReason || 'Not provided', resumeDate ); } break; default: logger.info(`[Email] No email configured for notification type: ${notificationType}`); } } } export const notificationService = new NotificationService(); notificationService.configure();