The series-pass gate in register.post.js was checking `series.allowIndividualEventTickets` at the top level, but the field lives under `series.tickets.allowIndividualEventTickets` per the Series schema. Top-level access was always undefined, so `!undefined` always fired the pass check — blocking drop-in registration even when an admin enabled `(requiresSeriesTicket=true, allowIndividualEventTickets=true)`. The bug failed closed (overprotective), so no bypass was possible. The existing test mirrored the bug by mocking the field at the top level; updated the three mocks to nest it under `tickets` so the test shape matches the real schema.
141 lines
4.5 KiB
JavaScript
141 lines
4.5 KiB
JavaScript
import Member from "../../../models/member.js";
|
|
import Series from "../../../models/series.js";
|
|
import Event from "../../../models/event.js";
|
|
import { sendEventRegistrationEmail } from "../../../utils/resend.js";
|
|
import { validateBody } from "../../../utils/validateBody.js";
|
|
import { eventRegistrationSchema } from "../../../utils/schemas.js";
|
|
import { loadPublicEvent } from "../../../utils/loadEvent.js";
|
|
import {
|
|
hasMemberAccess,
|
|
validateTicketPurchase,
|
|
checkUserSeriesPass,
|
|
} from "../../../utils/tickets.js";
|
|
|
|
export default defineEventHandler(async (event) => {
|
|
try {
|
|
const identifier = getRouterParam(event, "id");
|
|
const body = await validateBody(event, eventRegistrationSchema);
|
|
|
|
const eventData = await loadPublicEvent(event, identifier);
|
|
|
|
// Series-pass gate: when an event is linked to a series and that series
|
|
// requires a pass, reject drop-in registration unless the series
|
|
// explicitly allows individual event tickets.
|
|
if (
|
|
eventData.tickets?.requiresSeriesTicket &&
|
|
eventData.tickets?.seriesTicketReference
|
|
) {
|
|
const series = await Series.findById(
|
|
eventData.tickets.seriesTicketReference,
|
|
);
|
|
if (!series) {
|
|
throw createError({
|
|
statusCode: 500,
|
|
statusMessage: "Series referenced by this event could not be found",
|
|
});
|
|
}
|
|
if (!series.tickets?.allowIndividualEventTickets) {
|
|
const { hasPass } = checkUserSeriesPass(series, body.email);
|
|
if (!hasPass) {
|
|
throw createError({
|
|
statusCode: 403,
|
|
statusMessage:
|
|
"This event requires a series pass. See the series page to purchase.",
|
|
data: { seriesSlug: series.slug },
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Look up member for pricing/access purposes. hasMemberAccess treats
|
|
// guest/suspended/cancelled as non-members.
|
|
const member = await Member.findOne({ email: body.email.toLowerCase() });
|
|
|
|
// Single gate: validateTicketPurchase covers deadline, cancelled, already
|
|
// started, already-registered, members-only, and sold-out.
|
|
const validation = validateTicketPurchase(eventData, {
|
|
email: body.email,
|
|
name: body.name,
|
|
member: hasMemberAccess(member) ? member : null,
|
|
});
|
|
|
|
if (!validation.valid) {
|
|
const statusCode = /members only/i.test(validation.reason) ? 403 : 400;
|
|
throw createError({ statusCode, statusMessage: validation.reason });
|
|
}
|
|
|
|
const memberHasAccess = hasMemberAccess(member);
|
|
let isMember = false;
|
|
let membershipLevel = "non-member";
|
|
|
|
if (memberHasAccess) {
|
|
isMember = true;
|
|
membershipLevel = `${member.circle}-${member.contributionAmount}`;
|
|
}
|
|
|
|
// Add registration
|
|
const registration = {
|
|
memberId: member ? member._id : null,
|
|
name: body.name,
|
|
email: body.email.toLowerCase(),
|
|
membershipLevel,
|
|
isMember,
|
|
paymentStatus: "not_required", // Free events or member registrations
|
|
amountPaid: 0,
|
|
dietary: body.dietary || false,
|
|
registeredAt: new Date(),
|
|
};
|
|
|
|
// Use $push to avoid re-validating the whole document (e.g. legacy location formats)
|
|
const result = await Event.findByIdAndUpdate(
|
|
eventData._id,
|
|
{ $push: { registrations: registration } },
|
|
{ new: true, runValidators: false },
|
|
);
|
|
const newRegistration =
|
|
result.registrations[result.registrations.length - 1];
|
|
|
|
// Log activity
|
|
if (member) {
|
|
logActivity(member._id, 'event_registered', {
|
|
eventId: eventData._id,
|
|
eventTitle: eventData.title,
|
|
eventSlug: eventData.slug
|
|
})
|
|
}
|
|
|
|
// Send confirmation email — respect member notification preferences
|
|
const shouldSendEventEmail =
|
|
!member || member.notifications?.events !== false;
|
|
if (shouldSendEventEmail) {
|
|
try {
|
|
await sendEventRegistrationEmail(registration, eventData);
|
|
if (member) {
|
|
logActivity(member._id, 'email_sent', {
|
|
emailType: 'event_registration',
|
|
subject: `You're registered for ${eventData.title}`
|
|
})
|
|
}
|
|
} catch (emailError) {
|
|
console.error("Failed to send confirmation email:", emailError);
|
|
}
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
message: "Successfully registered for the event",
|
|
registrationId: newRegistration._id,
|
|
};
|
|
} catch (error) {
|
|
console.error("Error registering for event:", error);
|
|
|
|
if (error.statusCode) {
|
|
throw error;
|
|
}
|
|
|
|
throw createError({
|
|
statusCode: 500,
|
|
statusMessage: "Failed to register for event",
|
|
});
|
|
}
|
|
});
|