Add series management and ticketing features: Introduce series event functionality in event creation, enhance event display with series information, and implement ticketing options for public events. Update layouts and improve form handling for better user experience.

This commit is contained in:
Jennie Robinson Faber 2025-08-27 20:40:54 +01:00
parent c3a29fa47c
commit a88aa62198
24 changed files with 2897 additions and 44 deletions

View file

@ -0,0 +1,112 @@
import Event from '../../../models/event.js'
import { connectDB } from '../../../utils/mongoose.js'
import mongoose from 'mongoose'
export default defineEventHandler(async (event) => {
try {
await connectDB()
const identifier = getRouterParam(event, 'id')
const body = await readBody(event)
if (!identifier) {
throw createError({
statusCode: 400,
statusMessage: 'Event identifier is required'
})
}
// Validate required fields for guest registration
if (!body.name || !body.email) {
throw createError({
statusCode: 400,
statusMessage: 'Name and email are required'
})
}
// Fetch the event
let eventData
if (mongoose.Types.ObjectId.isValid(identifier)) {
eventData = await Event.findById(identifier)
}
if (!eventData) {
eventData = await Event.findOne({ slug: identifier })
}
if (!eventData) {
throw createError({
statusCode: 404,
statusMessage: 'Event not found'
})
}
// Check if event allows public registration (not members-only)
if (eventData.membersOnly) {
throw createError({
statusCode: 403,
statusMessage: 'This event is for members only. Please become a member to register.'
})
}
// If event requires payment, reject guest registration
if (eventData.pricing.paymentRequired && !eventData.pricing.isFree) {
throw createError({
statusCode: 402,
statusMessage: 'This event requires payment. Please use the payment registration endpoint.'
})
}
// Check if event is full
if (eventData.maxAttendees && eventData.registrations.length >= eventData.maxAttendees) {
throw createError({
statusCode: 400,
statusMessage: 'Event is full'
})
}
// Check if already registered
const alreadyRegistered = eventData.registrations.some(
reg => reg.email.toLowerCase() === body.email.toLowerCase()
)
if (alreadyRegistered) {
throw createError({
statusCode: 400,
statusMessage: 'You are already registered for this event'
})
}
// Add guest registration
eventData.registrations.push({
name: body.name,
email: body.email.toLowerCase(),
membershipLevel: 'guest',
isMember: false,
paymentStatus: 'not_required',
amountPaid: 0,
registeredAt: new Date()
})
await eventData.save()
// TODO: Send confirmation email for guest registration
return {
success: true,
message: 'Successfully registered as guest',
registrationId: eventData.registrations[eventData.registrations.length - 1]._id,
note: 'As a guest, you have access to this free public event. Consider becoming a member for access to all events!'
}
} catch (error) {
console.error('Error with guest registration:', error)
if (error.statusCode) {
throw error
}
throw createError({
statusCode: 500,
statusMessage: 'Failed to register as guest'
})
}
})

View file

@ -0,0 +1,136 @@
import Event from '../../../models/event.js'
import Member from '../../../models/member.js'
import { connectDB } from '../../../utils/mongoose.js'
import { processHelcimPayment } from '../../../utils/helcim.js'
import mongoose from 'mongoose'
export default defineEventHandler(async (event) => {
try {
await connectDB()
const identifier = getRouterParam(event, 'id')
const body = await readBody(event)
if (!identifier) {
throw createError({
statusCode: 400,
statusMessage: 'Event identifier is required'
})
}
// Validate required payment fields
if (!body.name || !body.email || !body.paymentToken) {
throw createError({
statusCode: 400,
statusMessage: 'Name, email, and payment token are required'
})
}
// Fetch the event
let eventData
if (mongoose.Types.ObjectId.isValid(identifier)) {
eventData = await Event.findById(identifier)
}
if (!eventData) {
eventData = await Event.findOne({ slug: identifier })
}
if (!eventData) {
throw createError({
statusCode: 404,
statusMessage: 'Event not found'
})
}
// Check if event requires payment
if (eventData.pricing.isFree || !eventData.pricing.paymentRequired) {
throw createError({
statusCode: 400,
statusMessage: 'This event does not require payment'
})
}
// Check if user is already registered
const existingRegistration = eventData.registrations.find(
reg => reg.email.toLowerCase() === body.email.toLowerCase()
)
if (existingRegistration) {
throw createError({
statusCode: 400,
statusMessage: 'You are already registered for this event'
})
}
// Check if user is a member (members get free access)
const member = await Member.findOne({ email: body.email.toLowerCase() })
if (member) {
// Members get free access - register directly without payment
eventData.registrations.push({
name: body.name,
email: body.email.toLowerCase(),
membershipLevel: `${member.circle}-${member.contributionTier}`,
isMember: true,
paymentStatus: 'not_required',
amountPaid: 0
})
await eventData.save()
return {
success: true,
message: 'Successfully registered as a member (no payment required)',
registration: eventData.registrations[eventData.registrations.length - 1]
}
}
// Process payment for non-members
const paymentResult = await processHelcimPayment({
amount: eventData.pricing.publicPrice,
paymentToken: body.paymentToken,
customerData: {
name: body.name,
email: body.email
}
})
if (!paymentResult.success) {
throw createError({
statusCode: 400,
statusMessage: paymentResult.message || 'Payment failed'
})
}
// Add registration with successful payment
eventData.registrations.push({
name: body.name,
email: body.email.toLowerCase(),
membershipLevel: 'non-member',
isMember: false,
paymentStatus: 'completed',
paymentId: paymentResult.transactionId,
amountPaid: eventData.pricing.publicPrice
})
await eventData.save()
return {
success: true,
message: 'Payment successful and registered for event',
paymentId: paymentResult.transactionId,
registration: eventData.registrations[eventData.registrations.length - 1]
}
} catch (error) {
console.error('Error processing event payment:', error)
if (error.statusCode) {
throw error
}
throw createError({
statusCode: 500,
statusMessage: 'Failed to process payment and registration'
})
}
})

View file

@ -65,27 +65,41 @@ export default defineEventHandler(async (event) => {
})
}
// Check member status if event is members-only
if (eventData.membersOnly && body.membershipLevel === 'non-member') {
// Check if email belongs to a member
const member = await Member.findOne({ email: body.email.toLowerCase() })
if (!member) {
throw createError({
statusCode: 403,
statusMessage: 'This event is for members only. Please become a member to register.'
})
}
// Update membership level from database
body.membershipLevel = `${member.circle}-${member.contributionTier}`
// Check member status and handle different registration scenarios
const member = await Member.findOne({ email: body.email.toLowerCase() })
if (eventData.membersOnly && !member) {
throw createError({
statusCode: 403,
statusMessage: 'This event is for members only. Please become a member to register.'
})
}
// If event requires payment and user is not a member, redirect to payment flow
if (eventData.pricing.paymentRequired && !eventData.pricing.isFree && !member) {
throw createError({
statusCode: 402, // Payment Required
statusMessage: 'This event requires payment. Please use the payment registration endpoint.'
})
}
// Set member status and membership level
let isMember = false
let membershipLevel = 'non-member'
if (member) {
isMember = true
membershipLevel = `${member.circle}-${member.contributionTier}`
}
// Add registration
eventData.registrations.push({
name: body.name,
email: body.email.toLowerCase(),
membershipLevel: body.membershipLevel || 'non-member',
membershipLevel,
isMember,
paymentStatus: 'not_required', // Free events or member registrations
amountPaid: 0,
dietary: body.dietary || false,
registeredAt: new Date()
})