Files
sharper.ly/apps/cms/src/lib/event-reminders.ts
T

159 lines
4.2 KiB
TypeScript

/**
* 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.`,
);
}