Fix event capacity: no negative availability, sold-out enforcement, admin override
- Backend: use calculateAvailableSeats so availableSeats is never negative - Backend: reject public booking when confirmed >= capacity; admin create/manual bypass capacity - Frontend: spotsLeft = max(0, capacity - bookedCount), isSoldOut when bookedCount >= capacity - Frontend: sold-out redirect on booking page, cap quantity by spotsLeft, never show negative Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,7 +4,7 @@ import { z } from 'zod';
|
||||
import { db, dbGet, dbAll, events, tickets, payments, eventPaymentOverrides, emailLogs, invoices, siteSettings } from '../db/index.js';
|
||||
import { eq, desc, and, gte, sql } from 'drizzle-orm';
|
||||
import { requireAuth, getAuthUser } from '../lib/auth.js';
|
||||
import { generateId, getNow, convertBooleansForDb, toDbDate } from '../lib/utils.js';
|
||||
import { generateId, getNow, convertBooleansForDb, toDbDate, calculateAvailableSeats } from '../lib/utils.js';
|
||||
|
||||
interface UserContext {
|
||||
id: string;
|
||||
@@ -151,10 +151,11 @@ eventsRouter.get('/', async (c) => {
|
||||
);
|
||||
|
||||
const normalized = normalizeEvent(event);
|
||||
const bookedCount = ticketCount?.count || 0;
|
||||
return {
|
||||
...normalized,
|
||||
bookedCount: ticketCount?.count || 0,
|
||||
availableSeats: normalized.capacity - (ticketCount?.count || 0),
|
||||
bookedCount,
|
||||
availableSeats: calculateAvailableSeats(normalized.capacity, bookedCount),
|
||||
};
|
||||
})
|
||||
);
|
||||
@@ -189,11 +190,12 @@ eventsRouter.get('/:id', async (c) => {
|
||||
);
|
||||
|
||||
const normalized = normalizeEvent(event);
|
||||
const bookedCount = ticketCount?.count || 0;
|
||||
return c.json({
|
||||
event: {
|
||||
...normalized,
|
||||
bookedCount: ticketCount?.count || 0,
|
||||
availableSeats: normalized.capacity - (ticketCount?.count || 0),
|
||||
bookedCount,
|
||||
availableSeats: calculateAvailableSeats(normalized.capacity, bookedCount),
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -277,7 +279,7 @@ eventsRouter.get('/next/upcoming', async (c) => {
|
||||
event: {
|
||||
...normalized,
|
||||
bookedCount,
|
||||
availableSeats: normalized.capacity - bookedCount,
|
||||
availableSeats: calculateAvailableSeats(normalized.capacity, bookedCount),
|
||||
isFeatured: true,
|
||||
},
|
||||
});
|
||||
@@ -308,7 +310,7 @@ eventsRouter.get('/next/upcoming', async (c) => {
|
||||
event: {
|
||||
...normalized,
|
||||
bookedCount,
|
||||
availableSeats: normalized.capacity - bookedCount,
|
||||
availableSeats: calculateAvailableSeats(normalized.capacity, bookedCount),
|
||||
isFeatured: false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import { z } from 'zod';
|
||||
import { db, dbGet, dbAll, tickets, events, users, payments, paymentOptions, siteSettings } from '../db/index.js';
|
||||
import { eq, and, sql } from 'drizzle-orm';
|
||||
import { requireAuth, getAuthUser } from '../lib/auth.js';
|
||||
import { generateId, generateTicketCode, getNow } from '../lib/utils.js';
|
||||
import { generateId, generateTicketCode, getNow, calculateAvailableSeats, isEventSoldOut } from '../lib/utils.js';
|
||||
import { createInvoice, isLNbitsConfigured } from '../lib/lnbits.js';
|
||||
import emailService from '../lib/email.js';
|
||||
import { generateTicketPDF, generateCombinedTicketsPDF } from '../lib/pdf.js';
|
||||
@@ -87,15 +87,16 @@ ticketsRouter.post('/', zValidator('json', createTicketSchema), async (c) => {
|
||||
)
|
||||
);
|
||||
|
||||
const availableSeats = event.capacity - (existingTicketCount?.count || 0);
|
||||
|
||||
if (availableSeats <= 0) {
|
||||
const confirmedCount = existingTicketCount?.count || 0;
|
||||
const availableSeats = calculateAvailableSeats(event.capacity, confirmedCount);
|
||||
|
||||
if (isEventSoldOut(event.capacity, confirmedCount)) {
|
||||
return c.json({ error: 'Event is sold out' }, 400);
|
||||
}
|
||||
|
||||
|
||||
if (ticketCount > availableSeats) {
|
||||
return c.json({
|
||||
error: `Not enough seats available. Only ${availableSeats} spot(s) remaining.`
|
||||
return c.json({
|
||||
error: `Not enough seats available. Only ${availableSeats} spot(s) remaining.`,
|
||||
}, 400);
|
||||
}
|
||||
|
||||
@@ -968,26 +969,11 @@ ticketsRouter.post('/admin/create', requireAuth(['admin', 'organizer', 'staff'])
|
||||
if (!event) {
|
||||
return c.json({ error: 'Event not found' }, 404);
|
||||
}
|
||||
|
||||
// Check capacity
|
||||
const ticketCount = await dbGet<any>(
|
||||
(db as any)
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(tickets)
|
||||
.where(
|
||||
and(
|
||||
eq((tickets as any).eventId, data.eventId),
|
||||
sql`${(tickets as any).status} IN ('confirmed', 'checked_in')`
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
if ((ticketCount?.count || 0) >= event.capacity) {
|
||||
return c.json({ error: 'Event is at capacity' }, 400);
|
||||
}
|
||||
|
||||
|
||||
// Admin create at door: bypass capacity check (allow over-capacity for walk-ins)
|
||||
|
||||
const now = getNow();
|
||||
|
||||
|
||||
// For door sales, email might be empty - use a generated placeholder
|
||||
const attendeeEmail = data.email && data.email.trim()
|
||||
? data.email.trim()
|
||||
@@ -1116,24 +1102,9 @@ ticketsRouter.post('/admin/manual', requireAuth(['admin', 'organizer', 'staff'])
|
||||
if (!event) {
|
||||
return c.json({ error: 'Event not found' }, 404);
|
||||
}
|
||||
|
||||
// Check capacity
|
||||
const existingCount = await dbGet<any>(
|
||||
(db as any)
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(tickets)
|
||||
.where(
|
||||
and(
|
||||
eq((tickets as any).eventId, data.eventId),
|
||||
sql`${(tickets as any).status} IN ('confirmed', 'checked_in')`
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
if ((existingCount?.count || 0) >= event.capacity) {
|
||||
return c.json({ error: 'Event is at capacity' }, 400);
|
||||
}
|
||||
|
||||
|
||||
// Admin manual ticket: bypass capacity check (allow over-capacity for admin-created tickets)
|
||||
|
||||
const now = getNow();
|
||||
const attendeeEmail = data.email.trim();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user