/** * Unified Notification Service * * Handles sending notifications via multiple channels: * - Email (via nodemailer) * - WhatsApp (via Meta or Twilio) */ import { prisma } from '@/lib/prisma' import { sendMagicLinkEmail, sendJuryInvitationEmail, sendEvaluationReminderEmail, sendAnnouncementEmail, } from '@/lib/email' import { getWhatsAppProvider, getWhatsAppProviderType } from '@/lib/whatsapp' import type { NotificationChannel } from '@prisma/client' export type NotificationType = | 'MAGIC_LINK' | 'JURY_INVITATION' | 'EVALUATION_REMINDER' | 'ANNOUNCEMENT' interface NotificationResult { success: boolean channels: { email?: { success: boolean; error?: string } whatsapp?: { success: boolean; messageId?: string; error?: string } } } /** * Send a notification to a user based on their preferences */ export async function sendNotification( userId: string, type: NotificationType, data: Record ): Promise { // Get user with notification preferences const user = await prisma.user.findUnique({ where: { id: userId }, select: { id: true, email: true, name: true, phoneNumber: true, notificationPreference: true, whatsappOptIn: true, }, }) if (!user) { return { success: false, channels: {}, } } const result: NotificationResult = { success: true, channels: {}, } const preference = user.notificationPreference // Determine which channels to use const sendEmail = preference === 'EMAIL' || preference === 'BOTH' const sendWhatsApp = (preference === 'WHATSAPP' || preference === 'BOTH') && user.whatsappOptIn && user.phoneNumber // Send via email if (sendEmail) { const emailResult = await sendEmailNotification(user.email, user.name, type, data) result.channels.email = emailResult // Log the notification await logNotification(user.id, 'EMAIL', 'SMTP', type, emailResult) } // Send via WhatsApp if (sendWhatsApp && user.phoneNumber) { const whatsappResult = await sendWhatsAppNotification( user.phoneNumber, user.name, type, data ) result.channels.whatsapp = whatsappResult // Log the notification const providerType = await getWhatsAppProviderType() await logNotification( user.id, 'WHATSAPP', providerType || 'UNKNOWN', type, whatsappResult ) } // Overall success if at least one channel succeeded result.success = (result.channels.email?.success ?? true) || (result.channels.whatsapp?.success ?? true) return result } /** * Send email notification */ async function sendEmailNotification( email: string, name: string | null, type: NotificationType, data: Record ): Promise<{ success: boolean; error?: string }> { try { switch (type) { case 'MAGIC_LINK': await sendMagicLinkEmail(email, data.url) return { success: true } case 'JURY_INVITATION': await sendJuryInvitationEmail( email, data.inviteUrl, data.programName, data.roundName ) return { success: true } case 'EVALUATION_REMINDER': await sendEvaluationReminderEmail( email, name, parseInt(data.pendingCount || '0'), data.roundName || 'Current Round', data.deadline || 'Soon', data.assignmentsUrl || `${process.env.NEXTAUTH_URL}/jury/assignments` ) return { success: true } case 'ANNOUNCEMENT': await sendAnnouncementEmail( email, name, data.title || 'Announcement', data.message || '', data.ctaText, data.ctaUrl ) return { success: true } default: return { success: false, error: `Unknown notification type: ${type}` } } } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Email send failed', } } } /** * Send WhatsApp notification */ async function sendWhatsAppNotification( phoneNumber: string, name: string | null, type: NotificationType, data: Record ): Promise<{ success: boolean; messageId?: string; error?: string }> { const provider = await getWhatsAppProvider() if (!provider) { return { success: false, error: 'WhatsApp not configured' } } try { // Map notification types to templates const templateMap: Record = { MAGIC_LINK: 'mopc_magic_link', JURY_INVITATION: 'mopc_jury_invitation', EVALUATION_REMINDER: 'mopc_evaluation_reminder', ANNOUNCEMENT: 'mopc_announcement', } const template = templateMap[type] // Build template params const params: Record = { name: name || 'User', ...data, } const result = await provider.sendTemplate(phoneNumber, template, params) return result } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'WhatsApp send failed', } } } /** * Log notification to database */ async function logNotification( userId: string, channel: NotificationChannel, provider: string, type: NotificationType, result: { success: boolean; messageId?: string; error?: string } ): Promise { try { await prisma.notificationLog.create({ data: { userId, channel, provider, type, status: result.success ? 'SENT' : 'FAILED', externalId: result.messageId, errorMsg: result.error, }, }) } catch (error) { console.error('Failed to log notification:', error) } } /** * Send bulk notifications to multiple users */ export async function sendBulkNotification( userIds: string[], type: NotificationType, data: Record ): Promise<{ sent: number; failed: number }> { let sent = 0 let failed = 0 for (const userId of userIds) { const result = await sendNotification(userId, type, data) if (result.success) { sent++ } else { failed++ } } return { sent, failed } } /** * Get notification statistics */ export async function getNotificationStats(options?: { userId?: string startDate?: Date endDate?: Date }): Promise<{ total: number byChannel: Record byStatus: Record byType: Record }> { const where: Record = {} if (options?.userId) { where.userId = options.userId } if (options?.startDate || options?.endDate) { where.createdAt = {} if (options.startDate) { (where.createdAt as Record).gte = options.startDate } if (options.endDate) { (where.createdAt as Record).lte = options.endDate } } const [total, byChannel, byStatus, byType] = await Promise.all([ prisma.notificationLog.count({ where }), prisma.notificationLog.groupBy({ by: ['channel'], where, _count: true, }), prisma.notificationLog.groupBy({ by: ['status'], where, _count: true, }), prisma.notificationLog.groupBy({ by: ['type'], where, _count: true, }), ]) return { total, byChannel: Object.fromEntries( byChannel.map((r) => [r.channel, r._count]) ), byStatus: Object.fromEntries( byStatus.map((r) => [r.status, r._count]) ), byType: Object.fromEntries( byType.map((r) => [r.type, r._count]) ), } }