feat: simplify mails and add reminders
This commit is contained in:
@@ -22,18 +22,26 @@ import {
|
||||
sendCancellationEmail,
|
||||
sendConfirmationEmail,
|
||||
sendPaymentReminderEmail,
|
||||
sendReminder24hEmail,
|
||||
sendReminderEmail,
|
||||
sendSubscriptionConfirmationEmail,
|
||||
sendUpdateEmail,
|
||||
} from "../email";
|
||||
import { adminProcedure, protectedProcedure, publicProcedure } from "../index";
|
||||
import { generateQrSecret } from "../lib/drinkkaart-utils";
|
||||
import { drinkkaartRouter } from "./drinkkaart";
|
||||
|
||||
// Registration opens at this date — reminders fire 1 hour before
|
||||
// Registration opens at this date — reminders fire 24 hours and 1 hour before
|
||||
const REGISTRATION_OPENS_AT = new Date("2026-03-16T19:00:00+01:00");
|
||||
const REMINDER_WINDOW_START = new Date(
|
||||
const REMINDER_1H_WINDOW_START = new Date(
|
||||
REGISTRATION_OPENS_AT.getTime() - 60 * 60 * 1000,
|
||||
);
|
||||
const REMINDER_24H_WINDOW_START = new Date(
|
||||
REGISTRATION_OPENS_AT.getTime() - 25 * 60 * 60 * 1000,
|
||||
);
|
||||
const REMINDER_24H_WINDOW_END = new Date(
|
||||
REGISTRATION_OPENS_AT.getTime() - 23 * 60 * 60 * 1000,
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared helpers
|
||||
@@ -827,9 +835,10 @@ export const appRouter = {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Subscribe an email address to receive a reminder 1 hour before registration
|
||||
* opens. Silently deduplicates — if the email is already subscribed, returns
|
||||
* ok:true without error. Returns ok:false if registration is already open.
|
||||
* Subscribe an email address to receive reminders before registration opens:
|
||||
* one 24 hours before and one 1 hour before. Silently deduplicates — if the
|
||||
* email is already subscribed, returns ok:true without error. Returns
|
||||
* ok:false if registration is already open.
|
||||
*/
|
||||
subscribeReminder: publicProcedure
|
||||
.input(z.object({ email: z.string().email() }))
|
||||
@@ -858,6 +867,11 @@ export const appRouter = {
|
||||
email: input.email,
|
||||
});
|
||||
|
||||
// Fire-and-forget — don't let a mail failure block the response
|
||||
sendSubscriptionConfirmationEmail({ to: input.email }).catch((err) =>
|
||||
console.error("Failed to send subscription confirmation email:", err),
|
||||
);
|
||||
|
||||
return { ok: true };
|
||||
}),
|
||||
|
||||
@@ -867,8 +881,9 @@ export const appRouter = {
|
||||
* header. Should be called by a Cloudflare Cron Trigger at the top of
|
||||
* every hour, or directly via `POST /api/cron/reminders`.
|
||||
*
|
||||
* Only sends when the current time is within the 1-hour reminder window
|
||||
* (between REGISTRATION_OPENS_AT - 1h and REGISTRATION_OPENS_AT).
|
||||
* Fires two waves:
|
||||
* - 24 hours before: window [REGISTRATION_OPENS_AT - 25h, REGISTRATION_OPENS_AT - 23h)
|
||||
* - 1 hour before: window [REGISTRATION_OPENS_AT - 1h, REGISTRATION_OPENS_AT)
|
||||
*/
|
||||
sendReminders: publicProcedure
|
||||
.input(z.object({ secret: z.string() }))
|
||||
@@ -879,34 +894,61 @@ export const appRouter = {
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const windowStart = REMINDER_WINDOW_START.getTime();
|
||||
const windowEnd = REGISTRATION_OPENS_AT.getTime();
|
||||
const in24hWindow =
|
||||
now >= REMINDER_24H_WINDOW_START.getTime() &&
|
||||
now < REMINDER_24H_WINDOW_END.getTime();
|
||||
const in1hWindow =
|
||||
now >= REMINDER_1H_WINDOW_START.getTime() &&
|
||||
now < REGISTRATION_OPENS_AT.getTime();
|
||||
|
||||
// Only send during the reminder window
|
||||
if (now < windowStart || now >= windowEnd) {
|
||||
if (!in24hWindow && !in1hWindow) {
|
||||
return { sent: 0, skipped: true, reason: "outside_window" as const };
|
||||
}
|
||||
|
||||
// Fetch all unsent reminders
|
||||
const pending = await db
|
||||
.select()
|
||||
.from(reminder)
|
||||
.where(isNull(reminder.sentAt));
|
||||
|
||||
let sent = 0;
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const row of pending) {
|
||||
try {
|
||||
await sendReminderEmail({ to: row.email });
|
||||
await db
|
||||
.update(reminder)
|
||||
.set({ sentAt: new Date() })
|
||||
.where(eq(reminder.id, row.id));
|
||||
sent++;
|
||||
} catch (err) {
|
||||
errors.push(`${row.email}: ${String(err)}`);
|
||||
console.error(`Failed to send reminder to ${row.email}:`, err);
|
||||
if (in24hWindow) {
|
||||
// Fetch reminders where the 24h email has not been sent yet
|
||||
const pending = await db
|
||||
.select()
|
||||
.from(reminder)
|
||||
.where(isNull(reminder.sent24hAt));
|
||||
|
||||
for (const row of pending) {
|
||||
try {
|
||||
await sendReminder24hEmail({ to: row.email });
|
||||
await db
|
||||
.update(reminder)
|
||||
.set({ sent24hAt: new Date() })
|
||||
.where(eq(reminder.id, row.id));
|
||||
sent++;
|
||||
} catch (err) {
|
||||
errors.push(`24h ${row.email}: ${String(err)}`);
|
||||
console.error(`Failed to send 24h reminder to ${row.email}:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (in1hWindow) {
|
||||
// Fetch reminders where the 1h email has not been sent yet
|
||||
const pending = await db
|
||||
.select()
|
||||
.from(reminder)
|
||||
.where(isNull(reminder.sentAt));
|
||||
|
||||
for (const row of pending) {
|
||||
try {
|
||||
await sendReminderEmail({ to: row.email });
|
||||
await db
|
||||
.update(reminder)
|
||||
.set({ sentAt: new Date() })
|
||||
.where(eq(reminder.id, row.id));
|
||||
sent++;
|
||||
} catch (err) {
|
||||
errors.push(`1h ${row.email}: ${String(err)}`);
|
||||
console.error(`Failed to send 1h reminder to ${row.email}:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1032,32 +1074,59 @@ export async function runSendReminders(): Promise<{
|
||||
errors: string[];
|
||||
}> {
|
||||
const now = Date.now();
|
||||
const windowStart = REMINDER_WINDOW_START.getTime();
|
||||
const windowEnd = REGISTRATION_OPENS_AT.getTime();
|
||||
const in24hWindow =
|
||||
now >= REMINDER_24H_WINDOW_START.getTime() &&
|
||||
now < REMINDER_24H_WINDOW_END.getTime();
|
||||
const in1hWindow =
|
||||
now >= REMINDER_1H_WINDOW_START.getTime() &&
|
||||
now < REGISTRATION_OPENS_AT.getTime();
|
||||
|
||||
if (now < windowStart || now >= windowEnd) {
|
||||
if (!in24hWindow && !in1hWindow) {
|
||||
return { sent: 0, skipped: true, reason: "outside_window", errors: [] };
|
||||
}
|
||||
|
||||
const pending = await db
|
||||
.select()
|
||||
.from(reminder)
|
||||
.where(isNull(reminder.sentAt));
|
||||
|
||||
let sent = 0;
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const row of pending) {
|
||||
try {
|
||||
await sendReminderEmail({ to: row.email });
|
||||
await db
|
||||
.update(reminder)
|
||||
.set({ sentAt: new Date() })
|
||||
.where(eq(reminder.id, row.id));
|
||||
sent++;
|
||||
} catch (err) {
|
||||
errors.push(`${row.email}: ${String(err)}`);
|
||||
console.error(`Failed to send reminder to ${row.email}:`, err);
|
||||
if (in24hWindow) {
|
||||
const pending = await db
|
||||
.select()
|
||||
.from(reminder)
|
||||
.where(isNull(reminder.sent24hAt));
|
||||
|
||||
for (const row of pending) {
|
||||
try {
|
||||
await sendReminder24hEmail({ to: row.email });
|
||||
await db
|
||||
.update(reminder)
|
||||
.set({ sent24hAt: new Date() })
|
||||
.where(eq(reminder.id, row.id));
|
||||
sent++;
|
||||
} catch (err) {
|
||||
errors.push(`24h ${row.email}: ${String(err)}`);
|
||||
console.error(`Failed to send 24h reminder to ${row.email}:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (in1hWindow) {
|
||||
const pending = await db
|
||||
.select()
|
||||
.from(reminder)
|
||||
.where(isNull(reminder.sentAt));
|
||||
|
||||
for (const row of pending) {
|
||||
try {
|
||||
await sendReminderEmail({ to: row.email });
|
||||
await db
|
||||
.update(reminder)
|
||||
.set({ sentAt: new Date() })
|
||||
.where(eq(reminder.id, row.id));
|
||||
sent++;
|
||||
} catch (err) {
|
||||
errors.push(`1h ${row.email}: ${String(err)}`);
|
||||
console.error(`Failed to send 1h reminder to ${row.email}:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user