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.