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:
Michilis
2026-02-03 18:40:39 +00:00
parent 9090d7bad2
commit 0fd8172e04
3 changed files with 74 additions and 27 deletions

View File

@@ -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,