diff --git a/server/utils/adminAlerts.js b/server/utils/adminAlerts.js index adb1ffa..58b7bd5 100644 --- a/server/utils/adminAlerts.js +++ b/server/utils/adminAlerts.js @@ -116,4 +116,49 @@ export async function detectSuspendedMembers() { } } +function preRegItem(preReg, sublabel) { + return { + id: String(preReg._id), + label: preReg.name || preReg.email, + sublabel, + href: '/admin/pre-registrants' + } +} + +export async function detectPreRegistrantSelectedNotInvited() { + await connectDB() + const cutoff = daysAgo(ALERT_THRESHOLDS.PREREG_SELECTED_DAYS) + const preRegs = await PreRegistration + .find({ + status: 'selected', + updatedAt: { $lte: cutoff } + }) + .select('name email updatedAt') + .lean() + return { + type: 'preregistrant_selected_not_invited', + severity: 'attention', + title: 'Pre-registrants selected but not invited', + items: preRegs.map((p) => + preRegItem(p, `${p.email} — ${daysSince(p.updatedAt)} days selected`) + ) + } +} + +export async function detectPreRegistrantExpired() { + await connectDB() + const preRegs = await PreRegistration + .find({ status: 'expired' }) + .select('name email updatedAt') + .lean() + return { + type: 'preregistrant_expired', + severity: 'attention', + title: 'Expired pre-registrant invitations', + items: preRegs.map((p) => + preRegItem(p, `${p.email} — expired ${daysSince(p.updatedAt)} days ago`) + ) + } +} + // Aggregator lands in task 8. diff --git a/tests/server/utils/adminAlerts.test.js b/tests/server/utils/adminAlerts.test.js index 7f1fa27..120d586 100644 --- a/tests/server/utils/adminAlerts.test.js +++ b/tests/server/utils/adminAlerts.test.js @@ -52,9 +52,12 @@ import { detectSlackInviteFailed, detectNoSlackHandleAfterWeek, detectStuckPendingPayment, - detectSuspendedMembers + detectSuspendedMembers, + detectPreRegistrantSelectedNotInvited, + detectPreRegistrantExpired } from '../../../server/utils/adminAlerts.js' import Member from '../../../server/models/member.js' +import PreRegistration from '../../../server/models/preRegistration.js' describe('adminAlerts module shell', () => { describe('ALERT_THRESHOLDS', () => { @@ -193,4 +196,59 @@ describe('adminAlerts module shell', () => { }) }) }) + + describe('pre-registrant alerts', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + function mockPreRegFind(result) { + PreRegistration.find.mockReturnValue({ + select: vi.fn().mockReturnValue({ + lean: vi.fn().mockResolvedValue(result) + }) + }) + } + + describe('detectPreRegistrantSelectedNotInvited', () => { + it('returns selected pre-registrants older than 3 days', async () => { + const fourDaysAgo = new Date(Date.now() - 4 * 86400000) + mockPreRegFind([ + { _id: 'p1', name: 'Fin', email: 'fin@example.com', updatedAt: fourDaysAgo } + ]) + + const alert = await detectPreRegistrantSelectedNotInvited() + + expect(alert.type).toBe('preregistrant_selected_not_invited') + expect(alert.severity).toBe('attention') + expect(alert.items).toHaveLength(1) + expect(alert.items[0]).toEqual({ + id: 'p1', + label: 'Fin', + sublabel: 'fin@example.com — 4 days selected', + href: '/admin/pre-registrants' + }) + + const [query] = PreRegistration.find.mock.calls[0] + expect(query.status).toBe('selected') + expect(query.updatedAt).toEqual({ $lte: expect.any(Date) }) + }) + }) + + describe('detectPreRegistrantExpired', () => { + it('returns expired pre-registrants', async () => { + const expiredAt = new Date(Date.now() - 2 * 86400000) + mockPreRegFind([ + { _id: 'p2', name: 'Gus', email: 'gus@example.com', updatedAt: expiredAt } + ]) + + const alert = await detectPreRegistrantExpired() + + expect(alert.type).toBe('preregistrant_expired') + expect(alert.severity).toBe('attention') + expect(alert.items[0].label).toBe('Gus') + expect(PreRegistration.find).toHaveBeenCalledWith({ status: 'expired' }) + }) + }) + }) })