Dealer_Onboarding_Backend/src/services/NotificationService.ts

223 lines
8.8 KiB
TypeScript

import { sendEmail } from '../common/utils/email.service.js';
import { getProspectPortalUrl } from '../common/utils/frontendUrl.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 }
) {
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 ?? getProspectPortalUrl(),
ctaLabel: 'Complete Questionnaire'
}
});
}
/**
* 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 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 || getProspectPortalUrl(),
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 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 || getProspectPortalUrl(),
ctaLabel: 'Acknowledge LOI',
phone: phone || ''
}
});
}
}