Files
kunstenkamp/packages/api/src/email-queue.ts
zias e5d2b13b21 feat(email): route all email sends through Cloudflare Queue
Introduces a CF Queue binding (kk-email-queue) to decouple email
delivery from request handlers, preventing slow responses and
providing automatic retries. All send*Email calls now go through
the queue when the binding is available, with direct-send fallbacks
for local dev. Reminder fan-outs mark DB rows optimistically before
enqueueing to prevent re-enqueue on subsequent cron ticks.
2026-03-11 17:13:35 +01:00

148 lines
3.5 KiB
TypeScript

/**
* Email queue types and dispatcher.
*
* All email sends are modelled as a discriminated union so the Cloudflare Queue
* consumer can pattern-match on `msg.type` and call the right send*Email().
*
* The `Queue<EmailMessage>` type used in context.ts / server.ts refers to the
* CF runtime binding type (`import type { Queue } from "@cloudflare/workers-types"`).
*/
import {
sendCancellationEmail,
sendConfirmationEmail,
sendPaymentConfirmationEmail,
sendPaymentReminderEmail,
sendReminder24hEmail,
sendReminderEmail,
sendSubscriptionConfirmationEmail,
sendUpdateEmail,
} from "./email";
import { sendDeductionEmail } from "./lib/drinkkaart-email";
// ---------------------------------------------------------------------------
// Message types — one variant per send*Email function
// ---------------------------------------------------------------------------
export type ConfirmationMessage = {
type: "registrationConfirmation";
to: string;
firstName: string;
managementToken: string;
wantsToPerform: boolean;
artForm?: string | null;
giftAmount?: number;
drinkCardValue?: number;
signupUrl?: string;
};
export type UpdateMessage = {
type: "updateConfirmation";
to: string;
firstName: string;
managementToken: string;
wantsToPerform: boolean;
artForm?: string | null;
giftAmount?: number;
drinkCardValue?: number;
};
export type CancellationMessage = {
type: "cancellation";
to: string;
firstName: string;
};
export type SubscriptionConfirmationMessage = {
type: "subscriptionConfirmation";
to: string;
};
export type Reminder24hMessage = {
type: "reminder24h";
to: string;
firstName?: string | null;
};
export type Reminder1hMessage = {
type: "reminder1h";
to: string;
firstName?: string | null;
};
export type PaymentReminderMessage = {
type: "paymentReminder";
to: string;
firstName: string;
managementToken: string;
drinkCardValue?: number;
giftAmount?: number;
};
export type PaymentConfirmationMessage = {
type: "paymentConfirmation";
to: string;
firstName: string;
managementToken: string;
drinkCardValue?: number;
giftAmount?: number;
};
export type DeductionMessage = {
type: "deduction";
to: string;
firstName: string;
amountCents: number;
newBalanceCents: number;
};
export type EmailMessage =
| ConfirmationMessage
| UpdateMessage
| CancellationMessage
| SubscriptionConfirmationMessage
| Reminder24hMessage
| Reminder1hMessage
| PaymentReminderMessage
| PaymentConfirmationMessage
| DeductionMessage;
// ---------------------------------------------------------------------------
// Consumer-side dispatcher — called once per queue message
// ---------------------------------------------------------------------------
export async function dispatchEmailMessage(msg: EmailMessage): Promise<void> {
switch (msg.type) {
case "registrationConfirmation":
await sendConfirmationEmail(msg);
break;
case "updateConfirmation":
await sendUpdateEmail(msg);
break;
case "cancellation":
await sendCancellationEmail(msg);
break;
case "subscriptionConfirmation":
await sendSubscriptionConfirmationEmail({ to: msg.to });
break;
case "reminder24h":
await sendReminder24hEmail(msg);
break;
case "reminder1h":
await sendReminderEmail(msg);
break;
case "paymentReminder":
await sendPaymentReminderEmail(msg);
break;
case "paymentConfirmation":
await sendPaymentConfirmationEmail(msg);
break;
case "deduction":
await sendDeductionEmail(msg);
break;
default:
// Exhaustiveness check — TypeScript will catch unhandled variants at compile time
msg satisfies never;
}
}