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>
120 lines
3.9 KiB
TypeScript
120 lines
3.9 KiB
TypeScript
// @vitest-environment node
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
import { getEscalationThreshold, escalateOverdueCapa } from '@/lib/notifications/capa-escalation'
|
|
|
|
vi.mock('resend', () => {
|
|
const sendMock = vi.fn().mockResolvedValue({ data: { id: 'email-id' }, error: null })
|
|
function ResendMock() {
|
|
return { emails: { send: sendMock } }
|
|
}
|
|
return { Resend: ResendMock }
|
|
})
|
|
|
|
vi.mock('@/lib/notifications/whatsapp', () => ({
|
|
sendWhatsAppMessage: vi.fn().mockResolvedValue(undefined),
|
|
}))
|
|
|
|
vi.mock('@/lib/settings', () => ({
|
|
getApiKey: vi.fn().mockResolvedValue('test-cred'),
|
|
}))
|
|
|
|
vi.mock('@/lib/notifications/in-app', () => ({
|
|
createInAppNotifications: vi.fn().mockResolvedValue({ created: 1 }),
|
|
}))
|
|
|
|
// At top of file, before other vi.mock calls
|
|
let mockAsAdminImpl: (fn: (db: unknown) => unknown) => unknown
|
|
vi.mock('@/lib/db/with-user', () => ({
|
|
asAdmin: vi.fn().mockImplementation((fn: (db: unknown) => unknown) => mockAsAdminImpl(fn)),
|
|
}))
|
|
|
|
describe('getEscalationThreshold', () => {
|
|
function daysFromNow(n: number): string {
|
|
const d = new Date()
|
|
d.setDate(d.getDate() + n)
|
|
return d.toISOString().split('T')[0]
|
|
}
|
|
|
|
it('returns warning_3d when due in 3 days', () => {
|
|
expect(getEscalationThreshold(daysFromNow(3))).toBe('warning_3d')
|
|
})
|
|
|
|
it('returns due_today when due today', () => {
|
|
expect(getEscalationThreshold(daysFromNow(0))).toBe('due_today')
|
|
})
|
|
|
|
it('returns overdue_3d when 3 days past due', () => {
|
|
expect(getEscalationThreshold(daysFromNow(-3))).toBe('overdue_3d')
|
|
})
|
|
|
|
it('returns overdue_7d when 7 days past due', () => {
|
|
expect(getEscalationThreshold(daysFromNow(-7))).toBe('overdue_7d')
|
|
})
|
|
|
|
it('returns null for 2 days before due (no threshold)', () => {
|
|
expect(getEscalationThreshold(daysFromNow(2))).toBeNull()
|
|
})
|
|
|
|
it('returns null for 4 days before due', () => {
|
|
expect(getEscalationThreshold(daysFromNow(4))).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('escalateOverdueCapa', () => {
|
|
function daysFromNow(n: number): string {
|
|
const d = new Date()
|
|
d.setDate(d.getDate() + n)
|
|
return d.toISOString().split('T')[0]
|
|
}
|
|
|
|
beforeEach(() => { vi.clearAllMocks() })
|
|
|
|
it('returns notified:0 when no active capas', async () => {
|
|
let call = 0
|
|
mockAsAdminImpl = (fn) => {
|
|
call++
|
|
if (call === 1) return fn({ select: () => ({ from: () => ({ leftJoin: () => ({ leftJoin: () => ({ where: () => [] }) }) }) }) })
|
|
return fn({})
|
|
}
|
|
const { notified } = await escalateOverdueCapa()
|
|
expect(notified).toBe(0)
|
|
})
|
|
|
|
it('skips capa with no threshold match', async () => {
|
|
let call = 0
|
|
mockAsAdminImpl = (fn) => {
|
|
call++
|
|
if (call === 1) {
|
|
// Return capa due in 2 days (no threshold)
|
|
return Promise.resolve([{
|
|
id: 'c1', description: 'Fix it', dueDate: daysFromNow(2),
|
|
incidentId: 'i1', ownerUserId: 'u1',
|
|
incidentRef: 'SITE-202507-0001', siteId: 's1',
|
|
ownerEmail: 'owner@example.com', ownerName: 'Owner', ownerPhone: null,
|
|
}])
|
|
}
|
|
return Promise.resolve([])
|
|
}
|
|
const { notified } = await escalateOverdueCapa()
|
|
expect(notified).toBe(0)
|
|
})
|
|
|
|
it('sends email and WhatsApp for overdue_3d capa with phone', async () => {
|
|
const { sendWhatsAppMessage } = await import('@/lib/notifications/whatsapp')
|
|
let call = 0
|
|
mockAsAdminImpl = (fn) => {
|
|
call++
|
|
if (call === 1) return Promise.resolve([{
|
|
id: 'c1', description: 'Fix it', dueDate: daysFromNow(-3),
|
|
incidentId: 'i1', ownerUserId: 'u1',
|
|
incidentRef: 'SITE-202507-0001', siteId: 's1',
|
|
ownerEmail: 'owner@example.com', ownerName: 'Owner', ownerPhone: '+60123456789',
|
|
}])
|
|
return Promise.resolve([]) // alreadySent = [], hseUsers = [], insert void
|
|
}
|
|
const { notified } = await escalateOverdueCapa()
|
|
expect(notified).toBe(1)
|
|
expect(sendWhatsAppMessage).toHaveBeenCalled()
|
|
})
|
|
})
|