293 lines
9.3 KiB
TypeScript
293 lines
9.3 KiB
TypeScript
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<any>(
|
|
(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<any>(
|
|
(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<any>(
|
|
(db as any)
|
|
.select()
|
|
.from(paymentOptions)
|
|
);
|
|
|
|
// Get event overrides
|
|
const overrides = await dbGet<any>(
|
|
(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<any>(
|
|
(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<any>(
|
|
(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<any>(
|
|
(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;
|