Close pre-sale registration a configurable time before events start.
Events gain nullable presale_closure_enabled / presale_close_minutes_before overrides; null inherits the new site_settings defaults (enabled, 120 min). A shared resolver computes the effective cutoff, which the public event API exposes as presaleClosesAt and the public booking endpoint enforces. Door and admin ticket creation are not gated. Admin: toggle + duration picker in the event modal (pre-filled from the site default) and a matching default card on Settings › General. Public: the event page shows "Registration Closed" after the cutoff and the checkout page redirects back. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
745af4184f
commit
59acc32a46
@@ -6,6 +6,7 @@ import { eq, and, gte } from 'drizzle-orm';
|
||||
import { requireAuth } from '../lib/auth.js';
|
||||
import { generateId, getNow, toDbBool } from '../lib/utils.js';
|
||||
import { revalidateFrontendCache } from '../lib/revalidate.js';
|
||||
import { DEFAULT_PRESALE_CLOSURE_ENABLED, DEFAULT_PRESALE_CLOSE_MINUTES } from '../lib/presale.js';
|
||||
|
||||
interface UserContext {
|
||||
id: string;
|
||||
@@ -43,8 +44,26 @@ const updateSiteSettingsSchema = z.object({
|
||||
maintenanceMode: z.boolean().optional(),
|
||||
maintenanceMessage: z.string().optional().nullable(),
|
||||
maintenanceMessageEs: z.string().optional().nullable(),
|
||||
// Pre-sale closure defaults inherited by events that don't override them
|
||||
presaleClosureEnabled: z.boolean().optional(),
|
||||
presaleCloseMinutesBefore: z.number().int().min(0).optional(),
|
||||
});
|
||||
|
||||
// Booleans are stored as 0/1 integers on PostgreSQL; hand the frontend real booleans.
|
||||
function normalizeSettings(row: any) {
|
||||
if (!row) return row;
|
||||
return {
|
||||
...row,
|
||||
maintenanceMode: Boolean(row.maintenanceMode),
|
||||
presaleClosureEnabled: row.presaleClosureEnabled == null
|
||||
? DEFAULT_PRESALE_CLOSURE_ENABLED
|
||||
: Boolean(row.presaleClosureEnabled),
|
||||
presaleCloseMinutesBefore: row.presaleCloseMinutesBefore == null
|
||||
? DEFAULT_PRESALE_CLOSE_MINUTES
|
||||
: Number(row.presaleCloseMinutesBefore),
|
||||
};
|
||||
}
|
||||
|
||||
// Get site settings (public - needed for frontend timezone)
|
||||
siteSettingsRouter.get('/', async (c) => {
|
||||
const settings = await dbGet(
|
||||
@@ -69,11 +88,13 @@ siteSettingsRouter.get('/', async (c) => {
|
||||
maintenanceMode: false,
|
||||
maintenanceMessage: null,
|
||||
maintenanceMessageEs: null,
|
||||
presaleClosureEnabled: DEFAULT_PRESALE_CLOSURE_ENABLED,
|
||||
presaleCloseMinutesBefore: DEFAULT_PRESALE_CLOSE_MINUTES,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return c.json({ settings });
|
||||
return c.json({ settings: normalizeSettings(settings) });
|
||||
});
|
||||
|
||||
// Get available timezones
|
||||
@@ -145,13 +166,15 @@ siteSettingsRouter.put('/', requireAuth(['admin']), zValidator('json', updateSit
|
||||
maintenanceMode: toDbBool(data.maintenanceMode || false),
|
||||
maintenanceMessage: data.maintenanceMessage || null,
|
||||
maintenanceMessageEs: data.maintenanceMessageEs || null,
|
||||
presaleClosureEnabled: toDbBool(data.presaleClosureEnabled ?? DEFAULT_PRESALE_CLOSURE_ENABLED),
|
||||
presaleCloseMinutesBefore: data.presaleCloseMinutesBefore ?? DEFAULT_PRESALE_CLOSE_MINUTES,
|
||||
updatedAt: now,
|
||||
updatedBy: user.id,
|
||||
};
|
||||
|
||||
await (db as any).insert(siteSettings).values(newSettings);
|
||||
|
||||
return c.json({ settings: newSettings, message: 'Settings created successfully' }, 201);
|
||||
return c.json({ settings: normalizeSettings(newSettings), message: 'Settings created successfully' }, 201);
|
||||
}
|
||||
|
||||
// Validate featured event if provided
|
||||
@@ -174,6 +197,9 @@ siteSettingsRouter.put('/', requireAuth(['admin']), zValidator('json', updateSit
|
||||
if (typeof data.maintenanceMode === 'boolean') {
|
||||
updateData.maintenanceMode = toDbBool(data.maintenanceMode);
|
||||
}
|
||||
if (typeof data.presaleClosureEnabled === 'boolean') {
|
||||
updateData.presaleClosureEnabled = toDbBool(data.presaleClosureEnabled);
|
||||
}
|
||||
|
||||
await (db as any)
|
||||
.update(siteSettings)
|
||||
@@ -184,12 +210,17 @@ siteSettingsRouter.put('/', requireAuth(['admin']), zValidator('json', updateSit
|
||||
(db as any).select().from(siteSettings).where(eq((siteSettings as any).id, existing.id))
|
||||
);
|
||||
|
||||
// Revalidate frontend cache if featured event changed
|
||||
if (data.featuredEventId !== undefined) {
|
||||
// Revalidate frontend cache if featured event changed or the pre-sale
|
||||
// defaults changed (public event pages embed the computed cutoff).
|
||||
if (
|
||||
data.featuredEventId !== undefined ||
|
||||
data.presaleClosureEnabled !== undefined ||
|
||||
data.presaleCloseMinutesBefore !== undefined
|
||||
) {
|
||||
revalidateFrontendCache();
|
||||
}
|
||||
|
||||
return c.json({ settings: updated, message: 'Settings updated successfully' });
|
||||
return c.json({ settings: normalizeSettings(updated), message: 'Settings updated successfully' });
|
||||
});
|
||||
|
||||
// Set featured event (admin only) - convenience endpoint for event editor
|
||||
|
||||
Reference in New Issue
Block a user