Files
ims/lib/notifications/effectiveness-recheck.ts
T
adminandClaude Sonnet 4.6 25f923f530 feat(db): phase 4 group 1 — lib/ settings + notifications to Drizzle
Convert lib/settings.ts, lib/notifications/{in-app,email,capa-escalation,
effectiveness-recheck}.ts from Supabase PostgREST to Drizzle asAdmin queries.
Drop supabase arg from all call sites in app/api/ and cron routes. Rewrite
notification unit tests to mock @/lib/db/with-user instead of SupabaseClient.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-23 16:37:51 +08:00

135 lines
4.7 KiB
TypeScript

import 'server-only'
import { Resend } from 'resend'
import { asAdmin } from '@/lib/db/with-user'
import { capaActions, incidents, users } from '@/lib/db/schema'
import { and, eq, inArray, isNotNull, lt } from 'drizzle-orm'
import { sendWhatsAppMessage } from '@/lib/notifications/whatsapp'
import { getApiKey } from '@/lib/settings'
const ROUND_LABELS = ['30-day', '60-day', '90-day'] as const
const NEXT_INTERVAL_DAYS: (number | null)[] = [60, 90, null]
export function getRoundLabel(round: number): string {
return ROUND_LABELS[round] ?? '90-day'
}
export function getNextRecheckDate(verifiedAt: string, currentRound: number): string | null {
const days = NEXT_INTERVAL_DAYS[currentRound]
if (days === null || days === undefined) return null
const base = new Date(verifiedAt)
base.setUTCDate(base.getUTCDate() + days)
return base.toISOString().split('T')[0]
}
export function shouldSendRecheck(
recheckDate: string | null,
round: number,
today: string = new Date().toISOString().split('T')[0],
): boolean {
if (!recheckDate || round >= 3) return false
return recheckDate <= today
}
export async function sendEffectivenessRecheckNotifications(): Promise<{ notified: number }> {
const today = new Date().toISOString().split('T')[0]
const capas = await asAdmin(db =>
db.select({
id: capaActions.id,
description: capaActions.description,
effectivenessRecheckDate: capaActions.effectivenessRecheckDate,
effectivenessRecheckRound: capaActions.effectivenessRecheckRound,
verifiedAt: capaActions.verifiedAt,
incidentId: capaActions.incidentId,
incidentRef: incidents.referenceNo,
verifierEmail: users.email,
verifierName: users.name,
verifierPhone: users.phone,
})
.from(capaActions)
.leftJoin(incidents, eq(capaActions.incidentId, incidents.id))
.leftJoin(users, eq(capaActions.verifiedBy, users.id))
.where(and(
inArray(capaActions.status, ['verified', 'closed']),
isNotNull(capaActions.effectivenessRecheckDate),
lt(capaActions.effectivenessRecheckRound, 3),
))
)
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 whatsappPhoneId: string | null = null
let whatsappToken: string | null = null
try {
whatsappPhoneId = await getApiKey('META_WHATSAPP_PHONE_NUMBER_ID')
whatsappToken = await getApiKey('META_WHATSAPP_ACCESS_TOKEN')
} catch {
// WhatsApp not configured — email only
}
let notified = 0
for (const capa of capas) {
const recheckDate = capa.effectivenessRecheckDate
const round = capa.effectivenessRecheckRound
if (!shouldSendRecheck(recheckDate, round, today)) continue
const verifierEmail = capa.verifierEmail
const verifierName = capa.verifierName ?? 'HSE Officer'
const verifierPhone = capa.verifierPhone ?? ''
if (!verifierEmail) continue
const incidentRef = capa.incidentRef ?? capa.incidentId
const roundLabel = getRoundLabel(round)
const capaDesc = capa.description
const capaUrl = `${siteUrl}/ims/hse/capa/${capa.id}`
const subject = `[IMS] ${roundLabel} effectiveness check — ${incidentRef}`
const html = `
<p>Hi ${verifierName},</p>
<p>This is the <strong>${roundLabel} effectiveness check</strong> for a corrective action you verified on incident <strong>${incidentRef}</strong>.</p>
<p><strong>Action:</strong> ${capaDesc}</p>
<p>Please confirm the corrective action is still in place and effective.</p>
<p><a href="${capaUrl}">View CAPA</a></p>
`
const text = `${roundLabel} effectiveness check for ${incidentRef}\nAction: ${capaDesc}\n${capaUrl}`
const { error } = await resend.emails.send({ from, to: [verifierEmail], subject, html, text })
if (error) {
console.error('Effectiveness recheck email error:', error)
continue
}
// WhatsApp (non-blocking, best-effort)
if (whatsappPhoneId && whatsappToken && verifierPhone) {
sendWhatsAppMessage(
verifierPhone,
'ims_effectiveness_recheck',
[roundLabel, incidentRef ?? '', capaDesc],
whatsappPhoneId,
whatsappToken,
).catch(err => console.error('WhatsApp recheck error:', err))
}
// Advance round
const nextDate = getNextRecheckDate(capa.verifiedAt!.toISOString(), round)
await asAdmin(db =>
db.update(capaActions)
.set({
effectivenessRecheckRound: round + 1,
effectivenessRecheckDate: nextDate ?? null,
})
.where(eq(capaActions.id, capa.id))
)
notified++
}
return { notified }
}