feat(log): improve logging for emails
This commit is contained in:
@@ -31,6 +31,25 @@ function getTransport(): nodemailer.Transporter | null {
|
|||||||
const from = env.SMTP_FROM ?? "Kunstenkamp <info@kunstenkamp.be>";
|
const from = env.SMTP_FROM ?? "Kunstenkamp <info@kunstenkamp.be>";
|
||||||
const baseUrl = env.BETTER_AUTH_URL ?? "https://kunstenkamp.be";
|
const baseUrl = env.BETTER_AUTH_URL ?? "https://kunstenkamp.be";
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Structured logger
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits a single-line JSON log that Cloudflare captures in its invocation
|
||||||
|
* log output (visible in the CF dashboard and via `wrangler tail`).
|
||||||
|
*
|
||||||
|
* Plain `console.warn` / `console.error` calls are NOT surfaced in the CF
|
||||||
|
* dashboard — only `console.log` with a JSON string payload is captured.
|
||||||
|
*/
|
||||||
|
export function emailLog(
|
||||||
|
level: "info" | "warn" | "error",
|
||||||
|
event: string,
|
||||||
|
extra?: Record<string, unknown>,
|
||||||
|
): void {
|
||||||
|
console.log(JSON.stringify({ level, event, ...extra }));
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Primitives
|
// Primitives
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -474,29 +493,45 @@ export async function sendConfirmationEmail(params: {
|
|||||||
/** Pre-filled signup URL, e.g. /login?signup=1&email=…&next=/account */
|
/** Pre-filled signup URL, e.g. /login?signup=1&email=…&next=/account */
|
||||||
signupUrl?: string;
|
signupUrl?: string;
|
||||||
}) {
|
}) {
|
||||||
|
const type = "registrationConfirmation";
|
||||||
|
emailLog("info", "email.attempt", { type, to: params.to });
|
||||||
const transport = getTransport();
|
const transport = getTransport();
|
||||||
if (!transport) {
|
if (!transport) {
|
||||||
console.warn("SMTP not configured — skipping confirmation email");
|
emailLog("warn", "email.skipped", {
|
||||||
|
type,
|
||||||
|
to: params.to,
|
||||||
|
reason: "smtp_not_configured",
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const manageUrl = `${baseUrl}/manage/${params.managementToken}`;
|
const manageUrl = `${baseUrl}/manage/${params.managementToken}`;
|
||||||
const signupUrl =
|
const signupUrl =
|
||||||
params.signupUrl ??
|
params.signupUrl ??
|
||||||
`${baseUrl}/login?signup=1&email=${encodeURIComponent(params.to)}&next=/account`;
|
`${baseUrl}/login?signup=1&email=${encodeURIComponent(params.to)}&next=/account`;
|
||||||
await transport.sendMail({
|
try {
|
||||||
from,
|
await transport.sendMail({
|
||||||
to: params.to,
|
from,
|
||||||
subject: "Bevestiging inschrijving — Open Mic Night",
|
to: params.to,
|
||||||
html: registrationConfirmationHtml({
|
subject: "Bevestiging inschrijving — Open Mic Night",
|
||||||
firstName: params.firstName,
|
html: registrationConfirmationHtml({
|
||||||
manageUrl,
|
firstName: params.firstName,
|
||||||
signupUrl,
|
manageUrl,
|
||||||
wantsToPerform: params.wantsToPerform,
|
signupUrl,
|
||||||
artForm: params.artForm,
|
wantsToPerform: params.wantsToPerform,
|
||||||
giftAmount: params.giftAmount,
|
artForm: params.artForm,
|
||||||
drinkCardValue: params.drinkCardValue,
|
giftAmount: params.giftAmount,
|
||||||
}),
|
drinkCardValue: params.drinkCardValue,
|
||||||
});
|
}),
|
||||||
|
});
|
||||||
|
emailLog("info", "email.sent", { type, to: params.to });
|
||||||
|
} catch (err) {
|
||||||
|
emailLog("error", "email.error", {
|
||||||
|
type,
|
||||||
|
to: params.to,
|
||||||
|
error: String(err),
|
||||||
|
});
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sendUpdateEmail(params: {
|
export async function sendUpdateEmail(params: {
|
||||||
@@ -508,94 +543,172 @@ export async function sendUpdateEmail(params: {
|
|||||||
giftAmount?: number;
|
giftAmount?: number;
|
||||||
drinkCardValue?: number;
|
drinkCardValue?: number;
|
||||||
}) {
|
}) {
|
||||||
|
const type = "updateConfirmation";
|
||||||
|
emailLog("info", "email.attempt", { type, to: params.to });
|
||||||
const transport = getTransport();
|
const transport = getTransport();
|
||||||
if (!transport) {
|
if (!transport) {
|
||||||
console.warn("SMTP not configured — skipping update email");
|
emailLog("warn", "email.skipped", {
|
||||||
|
type,
|
||||||
|
to: params.to,
|
||||||
|
reason: "smtp_not_configured",
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const manageUrl = `${baseUrl}/manage/${params.managementToken}`;
|
const manageUrl = `${baseUrl}/manage/${params.managementToken}`;
|
||||||
await transport.sendMail({
|
try {
|
||||||
from,
|
await transport.sendMail({
|
||||||
to: params.to,
|
from,
|
||||||
subject: "Inschrijving bijgewerkt — Open Mic Night",
|
to: params.to,
|
||||||
html: updateConfirmationHtml({
|
subject: "Inschrijving bijgewerkt — Open Mic Night",
|
||||||
firstName: params.firstName,
|
html: updateConfirmationHtml({
|
||||||
manageUrl,
|
firstName: params.firstName,
|
||||||
wantsToPerform: params.wantsToPerform,
|
manageUrl,
|
||||||
artForm: params.artForm,
|
wantsToPerform: params.wantsToPerform,
|
||||||
giftAmount: params.giftAmount,
|
artForm: params.artForm,
|
||||||
drinkCardValue: params.drinkCardValue,
|
giftAmount: params.giftAmount,
|
||||||
}),
|
drinkCardValue: params.drinkCardValue,
|
||||||
});
|
}),
|
||||||
|
});
|
||||||
|
emailLog("info", "email.sent", { type, to: params.to });
|
||||||
|
} catch (err) {
|
||||||
|
emailLog("error", "email.error", {
|
||||||
|
type,
|
||||||
|
to: params.to,
|
||||||
|
error: String(err),
|
||||||
|
});
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sendCancellationEmail(params: {
|
export async function sendCancellationEmail(params: {
|
||||||
to: string;
|
to: string;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
}) {
|
}) {
|
||||||
|
const type = "cancellation";
|
||||||
|
emailLog("info", "email.attempt", { type, to: params.to });
|
||||||
const transport = getTransport();
|
const transport = getTransport();
|
||||||
if (!transport) {
|
if (!transport) {
|
||||||
console.warn("SMTP not configured — skipping cancellation email");
|
emailLog("warn", "email.skipped", {
|
||||||
|
type,
|
||||||
|
to: params.to,
|
||||||
|
reason: "smtp_not_configured",
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await transport.sendMail({
|
try {
|
||||||
from,
|
await transport.sendMail({
|
||||||
to: params.to,
|
from,
|
||||||
subject: "Inschrijving geannuleerd — Open Mic Night",
|
to: params.to,
|
||||||
html: cancellationHtml({ firstName: params.firstName }),
|
subject: "Inschrijving geannuleerd — Open Mic Night",
|
||||||
});
|
html: cancellationHtml({ firstName: params.firstName }),
|
||||||
|
});
|
||||||
|
emailLog("info", "email.sent", { type, to: params.to });
|
||||||
|
} catch (err) {
|
||||||
|
emailLog("error", "email.error", {
|
||||||
|
type,
|
||||||
|
to: params.to,
|
||||||
|
error: String(err),
|
||||||
|
});
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sendSubscriptionConfirmationEmail(params: {
|
export async function sendSubscriptionConfirmationEmail(params: {
|
||||||
to: string;
|
to: string;
|
||||||
}) {
|
}) {
|
||||||
|
const type = "subscriptionConfirmation";
|
||||||
|
emailLog("info", "email.attempt", { type, to: params.to });
|
||||||
const transport = getTransport();
|
const transport = getTransport();
|
||||||
if (!transport) {
|
if (!transport) {
|
||||||
console.warn(
|
emailLog("warn", "email.skipped", {
|
||||||
"SMTP not configured — skipping subscription confirmation email",
|
type,
|
||||||
);
|
to: params.to,
|
||||||
|
reason: "smtp_not_configured",
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await transport.sendMail({
|
try {
|
||||||
from,
|
await transport.sendMail({
|
||||||
to: params.to,
|
from,
|
||||||
subject: "Herinnering ingepland — Open Mic Night",
|
to: params.to,
|
||||||
html: subscriptionConfirmationHtml({ email: params.to }),
|
subject: "Herinnering ingepland — Open Mic Night",
|
||||||
});
|
html: subscriptionConfirmationHtml({ email: params.to }),
|
||||||
|
});
|
||||||
|
emailLog("info", "email.sent", { type, to: params.to });
|
||||||
|
} catch (err) {
|
||||||
|
emailLog("error", "email.error", {
|
||||||
|
type,
|
||||||
|
to: params.to,
|
||||||
|
error: String(err),
|
||||||
|
});
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sendReminder24hEmail(params: {
|
export async function sendReminder24hEmail(params: {
|
||||||
to: string;
|
to: string;
|
||||||
firstName?: string | null;
|
firstName?: string | null;
|
||||||
}) {
|
}) {
|
||||||
|
const type = "reminder24h";
|
||||||
|
emailLog("info", "email.attempt", { type, to: params.to });
|
||||||
const transport = getTransport();
|
const transport = getTransport();
|
||||||
if (!transport) {
|
if (!transport) {
|
||||||
console.warn("SMTP not configured — skipping 24h reminder email");
|
emailLog("warn", "email.skipped", {
|
||||||
|
type,
|
||||||
|
to: params.to,
|
||||||
|
reason: "smtp_not_configured",
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await transport.sendMail({
|
try {
|
||||||
from,
|
await transport.sendMail({
|
||||||
to: params.to,
|
from,
|
||||||
subject: "Nog 24 uur — inschrijvingen openen morgen!",
|
to: params.to,
|
||||||
html: reminder24hHtml({ firstName: params.firstName }),
|
subject: "Nog 24 uur — inschrijvingen openen morgen!",
|
||||||
});
|
html: reminder24hHtml({ firstName: params.firstName }),
|
||||||
|
});
|
||||||
|
emailLog("info", "email.sent", { type, to: params.to });
|
||||||
|
} catch (err) {
|
||||||
|
emailLog("error", "email.error", {
|
||||||
|
type,
|
||||||
|
to: params.to,
|
||||||
|
error: String(err),
|
||||||
|
});
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sendReminderEmail(params: {
|
export async function sendReminderEmail(params: {
|
||||||
to: string;
|
to: string;
|
||||||
firstName?: string | null;
|
firstName?: string | null;
|
||||||
}) {
|
}) {
|
||||||
|
const type = "reminder1h";
|
||||||
|
emailLog("info", "email.attempt", { type, to: params.to });
|
||||||
const transport = getTransport();
|
const transport = getTransport();
|
||||||
if (!transport) {
|
if (!transport) {
|
||||||
console.warn("SMTP not configured — skipping reminder email");
|
emailLog("warn", "email.skipped", {
|
||||||
|
type,
|
||||||
|
to: params.to,
|
||||||
|
reason: "smtp_not_configured",
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await transport.sendMail({
|
try {
|
||||||
from,
|
await transport.sendMail({
|
||||||
to: params.to,
|
from,
|
||||||
subject: "Nog 1 uur — inschrijvingen openen straks!",
|
to: params.to,
|
||||||
html: reminderHtml({ firstName: params.firstName }),
|
subject: "Nog 1 uur — inschrijvingen openen straks!",
|
||||||
});
|
html: reminderHtml({ firstName: params.firstName }),
|
||||||
|
});
|
||||||
|
emailLog("info", "email.sent", { type, to: params.to });
|
||||||
|
} catch (err) {
|
||||||
|
emailLog("error", "email.error", {
|
||||||
|
type,
|
||||||
|
to: params.to,
|
||||||
|
error: String(err),
|
||||||
|
});
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sendPaymentReminderEmail(params: {
|
export async function sendPaymentReminderEmail(params: {
|
||||||
@@ -605,25 +718,41 @@ export async function sendPaymentReminderEmail(params: {
|
|||||||
drinkCardValue?: number;
|
drinkCardValue?: number;
|
||||||
giftAmount?: number;
|
giftAmount?: number;
|
||||||
}) {
|
}) {
|
||||||
|
const type = "paymentReminder";
|
||||||
|
emailLog("info", "email.attempt", { type, to: params.to });
|
||||||
const transport = getTransport();
|
const transport = getTransport();
|
||||||
if (!transport) {
|
if (!transport) {
|
||||||
console.warn("SMTP not configured — skipping payment reminder email");
|
emailLog("warn", "email.skipped", {
|
||||||
|
type,
|
||||||
|
to: params.to,
|
||||||
|
reason: "smtp_not_configured",
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const accountUrl = `${baseUrl}/account`;
|
const accountUrl = `${baseUrl}/account`;
|
||||||
const manageUrl = `${baseUrl}/manage/${params.managementToken}`;
|
const manageUrl = `${baseUrl}/manage/${params.managementToken}`;
|
||||||
await transport.sendMail({
|
try {
|
||||||
from,
|
await transport.sendMail({
|
||||||
to: params.to,
|
from,
|
||||||
subject: "Herinnering: betaling open — Open Mic Night",
|
to: params.to,
|
||||||
html: paymentReminderHtml({
|
subject: "Herinnering: betaling open — Open Mic Night",
|
||||||
firstName: params.firstName,
|
html: paymentReminderHtml({
|
||||||
accountUrl,
|
firstName: params.firstName,
|
||||||
manageUrl,
|
accountUrl,
|
||||||
drinkCardValue: params.drinkCardValue,
|
manageUrl,
|
||||||
giftAmount: params.giftAmount,
|
drinkCardValue: params.drinkCardValue,
|
||||||
}),
|
giftAmount: params.giftAmount,
|
||||||
});
|
}),
|
||||||
|
});
|
||||||
|
emailLog("info", "email.sent", { type, to: params.to });
|
||||||
|
} catch (err) {
|
||||||
|
emailLog("error", "email.error", {
|
||||||
|
type,
|
||||||
|
to: params.to,
|
||||||
|
error: String(err),
|
||||||
|
});
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sendPaymentConfirmationEmail(params: {
|
export async function sendPaymentConfirmationEmail(params: {
|
||||||
@@ -633,21 +762,37 @@ export async function sendPaymentConfirmationEmail(params: {
|
|||||||
drinkCardValue?: number;
|
drinkCardValue?: number;
|
||||||
giftAmount?: number;
|
giftAmount?: number;
|
||||||
}) {
|
}) {
|
||||||
|
const type = "paymentConfirmation";
|
||||||
|
emailLog("info", "email.attempt", { type, to: params.to });
|
||||||
const transport = getTransport();
|
const transport = getTransport();
|
||||||
if (!transport) {
|
if (!transport) {
|
||||||
console.warn("SMTP not configured — skipping payment confirmation email");
|
emailLog("warn", "email.skipped", {
|
||||||
|
type,
|
||||||
|
to: params.to,
|
||||||
|
reason: "smtp_not_configured",
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const manageUrl = `${baseUrl}/manage/${params.managementToken}`;
|
const manageUrl = `${baseUrl}/manage/${params.managementToken}`;
|
||||||
await transport.sendMail({
|
try {
|
||||||
from,
|
await transport.sendMail({
|
||||||
to: params.to,
|
from,
|
||||||
subject: "Betaling ontvangen — Open Mic Night",
|
to: params.to,
|
||||||
html: paymentConfirmationHtml({
|
subject: "Betaling ontvangen — Open Mic Night",
|
||||||
firstName: params.firstName,
|
html: paymentConfirmationHtml({
|
||||||
manageUrl,
|
firstName: params.firstName,
|
||||||
drinkCardValue: params.drinkCardValue,
|
manageUrl,
|
||||||
giftAmount: params.giftAmount,
|
drinkCardValue: params.drinkCardValue,
|
||||||
}),
|
giftAmount: params.giftAmount,
|
||||||
});
|
}),
|
||||||
|
});
|
||||||
|
emailLog("info", "email.sent", { type, to: params.to });
|
||||||
|
} catch (err) {
|
||||||
|
emailLog("error", "email.error", {
|
||||||
|
type,
|
||||||
|
to: params.to,
|
||||||
|
error: String(err),
|
||||||
|
});
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
} from "drizzle-orm";
|
} from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
import {
|
||||||
|
emailLog,
|
||||||
sendCancellationEmail,
|
sendCancellationEmail,
|
||||||
sendConfirmationEmail,
|
sendConfirmationEmail,
|
||||||
sendPaymentReminderEmail,
|
sendPaymentReminderEmail,
|
||||||
@@ -324,7 +325,10 @@ export const appRouter = {
|
|||||||
giftAmount: input.giftAmount,
|
giftAmount: input.giftAmount,
|
||||||
drinkCardValue: isPerformer ? 0 : drinkCardEuros(guests.length),
|
drinkCardValue: isPerformer ? 0 : drinkCardEuros(guests.length),
|
||||||
}).catch((err) =>
|
}).catch((err) =>
|
||||||
console.error("Failed to send confirmation email:", err),
|
emailLog("error", "email.catch", {
|
||||||
|
type: "confirmation",
|
||||||
|
error: String(err),
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
return { success: true, managementToken };
|
return { success: true, managementToken };
|
||||||
@@ -412,7 +416,12 @@ export const appRouter = {
|
|||||||
artForm: input.artForm,
|
artForm: input.artForm,
|
||||||
giftAmount: input.giftAmount,
|
giftAmount: input.giftAmount,
|
||||||
drinkCardValue: isPerformer ? 0 : drinkCardEuros(guests.length),
|
drinkCardValue: isPerformer ? 0 : drinkCardEuros(guests.length),
|
||||||
}).catch((err) => console.error("Failed to send update email:", err));
|
}).catch((err) =>
|
||||||
|
emailLog("error", "email.catch", {
|
||||||
|
type: "update",
|
||||||
|
error: String(err),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}),
|
}),
|
||||||
@@ -431,7 +440,10 @@ export const appRouter = {
|
|||||||
to: row.email,
|
to: row.email,
|
||||||
firstName: row.firstName,
|
firstName: row.firstName,
|
||||||
}).catch((err) =>
|
}).catch((err) =>
|
||||||
console.error("Failed to send cancellation email:", err),
|
emailLog("error", "email.catch", {
|
||||||
|
type: "cancellation",
|
||||||
|
error: String(err),
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
@@ -869,7 +881,11 @@ export const appRouter = {
|
|||||||
|
|
||||||
// Fire-and-forget — don't let a mail failure block the response
|
// Fire-and-forget — don't let a mail failure block the response
|
||||||
sendSubscriptionConfirmationEmail({ to: input.email }).catch((err) =>
|
sendSubscriptionConfirmationEmail({ to: input.email }).catch((err) =>
|
||||||
console.error("Failed to send subscription confirmation email:", err),
|
emailLog("error", "email.catch", {
|
||||||
|
type: "subscription_confirmation",
|
||||||
|
to: input.email,
|
||||||
|
error: String(err),
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
@@ -901,6 +917,9 @@ export const appRouter = {
|
|||||||
now >= REMINDER_1H_WINDOW_START.getTime() &&
|
now >= REMINDER_1H_WINDOW_START.getTime() &&
|
||||||
now < REGISTRATION_OPENS_AT.getTime();
|
now < REGISTRATION_OPENS_AT.getTime();
|
||||||
|
|
||||||
|
const activeWindow = in24hWindow ? "24h" : in1hWindow ? "1h" : null;
|
||||||
|
emailLog("info", "reminders.cron.tick", { activeWindow });
|
||||||
|
|
||||||
if (!in24hWindow && !in1hWindow) {
|
if (!in24hWindow && !in1hWindow) {
|
||||||
return { sent: 0, skipped: true, reason: "outside_window" as const };
|
return { sent: 0, skipped: true, reason: "outside_window" as const };
|
||||||
}
|
}
|
||||||
@@ -915,6 +934,8 @@ export const appRouter = {
|
|||||||
.from(reminder)
|
.from(reminder)
|
||||||
.where(isNull(reminder.sent24hAt));
|
.where(isNull(reminder.sent24hAt));
|
||||||
|
|
||||||
|
emailLog("info", "reminders.24h.pending", { count: pending.length });
|
||||||
|
|
||||||
for (const row of pending) {
|
for (const row of pending) {
|
||||||
try {
|
try {
|
||||||
await sendReminder24hEmail({ to: row.email });
|
await sendReminder24hEmail({ to: row.email });
|
||||||
@@ -925,7 +946,11 @@ export const appRouter = {
|
|||||||
sent++;
|
sent++;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
errors.push(`24h ${row.email}: ${String(err)}`);
|
errors.push(`24h ${row.email}: ${String(err)}`);
|
||||||
console.error(`Failed to send 24h reminder to ${row.email}:`, err);
|
emailLog("error", "email.catch", {
|
||||||
|
type: "reminder_24h",
|
||||||
|
to: row.email,
|
||||||
|
error: String(err),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -937,6 +962,8 @@ export const appRouter = {
|
|||||||
.from(reminder)
|
.from(reminder)
|
||||||
.where(isNull(reminder.sentAt));
|
.where(isNull(reminder.sentAt));
|
||||||
|
|
||||||
|
emailLog("info", "reminders.1h.pending", { count: pending.length });
|
||||||
|
|
||||||
for (const row of pending) {
|
for (const row of pending) {
|
||||||
try {
|
try {
|
||||||
await sendReminderEmail({ to: row.email });
|
await sendReminderEmail({ to: row.email });
|
||||||
@@ -947,11 +974,20 @@ export const appRouter = {
|
|||||||
sent++;
|
sent++;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
errors.push(`1h ${row.email}: ${String(err)}`);
|
errors.push(`1h ${row.email}: ${String(err)}`);
|
||||||
console.error(`Failed to send 1h reminder to ${row.email}:`, err);
|
emailLog("error", "email.catch", {
|
||||||
|
type: "reminder_1h",
|
||||||
|
to: row.email,
|
||||||
|
error: String(err),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
emailLog("info", "reminders.cron.done", {
|
||||||
|
sent,
|
||||||
|
errorCount: errors.length,
|
||||||
|
});
|
||||||
|
|
||||||
return { sent, skipped: false, errors };
|
return { sent, skipped: false, errors };
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -1081,6 +1117,9 @@ export async function runSendReminders(): Promise<{
|
|||||||
now >= REMINDER_1H_WINDOW_START.getTime() &&
|
now >= REMINDER_1H_WINDOW_START.getTime() &&
|
||||||
now < REGISTRATION_OPENS_AT.getTime();
|
now < REGISTRATION_OPENS_AT.getTime();
|
||||||
|
|
||||||
|
const activeWindow = in24hWindow ? "24h" : in1hWindow ? "1h" : null;
|
||||||
|
emailLog("info", "reminders.cron.tick", { activeWindow });
|
||||||
|
|
||||||
if (!in24hWindow && !in1hWindow) {
|
if (!in24hWindow && !in1hWindow) {
|
||||||
return { sent: 0, skipped: true, reason: "outside_window", errors: [] };
|
return { sent: 0, skipped: true, reason: "outside_window", errors: [] };
|
||||||
}
|
}
|
||||||
@@ -1094,6 +1133,8 @@ export async function runSendReminders(): Promise<{
|
|||||||
.from(reminder)
|
.from(reminder)
|
||||||
.where(isNull(reminder.sent24hAt));
|
.where(isNull(reminder.sent24hAt));
|
||||||
|
|
||||||
|
emailLog("info", "reminders.24h.pending", { count: pending.length });
|
||||||
|
|
||||||
for (const row of pending) {
|
for (const row of pending) {
|
||||||
try {
|
try {
|
||||||
await sendReminder24hEmail({ to: row.email });
|
await sendReminder24hEmail({ to: row.email });
|
||||||
@@ -1104,7 +1145,11 @@ export async function runSendReminders(): Promise<{
|
|||||||
sent++;
|
sent++;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
errors.push(`24h ${row.email}: ${String(err)}`);
|
errors.push(`24h ${row.email}: ${String(err)}`);
|
||||||
console.error(`Failed to send 24h reminder to ${row.email}:`, err);
|
emailLog("error", "email.catch", {
|
||||||
|
type: "reminder_24h",
|
||||||
|
to: row.email,
|
||||||
|
error: String(err),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1115,6 +1160,8 @@ export async function runSendReminders(): Promise<{
|
|||||||
.from(reminder)
|
.from(reminder)
|
||||||
.where(isNull(reminder.sentAt));
|
.where(isNull(reminder.sentAt));
|
||||||
|
|
||||||
|
emailLog("info", "reminders.1h.pending", { count: pending.length });
|
||||||
|
|
||||||
for (const row of pending) {
|
for (const row of pending) {
|
||||||
try {
|
try {
|
||||||
await sendReminderEmail({ to: row.email });
|
await sendReminderEmail({ to: row.email });
|
||||||
@@ -1125,7 +1172,11 @@ export async function runSendReminders(): Promise<{
|
|||||||
sent++;
|
sent++;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
errors.push(`1h ${row.email}: ${String(err)}`);
|
errors.push(`1h ${row.email}: ${String(err)}`);
|
||||||
console.error(`Failed to send 1h reminder to ${row.email}:`, err);
|
emailLog("error", "email.catch", {
|
||||||
|
type: "reminder_1h",
|
||||||
|
to: row.email,
|
||||||
|
error: String(err),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1144,6 +1195,10 @@ export async function runSendReminders(): Promise<{
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
emailLog("info", "reminders.payment.pending", {
|
||||||
|
count: unpaidWatchers.length,
|
||||||
|
});
|
||||||
|
|
||||||
for (const reg of unpaidWatchers) {
|
for (const reg of unpaidWatchers) {
|
||||||
if (!reg.managementToken) continue;
|
if (!reg.managementToken) continue;
|
||||||
try {
|
try {
|
||||||
@@ -1161,9 +1216,15 @@ export async function runSendReminders(): Promise<{
|
|||||||
sent++;
|
sent++;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
errors.push(`payment-reminder ${reg.email}: ${String(err)}`);
|
errors.push(`payment-reminder ${reg.email}: ${String(err)}`);
|
||||||
console.error(`Failed to send payment reminder to ${reg.email}:`, err);
|
emailLog("error", "email.catch", {
|
||||||
|
type: "payment_reminder",
|
||||||
|
to: reg.email,
|
||||||
|
error: String(err),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
emailLog("info", "reminders.cron.done", { sent, errorCount: errors.length });
|
||||||
|
|
||||||
return { sent, skipped: false, errors };
|
return { sent, skipped: false, errors };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user