Split oversized frontend API client, email service, and admin/booking pages into focused modules while preserving import surfaces, and add Redis-backed queues, stale booking cleanup, stronger auth, and scale deployment configs. Co-authored-by: Cursor <cursoragent@cursor.com>
102 lines
3.2 KiB
TypeScript
102 lines
3.2 KiB
TypeScript
// Event-wide bulk email sending via the background queue.
|
|
|
|
import { db, dbGet, dbAll, events, tickets } from '../../db/index.js';
|
|
import { eq, and } from 'drizzle-orm';
|
|
import { enqueueBulkEmails, type TemplateEmailJobParams } from '../emailQueue.js';
|
|
import { getTemplate } from './templateService.js';
|
|
import { formatDate, formatTime, getSiteTimezone } from './formatting.js';
|
|
|
|
/**
|
|
* Queue emails for event attendees (non-blocking).
|
|
* Adds all matching recipients to the background email queue and returns immediately.
|
|
* Rate limiting and actual sending is handled by the email queue.
|
|
*/
|
|
export async function queueEventEmails(params: {
|
|
eventId: string;
|
|
templateSlug: string;
|
|
customVariables?: Record<string, any>;
|
|
recipientFilter?: 'all' | 'confirmed' | 'pending' | 'checked_in';
|
|
sentBy: string;
|
|
}): Promise<{ success: boolean; queuedCount: number; error?: string }> {
|
|
const { eventId, templateSlug, customVariables = {}, recipientFilter = 'confirmed', sentBy } = params;
|
|
|
|
// Validate event exists
|
|
const event = await dbGet<any>(
|
|
(db as any)
|
|
.select()
|
|
.from(events)
|
|
.where(eq((events as any).id, eventId))
|
|
);
|
|
|
|
if (!event) {
|
|
return { success: false, queuedCount: 0, error: 'Event not found' };
|
|
}
|
|
|
|
// Validate template exists
|
|
const template = await getTemplate(templateSlug);
|
|
if (!template) {
|
|
return { success: false, queuedCount: 0, error: `Template "${templateSlug}" not found` };
|
|
}
|
|
|
|
// Get tickets based on filter
|
|
let ticketQuery = (db as any)
|
|
.select()
|
|
.from(tickets)
|
|
.where(eq((tickets as any).eventId, eventId));
|
|
|
|
if (recipientFilter !== 'all') {
|
|
ticketQuery = ticketQuery.where(
|
|
and(
|
|
eq((tickets as any).eventId, eventId),
|
|
eq((tickets as any).status, recipientFilter)
|
|
)
|
|
);
|
|
}
|
|
|
|
const eventTickets = await dbAll<any>(ticketQuery);
|
|
|
|
if (eventTickets.length === 0) {
|
|
return { success: true, queuedCount: 0, error: 'No recipients found' };
|
|
}
|
|
|
|
// Get site timezone for proper date/time formatting
|
|
const timezone = await getSiteTimezone();
|
|
|
|
// Build individual email jobs for the queue
|
|
const jobs: TemplateEmailJobParams[] = eventTickets.map((ticket: any) => {
|
|
const locale = ticket.preferredLanguage || 'en';
|
|
const eventTitle = locale === 'es' && event.titleEs ? event.titleEs : event.title;
|
|
const fullName = `${ticket.attendeeFirstName} ${ticket.attendeeLastName || ''}`.trim();
|
|
|
|
return {
|
|
templateSlug,
|
|
to: ticket.attendeeEmail,
|
|
toName: fullName,
|
|
locale,
|
|
eventId: event.id,
|
|
sentBy,
|
|
variables: {
|
|
attendeeName: fullName,
|
|
attendeeEmail: ticket.attendeeEmail,
|
|
ticketId: ticket.id,
|
|
eventTitle,
|
|
eventDate: formatDate(event.startDatetime, locale, timezone),
|
|
eventTime: formatTime(event.startDatetime, locale, timezone),
|
|
eventLocation: event.location,
|
|
eventLocationUrl: event.locationUrl || '',
|
|
...customVariables,
|
|
},
|
|
};
|
|
});
|
|
|
|
// Enqueue all emails for background processing
|
|
enqueueBulkEmails(jobs);
|
|
|
|
console.log(`[Email] Queued ${jobs.length} emails for event "${event.title}" (filter: ${recipientFilter})`);
|
|
|
|
return {
|
|
success: true,
|
|
queuedCount: jobs.length,
|
|
};
|
|
}
|