import Member from "../../../../models/member.js"; import Series from "../../../../models/series.js"; import { loadPublicEvent } from "../../../../utils/loadEvent.js"; import { calculateTicketPrice, checkTicketAvailability, checkUserSeriesPass, formatPrice, hasMemberAccess, } from "../../../../utils/tickets.js"; /** * GET /api/events/[id]/tickets/available * Check ticket availability and pricing for current user * Query params: ?email=user@example.com (optional) */ export default defineEventHandler(async (event) => { try { const identifier = getRouterParam(event, "id"); const query = getQuery(event); const userEmail = query.email; const eventData = await loadPublicEvent(event, identifier); if (eventData.isCancelled) { return { available: false, reason: "Event has been cancelled" }; } if (new Date(eventData.startDate) < new Date()) { return { available: false, reason: "Event has already started" }; } if ( eventData.registrationDeadline && new Date(eventData.registrationDeadline) < new Date() ) { return { available: false, reason: "Registration deadline has passed" }; } // Series-pass gate: when an event is linked to a series that requires a // pass and doesn't allow drop-ins, surface a structured response so the UI // can route to the series page instead of rendering a generic "Sold Out" // block. The register/purchase handlers enforce this gate independently. if ( eventData.tickets?.requiresSeriesTicket && eventData.tickets?.seriesTicketReference ) { const series = await Series.findById( eventData.tickets.seriesTicketReference, ); if (series && !series.tickets?.allowIndividualEventTickets) { const hasPass = userEmail ? checkUserSeriesPass(series, userEmail).hasPass : false; if (!hasPass) { return { available: false, reason: "series_pass_required", requiresSeriesPass: true, series: { id: series.id, title: series.title, slug: series.slug, }, }; } } } let member = null; if (userEmail) { member = await Member.findOne({ email: userEmail.toLowerCase() }); } const ticketInfo = calculateTicketPrice(eventData, member); if (!ticketInfo) { return { available: false, reason: eventData.membersOnly ? "This event is for members only" : "No tickets available", membersOnly: eventData.membersOnly, }; } const availability = checkTicketAvailability( eventData, ticketInfo.ticketType, ); const response = { available: availability.available, ticketType: ticketInfo.ticketType, price: ticketInfo.price, currency: ticketInfo.currency, formattedPrice: formatPrice(ticketInfo.price, ticketInfo.currency), isFree: ticketInfo.isFree, isEarlyBird: ticketInfo.isEarlyBird, name: ticketInfo.name, description: ticketInfo.description, remaining: availability.remaining, waitlistAvailable: availability.waitlistAvailable, }; if (ticketInfo.isEarlyBird && eventData.tickets?.public?.earlyBirdDeadline) { response.earlyBirdDeadline = eventData.tickets.public.earlyBirdDeadline; response.regularPrice = eventData.tickets.public.price; response.formattedRegularPrice = formatPrice( eventData.tickets.public.price, ticketInfo.currency, ); } if (hasMemberAccess(member) && eventData.tickets?.public?.available) { response.publicTicket = { price: eventData.tickets.public.price, formattedPrice: formatPrice( eventData.tickets.public.price, eventData.tickets.currency, ), }; response.memberSavings = eventData.tickets.public.price - ticketInfo.price; } if (userEmail) { const alreadyRegistered = eventData.registrations?.some( (reg) => reg.email.toLowerCase() === userEmail.toLowerCase() && !reg.cancelledAt, ); if (alreadyRegistered) { response.alreadyRegistered = true; response.available = false; response.reason = "You are already registered for this event"; } } return response; } catch (error) { if (error.statusCode) { throw error; } console.error("Error checking ticket availability:", error); throw createError({ statusCode: 500, statusMessage: "Failed to check ticket availability", }); } });