Use admin timezone for emails and ticket PDFs
- Email: formatDate/formatTime use site timezone from settings - PDF tickets: date/time formatted in site timezone - Tickets routes: fetch timezone and pass to PDF generation
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
// Email service for Spanglish platform
|
||||
// Supports multiple email providers: Resend, SMTP (Nodemailer)
|
||||
|
||||
import { db, dbGet, dbAll, emailTemplates, emailLogs, events, tickets, payments, users, paymentOptions, eventPaymentOverrides } from '../db/index.js';
|
||||
import { db, dbGet, dbAll, emailTemplates, emailLogs, events, tickets, payments, users, paymentOptions, eventPaymentOverrides, siteSettings } from '../db/index.js';
|
||||
import { eq, and } from 'drizzle-orm';
|
||||
import { getNow, generateId } from './utils.js';
|
||||
import {
|
||||
@@ -324,26 +324,38 @@ export const emailService = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Format date for emails
|
||||
* Get the site timezone from settings (cached for performance)
|
||||
*/
|
||||
formatDate(dateStr: string, locale: string = 'en'): string {
|
||||
async getSiteTimezone(): Promise<string> {
|
||||
const settings = await dbGet<any>(
|
||||
(db as any).select().from(siteSettings).limit(1)
|
||||
);
|
||||
return settings?.timezone || 'America/Asuncion';
|
||||
},
|
||||
|
||||
/**
|
||||
* Format date for emails using site timezone
|
||||
*/
|
||||
formatDate(dateStr: string, locale: string = 'en', timezone: string = 'America/Asuncion'): string {
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleDateString(locale === 'es' ? 'es-ES' : 'en-US', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
timeZone: timezone,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Format time for emails
|
||||
* Format time for emails using site timezone
|
||||
*/
|
||||
formatTime(dateStr: string, locale: string = 'en'): string {
|
||||
formatTime(dateStr: string, locale: string = 'en', timezone: string = 'America/Asuncion'): string {
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleTimeString(locale === 'es' ? 'es-ES' : 'en-US', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
timeZone: timezone,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -579,6 +591,9 @@ export const emailService = {
|
||||
// Calculate total price for multi-ticket bookings
|
||||
const totalPrice = event.price * ticketCount;
|
||||
|
||||
// Get site timezone for proper date/time formatting
|
||||
const timezone = await this.getSiteTimezone();
|
||||
|
||||
return this.sendTemplateEmail({
|
||||
templateSlug: 'booking-confirmation',
|
||||
to: ticket.attendeeEmail,
|
||||
@@ -593,8 +608,8 @@ export const emailService = {
|
||||
qrCode: ticket.qrCode || '',
|
||||
ticketPdfUrl,
|
||||
eventTitle,
|
||||
eventDate: this.formatDate(event.startDatetime, locale),
|
||||
eventTime: this.formatTime(event.startDatetime, locale),
|
||||
eventDate: this.formatDate(event.startDatetime, locale, timezone),
|
||||
eventTime: this.formatTime(event.startDatetime, locale, timezone),
|
||||
eventLocation: event.location,
|
||||
eventLocationUrl: event.locationUrl || '',
|
||||
eventPrice: this.formatCurrency(event.price, event.currency),
|
||||
@@ -687,6 +702,9 @@ export const emailService = {
|
||||
? `${this.formatCurrency(totalAmount, payment.currency)} (${ticketCount} tickets)`
|
||||
: this.formatCurrency(totalAmount, payment.currency);
|
||||
|
||||
// Get site timezone for proper date/time formatting
|
||||
const timezone = await this.getSiteTimezone();
|
||||
|
||||
return this.sendTemplateEmail({
|
||||
templateSlug: 'payment-receipt',
|
||||
to: ticket.attendeeEmail,
|
||||
@@ -697,11 +715,11 @@ export const emailService = {
|
||||
attendeeName: receiptFullName,
|
||||
ticketId: ticket.bookingId || ticket.id,
|
||||
eventTitle,
|
||||
eventDate: this.formatDate(event.startDatetime, locale),
|
||||
eventDate: this.formatDate(event.startDatetime, locale, timezone),
|
||||
paymentAmount: amountDisplay,
|
||||
paymentMethod: paymentMethodNames[locale]?.[payment.provider] || payment.provider,
|
||||
paymentReference: payment.reference || payment.id,
|
||||
paymentDate: this.formatDate(payment.paidAt || payment.createdAt, locale),
|
||||
paymentDate: this.formatDate(payment.paidAt || payment.createdAt, locale, timezone),
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -846,14 +864,17 @@ export const emailService = {
|
||||
? `${this.formatCurrency(totalPrice, event.currency)} (${ticketCount} tickets)`
|
||||
: this.formatCurrency(totalPrice, event.currency);
|
||||
|
||||
// Get site timezone for proper date/time formatting
|
||||
const timezone = await this.getSiteTimezone();
|
||||
|
||||
// Build variables based on payment method
|
||||
const variables: Record<string, any> = {
|
||||
attendeeName: attendeeFullName,
|
||||
attendeeEmail: ticket.attendeeEmail,
|
||||
ticketId: ticket.bookingId || ticket.id,
|
||||
eventTitle,
|
||||
eventDate: this.formatDate(event.startDatetime, locale),
|
||||
eventTime: this.formatTime(event.startDatetime, locale),
|
||||
eventDate: this.formatDate(event.startDatetime, locale, timezone),
|
||||
eventTime: this.formatTime(event.startDatetime, locale, timezone),
|
||||
eventLocation: event.location,
|
||||
eventLocationUrl: event.locationUrl || '',
|
||||
paymentAmount: amountDisplay,
|
||||
@@ -934,6 +955,9 @@ export const emailService = {
|
||||
const frontendUrl = process.env.FRONTEND_URL || 'https://spanglish.com';
|
||||
const newBookingUrl = `${frontendUrl}/book/${event.id}`;
|
||||
|
||||
// Get site timezone for proper date/time formatting
|
||||
const timezone = await this.getSiteTimezone();
|
||||
|
||||
console.log(`[Email] Sending payment rejection email to ${ticket.attendeeEmail}`);
|
||||
|
||||
return this.sendTemplateEmail({
|
||||
@@ -947,8 +971,8 @@ export const emailService = {
|
||||
attendeeEmail: ticket.attendeeEmail,
|
||||
ticketId: ticket.id,
|
||||
eventTitle,
|
||||
eventDate: this.formatDate(event.startDatetime, locale),
|
||||
eventTime: this.formatTime(event.startDatetime, locale),
|
||||
eventDate: this.formatDate(event.startDatetime, locale, timezone),
|
||||
eventTime: this.formatTime(event.startDatetime, locale, timezone),
|
||||
eventLocation: event.location,
|
||||
eventLocationUrl: event.locationUrl || '',
|
||||
newBookingUrl,
|
||||
@@ -1001,6 +1025,9 @@ export const emailService = {
|
||||
return { success: true, sentCount: 0, failedCount: 0, errors: ['No recipients found'] };
|
||||
}
|
||||
|
||||
// Get site timezone for proper date/time formatting
|
||||
const timezone = await this.getSiteTimezone();
|
||||
|
||||
let sentCount = 0;
|
||||
let failedCount = 0;
|
||||
const errors: string[] = [];
|
||||
@@ -1023,8 +1050,8 @@ export const emailService = {
|
||||
attendeeEmail: ticket.attendeeEmail,
|
||||
ticketId: ticket.id,
|
||||
eventTitle,
|
||||
eventDate: this.formatDate(event.startDatetime, locale),
|
||||
eventTime: this.formatTime(event.startDatetime, locale),
|
||||
eventDate: this.formatDate(event.startDatetime, locale, timezone),
|
||||
eventTime: this.formatTime(event.startDatetime, locale, timezone),
|
||||
eventLocation: event.location,
|
||||
eventLocationUrl: event.locationUrl || '',
|
||||
...customVariables,
|
||||
|
||||
@@ -14,6 +14,7 @@ interface TicketData {
|
||||
location: string;
|
||||
locationUrl?: string;
|
||||
};
|
||||
timezone?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -29,27 +30,29 @@ async function generateQRCode(data: string): Promise<Buffer> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date for display
|
||||
* Format date for display using site timezone
|
||||
*/
|
||||
function formatDate(dateStr: string): string {
|
||||
function formatDate(dateStr: string, timezone: string = 'America/Asuncion'): string {
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleDateString('en-US', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
timeZone: timezone,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Format time for display
|
||||
* Format time for display using site timezone
|
||||
*/
|
||||
function formatTime(dateStr: string): string {
|
||||
function formatTime(dateStr: string, timezone: string = 'America/Asuncion'): string {
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleTimeString('en-US', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: true,
|
||||
timeZone: timezone,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -89,12 +92,13 @@ export async function generateTicketPDF(ticket: TicketData): Promise<Buffer> {
|
||||
doc.fontSize(22).fillColor('#1a1a1a').text(ticket.event.title, { align: 'center' });
|
||||
doc.moveDown(0.5);
|
||||
|
||||
// Date and time
|
||||
// Date and time (using site timezone)
|
||||
const tz = ticket.timezone || 'America/Asuncion';
|
||||
doc.fontSize(14).fillColor('#333');
|
||||
doc.text(formatDate(ticket.event.startDatetime), { align: 'center' });
|
||||
doc.text(formatDate(ticket.event.startDatetime, tz), { align: 'center' });
|
||||
|
||||
const startTime = formatTime(ticket.event.startDatetime);
|
||||
const endTime = ticket.event.endDatetime ? formatTime(ticket.event.endDatetime) : null;
|
||||
const startTime = formatTime(ticket.event.startDatetime, tz);
|
||||
const endTime = ticket.event.endDatetime ? formatTime(ticket.event.endDatetime, tz) : null;
|
||||
const timeRange = endTime ? `${startTime} - ${endTime}` : startTime;
|
||||
doc.text(timeRange, { align: 'center' });
|
||||
|
||||
@@ -184,11 +188,13 @@ export async function generateCombinedTicketsPDF(tickets: TicketData[]): Promise
|
||||
doc.fontSize(22).fillColor('#1a1a1a').text(ticket.event.title, { align: 'center' });
|
||||
doc.moveDown(0.5);
|
||||
|
||||
// Date and time (using site timezone)
|
||||
const tz = ticket.timezone || 'America/Asuncion';
|
||||
doc.fontSize(14).fillColor('#333');
|
||||
doc.text(formatDate(ticket.event.startDatetime), { align: 'center' });
|
||||
doc.text(formatDate(ticket.event.startDatetime, tz), { align: 'center' });
|
||||
|
||||
const startTime = formatTime(ticket.event.startDatetime);
|
||||
const endTime = ticket.event.endDatetime ? formatTime(ticket.event.endDatetime) : null;
|
||||
const startTime = formatTime(ticket.event.startDatetime, tz);
|
||||
const endTime = ticket.event.endDatetime ? formatTime(ticket.event.endDatetime, tz) : null;
|
||||
const timeRange = endTime ? `${startTime} - ${endTime}` : startTime;
|
||||
doc.text(timeRange, { align: 'center' });
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Hono } from 'hono';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
import { z } from 'zod';
|
||||
import { db, dbGet, dbAll, tickets, events, users, payments, paymentOptions } from '../db/index.js';
|
||||
import { db, dbGet, dbAll, tickets, events, users, payments, paymentOptions, siteSettings } from '../db/index.js';
|
||||
import { eq, and, sql } from 'drizzle-orm';
|
||||
import { requireAuth, getAuthUser } from '../lib/auth.js';
|
||||
import { generateId, generateTicketCode, getNow } from '../lib/utils.js';
|
||||
@@ -378,6 +378,12 @@ ticketsRouter.get('/booking/:bookingId/pdf', async (c) => {
|
||||
|
||||
console.log(`[PDF] Generating PDF with ${confirmedTickets.length} confirmed tickets`);
|
||||
|
||||
// Get site timezone for proper date/time formatting
|
||||
const settings = await dbGet<any>(
|
||||
(db as any).select().from(siteSettings).limit(1)
|
||||
);
|
||||
const timezone = settings?.timezone || 'America/Asuncion';
|
||||
|
||||
const ticketsData = confirmedTickets.map((ticket: any) => ({
|
||||
id: ticket.id,
|
||||
qrCode: ticket.qrCode,
|
||||
@@ -390,6 +396,7 @@ ticketsRouter.get('/booking/:bookingId/pdf', async (c) => {
|
||||
location: event.location,
|
||||
locationUrl: event.locationUrl,
|
||||
},
|
||||
timezone,
|
||||
}));
|
||||
|
||||
const pdfBuffer = await generateCombinedTicketsPDF(ticketsData);
|
||||
@@ -448,6 +455,12 @@ ticketsRouter.get('/:id/pdf', async (c) => {
|
||||
}
|
||||
|
||||
try {
|
||||
// Get site timezone for proper date/time formatting
|
||||
const settings = await dbGet<any>(
|
||||
(db as any).select().from(siteSettings).limit(1)
|
||||
);
|
||||
const timezone = settings?.timezone || 'America/Asuncion';
|
||||
|
||||
const pdfBuffer = await generateTicketPDF({
|
||||
id: ticket.id,
|
||||
qrCode: ticket.qrCode,
|
||||
@@ -460,6 +473,7 @@ ticketsRouter.get('/:id/pdf', async (c) => {
|
||||
location: event.location,
|
||||
locationUrl: event.locationUrl,
|
||||
},
|
||||
timezone,
|
||||
});
|
||||
|
||||
// Set response headers for PDF download
|
||||
|
||||
Reference in New Issue
Block a user