Files
Spanglish/backend/src/lib/email/bookingEmails.ts
T
MichilisandCursor 613bd7be1d Refactor monolithic modules and harden booking, email, and auth infrastructure.
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>
2026-06-25 07:12:59 +00:00

97 lines
3.1 KiB
TypeScript

// High-level booking confirmation email sender.
import { db, dbGet, dbAll, events, tickets } from '../../db/index.js';
import { eq } from 'drizzle-orm';
import { sendTemplateEmail } from './templateService.js';
import { formatDate, formatTime, formatCurrency, getSiteTimezone } from './formatting.js';
/**
* Send booking confirmation email
* Supports multi-ticket bookings - includes all tickets in the booking
*/
export async function sendBookingConfirmation(ticketId: string): Promise<{ success: boolean; error?: string }> {
// Get ticket with event info
const ticket = await dbGet<any>(
(db as any)
.select()
.from(tickets)
.where(eq((tickets as any).id, ticketId))
);
if (!ticket) {
return { success: false, error: 'Ticket not found' };
}
const event = await dbGet<any>(
(db as any)
.select()
.from(events)
.where(eq((events as any).id, ticket.eventId))
);
if (!event) {
return { success: false, error: 'Event not found' };
}
// Get all tickets in this booking (if multi-ticket)
let allTickets: any[] = [ticket];
if (ticket.bookingId) {
allTickets = await dbAll(
(db as any)
.select()
.from(tickets)
.where(eq((tickets as any).bookingId, ticket.bookingId))
);
}
const ticketCount = allTickets.length;
const locale = ticket.preferredLanguage || 'en';
const eventTitle = locale === 'es' && event.titleEs ? event.titleEs : event.title;
// Generate ticket PDF URL (primary ticket, or use combined endpoint for multi)
const apiUrl = process.env.API_URL || 'http://localhost:3001';
const ticketPdfUrl = ticketCount > 1 && ticket.bookingId
? `${apiUrl}/api/tickets/booking/${ticket.bookingId}/pdf`
: `${apiUrl}/api/tickets/${ticket.id}/pdf`;
const attendeeFullName = `${ticket.attendeeFirstName} ${ticket.attendeeLastName || ''}`.trim();
// Build attendee list for multi-ticket emails
const attendeeNames = allTickets.map(t =>
`${t.attendeeFirstName} ${t.attendeeLastName || ''}`.trim()
).join(', ');
// Calculate total price for multi-ticket bookings
const totalPrice = event.price * ticketCount;
// Get site timezone for proper date/time formatting
const timezone = await getSiteTimezone();
return sendTemplateEmail({
templateSlug: 'booking-confirmation',
to: ticket.attendeeEmail,
toName: attendeeFullName,
locale,
eventId: event.id,
variables: {
attendeeName: attendeeFullName,
attendeeEmail: ticket.attendeeEmail,
ticketId: ticket.id,
bookingId: ticket.bookingId || ticket.id,
qrCode: ticket.qrCode || '',
ticketPdfUrl,
eventTitle,
eventDate: formatDate(event.startDatetime, locale, timezone),
eventTime: formatTime(event.startDatetime, locale, timezone),
eventLocation: event.location,
eventLocationUrl: event.locationUrl || '',
eventPrice: formatCurrency(event.price, event.currency),
// Multi-ticket specific variables
ticketCount: ticketCount.toString(),
totalPrice: formatCurrency(totalPrice, event.currency),
attendeeNames,
isMultiTicket: ticketCount > 1 ? 'true' : 'false',
},
});
}