import { sendEmail } from '../common/utils/email.service.js'; import db from '../database/models/index.js'; const { Notification, PushSubscription } = db; /** * Global Notification Service to handle Email, WhatsApp, and Push Notifications. * This satisfies the SRS v2.0 requirement for centralized communication logic. */ export class NotificationService { /** * Sends a unified notification across multiple channels */ static async notify(userId: string | null, email: string | null, data: { title: string, message: string, channels: ('email' | 'whatsapp' | 'push' | 'system')[], templateCode?: string, placeholders?: any, metadata?: any }) { const { title, message, channels, templateCode, placeholders, metadata } = data; // 1. System Notification (In-app) - Always synchronous for immediate feedback if (channels.includes('system') && userId) { try { const notification = await Notification.create({ userId, title, message, type: metadata?.type || 'info', link: placeholders?.link || metadata?.link || null, isRead: false }); // Emit realtime update via Socket.io const { getIO } = await import('../common/utils/socket.js'); const io = getIO(); if (io) { const roomName = `user_${userId}`; io.to(roomName).emit('notification', { id: notification.id, title, message, type: notification.type, link: notification.link, createdAt: notification.createdAt }); } } catch (err) { console.error('[NotificationService] Failed to create system notification:', err); } } // 2. Offload other channels to Job Queue (BullMQ) or Send Synchronously if Redis is disabled const asyncChannels = channels.filter(c => c !== 'system'); if (asyncChannels.length > 0) { if (process.env.ENABLE_REDIS === 'true') { const { addNotificationJob } = await import('../common/queues/notification.queue.js'); await addNotificationJob({ userId, email, title, message, channels: asyncChannels, templateCode, placeholders, metadata }); } else { console.log(`[Notification Service] Redis disabled. Sending ${asyncChannels.join(', ')} synchronously...`); // Fallback: Process immediately if queueing is disabled await this.processJob({ userId, email, title, message, channels: asyncChannels, templateCode, placeholders, metadata }); } } } /** * Processor for BullMQ jobs */ static async processJob(jobData: any) { const { userId, email, title, message, channels, templateCode, placeholders, metadata } = jobData; for (const channel of channels) { try { if (channel === 'email' && email) { await sendEmail(email, title, templateCode || 'GENERIC_NOTIFICATION', { ...placeholders, title, message }); } else if (channel === 'whatsapp') { const phoneNumber = placeholders?.phone || placeholders?.mobileNumber || 'Unknown'; await this.sendWhatsApp(phoneNumber, templateCode || 'generic_msg', placeholders); } else if (channel === 'push' && userId) { await this.sendPush(userId, title, message, metadata); } } catch (error) { console.error(`[Notification Service] Failed to process ${channel} for ${userId || email}:`, error); throw error; // Re-throw to trigger BullMQ retry } } } /** * Mock WhatsApp integration as requested in SRS */ static async sendWhatsApp(to: string, templateCode: string, placeholders: any) { // Log to console for audit during dev/mock phase console.log(`[WhatsApp Service] Triggered for ${to} using template ${templateCode}`); console.log(`[WhatsApp Service] Payload:`, placeholders); // In reality, this would call Meta's WhatsApp Business API or Twilio // return await WhatsAppAPI.send(to, templateCode, placeholders); return true; } /** * Web Push Notification logic */ private static async sendPush(userId: string, title: string, message: string, metadata: any) { try { const subscriptions = await PushSubscription.findAll({ where: { userId } }); if (subscriptions.length === 0) return; console.log(`[Push Service] Sending ${subscriptions.length} push notifications to User: ${userId}`); // Integration with web-push library would go here } catch (error) { console.error('[Push Service] Error:', error); } } /** * Specific Trigger: Questionnaire Reminder */ static async sendQuestionnaireReminder( email: string, phone: string, applicantName: string, options?: { location?: string; link?: string } ) { const base = process.env.FRONTEND_URL || 'http://localhost:5173'; await this.notify(null, email, { title: 'Action Required: Complete your Dealership Questionnaire', message: `Hi ${applicantName}, please complete the questionnaire to proceed with your application.`, channels: ['email', 'whatsapp'], templateCode: 'QUESTIONNAIRE_REMINDER', placeholders: { applicantName, phone, location: options?.location ?? '', link: options?.link ?? `${base}/login`, ctaLabel: 'Complete Now' } }); } /** * Reminder for pending onboarding document uploads (bulk or scheduled send). */ static async sendDocumentSubmissionReminder( email: string, phone: string | null | undefined, applicantName: string, options: { applicationId: string; pendingDocuments?: string; dueDate?: string; link?: string } ) { const base = process.env.FRONTEND_URL || 'http://localhost:5173'; const channels: ('email' | 'whatsapp')[] = ['email']; if (phone) channels.push('whatsapp'); await this.notify(null, email, { title: `Reminder: Pending documents — ${options.applicationId}`, message: `Hi ${applicantName}, please upload pending documents for ${options.applicationId}.`, channels, templateCode: 'DOCUMENT_SUBMISSION_REMINDER', placeholders: { applicantName, applicationId: options.applicationId, pendingDocuments: options.pendingDocuments || 'Please sign in to the Dealer Portal to view your personalised document checklist.', dueDate: options.dueDate || 'within seven calendar days', link: options.link || `${base}/applications`, ctaLabel: 'Upload documents', phone: phone || '' } }); } /** * Chase applicant to acknowledge issued LOI (bulk send from DD-Admin). */ static async sendLoiAcknowledgementReminder( email: string, phone: string | null | undefined, applicantName: string, options: { applicationId: string; link?: string; dueDate?: string } ) { const base = process.env.FRONTEND_URL || 'http://localhost:5173'; const channels: ('email' | 'whatsapp')[] = ['email']; if (phone) channels.push('whatsapp'); await this.notify(null, email, { title: `Acknowledge your LOI — ${options.applicationId}`, message: `Hi ${applicantName}, your Letter of Intent is awaiting acknowledgement on the portal.`, channels, templateCode: 'LOI_ACKNOWLEDGEMENT_REQUEST', placeholders: { applicantName, applicationId: options.applicationId, dueDate: options.dueDate || 'within seven calendar days', link: options.link || `${base}/prospect-login`, ctaLabel: 'Acknowledge LOI', phone: phone || '' } }); } }