Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FFDuBhMKvoWjrWT3ZnGmmr
155 lines
4.8 KiB
TypeScript
155 lines
4.8 KiB
TypeScript
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'),
|
|
}))
|
|
|
|
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 — WhatsApp', () => {
|
|
function daysFromNow(n: number): string {
|
|
const d = new Date()
|
|
d.setDate(d.getDate() + n)
|
|
return d.toISOString().split('T')[0]
|
|
}
|
|
|
|
function makeSupabaseMock(overrides: {
|
|
capas?: unknown[]
|
|
alreadySent?: unknown
|
|
hseUsers?: unknown[]
|
|
}) {
|
|
const { capas = [], alreadySent = null, hseUsers = [] } = overrides
|
|
|
|
// Each .from() call returns a fresh chainable builder
|
|
// We track call order: first from('capa_actions'), then from('notifications_log'), then from('users'), then from('notifications_log').insert
|
|
let fromCallIndex = 0
|
|
|
|
const makeChain = (resolvedValue: unknown) => {
|
|
const chain: Record<string, unknown> = {}
|
|
const methods = ['select', 'not', 'eq', 'in', 'limit', 'maybeSingle', 'insert']
|
|
for (const m of methods) {
|
|
chain[m] = vi.fn(() => chain)
|
|
}
|
|
// Terminal: awaiting the chain resolves to resolvedValue
|
|
Object.defineProperty(chain, 'then', {
|
|
get() {
|
|
return (resolve: (v: unknown) => unknown) => Promise.resolve(resolvedValue).then(resolve)
|
|
},
|
|
})
|
|
return chain
|
|
}
|
|
|
|
const supabase = {
|
|
from: vi.fn(() => {
|
|
const index = fromCallIndex++
|
|
if (index === 0) return makeChain({ data: capas, error: null }) // capa_actions
|
|
if (index === 1) return makeChain({ data: alreadySent, error: null }) // notifications_log check
|
|
if (index === 2) return makeChain({ data: hseUsers, error: null }) // hse users for CC
|
|
return makeChain({ data: null, error: null }) // notifications_log insert
|
|
}),
|
|
}
|
|
|
|
return supabase as unknown as import('@supabase/supabase-js').SupabaseClient
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
it('sends WhatsApp when threshold is overdue_7d and owner has phone', async () => {
|
|
const { sendWhatsAppMessage: mockWA } = await import('@/lib/notifications/whatsapp')
|
|
|
|
const supabase = makeSupabaseMock({
|
|
capas: [
|
|
{
|
|
id: 'capa-1',
|
|
description: 'Fix safety barrier',
|
|
due_date: daysFromNow(-7),
|
|
incident_id: 'inc-1',
|
|
incidents: { reference_no: 'KL-202501-0001', site_id: 'site-1' },
|
|
owner: { email: 'owner@test.com', name: 'Alice', phone: '60123456789' },
|
|
},
|
|
],
|
|
alreadySent: null,
|
|
hseUsers: [],
|
|
})
|
|
|
|
await escalateOverdueCapa(supabase)
|
|
|
|
expect(mockWA).toHaveBeenCalledWith(
|
|
expect.stringMatching(/^\d+$/),
|
|
'ims_capa_overdue',
|
|
expect.arrayContaining([expect.any(String)]),
|
|
'test-cred',
|
|
'test-cred',
|
|
)
|
|
})
|
|
|
|
it('does NOT send WhatsApp when threshold is warning_3d', async () => {
|
|
const { sendWhatsAppMessage: mockWA } = await import('@/lib/notifications/whatsapp')
|
|
vi.clearAllMocks()
|
|
|
|
const supabase = makeSupabaseMock({
|
|
capas: [
|
|
{
|
|
id: 'capa-2',
|
|
description: 'Inspect equipment',
|
|
due_date: daysFromNow(3),
|
|
incident_id: 'inc-2',
|
|
incidents: { reference_no: 'KL-202501-0002', site_id: 'site-1' },
|
|
owner: { email: 'owner@test.com', name: 'Bob', phone: '60129876543' },
|
|
},
|
|
],
|
|
alreadySent: null,
|
|
hseUsers: [],
|
|
})
|
|
|
|
await escalateOverdueCapa(supabase)
|
|
|
|
expect(mockWA).not.toHaveBeenCalled()
|
|
})
|
|
})
|