119 lines
3 KiB
JavaScript
119 lines
3 KiB
JavaScript
import { createHash } from 'node:crypto'
|
|
import Member from '../models/member.js'
|
|
import Event from '../models/event.js'
|
|
import PreRegistration from '../models/preRegistration.js'
|
|
import TagSuggestion from '../models/tagSuggestion.js'
|
|
import AdminAlertDismissal, { ADMIN_ALERT_TYPES } from '../models/adminAlertDismissal.js'
|
|
import { connectDB } from './mongoose.js'
|
|
|
|
export const ALERT_THRESHOLDS = {
|
|
NO_SLACK_DAYS: 7,
|
|
STUCK_PAYMENT_DAYS: 7,
|
|
PREREG_SELECTED_DAYS: 3,
|
|
DRAFT_IMMINENT_DAYS: 14,
|
|
NEAR_CAPACITY_RATIO: 0.8
|
|
}
|
|
|
|
const DAY_MS = 24 * 60 * 60 * 1000
|
|
|
|
function daysAgo(days) {
|
|
return new Date(Date.now() - days * DAY_MS)
|
|
}
|
|
|
|
function daysSince(date) {
|
|
if (!date) return null
|
|
return Math.floor((Date.now() - new Date(date).getTime()) / DAY_MS)
|
|
}
|
|
|
|
export function computeSignature(ids) {
|
|
const normalized = ids
|
|
.map((id) => (id == null ? '' : String(id)))
|
|
.sort()
|
|
const hash = createHash('sha1')
|
|
hash.update(JSON.stringify(normalized))
|
|
return hash.digest('hex')
|
|
}
|
|
|
|
function memberItem(member, sublabel) {
|
|
return {
|
|
id: String(member._id),
|
|
label: member.name,
|
|
sublabel: sublabel ?? member.email,
|
|
href: `/admin/members/${member._id}`
|
|
}
|
|
}
|
|
|
|
export async function detectSlackInviteFailed() {
|
|
await connectDB()
|
|
const members = await Member
|
|
.find({ slackInviteStatus: 'failed' })
|
|
.select('name email')
|
|
.lean()
|
|
return {
|
|
type: 'slack_invite_failed',
|
|
severity: 'critical',
|
|
title: 'Slack invites failed',
|
|
items: members.map((m) => memberItem(m))
|
|
}
|
|
}
|
|
|
|
export async function detectNoSlackHandleAfterWeek() {
|
|
await connectDB()
|
|
const cutoff = daysAgo(ALERT_THRESHOLDS.NO_SLACK_DAYS)
|
|
const members = await Member
|
|
.find({
|
|
status: 'active',
|
|
createdAt: { $lte: cutoff },
|
|
$or: [
|
|
{ slackUserId: { $exists: false } },
|
|
{ slackUserId: null },
|
|
{ slackUserId: '' }
|
|
]
|
|
})
|
|
.select('name email createdAt')
|
|
.lean()
|
|
return {
|
|
type: 'no_slack_handle_week',
|
|
severity: 'attention',
|
|
title: 'Active members without a Slack handle',
|
|
items: members.map((m) =>
|
|
memberItem(m, `${daysSince(m.createdAt)} days since joining`)
|
|
)
|
|
}
|
|
}
|
|
|
|
export async function detectStuckPendingPayment() {
|
|
await connectDB()
|
|
const cutoff = daysAgo(ALERT_THRESHOLDS.STUCK_PAYMENT_DAYS)
|
|
const members = await Member
|
|
.find({
|
|
status: 'pending_payment',
|
|
createdAt: { $lte: cutoff }
|
|
})
|
|
.select('name email createdAt')
|
|
.lean()
|
|
return {
|
|
type: 'stuck_pending_payment',
|
|
severity: 'attention',
|
|
title: 'Members stuck in pending payment',
|
|
items: members.map((m) =>
|
|
memberItem(m, `${daysSince(m.createdAt)} days stuck`)
|
|
)
|
|
}
|
|
}
|
|
|
|
export async function detectSuspendedMembers() {
|
|
await connectDB()
|
|
const members = await Member
|
|
.find({ status: 'suspended' })
|
|
.select('name email')
|
|
.lean()
|
|
return {
|
|
type: 'member_suspended',
|
|
severity: 'attention',
|
|
title: 'Suspended members',
|
|
items: members.map((m) => memberItem(m))
|
|
}
|
|
}
|
|
|
|
// Aggregator lands in task 8.
|