first commit

This commit is contained in:
Michaël
2026-01-29 14:13:11 -03:00
commit 2302748c87
105 changed files with 93301 additions and 0 deletions

View File

@@ -0,0 +1,431 @@
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
import { db, payments, tickets, events } from '../db/index.js';
import { eq, desc, and, or, sql } from 'drizzle-orm';
import { requireAuth } from '../lib/auth.js';
import { getNow } from '../lib/utils.js';
import emailService from '../lib/email.js';
const paymentsRouter = new Hono();
const updatePaymentSchema = z.object({
status: z.enum(['pending', 'pending_approval', 'paid', 'refunded', 'failed']),
reference: z.string().optional(),
adminNote: z.string().optional(),
});
const approvePaymentSchema = z.object({
adminNote: z.string().optional(),
});
const rejectPaymentSchema = z.object({
adminNote: z.string().optional(),
});
// Get all payments (admin) - with ticket and event details
paymentsRouter.get('/', requireAuth(['admin']), async (c) => {
const status = c.req.query('status');
const provider = c.req.query('provider');
const pendingApproval = c.req.query('pendingApproval');
// Get all payments with their associated tickets
let allPayments = await (db as any)
.select()
.from(payments)
.orderBy(desc((payments as any).createdAt))
.all();
// Filter by status
if (status) {
allPayments = allPayments.filter((p: any) => p.status === status);
}
// Filter for pending approval specifically
if (pendingApproval === 'true') {
allPayments = allPayments.filter((p: any) => p.status === 'pending_approval');
}
// Filter by provider
if (provider) {
allPayments = allPayments.filter((p: any) => p.provider === provider);
}
// Enrich with ticket and event data
const enrichedPayments = await Promise.all(
allPayments.map(async (payment: any) => {
const ticket = await (db as any)
.select()
.from(tickets)
.where(eq((tickets as any).id, payment.ticketId))
.get();
let event = null;
if (ticket) {
event = await (db as any)
.select()
.from(events)
.where(eq((events as any).id, ticket.eventId))
.get();
}
return {
...payment,
ticket: ticket ? {
id: ticket.id,
attendeeFirstName: ticket.attendeeFirstName,
attendeeLastName: ticket.attendeeLastName,
attendeeEmail: ticket.attendeeEmail,
attendeePhone: ticket.attendeePhone,
status: ticket.status,
} : null,
event: event ? {
id: event.id,
title: event.title,
startDatetime: event.startDatetime,
} : null,
};
})
);
return c.json({ payments: enrichedPayments });
});
// Get payments pending approval (admin dashboard view)
paymentsRouter.get('/pending-approval', requireAuth(['admin', 'organizer']), async (c) => {
const pendingPayments = await (db as any)
.select()
.from(payments)
.where(eq((payments as any).status, 'pending_approval'))
.orderBy(desc((payments as any).userMarkedPaidAt))
.all();
// Enrich with ticket and event data
const enrichedPayments = await Promise.all(
pendingPayments.map(async (payment: any) => {
const ticket = await (db as any)
.select()
.from(tickets)
.where(eq((tickets as any).id, payment.ticketId))
.get();
let event = null;
if (ticket) {
event = await (db as any)
.select()
.from(events)
.where(eq((events as any).id, ticket.eventId))
.get();
}
return {
...payment,
ticket: ticket ? {
id: ticket.id,
attendeeFirstName: ticket.attendeeFirstName,
attendeeLastName: ticket.attendeeLastName,
attendeeEmail: ticket.attendeeEmail,
attendeePhone: ticket.attendeePhone,
status: ticket.status,
} : null,
event: event ? {
id: event.id,
title: event.title,
startDatetime: event.startDatetime,
} : null,
};
})
);
return c.json({ payments: enrichedPayments });
});
// Get payment by ID (admin)
paymentsRouter.get('/:id', requireAuth(['admin', 'organizer']), async (c) => {
const id = c.req.param('id');
const payment = await (db as any)
.select()
.from(payments)
.where(eq((payments as any).id, id))
.get();
if (!payment) {
return c.json({ error: 'Payment not found' }, 404);
}
// Get associated ticket
const ticket = await (db as any)
.select()
.from(tickets)
.where(eq((tickets as any).id, payment.ticketId))
.get();
return c.json({ payment: { ...payment, ticket } });
});
// Update payment (admin) - for manual payment confirmation
paymentsRouter.put('/:id', requireAuth(['admin', 'organizer']), zValidator('json', updatePaymentSchema), async (c) => {
const id = c.req.param('id');
const data = c.req.valid('json');
const user = (c as any).get('user');
const existing = await (db as any)
.select()
.from(payments)
.where(eq((payments as any).id, id))
.get();
if (!existing) {
return c.json({ error: 'Payment not found' }, 404);
}
const now = getNow();
const updateData: any = { ...data, updatedAt: now };
// If marking as paid, record who approved it and when
if (data.status === 'paid' && existing.status !== 'paid') {
updateData.paidAt = now;
updateData.paidByAdminId = user.id;
}
await (db as any)
.update(payments)
.set(updateData)
.where(eq((payments as any).id, id));
// If payment confirmed, update ticket status and send emails
if (data.status === 'paid') {
await (db as any)
.update(tickets)
.set({ status: 'confirmed' })
.where(eq((tickets as any).id, existing.ticketId));
// Send confirmation emails asynchronously (don't block the response)
Promise.all([
emailService.sendBookingConfirmation(existing.ticketId),
emailService.sendPaymentReceipt(id),
]).catch(err => {
console.error('[Email] Failed to send confirmation emails:', err);
});
}
const updated = await (db as any)
.select()
.from(payments)
.where(eq((payments as any).id, id))
.get();
return c.json({ payment: updated });
});
// 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 user = (c as any).get('user');
const payment = await (db as any)
.select()
.from(payments)
.where(eq((payments as any).id, id))
.get();
if (!payment) {
return c.json({ error: 'Payment not found' }, 404);
}
// Can approve pending or pending_approval payments
if (!['pending', 'pending_approval'].includes(payment.status)) {
return c.json({ error: 'Payment cannot be approved in its current state' }, 400);
}
const now = getNow();
// Update payment status to paid
await (db as any)
.update(payments)
.set({
status: 'paid',
paidAt: now,
paidByAdminId: user.id,
adminNote: adminNote || payment.adminNote,
updatedAt: now,
})
.where(eq((payments as any).id, id));
// Update ticket status to confirmed
await (db as any)
.update(tickets)
.set({ status: 'confirmed' })
.where(eq((tickets as any).id, payment.ticketId));
// Send confirmation emails asynchronously
Promise.all([
emailService.sendBookingConfirmation(payment.ticketId),
emailService.sendPaymentReceipt(id),
]).catch(err => {
console.error('[Email] Failed to send confirmation emails:', err);
});
const updated = await (db as any)
.select()
.from(payments)
.where(eq((payments as any).id, id))
.get();
return c.json({ payment: updated, message: 'Payment approved successfully' });
});
// 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 user = (c as any).get('user');
const payment = await (db as any)
.select()
.from(payments)
.where(eq((payments as any).id, id))
.get();
if (!payment) {
return c.json({ error: 'Payment not found' }, 404);
}
if (!['pending', 'pending_approval'].includes(payment.status)) {
return c.json({ error: 'Payment cannot be rejected in its current state' }, 400);
}
const now = getNow();
// Update payment status to failed
await (db as any)
.update(payments)
.set({
status: 'failed',
paidByAdminId: user.id,
adminNote: adminNote || payment.adminNote,
updatedAt: now,
})
.where(eq((payments as any).id, id));
// Note: We don't cancel the ticket automatically - admin can do that separately if needed
const updated = await (db as any)
.select()
.from(payments)
.where(eq((payments as any).id, id))
.get();
return c.json({ payment: updated, message: 'Payment rejected' });
});
// Update admin note
paymentsRouter.post('/:id/note', requireAuth(['admin', 'organizer']), async (c) => {
const id = c.req.param('id');
const body = await c.req.json();
const { adminNote } = body;
const payment = await (db as any)
.select()
.from(payments)
.where(eq((payments as any).id, id))
.get();
if (!payment) {
return c.json({ error: 'Payment not found' }, 404);
}
const now = getNow();
await (db as any)
.update(payments)
.set({
adminNote: adminNote || null,
updatedAt: now,
})
.where(eq((payments as any).id, id));
const updated = await (db as any)
.select()
.from(payments)
.where(eq((payments as any).id, id))
.get();
return c.json({ payment: updated, message: 'Note updated' });
});
// Process refund (admin)
paymentsRouter.post('/:id/refund', requireAuth(['admin']), async (c) => {
const id = c.req.param('id');
const payment = await (db as any)
.select()
.from(payments)
.where(eq((payments as any).id, id))
.get();
if (!payment) {
return c.json({ error: 'Payment not found' }, 404);
}
if (payment.status !== 'paid') {
return c.json({ error: 'Can only refund paid payments' }, 400);
}
const now = getNow();
// Update payment status
await (db as any)
.update(payments)
.set({ status: 'refunded', updatedAt: now })
.where(eq((payments as any).id, id));
// Cancel associated ticket
await (db as any)
.update(tickets)
.set({ status: 'cancelled' })
.where(eq((tickets as any).id, payment.ticketId));
return c.json({ message: 'Refund processed successfully' });
});
// Payment webhook (for Stripe/MercadoPago)
paymentsRouter.post('/webhook', async (c) => {
// This would handle webhook notifications from payment providers
// Implementation depends on which provider is used
const body = await c.req.json();
// Log webhook for debugging
console.log('Payment webhook received:', body);
// TODO: Implement provider-specific webhook handling
// - Verify webhook signature
// - Update payment status
// - Update ticket status
return c.json({ received: true });
});
// Get payment statistics (admin)
paymentsRouter.get('/stats/overview', requireAuth(['admin']), async (c) => {
const allPayments = await (db as any).select().from(payments).all();
const stats = {
total: allPayments.length,
pending: allPayments.filter((p: any) => p.status === 'pending').length,
paid: allPayments.filter((p: any) => p.status === 'paid').length,
refunded: allPayments.filter((p: any) => p.status === 'refunded').length,
failed: allPayments.filter((p: any) => p.status === 'failed').length,
totalRevenue: allPayments
.filter((p: any) => p.status === 'paid')
.reduce((sum: number, p: any) => sum + (p.amount || 0), 0),
};
return c.json({ stats });
});
export default paymentsRouter;