223 lines
8.8 KiB
TypeScript
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 || ''
|
|
}
|
|
});
|
|
}
|
|
}
|