feat: event registration emails and reminders (reminders untested)
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* event-reminders — the hourly cron task that sends reminder e-mails to
|
||||
* confirmed registrations whose event starts within the configured lead time.
|
||||
*
|
||||
* Semantics are at-least-once: `reminderSentAt` is written ONLY after the
|
||||
* e-mail sent successfully, so a failure on one run is retried on the next.
|
||||
* One failing registration never stops the batch — every registration is
|
||||
* wrapped in its own try/catch.
|
||||
*
|
||||
* Registered from src/index.ts only when `server.cron.enabled` is true.
|
||||
*/
|
||||
|
||||
import type { Core } from '@strapi/strapi';
|
||||
import { sendEventReminderMail } from './event-mail';
|
||||
|
||||
const REGISTRATION_UID = 'api::event-registration.event-registration';
|
||||
const REMINDER_SETTINGS_UID = 'api::reminder-settings.reminder-settings';
|
||||
|
||||
const LOG_PREFIX = '[event-reminders]';
|
||||
|
||||
const DEFAULT_LEAD_HOURS = 48;
|
||||
const DEFAULT_REMINDER_ENABLED = true;
|
||||
|
||||
const PAGE_SIZE = 50;
|
||||
|
||||
type ReminderSettings = {
|
||||
enabled: boolean;
|
||||
leadHours: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Read the dashboard-controlled settings, falling back to the seed defaults
|
||||
* when the single type is missing or its values are empty.
|
||||
*/
|
||||
async function readReminderSettings(
|
||||
strapi: Core.Strapi,
|
||||
): Promise<ReminderSettings> {
|
||||
try {
|
||||
const settings = await strapi
|
||||
.documents(REMINDER_SETTINGS_UID)
|
||||
.findFirst({});
|
||||
|
||||
if (!settings) {
|
||||
return { enabled: DEFAULT_REMINDER_ENABLED, leadHours: DEFAULT_LEAD_HOURS };
|
||||
}
|
||||
|
||||
const leadHours =
|
||||
typeof settings.leadHours === 'number' && settings.leadHours >= 0
|
||||
? settings.leadHours
|
||||
: DEFAULT_LEAD_HOURS;
|
||||
|
||||
return {
|
||||
enabled: settings.enabled !== false,
|
||||
leadHours,
|
||||
};
|
||||
} catch (error) {
|
||||
strapi.log.warn(
|
||||
`${LOG_PREFIX} could not read reminder settings, using defaults: ${String(error)}`,
|
||||
);
|
||||
return { enabled: DEFAULT_REMINDER_ENABLED, leadHours: DEFAULT_LEAD_HOURS };
|
||||
}
|
||||
}
|
||||
|
||||
export async function runEventReminders({
|
||||
strapi,
|
||||
}: {
|
||||
strapi: Core.Strapi;
|
||||
}): Promise<void> {
|
||||
const settings = await readReminderSettings(strapi);
|
||||
|
||||
if (!settings.enabled) {
|
||||
strapi.log.debug(`${LOG_PREFIX} disabled in reminder settings, skipping.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const horizon = new Date(now.getTime() + settings.leadHours * 3_600_000);
|
||||
|
||||
strapi.log.info(
|
||||
`${LOG_PREFIX} running for events starting between ${now.toISOString()} and ${horizon.toISOString()}.`,
|
||||
);
|
||||
|
||||
let page = 1;
|
||||
let sent = 0;
|
||||
let failed = 0;
|
||||
|
||||
while (true) {
|
||||
const registrations = await strapi
|
||||
.documents(REGISTRATION_UID)
|
||||
.findMany({
|
||||
filters: {
|
||||
registrationState: { $eq: 'confirmed' },
|
||||
reminderSentAt: { $null: true },
|
||||
event: {
|
||||
startDateTime: {
|
||||
$gte: now.toISOString(),
|
||||
$lte: horizon.toISOString(),
|
||||
},
|
||||
},
|
||||
},
|
||||
populate: { event: true },
|
||||
pagination: { page, pageSize: PAGE_SIZE },
|
||||
});
|
||||
|
||||
for (const registration of registrations) {
|
||||
try {
|
||||
const event = registration.event as
|
||||
| { documentId?: string; id?: number }
|
||||
| null
|
||||
| undefined;
|
||||
|
||||
const reference = event?.documentId
|
||||
? { documentId: event.documentId }
|
||||
: event?.id !== undefined
|
||||
? { id: event.id }
|
||||
: undefined;
|
||||
|
||||
if (!event || !reference) {
|
||||
strapi.log.warn(
|
||||
`${LOG_PREFIX} registration ${registration.documentId} has no event, skipping.`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const ok = await sendEventReminderMail({
|
||||
strapi,
|
||||
to: registration.email as string,
|
||||
eventReference: reference,
|
||||
});
|
||||
|
||||
if (!ok) {
|
||||
failed += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
await strapi
|
||||
.documents(REGISTRATION_UID)
|
||||
.update({
|
||||
documentId: registration.documentId as string,
|
||||
data: { reminderSentAt: new Date().toISOString() },
|
||||
});
|
||||
|
||||
sent += 1;
|
||||
} catch (error) {
|
||||
failed += 1;
|
||||
strapi.log.error(
|
||||
`${LOG_PREFIX} failed for registration ${registration.documentId}: ${String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (registrations.length < PAGE_SIZE) break;
|
||||
page += 1;
|
||||
}
|
||||
|
||||
strapi.log.info(
|
||||
`${LOG_PREFIX} finished: ${sent} reminder(s) sent, ${failed} failed.`,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user