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>
97 lines
3.1 KiB
TypeScript
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',
|
|
},
|
|
});
|
|
}
|