feat: add payment management improvements and reminder emails

- Add option to approve/reject payments without sending notification emails
  (checkbox in review popup, default enabled)
- Add payment reminder email template and send functionality
- Track when reminder emails are sent (reminderSentAt field)
- Display reminder sent timestamp in payment review popup
- Make payment review popup scrollable for better UX
- Add payment-reminder template to email system (available in admin emails)
This commit is contained in:
Michilis
2026-02-05 04:13:42 +00:00
parent 0c142884c7
commit 23d0325d8d
8 changed files with 357 additions and 23 deletions

View File

@@ -17,10 +17,12 @@ const updatePaymentSchema = z.object({
const approvePaymentSchema = z.object({
adminNote: z.string().optional(),
sendEmail: z.boolean().optional().default(true),
});
const rejectPaymentSchema = z.object({
adminNote: z.string().optional(),
sendEmail: z.boolean().optional().default(true),
});
// Get all payments (admin) - with ticket and event details
@@ -266,7 +268,7 @@ paymentsRouter.put('/:id', requireAuth(['admin', 'organizer']), zValidator('json
// Approve payment (admin) - specifically for pending_approval payments
paymentsRouter.post('/:id/approve', requireAuth(['admin', 'organizer']), zValidator('json', approvePaymentSchema), async (c) => {
const id = c.req.param('id');
const { adminNote } = c.req.valid('json');
const { adminNote, sendEmail } = c.req.valid('json');
const user = (c as any).get('user');
const payment = await dbGet<any>(
@@ -329,13 +331,17 @@ paymentsRouter.post('/:id/approve', requireAuth(['admin', 'organizer']), zValida
.where(eq((tickets as any).id, (t as any).id));
}
// Send confirmation emails asynchronously
Promise.all([
emailService.sendBookingConfirmation(payment.ticketId),
emailService.sendPaymentReceipt(id),
]).catch(err => {
console.error('[Email] Failed to send confirmation emails:', err);
});
// Send confirmation emails asynchronously (if sendEmail is true, which is the default)
if (sendEmail !== false) {
Promise.all([
emailService.sendBookingConfirmation(payment.ticketId),
emailService.sendPaymentReceipt(id),
]).catch(err => {
console.error('[Email] Failed to send confirmation emails:', err);
});
} else {
console.log('[Payment] Skipping confirmation emails per admin request');
}
const updated = await dbGet(
(db as any)
@@ -350,7 +356,7 @@ paymentsRouter.post('/:id/approve', requireAuth(['admin', 'organizer']), zValida
// Reject payment (admin)
paymentsRouter.post('/:id/reject', requireAuth(['admin', 'organizer']), zValidator('json', rejectPaymentSchema), async (c) => {
const id = c.req.param('id');
const { adminNote } = c.req.valid('json');
const { adminNote, sendEmail } = c.req.valid('json');
const user = (c as any).get('user');
const payment = await dbGet<any>(
@@ -390,11 +396,13 @@ paymentsRouter.post('/:id/reject', requireAuth(['admin', 'organizer']), zValidat
})
.where(eq((tickets as any).id, payment.ticketId));
// Send rejection email asynchronously (for manual payment methods only)
if (['bank_transfer', 'tpago'].includes(payment.provider)) {
// Send rejection email asynchronously (for manual payment methods only, if sendEmail is true)
if (sendEmail !== false && ['bank_transfer', 'tpago'].includes(payment.provider)) {
emailService.sendPaymentRejectionEmail(id).catch(err => {
console.error('[Email] Failed to send payment rejection email:', err);
});
} else if (sendEmail === false) {
console.log('[Payment] Skipping rejection email per admin request');
}
const updated = await dbGet(
@@ -407,6 +415,51 @@ paymentsRouter.post('/:id/reject', requireAuth(['admin', 'organizer']), zValidat
return c.json({ payment: updated, message: 'Payment rejected and booking cancelled' });
});
// Send payment reminder email
paymentsRouter.post('/:id/send-reminder', requireAuth(['admin', 'organizer']), async (c) => {
const id = c.req.param('id');
const payment = await dbGet<any>(
(db as any)
.select()
.from(payments)
.where(eq((payments as any).id, id))
);
if (!payment) {
return c.json({ error: 'Payment not found' }, 404);
}
// Only allow sending reminders for pending payments
if (!['pending', 'pending_approval'].includes(payment.status)) {
return c.json({ error: 'Payment reminder can only be sent for pending payments' }, 400);
}
try {
const result = await emailService.sendPaymentReminder(id);
if (result.success) {
const now = getNow();
// Record when reminder was sent
await (db as any)
.update(payments)
.set({
reminderSentAt: now,
updatedAt: now,
})
.where(eq((payments as any).id, id));
return c.json({ message: 'Payment reminder sent successfully', reminderSentAt: now });
} else {
return c.json({ error: result.error || 'Failed to send payment reminder' }, 500);
}
} catch (err: any) {
console.error('[Payment] Failed to send payment reminder:', err);
return c.json({ error: 'Failed to send payment reminder' }, 500);
}
});
// Update admin note
paymentsRouter.post('/:id/note', requireAuth(['admin', 'organizer']), async (c) => {
const id = c.req.param('id');