import { Hono } from 'hono'; import { zValidator } from '@hono/zod-validator'; import { z } from 'zod'; import { db, dbGet, paymentOptions, eventPaymentOverrides, events } from '../db/index.js'; import { eq } from 'drizzle-orm'; import { requireAuth } from '../lib/auth.js'; import { generateId, getNow, convertBooleansForDb } from '../lib/utils.js'; const paymentOptionsRouter = new Hono(); // Helper to normalize boolean (handles true/false and 0/1 from database) const booleanOrNumber = z.union([z.boolean(), z.number()]).transform((val) => { if (typeof val === 'boolean') return val; return val !== 0; }); // Schema for updating global payment options const updatePaymentOptionsSchema = z.object({ tpagoEnabled: booleanOrNumber.optional(), tpagoLink: z.string().optional().nullable(), tpagoInstructions: z.string().optional().nullable(), tpagoInstructionsEs: z.string().optional().nullable(), bankTransferEnabled: booleanOrNumber.optional(), bankName: z.string().optional().nullable(), bankAccountHolder: z.string().optional().nullable(), bankAccountNumber: z.string().optional().nullable(), bankAlias: z.string().optional().nullable(), bankPhone: z.string().optional().nullable(), bankNotes: z.string().optional().nullable(), bankNotesEs: z.string().optional().nullable(), lightningEnabled: booleanOrNumber.optional(), cashEnabled: booleanOrNumber.optional(), cashInstructions: z.string().optional().nullable(), cashInstructionsEs: z.string().optional().nullable(), // Booking settings allowDuplicateBookings: booleanOrNumber.optional(), }); // Schema for event-level overrides const updateEventOverridesSchema = z.object({ tpagoEnabled: booleanOrNumber.optional().nullable(), tpagoLink: z.string().optional().nullable(), tpagoInstructions: z.string().optional().nullable(), tpagoInstructionsEs: z.string().optional().nullable(), bankTransferEnabled: booleanOrNumber.optional().nullable(), bankName: z.string().optional().nullable(), bankAccountHolder: z.string().optional().nullable(), bankAccountNumber: z.string().optional().nullable(), bankAlias: z.string().optional().nullable(), bankPhone: z.string().optional().nullable(), bankNotes: z.string().optional().nullable(), bankNotesEs: z.string().optional().nullable(), lightningEnabled: booleanOrNumber.optional().nullable(), cashEnabled: booleanOrNumber.optional().nullable(), cashInstructions: z.string().optional().nullable(), cashInstructionsEs: z.string().optional().nullable(), }); // Get global payment options paymentOptionsRouter.get('/', requireAuth(['admin']), async (c) => { const options = await dbGet( (db as any).select().from(paymentOptions) ); // If no options exist yet, return defaults if (!options) { return c.json({ paymentOptions: { tpagoEnabled: false, tpagoLink: null, tpagoInstructions: null, tpagoInstructionsEs: null, bankTransferEnabled: false, bankName: null, bankAccountHolder: null, bankAccountNumber: null, bankAlias: null, bankPhone: null, bankNotes: null, bankNotesEs: null, lightningEnabled: true, cashEnabled: true, cashInstructions: null, cashInstructionsEs: null, allowDuplicateBookings: false, }, }); } return c.json({ paymentOptions: options }); }); // Update global payment options paymentOptionsRouter.put('/', requireAuth(['admin']), zValidator('json', updatePaymentOptionsSchema), async (c) => { const data = c.req.valid('json'); const user = (c as any).get('user'); const now = getNow(); // Check if options exist const existing = await dbGet( (db as any) .select() .from(paymentOptions) ); // Convert boolean fields for database compatibility const dbData = convertBooleansForDb(data); if (existing) { // Update existing await (db as any) .update(paymentOptions) .set({ ...dbData, updatedAt: now, updatedBy: user.id, }) .where(eq((paymentOptions as any).id, existing.id)); } else { // Create new const id = generateId(); await (db as any).insert(paymentOptions).values({ id, ...dbData, updatedAt: now, updatedBy: user.id, }); } const updated = await dbGet( (db as any) .select() .from(paymentOptions) ); return c.json({ paymentOptions: updated, message: 'Payment options updated successfully' }); }); // Get payment options for a specific event (merged with global) paymentOptionsRouter.get('/event/:eventId', async (c) => { const eventId = c.req.param('eventId'); // Get the event first to verify it exists const event = await dbGet( (db as any) .select() .from(events) .where(eq((events as any).id, eventId)) ); if (!event) { return c.json({ error: 'Event not found' }, 404); } // Get global options const globalOptions = await dbGet( (db as any) .select() .from(paymentOptions) ); // Get event overrides const overrides = await dbGet( (db as any) .select() .from(eventPaymentOverrides) .where(eq((eventPaymentOverrides as any).eventId, eventId)) ); // Merge global with overrides (override takes precedence if not null) const defaults = { tpagoEnabled: false, tpagoLink: null, tpagoInstructions: null, tpagoInstructionsEs: null, bankTransferEnabled: false, bankName: null, bankAccountHolder: null, bankAccountNumber: null, bankAlias: null, bankPhone: null, bankNotes: null, bankNotesEs: null, lightningEnabled: true, cashEnabled: true, cashInstructions: null, cashInstructionsEs: null, }; const global = globalOptions || defaults; // Merge: override values take precedence if they're not null/undefined const merged = { tpagoEnabled: overrides?.tpagoEnabled ?? global.tpagoEnabled, tpagoLink: overrides?.tpagoLink ?? global.tpagoLink, tpagoInstructions: overrides?.tpagoInstructions ?? global.tpagoInstructions, tpagoInstructionsEs: overrides?.tpagoInstructionsEs ?? global.tpagoInstructionsEs, bankTransferEnabled: overrides?.bankTransferEnabled ?? global.bankTransferEnabled, bankName: overrides?.bankName ?? global.bankName, bankAccountHolder: overrides?.bankAccountHolder ?? global.bankAccountHolder, bankAccountNumber: overrides?.bankAccountNumber ?? global.bankAccountNumber, bankAlias: overrides?.bankAlias ?? global.bankAlias, bankPhone: overrides?.bankPhone ?? global.bankPhone, bankNotes: overrides?.bankNotes ?? global.bankNotes, bankNotesEs: overrides?.bankNotesEs ?? global.bankNotesEs, lightningEnabled: overrides?.lightningEnabled ?? global.lightningEnabled, cashEnabled: overrides?.cashEnabled ?? global.cashEnabled, cashInstructions: overrides?.cashInstructions ?? global.cashInstructions, cashInstructionsEs: overrides?.cashInstructionsEs ?? global.cashInstructionsEs, }; return c.json({ paymentOptions: merged, hasOverrides: !!overrides, }); }); // Get event payment overrides (admin only) paymentOptionsRouter.get('/event/:eventId/overrides', requireAuth(['admin', 'organizer']), async (c) => { const eventId = c.req.param('eventId'); const overrides = await dbGet( (db as any).select().from(eventPaymentOverrides).where(eq((eventPaymentOverrides as any).eventId, eventId)) ); return c.json({ overrides: overrides || null }); }); // Update event payment overrides paymentOptionsRouter.put('/event/:eventId/overrides', requireAuth(['admin', 'organizer']), zValidator('json', updateEventOverridesSchema), async (c) => { const eventId = c.req.param('eventId'); const data = c.req.valid('json'); const now = getNow(); // Verify event exists const event = await dbGet( (db as any).select().from(events).where(eq((events as any).id, eventId)) ); if (!event) { return c.json({ error: 'Event not found' }, 404); } // Check if overrides exist const existing = await dbGet( (db as any).select().from(eventPaymentOverrides).where(eq((eventPaymentOverrides as any).eventId, eventId)) ); // Convert boolean fields for database compatibility const dbData = convertBooleansForDb(data); if (existing) { await (db as any) .update(eventPaymentOverrides) .set({ ...dbData, updatedAt: now, }) .where(eq((eventPaymentOverrides as any).id, existing.id)); } else { const id = generateId(); await (db as any).insert(eventPaymentOverrides).values({ id, eventId, ...dbData, createdAt: now, updatedAt: now, }); } const updated = await dbGet( (db as any) .select() .from(eventPaymentOverrides) .where(eq((eventPaymentOverrides as any).eventId, eventId)) ); return c.json({ overrides: updated, message: 'Event payment overrides updated successfully' }); }); // Delete event payment overrides (revert to global) paymentOptionsRouter.delete('/event/:eventId/overrides', requireAuth(['admin', 'organizer']), async (c) => { const eventId = c.req.param('eventId'); await (db as any) .delete(eventPaymentOverrides) .where(eq((eventPaymentOverrides as any).eventId, eventId)); return c.json({ message: 'Event payment overrides removed' }); }); export default paymentOptionsRouter;