feat(admin): add pre-registrant alert detectors

This commit is contained in:
Jennie Robinson Faber 2026-04-08 11:09:39 +01:00
parent 824364d526
commit 4bae4b0ec3
2 changed files with 104 additions and 1 deletions

View file

@ -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. // Aggregator lands in task 8.

View file

@ -52,9 +52,12 @@ import {
detectSlackInviteFailed, detectSlackInviteFailed,
detectNoSlackHandleAfterWeek, detectNoSlackHandleAfterWeek,
detectStuckPendingPayment, detectStuckPendingPayment,
detectSuspendedMembers detectSuspendedMembers,
detectPreRegistrantSelectedNotInvited,
detectPreRegistrantExpired
} from '../../../server/utils/adminAlerts.js' } from '../../../server/utils/adminAlerts.js'
import Member from '../../../server/models/member.js' import Member from '../../../server/models/member.js'
import PreRegistration from '../../../server/models/preRegistration.js'
describe('adminAlerts module shell', () => { describe('adminAlerts module shell', () => {
describe('ALERT_THRESHOLDS', () => { 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' })
})
})
})
}) })