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:
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user