import { prisma } from '@/lib/prisma' import { sendStyledNotificationEmail } from '@/lib/email' interface DigestResult { sent: number errors: number } interface DigestSection { title: string items: string[] } /** * Process and send email digests for all opted-in users. * Called by cron endpoint. */ export async function processDigests( type: 'daily' | 'weekly' ): Promise { let sent = 0 let errors = 0 // Check if digest feature is enabled const enabledSetting = await prisma.systemSettings.findUnique({ where: { key: 'digest_enabled' }, }) if (enabledSetting?.value === 'false') { return { sent: 0, errors: 0 } } // Find users who opted in for this digest frequency const users = await prisma.user.findMany({ where: { digestFrequency: type, status: 'ACTIVE', }, select: { id: true, name: true, email: true, }, }) if (users.length === 0) { return { sent: 0, errors: 0 } } // Load enabled sections from settings const sectionsSetting = await prisma.systemSettings.findUnique({ where: { key: 'digest_sections' }, }) const enabledSections: string[] = sectionsSetting?.value ? JSON.parse(sectionsSetting.value) : ['pending_evaluations', 'upcoming_deadlines', 'new_assignments', 'unread_notifications'] const baseUrl = process.env.NEXTAUTH_URL || 'https://monaco-opc.com' for (const user of users) { try { const content = await getDigestContent(user.id, enabledSections) // Skip if there's nothing to report if (content.sections.length === 0) continue // Build email body from sections const bodyParts: string[] = [] for (const section of content.sections) { bodyParts.push(`**${section.title}**`) for (const item of section.items) { bodyParts.push(`- ${item}`) } bodyParts.push('') } await sendStyledNotificationEmail( user.email, user.name || '', 'DIGEST', { name: user.name || undefined, title: `Your ${type === 'daily' ? 'Daily' : 'Weekly'} Digest`, message: bodyParts.join('\n'), linkUrl: `${baseUrl}/dashboard`, metadata: { digestType: type, pendingEvaluations: content.pendingEvaluations, upcomingDeadlines: content.upcomingDeadlines, newAssignments: content.newAssignments, unreadNotifications: content.unreadNotifications, }, } ) // Log the digest await prisma.digestLog.create({ data: { userId: user.id, digestType: type, contentJson: { pendingEvaluations: content.pendingEvaluations, upcomingDeadlines: content.upcomingDeadlines, newAssignments: content.newAssignments, unreadNotifications: content.unreadNotifications, }, }, }) sent++ } catch (error) { console.error( `[Digest] Failed to send ${type} digest to ${user.email}:`, error ) errors++ } } return { sent, errors } } /** * Compile digest content for a single user. */ async function getDigestContent( userId: string, enabledSections: string[] ): Promise<{ sections: DigestSection[] pendingEvaluations: number upcomingDeadlines: number newAssignments: number unreadNotifications: number }> { const now = new Date() const sections: DigestSection[] = [] let pendingEvaluations = 0 let upcomingDeadlines = 0 let newAssignments = 0 let unreadNotifications = 0 // 1. Pending evaluations if (enabledSections.includes('pending_evaluations')) { const pendingAssignments = await prisma.assignment.findMany({ where: { userId, isCompleted: false, round: { status: 'ROUND_ACTIVE', windowCloseAt: { gt: now }, }, }, include: { project: { select: { id: true, title: true } }, round: { select: { name: true, windowCloseAt: true } }, }, }) pendingEvaluations = pendingAssignments.length if (pendingAssignments.length > 0) { sections.push({ title: `Pending Evaluations (${pendingAssignments.length})`, items: pendingAssignments.map( (a) => `${a.project.title} - ${a.round?.name ?? 'Unknown'}${ a.round?.windowCloseAt ? ` (due ${a.round.windowCloseAt.toLocaleDateString('en-US', { month: 'short', day: 'numeric', })})` : '' }` ), }) } } // 2. Upcoming deadlines (rounds closing within 7 days) if (enabledSections.includes('upcoming_deadlines')) { const sevenDaysFromNow = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000) const upcomingRounds = await prisma.round.findMany({ where: { status: 'ROUND_ACTIVE', windowCloseAt: { gt: now, lte: sevenDaysFromNow, }, assignments: { some: { userId, isCompleted: false, }, }, }, select: { name: true, windowCloseAt: true, }, }) upcomingDeadlines = upcomingRounds.length if (upcomingRounds.length > 0) { sections.push({ title: 'Upcoming Deadlines', items: upcomingRounds.map( (s) => `${s.name} - ${s.windowCloseAt?.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', })}` ), }) } } // 3. New assignments since last digest if (enabledSections.includes('new_assignments')) { const lastDigest = await prisma.digestLog.findFirst({ where: { userId }, orderBy: { sentAt: 'desc' }, select: { sentAt: true }, }) const sinceDate = lastDigest?.sentAt || new Date(now.getTime() - 24 * 60 * 60 * 1000) const recentAssignments = await prisma.assignment.findMany({ where: { userId, createdAt: { gt: sinceDate }, }, include: { project: { select: { id: true, title: true } }, round: { select: { name: true } }, }, }) newAssignments = recentAssignments.length if (recentAssignments.length > 0) { sections.push({ title: `New Assignments (${recentAssignments.length})`, items: recentAssignments.map( (a) => `${a.project.title} - ${a.round?.name ?? 'Unknown'}` ), }) } } // 4. Unread notifications count if (enabledSections.includes('unread_notifications')) { const unreadCount = await prisma.inAppNotification.count({ where: { userId, isRead: false, }, }) unreadNotifications = unreadCount if (unreadCount > 0) { sections.push({ title: 'Notifications', items: [`You have ${unreadCount} unread notification${unreadCount !== 1 ? 's' : ''}`], }) } } return { sections, pendingEvaluations, upcomingDeadlines, newAssignments, unreadNotifications, } }