Update site changes
This commit is contained in:
@@ -83,11 +83,21 @@ async function migrate() {
|
||||
capacity INTEGER NOT NULL DEFAULT 50,
|
||||
status TEXT NOT NULL DEFAULT 'draft',
|
||||
banner_url TEXT,
|
||||
external_booking_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
external_booking_url TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
)
|
||||
`);
|
||||
|
||||
// Add external booking columns to events if they don't exist (for existing databases)
|
||||
try {
|
||||
await (db as any).run(sql`ALTER TABLE events ADD COLUMN external_booking_enabled INTEGER NOT NULL DEFAULT 0`);
|
||||
} catch (e) { /* column may already exist */ }
|
||||
try {
|
||||
await (db as any).run(sql`ALTER TABLE events ADD COLUMN external_booking_url TEXT`);
|
||||
} catch (e) { /* column may already exist */ }
|
||||
|
||||
await (db as any).run(sql`
|
||||
CREATE TABLE IF NOT EXISTS tickets (
|
||||
id TEXT PRIMARY KEY,
|
||||
@@ -414,11 +424,21 @@ async function migrate() {
|
||||
capacity INTEGER NOT NULL DEFAULT 50,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'draft',
|
||||
banner_url VARCHAR(500),
|
||||
external_booking_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
external_booking_url VARCHAR(500),
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP NOT NULL
|
||||
)
|
||||
`);
|
||||
|
||||
// Add external booking columns to events if they don't exist (for existing databases)
|
||||
try {
|
||||
await (db as any).execute(sql`ALTER TABLE events ADD COLUMN external_booking_enabled INTEGER NOT NULL DEFAULT 0`);
|
||||
} catch (e) { /* column may already exist */ }
|
||||
try {
|
||||
await (db as any).execute(sql`ALTER TABLE events ADD COLUMN external_booking_url VARCHAR(500)`);
|
||||
} catch (e) { /* column may already exist */ }
|
||||
|
||||
await (db as any).execute(sql`
|
||||
CREATE TABLE IF NOT EXISTS tickets (
|
||||
id UUID PRIMARY KEY,
|
||||
|
||||
@@ -75,6 +75,8 @@ export const sqliteEvents = sqliteTable('events', {
|
||||
capacity: integer('capacity').notNull().default(50),
|
||||
status: text('status', { enum: ['draft', 'published', 'cancelled', 'completed', 'archived'] }).notNull().default('draft'),
|
||||
bannerUrl: text('banner_url'),
|
||||
externalBookingEnabled: integer('external_booking_enabled', { mode: 'boolean' }).notNull().default(false),
|
||||
externalBookingUrl: text('external_booking_url'),
|
||||
createdAt: text('created_at').notNull(),
|
||||
updatedAt: text('updated_at').notNull(),
|
||||
});
|
||||
@@ -315,6 +317,8 @@ export const pgEvents = pgTable('events', {
|
||||
capacity: pgInteger('capacity').notNull().default(50),
|
||||
status: varchar('status', { length: 20 }).notNull().default('draft'),
|
||||
bannerUrl: varchar('banner_url', { length: 500 }),
|
||||
externalBookingEnabled: pgInteger('external_booking_enabled').notNull().default(0),
|
||||
externalBookingUrl: varchar('external_booking_url', { length: 500 }),
|
||||
createdAt: timestamp('created_at').notNull(),
|
||||
updatedAt: timestamp('updated_at').notNull(),
|
||||
});
|
||||
|
||||
@@ -786,6 +786,74 @@ export const emailService = {
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Send payment rejection email
|
||||
* This email is sent when admin rejects a TPago or Bank Transfer payment
|
||||
*/
|
||||
async sendPaymentRejectionEmail(paymentId: string): Promise<{ success: boolean; error?: string }> {
|
||||
// Get payment
|
||||
const payment = await (db as any)
|
||||
.select()
|
||||
.from(payments)
|
||||
.where(eq((payments as any).id, paymentId))
|
||||
.get();
|
||||
|
||||
if (!payment) {
|
||||
return { success: false, error: 'Payment not found' };
|
||||
}
|
||||
|
||||
// Get ticket
|
||||
const ticket = await (db as any)
|
||||
.select()
|
||||
.from(tickets)
|
||||
.where(eq((tickets as any).id, payment.ticketId))
|
||||
.get();
|
||||
|
||||
if (!ticket) {
|
||||
return { success: false, error: 'Ticket not found' };
|
||||
}
|
||||
|
||||
// Get event
|
||||
const event = await (db as any)
|
||||
.select()
|
||||
.from(events)
|
||||
.where(eq((events as any).id, ticket.eventId))
|
||||
.get();
|
||||
|
||||
if (!event) {
|
||||
return { success: false, error: 'Event not found' };
|
||||
}
|
||||
|
||||
const locale = ticket.preferredLanguage || 'en';
|
||||
const eventTitle = locale === 'es' && event.titleEs ? event.titleEs : event.title;
|
||||
const attendeeFullName = `${ticket.attendeeFirstName} ${ticket.attendeeLastName || ''}`.trim();
|
||||
|
||||
// Generate a new booking URL for the event
|
||||
const frontendUrl = process.env.FRONTEND_URL || 'https://spanglish.com';
|
||||
const newBookingUrl = `${frontendUrl}/book/${event.id}`;
|
||||
|
||||
console.log(`[Email] Sending payment rejection email to ${ticket.attendeeEmail}`);
|
||||
|
||||
return this.sendTemplateEmail({
|
||||
templateSlug: 'payment-rejected',
|
||||
to: ticket.attendeeEmail,
|
||||
toName: attendeeFullName,
|
||||
locale,
|
||||
eventId: event.id,
|
||||
variables: {
|
||||
attendeeName: attendeeFullName,
|
||||
attendeeEmail: ticket.attendeeEmail,
|
||||
ticketId: ticket.id,
|
||||
eventTitle,
|
||||
eventDate: this.formatDate(event.startDatetime, locale),
|
||||
eventTime: this.formatTime(event.startDatetime, locale),
|
||||
eventLocation: event.location,
|
||||
eventLocationUrl: event.locationUrl || '',
|
||||
newBookingUrl,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Send custom email to event attendees
|
||||
*/
|
||||
|
||||
@@ -673,6 +673,13 @@ El Equipo de Spanglish`,
|
||||
If the button doesn't work: <a href="{{tpagoLink}}" style="color: #3b82f6; word-break: break-all;">{{tpagoLink}}</a>
|
||||
</p>
|
||||
|
||||
<div class="note" style="background-color: #fef3c7; border-left-color: #f59e0b;">
|
||||
<strong>Important - Manual Verification Process:</strong><br>
|
||||
Please make sure you complete the payment before clicking "I have paid".<br>
|
||||
The Spanglish team will review the payment manually.<br>
|
||||
<strong>Your booking is only confirmed after you receive a confirmation email from us.</strong>
|
||||
</div>
|
||||
|
||||
<div class="note">
|
||||
<strong>After completing the payment:</strong><br>
|
||||
Return to the website and click <strong>"I have paid"</strong> or click the button below to notify us.
|
||||
@@ -685,8 +692,6 @@ El Equipo de Spanglish`,
|
||||
Or use this link: <a href="{{bookingUrl}}" style="color: #f59e0b; word-break: break-all;">{{bookingUrl}}</a>
|
||||
</p>
|
||||
|
||||
<p>Your spot will be confirmed once we verify the payment.</p>
|
||||
|
||||
<p>If you have any questions, just reply to this email.</p>
|
||||
<p>See you soon,<br>Spanglish</p>
|
||||
`,
|
||||
@@ -711,6 +716,13 @@ El Equipo de Spanglish`,
|
||||
Si el botón no funciona: <a href="{{tpagoLink}}" style="color: #3b82f6; word-break: break-all;">{{tpagoLink}}</a>
|
||||
</p>
|
||||
|
||||
<div class="note" style="background-color: #fef3c7; border-left-color: #f59e0b;">
|
||||
<strong>Importante - Proceso de Verificación Manual:</strong><br>
|
||||
Por favor asegúrate de completar el pago antes de hacer clic en "Ya pagué".<br>
|
||||
El equipo de Spanglish revisará el pago manualmente.<br>
|
||||
<strong>Tu reserva solo será confirmada después de que recibas un email de confirmación de nuestra parte.</strong>
|
||||
</div>
|
||||
|
||||
<div class="note">
|
||||
<strong>Después de completar el pago:</strong><br>
|
||||
Vuelve al sitio web y haz clic en <strong>"Ya pagué"</strong> o haz clic en el botón de abajo para notificarnos.
|
||||
@@ -723,8 +735,6 @@ El Equipo de Spanglish`,
|
||||
O usa este enlace: <a href="{{bookingUrl}}" style="color: #f59e0b; word-break: break-all;">{{bookingUrl}}</a>
|
||||
</p>
|
||||
|
||||
<p>Tu lugar será confirmado una vez que verifiquemos el pago.</p>
|
||||
|
||||
<p>Si tienes alguna pregunta, simplemente responde a este email.</p>
|
||||
<p>¡Nos vemos pronto!<br>Spanglish</p>
|
||||
`,
|
||||
@@ -743,12 +753,15 @@ Please complete your payment using TPago at the link below:
|
||||
|
||||
👉 Pay with card: {{tpagoLink}}
|
||||
|
||||
⚠️ IMPORTANT - Manual Verification Process:
|
||||
Please make sure you complete the payment before clicking "I have paid".
|
||||
The Spanglish team will review the payment manually.
|
||||
Your booking is only confirmed after you receive a confirmation email from us.
|
||||
|
||||
After completing the payment, return to the website and click "I have paid" or use this link to notify us:
|
||||
|
||||
{{bookingUrl}}
|
||||
|
||||
Your spot will be confirmed once we verify the payment.
|
||||
|
||||
If you have any questions, just reply to this email.
|
||||
|
||||
See you soon,
|
||||
@@ -768,12 +781,15 @@ Por favor completa tu pago usando TPago en el siguiente enlace:
|
||||
|
||||
👉 Pagar con tarjeta: {{tpagoLink}}
|
||||
|
||||
⚠️ IMPORTANTE - Proceso de Verificación Manual:
|
||||
Por favor asegúrate de completar el pago antes de hacer clic en "Ya pagué".
|
||||
El equipo de Spanglish revisará el pago manualmente.
|
||||
Tu reserva solo será confirmada después de que recibas un email de confirmación de nuestra parte.
|
||||
|
||||
Después de completar el pago, vuelve al sitio web y haz clic en "Ya pagué" o usa este enlace para notificarnos:
|
||||
|
||||
{{bookingUrl}}
|
||||
|
||||
Tu lugar será confirmado una vez que verifiquemos el pago.
|
||||
|
||||
Si tienes alguna pregunta, simplemente responde a este email.
|
||||
|
||||
¡Nos vemos pronto!
|
||||
@@ -824,6 +840,13 @@ Spanglish`,
|
||||
<div class="event-detail"><strong>Reference:</strong> <span style="font-family: monospace; background: #fff; padding: 2px 6px; border-radius: 4px;">{{paymentReference}}</span></div>
|
||||
</div>
|
||||
|
||||
<div class="note" style="background-color: #fef3c7; border-left-color: #f59e0b;">
|
||||
<strong>Important - Manual Verification Process:</strong><br>
|
||||
Please make sure you complete the payment before clicking "I have paid".<br>
|
||||
The Spanglish team will review the payment manually.<br>
|
||||
<strong>Your booking is only confirmed after you receive a confirmation email from us.</strong>
|
||||
</div>
|
||||
|
||||
<div class="note">
|
||||
<strong>After making the transfer:</strong><br>
|
||||
Return to the website and click <strong>"I have paid"</strong> or click the button below to notify us.
|
||||
@@ -836,8 +859,6 @@ Spanglish`,
|
||||
Or use this link: <a href="{{bookingUrl}}" style="color: #f59e0b; word-break: break-all;">{{bookingUrl}}</a>
|
||||
</p>
|
||||
|
||||
<p>We'll confirm your spot as soon as the payment is received.</p>
|
||||
|
||||
<p>If you need help, reply to this email.</p>
|
||||
<p>See you at the event,<br>Spanglish</p>
|
||||
`,
|
||||
@@ -872,6 +893,13 @@ Spanglish`,
|
||||
<div class="event-detail"><strong>Referencia:</strong> <span style="font-family: monospace; background: #fff; padding: 2px 6px; border-radius: 4px;">{{paymentReference}}</span></div>
|
||||
</div>
|
||||
|
||||
<div class="note" style="background-color: #fef3c7; border-left-color: #f59e0b;">
|
||||
<strong>Importante - Proceso de Verificación Manual:</strong><br>
|
||||
Por favor asegúrate de completar el pago antes de hacer clic en "Ya pagué".<br>
|
||||
El equipo de Spanglish revisará el pago manualmente.<br>
|
||||
<strong>Tu reserva solo será confirmada después de que recibas un email de confirmación de nuestra parte.</strong>
|
||||
</div>
|
||||
|
||||
<div class="note">
|
||||
<strong>Después de realizar la transferencia:</strong><br>
|
||||
Vuelve al sitio web y haz clic en <strong>"Ya pagué"</strong> o haz clic en el botón de abajo para notificarnos.
|
||||
@@ -884,8 +912,6 @@ Spanglish`,
|
||||
O usa este enlace: <a href="{{bookingUrl}}" style="color: #f59e0b; word-break: break-all;">{{bookingUrl}}</a>
|
||||
</p>
|
||||
|
||||
<p>Confirmaremos tu lugar tan pronto como recibamos el pago.</p>
|
||||
|
||||
<p>Si necesitas ayuda, responde a este email.</p>
|
||||
<p>¡Nos vemos en el evento!<br>Spanglish</p>
|
||||
`,
|
||||
@@ -907,12 +933,15 @@ Bank Transfer Details:
|
||||
- Phone: {{bankPhone}}
|
||||
- Reference: {{paymentReference}}
|
||||
|
||||
⚠️ IMPORTANT - Manual Verification Process:
|
||||
Please make sure you complete the payment before clicking "I have paid".
|
||||
The Spanglish team will review the payment manually.
|
||||
Your booking is only confirmed after you receive a confirmation email from us.
|
||||
|
||||
After making the transfer, return to the website and click "I have paid" or use this link to notify us:
|
||||
|
||||
{{bookingUrl}}
|
||||
|
||||
We'll confirm your spot as soon as the payment is received.
|
||||
|
||||
If you need help, reply to this email.
|
||||
|
||||
See you at the event,
|
||||
@@ -935,12 +964,15 @@ Datos de Transferencia:
|
||||
- Teléfono: {{bankPhone}}
|
||||
- Referencia: {{paymentReference}}
|
||||
|
||||
Después de realizar la transferencia, vuelve al sitio web y haz clic en "Ya pagué" o usa este enlace para notificarnos:
|
||||
⚠️ IMPORTANTE - Proceso de Verificación Manual:
|
||||
Por favor asegúrate de completar el pago antes de hacer clic en "Ya pagué".
|
||||
El equipo de Spanglish revisará el pago manualmente.
|
||||
Tu reserva solo será confirmada después de que recibas un email de confirmación de nuestra parte.
|
||||
|
||||
Después de completar el pago, vuelve al sitio web y haz clic en "Ya pagué" o usa este enlace para notificarnos:
|
||||
|
||||
{{bookingUrl}}
|
||||
|
||||
Confirmaremos tu lugar tan pronto como recibamos el pago.
|
||||
|
||||
Si necesitas ayuda, responde a este email.
|
||||
|
||||
¡Nos vemos en el evento!
|
||||
@@ -960,6 +992,117 @@ Spanglish`,
|
||||
],
|
||||
isSystem: true,
|
||||
},
|
||||
{
|
||||
name: 'Payment Rejected',
|
||||
slug: 'payment-rejected',
|
||||
subject: 'Payment not found for your Spanglish booking',
|
||||
subjectEs: 'No encontramos el pago de tu reserva en Spanglish',
|
||||
bodyHtml: `
|
||||
<h2>About Your Booking</h2>
|
||||
<p>Hi {{attendeeName}},</p>
|
||||
<p>Thanks for your interest in Spanglish.</p>
|
||||
<p>Unfortunately, we were unable to find or confirm the payment for your booking for:</p>
|
||||
|
||||
<div class="event-card">
|
||||
<h3>{{eventTitle}}</h3>
|
||||
<div class="event-detail"><strong>📅 Date:</strong> {{eventDate}}</div>
|
||||
<div class="event-detail"><strong>📍 Location:</strong> {{eventLocation}}</div>
|
||||
</div>
|
||||
|
||||
<p>Because of this, the booking has been cancelled.</p>
|
||||
|
||||
<div class="note">
|
||||
<strong>Did you already pay?</strong><br>
|
||||
If you believe this was a mistake or if you have already made the payment, please reply to this email and we'll be happy to check it with you.
|
||||
</div>
|
||||
|
||||
<p>You're always welcome to make a new booking if spots are still available.</p>
|
||||
|
||||
{{#if newBookingUrl}}
|
||||
<p style="text-align: center; margin: 24px 0;">
|
||||
<a href="{{newBookingUrl}}" class="btn">Make a New Booking</a>
|
||||
</p>
|
||||
{{/if}}
|
||||
|
||||
<p>Warm regards,<br>Spanglish</p>
|
||||
`,
|
||||
bodyHtmlEs: `
|
||||
<h2>Sobre Tu Reserva</h2>
|
||||
<p>Hola {{attendeeName}},</p>
|
||||
<p>Gracias por tu interés en Spanglish.</p>
|
||||
<p>Lamentablemente, no pudimos encontrar o confirmar el pago de tu reserva para:</p>
|
||||
|
||||
<div class="event-card">
|
||||
<h3>{{eventTitle}}</h3>
|
||||
<div class="event-detail"><strong>📅 Fecha:</strong> {{eventDate}}</div>
|
||||
<div class="event-detail"><strong>📍 Ubicación:</strong> {{eventLocation}}</div>
|
||||
</div>
|
||||
|
||||
<p>Por esta razón, la reserva ha sido cancelada.</p>
|
||||
|
||||
<div class="note">
|
||||
<strong>¿Ya realizaste el pago?</strong><br>
|
||||
Si crees que esto fue un error o si ya realizaste el pago, por favor responde a este correo y con gusto lo revisaremos contigo.
|
||||
</div>
|
||||
|
||||
<p>Siempre eres bienvenido/a a hacer una nueva reserva si aún hay lugares disponibles.</p>
|
||||
|
||||
{{#if newBookingUrl}}
|
||||
<p style="text-align: center; margin: 24px 0;">
|
||||
<a href="{{newBookingUrl}}" class="btn">Hacer una Nueva Reserva</a>
|
||||
</p>
|
||||
{{/if}}
|
||||
|
||||
<p>Saludos cordiales,<br>Spanglish</p>
|
||||
`,
|
||||
bodyText: `About Your Booking
|
||||
|
||||
Hi {{attendeeName}},
|
||||
|
||||
Thanks for your interest in Spanglish.
|
||||
|
||||
Unfortunately, we were unable to find or confirm the payment for your booking for:
|
||||
|
||||
{{eventTitle}}
|
||||
📅 Date: {{eventDate}}
|
||||
📍 Location: {{eventLocation}}
|
||||
|
||||
Because of this, the booking has been cancelled.
|
||||
|
||||
If you believe this was a mistake or if you have already made the payment, please reply to this email and we'll be happy to check it with you.
|
||||
|
||||
You're always welcome to make a new booking if spots are still available.
|
||||
|
||||
Warm regards,
|
||||
Spanglish`,
|
||||
bodyTextEs: `Sobre Tu Reserva
|
||||
|
||||
Hola {{attendeeName}},
|
||||
|
||||
Gracias por tu interés en Spanglish.
|
||||
|
||||
Lamentablemente, no pudimos encontrar o confirmar el pago de tu reserva para:
|
||||
|
||||
{{eventTitle}}
|
||||
📅 Fecha: {{eventDate}}
|
||||
📍 Ubicación: {{eventLocation}}
|
||||
|
||||
Por esta razón, la reserva ha sido cancelada.
|
||||
|
||||
Si crees que esto fue un error o si ya realizaste el pago, por favor responde a este correo y con gusto lo revisaremos contigo.
|
||||
|
||||
Siempre eres bienvenido/a a hacer una nueva reserva si aún hay lugares disponibles.
|
||||
|
||||
Saludos cordiales,
|
||||
Spanglish`,
|
||||
description: 'Sent when admin rejects a TPago or Bank Transfer payment',
|
||||
variables: [
|
||||
...commonVariables,
|
||||
...bookingVariables,
|
||||
{ name: 'newBookingUrl', description: 'URL to make a new booking (optional)', example: 'https://spanglish.com/book/event123' },
|
||||
],
|
||||
isSystem: true,
|
||||
},
|
||||
];
|
||||
|
||||
// Helper function to replace template variables
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Hono } from 'hono';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
import { z } from 'zod';
|
||||
import { db, events, tickets } from '../db/index.js';
|
||||
import { db, events, tickets, payments, eventPaymentOverrides, emailLogs, invoices } from '../db/index.js';
|
||||
import { eq, desc, and, gte, sql } from 'drizzle-orm';
|
||||
import { requireAuth, getAuthUser } from '../lib/auth.js';
|
||||
import { generateId, getNow } from '../lib/utils.js';
|
||||
@@ -23,7 +23,7 @@ const validationHook = (result: any, c: any) => {
|
||||
}
|
||||
};
|
||||
|
||||
const createEventSchema = z.object({
|
||||
const baseEventSchema = z.object({
|
||||
title: z.string().min(1),
|
||||
titleEs: z.string().optional().nullable(),
|
||||
description: z.string().min(1),
|
||||
@@ -38,9 +38,38 @@ const createEventSchema = z.object({
|
||||
status: z.enum(['draft', 'published', 'cancelled', 'completed', 'archived']).default('draft'),
|
||||
// Accept relative paths (/uploads/...) or full URLs
|
||||
bannerUrl: z.string().optional().nullable().or(z.literal('')),
|
||||
// External booking support
|
||||
externalBookingEnabled: z.boolean().default(false),
|
||||
externalBookingUrl: z.string().url().optional().nullable().or(z.literal('')),
|
||||
});
|
||||
|
||||
const updateEventSchema = createEventSchema.partial();
|
||||
const createEventSchema = baseEventSchema.refine(
|
||||
(data) => {
|
||||
// If external booking is enabled, URL must be provided and must start with https://
|
||||
if (data.externalBookingEnabled) {
|
||||
return !!(data.externalBookingUrl && data.externalBookingUrl.startsWith('https://'));
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: 'External booking URL is required and must be a valid HTTPS link when external booking is enabled',
|
||||
path: ['externalBookingUrl'],
|
||||
}
|
||||
);
|
||||
|
||||
const updateEventSchema = baseEventSchema.partial().refine(
|
||||
(data) => {
|
||||
// If external booking is enabled, URL must be provided and must start with https://
|
||||
if (data.externalBookingEnabled) {
|
||||
return !!(data.externalBookingUrl && data.externalBookingUrl.startsWith('https://'));
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: 'External booking URL is required and must be a valid HTTPS link when external booking is enabled',
|
||||
path: ['externalBookingUrl'],
|
||||
}
|
||||
);
|
||||
|
||||
// Get all events (public)
|
||||
eventsRouter.get('/', async (c) => {
|
||||
@@ -211,6 +240,44 @@ eventsRouter.delete('/:id', requireAuth(['admin']), async (c) => {
|
||||
return c.json({ error: 'Event not found' }, 404);
|
||||
}
|
||||
|
||||
// Get all tickets for this event
|
||||
const eventTickets = await (db as any)
|
||||
.select()
|
||||
.from(tickets)
|
||||
.where(eq((tickets as any).eventId, id))
|
||||
.all();
|
||||
|
||||
// Delete invoices and payments for all tickets of this event
|
||||
for (const ticket of eventTickets) {
|
||||
// Get payments for this ticket
|
||||
const ticketPayments = await (db as any)
|
||||
.select()
|
||||
.from(payments)
|
||||
.where(eq((payments as any).ticketId, ticket.id))
|
||||
.all();
|
||||
|
||||
// Delete invoices for each payment
|
||||
for (const payment of ticketPayments) {
|
||||
await (db as any).delete(invoices).where(eq((invoices as any).paymentId, payment.id));
|
||||
}
|
||||
|
||||
// Delete payments for this ticket
|
||||
await (db as any).delete(payments).where(eq((payments as any).ticketId, ticket.id));
|
||||
}
|
||||
|
||||
// Delete all tickets for this event
|
||||
await (db as any).delete(tickets).where(eq((tickets as any).eventId, id));
|
||||
|
||||
// Delete event payment overrides
|
||||
await (db as any).delete(eventPaymentOverrides).where(eq((eventPaymentOverrides as any).eventId, id));
|
||||
|
||||
// Set eventId to null on email logs (they reference this event but can exist without it)
|
||||
await (db as any)
|
||||
.update(emailLogs)
|
||||
.set({ eventId: null })
|
||||
.where(eq((emailLogs as any).eventId, id));
|
||||
|
||||
// Finally delete the event
|
||||
await (db as any).delete(events).where(eq((events as any).id, id));
|
||||
|
||||
return c.json({ message: 'Event deleted successfully' });
|
||||
@@ -257,6 +324,8 @@ eventsRouter.post('/:id/duplicate', requireAuth(['admin', 'organizer']), async (
|
||||
capacity: existing.capacity,
|
||||
status: 'draft',
|
||||
bannerUrl: existing.bannerUrl,
|
||||
externalBookingEnabled: existing.externalBookingEnabled || false,
|
||||
externalBookingUrl: existing.externalBookingUrl,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
@@ -311,7 +311,21 @@ paymentsRouter.post('/:id/reject', requireAuth(['admin', 'organizer']), zValidat
|
||||
})
|
||||
.where(eq((payments as any).id, id));
|
||||
|
||||
// Note: We don't cancel the ticket automatically - admin can do that separately if needed
|
||||
// Cancel the ticket - booking is no longer valid after rejection
|
||||
await (db as any)
|
||||
.update(tickets)
|
||||
.set({
|
||||
status: 'cancelled',
|
||||
updatedAt: now,
|
||||
})
|
||||
.where(eq((tickets as any).id, payment.ticketId));
|
||||
|
||||
// Send rejection email asynchronously (for manual payment methods only)
|
||||
if (['bank_transfer', 'tpago'].includes(payment.provider)) {
|
||||
emailService.sendPaymentRejectionEmail(id).catch(err => {
|
||||
console.error('[Email] Failed to send payment rejection email:', err);
|
||||
});
|
||||
}
|
||||
|
||||
const updated = await (db as any)
|
||||
.select()
|
||||
@@ -319,7 +333,7 @@ paymentsRouter.post('/:id/reject', requireAuth(['admin', 'organizer']), zValidat
|
||||
.where(eq((payments as any).id, id))
|
||||
.get();
|
||||
|
||||
return c.json({ payment: updated, message: 'Payment rejected' });
|
||||
return c.json({ payment: updated, message: 'Payment rejected and booking cancelled' });
|
||||
});
|
||||
|
||||
// Update admin note
|
||||
|
||||
Reference in New Issue
Block a user