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>
This commit is contained in:
2026-07-23 16:37:51 +08:00
co-authored by Claude Sonnet 4.6
parent d18d29168a
commit 25f923f530
19 changed files with 296 additions and 300 deletions
+47 -36
View File
@@ -1,5 +1,8 @@
import 'server-only'
import { Resend } from 'resend'
import type { SupabaseClient } from '@supabase/supabase-js'
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'
@@ -27,22 +30,31 @@ export function shouldSendRecheck(
return recheckDate <= today
}
export async function sendEffectivenessRecheckNotifications(
supabase: SupabaseClient,
): Promise<{ notified: number }> {
export async function sendEffectivenessRecheckNotifications(): Promise<{ notified: number }> {
const today = new Date().toISOString().split('T')[0]
const { data: capas } = await supabase
.from('capa_actions')
.select(`
id, description, effectiveness_recheck_date, effectiveness_recheck_round,
verified_at, incident_id,
incidents (reference_no),
verifier:users!verified_by (email, name, phone)
`)
.in('status', ['verified', 'closed'])
.not('effectiveness_recheck_date', 'is', null)
.lt('effectiveness_recheck_round', 3)
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 }
@@ -53,8 +65,8 @@ export async function sendEffectivenessRecheckNotifications(
let whatsappPhoneId: string | null = null
let whatsappToken: string | null = null
try {
whatsappPhoneId = await getApiKey(supabase, 'META_WHATSAPP_PHONE_NUMBER_ID')
whatsappToken = await getApiKey(supabase, 'META_WHATSAPP_ACCESS_TOKEN')
whatsappPhoneId = await getApiKey('META_WHATSAPP_PHONE_NUMBER_ID')
whatsappToken = await getApiKey('META_WHATSAPP_ACCESS_TOKEN')
} catch {
// WhatsApp not configured — email only
}
@@ -62,21 +74,20 @@ export async function sendEffectivenessRecheckNotifications(
let notified = 0
for (const capa of capas) {
const recheckDate = (capa as { effectiveness_recheck_date: string | null }).effectiveness_recheck_date
const round = (capa as { effectiveness_recheck_round: number }).effectiveness_recheck_round
const recheckDate = capa.effectivenessRecheckDate
const round = capa.effectivenessRecheckRound
if (!shouldSendRecheck(recheckDate, round, today)) continue
const verifierEmail = (capa.verifier as unknown as { email: string } | null)?.email
const verifierName = (capa.verifier as unknown as { name: string } | null)?.name ?? 'HSE Officer'
const verifierPhone = (capa.verifier as unknown as { phone: string | null } | null)?.phone ?? ''
const verifierEmail = capa.verifierEmail
const verifierName = capa.verifierName ?? 'HSE Officer'
const verifierPhone = capa.verifierPhone ?? ''
if (!verifierEmail) continue
const incidentRef = (capa.incidents as unknown as { reference_no: string | null } | null)?.reference_no
?? (capa as { incident_id: string }).incident_id
const incidentRef = capa.incidentRef ?? capa.incidentId
const roundLabel = getRoundLabel(round)
const capaDesc = (capa as { description: string }).description
const capaUrl = `${siteUrl}/ims/hse/capa/${(capa as { id: string }).id}`
const capaDesc = capa.description
const capaUrl = `${siteUrl}/ims/hse/capa/${capa.id}`
const subject = `[IMS] ${roundLabel} effectiveness check — ${incidentRef}`
const html = `
@@ -99,22 +110,22 @@ export async function sendEffectivenessRecheckNotifications(
sendWhatsAppMessage(
verifierPhone,
'ims_effectiveness_recheck',
[roundLabel, incidentRef, capaDesc],
[roundLabel, incidentRef ?? '', capaDesc],
whatsappPhoneId,
whatsappToken,
).catch(err => console.error('WhatsApp recheck error:', err))
}
// Advance round
const verifiedAt = (capa as { verified_at: string }).verified_at
const nextDate = getNextRecheckDate(verifiedAt, round)
await supabase
.from('capa_actions')
.update({
effectiveness_recheck_round: round + 1,
effectiveness_recheck_date: nextDate ?? null,
})
.eq('id', (capa as { id: string }).id)
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++
}