feat: WhatsApp notifications — new incident alert and CAPA overdue escalation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFDuBhMKvoWjrWT3ZnGmmr
This commit is contained in:
2026-07-11 18:50:25 +08:00
co-authored by Claude Sonnet 4.6
parent 121433ddf8
commit a47f3aabc9
6 changed files with 282 additions and 4 deletions
+23 -1
View File
@@ -1,5 +1,7 @@
import { Resend } from 'resend'
import type { SupabaseClient } from '@supabase/supabase-js'
import { sendWhatsAppMessage } from '@/lib/notifications/whatsapp'
import { getApiKey } from '@/lib/settings'
export type EscalationThreshold = 'warning_3d' | 'due_today' | 'overdue_3d' | 'overdue_7d'
@@ -32,7 +34,7 @@ export async function escalateOverdueCapa(
.select(`
id, description, due_date, incident_id,
incidents (reference_no, site_id),
owner:users!owner_user_id (email, name)
owner:users!owner_user_id (email, name, phone)
`)
.not('status', 'in', '(verified,closed)')
@@ -91,6 +93,26 @@ export async function escalateOverdueCapa(
continue
}
// WhatsApp for urgent thresholds only (owner must have a phone number)
if (['overdue_3d', 'overdue_7d'].includes(threshold)) {
const ownerPhone = (capa.owner as unknown as { phone: string | null } | null)?.phone ?? ''
if (ownerPhone) {
try {
const phoneNumberId = await getApiKey(supabase, 'META_WHATSAPP_PHONE_NUMBER_ID')
const accessToken = await getApiKey(supabase, 'META_WHATSAPP_ACCESS_TOKEN')
await sendWhatsAppMessage(
ownerPhone,
'ims_capa_overdue',
[incidentRef, (capa as { description: string }).description, (capa as { due_date: string }).due_date],
phoneNumberId,
accessToken,
)
} catch (waErr) {
console.error('WhatsApp escalation error:', waErr)
}
}
}
await supabase.from('notifications_log').insert({
capa_id: capa.id,
channel: 'email',
+42
View File
@@ -0,0 +1,42 @@
export async function sendWhatsAppMessage(
phoneNumber: string,
templateName: string,
parameters: string[],
phoneNumberId: string,
accessToken: string,
): Promise<void> {
const sanitized = phoneNumber.replace(/[^0-9]/g, '')
if (!sanitized) return
const body = {
messaging_product: 'whatsapp',
to: sanitized,
type: 'template',
template: {
name: templateName,
language: { code: 'en_US' },
components: [
{
type: 'body',
parameters: parameters.map(text => ({ type: 'text', text })),
},
],
},
}
const res = await fetch(
`https://graph.facebook.com/v19.0/${phoneNumberId}/messages`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`,
},
body: JSON.stringify(body),
}
)
if (!res.ok) {
throw new Error(`WhatsApp API error: ${res.status}`)
}
}