// @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() }) })