From 8f0648de57795e56c63e63106f877eaec9547fa0 Mon Sep 17 00:00:00 2001 From: Jennie Robinson Faber Date: Mon, 20 Apr 2026 20:13:36 +0100 Subject: [PATCH] fix(events): surface series-pass-required in ticket availability response When a series requires a pass and doesn't allow drop-ins, the per-event availability endpoint returned a generic "No tickets available" reason, leaving the UI to render an "Event Sold Out" block for guests (logged-in users short-circuit via check-series-access first). Detect the gate server-side and return {available:false, reason:"series_pass_required", requiresSeriesPass:true, series:{id,title,slug}} so EventTicketPurchase's existing requiresSeriesPass branch renders a pass-required CTA with a link to the series page. The register and purchase handlers already enforce the gate server-side; this is a messaging fix only. --- .../api/events/[id]/tickets/available.get.js | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/server/api/events/[id]/tickets/available.get.js b/server/api/events/[id]/tickets/available.get.js index 4107c0a..2fd64a3 100644 --- a/server/api/events/[id]/tickets/available.get.js +++ b/server/api/events/[id]/tickets/available.get.js @@ -1,8 +1,10 @@ import Member from "../../../../models/member.js"; +import Series from "../../../../models/series.js"; import { loadPublicEvent } from "../../../../utils/loadEvent.js"; import { calculateTicketPrice, checkTicketAvailability, + checkUserSeriesPass, formatPrice, } from "../../../../utils/tickets.js"; @@ -34,6 +36,36 @@ export default defineEventHandler(async (event) => { 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() });