import { getPayload } from "payload"; import config from "@payload-config"; import { combineDateAndTime } from "./status"; import { sendReminderEmail } from "@/lib/email/sendReminderEmail"; const REMINDER_TOLERANCE_MS = 10 * 60 * 1000; // cron may run every few minutes function isDue(reminderAt: number, now: number): boolean { return now >= reminderAt && now < reminderAt + REMINDER_TOLERANCE_MS; } function formatDate(date: Date): string { return date.toLocaleDateString("de-DE", { day: "2-digit", month: "long", year: "numeric" }); } function formatTime(date: Date): string { return date.toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" }); } /** * Checks all upcoming online events for due 60/30-minute reminders and sends * them — to registered, non-cancelled participants and (optionally) to the * admin host. Idempotent: every send is guarded by a "...Sent" flag so a * cron job that runs every few minutes never double-sends. */ export async function runEventReminders() { const payload = await getPayload({ config }); const now = new Date(); const serverUrl = process.env.NEXT_PUBLIC_SERVER_URL || "http://localhost:3000"; const windowStart = new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString(); const windowEnd = new Date(now.getTime() + 2 * 24 * 60 * 60 * 1000).toISOString(); const { docs: events } = await payload.find({ collection: "events", where: { and: [ { isOnline: { equals: true } }, { date: { greater_than_equal: windowStart } }, { date: { less_than_equal: windowEnd } }, ], }, limit: 200, }); const results = { emailsSent: 0, eventsChecked: events.length }; for (const event of events) { if (!event.slug) continue; const start = combineDateAndTime(event.date, event.startTime); const dateLabel = formatDate(start); const timeLabel = formatTime(start); const joinUrl = `${serverUrl}/termine/${event.slug}/beitreten`; const thresholds: { minutes: 60 | 30; enabled: boolean; participantField: "reminder60Sent" | "reminder30Sent"; hostField: "hostReminder60Sent" | "hostReminder30Sent" }[] = [ { minutes: 60, enabled: Boolean(event.reminder60Enabled), participantField: "reminder60Sent", hostField: "hostReminder60Sent" }, { minutes: 30, enabled: Boolean(event.reminder30Enabled), participantField: "reminder30Sent", hostField: "hostReminder30Sent" }, ]; for (const threshold of thresholds) { if (!threshold.enabled) continue; const reminderAt = start.getTime() - threshold.minutes * 60_000; if (!isDue(reminderAt, now.getTime())) continue; // Host reminder (per event, not per registration). if (event.hostReminderEnabled && !event[threshold.hostField]) { const { docs: admins } = await payload.find({ collection: "users", limit: 50 }); for (const admin of admins) { if (!admin.email) continue; await sendReminderEmail(admin.email, { recipientName: admin.name || "Team ANOUMA", eventTitle: event.title, dateLabel, timeLabel, joinUrl, meetingPassword: event.meetingPassword || "—", minutesBefore: threshold.minutes, }); results.emailsSent++; } await payload.update({ collection: "events", id: event.id, data: { [threshold.hostField]: true } }); } // Participant reminders — only registered (not cancelled), not yet sent. const { docs: registrations } = await payload.find({ collection: "event-registrations", where: { and: [ { event: { equals: event.id } }, { status: { equals: "registered" } }, { [threshold.participantField]: { equals: false } }, ], }, limit: 500, }); for (const registration of registrations) { await sendReminderEmail(registration.email, { recipientName: registration.name, eventTitle: event.title, dateLabel, timeLabel, joinUrl, meetingPassword: event.meetingPassword || "—", minutesBefore: threshold.minutes, }); results.emailsSent++; await payload.update({ collection: "event-registrations", id: registration.id, data: { [threshold.participantField]: true }, }); } } } return results; }