import 'server-only' import { Resend } from 'resend' import { asAdmin } from '@/lib/db/with-user' import { capaActions, incidents, users, notificationsLog } from '@/lib/db/schema' import { and, eq, inArray, not } from 'drizzle-orm' import { sendWhatsAppMessage } from '@/lib/notifications/whatsapp' import { createInAppNotifications } from '@/lib/notifications/in-app' import { getApiKey } from '@/lib/settings' export type EscalationThreshold = 'warning_3d' | 'due_today' | 'overdue_3d' | 'overdue_7d' export function getEscalationThreshold(dueDateIso: string): EscalationThreshold | null { const today = new Date() today.setUTCHours(0, 0, 0, 0) const due = new Date(dueDateIso) // date-only ISO strings are parsed as UTC midnight — keep due in UTC too const diffDays = Math.round((due.getTime() - today.getTime()) / 86_400_000) if (diffDays === 3) return 'warning_3d' if (diffDays === 0) return 'due_today' if (diffDays === -3) return 'overdue_3d' if (diffDays === -7) return 'overdue_7d' return null } const THRESHOLD_SUBJECT: Record = { warning_3d: '[IMS] CAPA action due in 3 days', due_today: '[IMS] CAPA action due TODAY', overdue_3d: '[IMS] CAPA action 3 days OVERDUE', overdue_7d: '[IMS] URGENT: CAPA action 7 days OVERDUE', } export async function escalateOverdueCapa(): Promise<{ notified: number }> { const capas = await asAdmin(db => db.select({ id: capaActions.id, description: capaActions.description, dueDate: capaActions.dueDate, incidentId: capaActions.incidentId, ownerUserId: capaActions.ownerUserId, incidentRef: incidents.referenceNo, siteId: incidents.siteId, ownerEmail: users.email, ownerName: users.name, ownerPhone: users.phone, }) .from(capaActions) .leftJoin(incidents, eq(capaActions.incidentId, incidents.id)) .leftJoin(users, eq(capaActions.ownerUserId, users.id)) .where(not(inArray(capaActions.status, ['verified', 'closed']))) ) if (!capas || capas.length === 0) return { notified: 0 } const resend = new Resend(process.env.RESEND_API_KEY) const from = process.env.RESEND_FROM_EMAIL ?? 'onboarding@resend.dev' const siteUrl = process.env.NEXT_PUBLIC_SITE_URL ?? 'http://localhost:3000' let notified = 0 const whatsappPhoneNumberId = await getApiKey('META_WHATSAPP_PHONE_NUMBER_ID') const whatsappAccessToken = await getApiKey('META_WHATSAPP_ACCESS_TOKEN') for (const capa of capas) { const threshold = getEscalationThreshold(capa.dueDate) if (!threshold) continue const alreadySent = await asAdmin(db => db.select({ id: notificationsLog.id }) .from(notificationsLog) .where(and( eq(notificationsLog.capaId, capa.id), eq(notificationsLog.channel, 'email'), eq(notificationsLog.status, threshold), )) .limit(1) ) if (alreadySent.length > 0) continue const ownerEmail = capa.ownerEmail const ownerName = capa.ownerName ?? 'Owner' if (!ownerEmail) continue const incidentRef = capa.incidentRef ?? capa.incidentId const capaUrl = `${siteUrl}/ims/hse/capa/${capa.id}` const subject = THRESHOLD_SUBJECT[threshold] const html = `

Hi ${ownerName},

CAPA action for incident ${incidentRef} requires attention.

Action: ${capa.description}

Due date: ${capa.dueDate}

View CAPA

` const text = `CAPA ${incidentRef}: ${capa.description}\nDue: ${capa.dueDate}\n${capaUrl}` const to = [ownerEmail] if (capa.siteId && ['due_today', 'overdue_3d', 'overdue_7d'].includes(threshold)) { const hseUsers = await asAdmin(db => db.select({ email: users.email }) .from(users) .where(and( eq(users.siteId, capa.siteId!), inArray(users.role, ['hse', 'supervisor', 'management']), )) ) to.push(...hseUsers.map(u => u.email).filter(Boolean) as string[]) } const { error } = await resend.emails.send({ from, to: [...new Set(to)], subject, html, text }) if (error) { console.error('Escalation email error:', error) continue } // WhatsApp for urgent thresholds only (owner must have a phone number) if (['overdue_3d', 'overdue_7d'].includes(threshold)) { const ownerPhone = capa.ownerPhone ?? '' if (ownerPhone) { try { await sendWhatsAppMessage( ownerPhone, 'ims_capa_overdue', [incidentRef ?? '', capa.description, capa.dueDate], whatsappPhoneNumberId, whatsappAccessToken, ) } catch (waErr) { console.error('WhatsApp escalation error:', waErr) } } } await asAdmin(db => db.insert(notificationsLog).values({ capaId: capa.id, channel: 'email', recipient: [...new Set(to)].join(','), status: threshold, }) ) if (capa.ownerUserId) { await createInAppNotifications([{ userId: capa.ownerUserId, title: `${THRESHOLD_SUBJECT[threshold].replace('[IMS] ', '')} — ${capa.incidentRef ?? capa.incidentId}`, link: `/hse/capa/${capa.id}`, incidentId: capa.incidentId, capaId: capa.id, }]) } notified++ } return { notified } }