251 lines
8.2 KiB
TypeScript
251 lines
8.2 KiB
TypeScript
// server/api/events/[id]/rsvp.post.ts
|
|
import { createNocoDBEventsClient } from '~/server/utils/nocodb-events';
|
|
import { getMemberByKeycloakId } from '~/server/utils/nocodb';
|
|
import { createSessionManager } from '~/server/utils/session';
|
|
import type { EventRSVPRequest } from '~/utils/types';
|
|
|
|
export default defineEventHandler(async (event) => {
|
|
try {
|
|
const eventId = getRouterParam(event, 'id');
|
|
const body = await readBody(event) as EventRSVPRequest;
|
|
|
|
if (!eventId) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
statusMessage: 'Event ID is required'
|
|
});
|
|
}
|
|
|
|
// Get user session using the proper SessionManager
|
|
const sessionManager = createSessionManager();
|
|
const cookieHeader = getHeader(event, 'cookie');
|
|
const session = sessionManager.getSession(cookieHeader);
|
|
|
|
if (!session || !session.user) {
|
|
throw createError({
|
|
statusCode: 401,
|
|
statusMessage: 'Authentication required'
|
|
});
|
|
}
|
|
|
|
const eventsClient = createNocoDBEventsClient();
|
|
|
|
// Get the event details
|
|
const eventDetails = await eventsClient.findOne(eventId);
|
|
if (!eventDetails) {
|
|
throw createError({
|
|
statusCode: 404,
|
|
statusMessage: 'Event not found'
|
|
});
|
|
}
|
|
|
|
// Check if event is active
|
|
if (eventDetails.status !== 'active') {
|
|
throw createError({
|
|
statusCode: 400,
|
|
statusMessage: 'Cannot RSVP to inactive events'
|
|
});
|
|
}
|
|
|
|
// Check if event is in the past
|
|
const eventDate = new Date(eventDetails.start_datetime);
|
|
const now = new Date();
|
|
if (eventDate < now) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
statusMessage: 'Cannot RSVP to past events'
|
|
});
|
|
}
|
|
|
|
// Get member details for pricing logic
|
|
const member = await getMemberByKeycloakId(session.user.id);
|
|
if (!member) {
|
|
throw createError({
|
|
statusCode: 404,
|
|
statusMessage: 'Member record not found'
|
|
});
|
|
}
|
|
|
|
// Generate payment reference
|
|
const paymentReference = generatePaymentReference(member.member_id || member.Id);
|
|
|
|
// Determine pricing and payment status
|
|
let paymentStatus: 'pending' | 'not_required' | 'paid' | 'overdue' = 'not_required';
|
|
let isMemberPricing = 'false';
|
|
|
|
if (eventDetails.is_paid === 'true' && body.rsvp_status === 'confirmed') {
|
|
paymentStatus = 'pending';
|
|
|
|
// Check if member qualifies for member pricing
|
|
const isDuesCurrent = member.current_year_dues_paid === 'true';
|
|
const memberPricingEnabled = eventDetails.member_pricing_enabled === 'true';
|
|
|
|
if (isDuesCurrent && memberPricingEnabled) {
|
|
isMemberPricing = 'true';
|
|
}
|
|
}
|
|
|
|
// Validate guest count if event allows guests
|
|
const extraGuests = parseInt((body as any).extra_guests || '0');
|
|
if (extraGuests > 0) {
|
|
if (eventDetails.guests_permitted !== 'true') {
|
|
throw createError({
|
|
statusCode: 400,
|
|
statusMessage: 'This event does not allow guests'
|
|
});
|
|
}
|
|
|
|
const maxGuestsAllowed = parseInt(eventDetails.max_guests_permitted || '0');
|
|
if (extraGuests > maxGuestsAllowed) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
statusMessage: `Maximum ${maxGuestsAllowed} guests allowed per person`
|
|
});
|
|
}
|
|
}
|
|
|
|
// Check event capacity including guests
|
|
if (eventDetails.max_attendees && body.rsvp_status === 'confirmed') {
|
|
const maxCapacity = parseInt(eventDetails.max_attendees);
|
|
const currentAttendees = parseInt(eventDetails.current_attendees || '0');
|
|
const totalRequested = 1 + extraGuests; // Member + guests
|
|
|
|
if (currentAttendees + totalRequested > maxCapacity) {
|
|
// Auto-set to waitlist if over capacity
|
|
body.rsvp_status = 'waitlist';
|
|
console.log(`[RSVP] Event at capacity, placing on waitlist: ${currentAttendees} + ${totalRequested} > ${maxCapacity}`);
|
|
}
|
|
}
|
|
|
|
// Get the event to find the proper event_id for RSVP tracking
|
|
let rsvpEventId = eventId; // Default to the provided eventId
|
|
try {
|
|
// Try to get the event_id field if it exists in the event
|
|
if (eventDetails.event_id) {
|
|
rsvpEventId = eventDetails.event_id;
|
|
console.log('[RSVP] Using event.event_id for RSVP:', rsvpEventId);
|
|
} else {
|
|
console.log('[RSVP] Using provided eventId for RSVP:', rsvpEventId);
|
|
}
|
|
} catch (eventIdError) {
|
|
console.log('[RSVP] Could not get event_id, using provided eventId:', rsvpEventId);
|
|
}
|
|
|
|
// Create RSVP record
|
|
const rsvpData = {
|
|
record_type: 'rsvp',
|
|
event_id: rsvpEventId, // Use the business event_id field
|
|
member_id: member.member_id || member.Id,
|
|
rsvp_status: body.rsvp_status,
|
|
payment_status: paymentStatus,
|
|
payment_reference: paymentReference,
|
|
attended: 'false',
|
|
rsvp_notes: body.rsvp_notes || '',
|
|
extra_guests: extraGuests.toString(),
|
|
is_member_pricing: isMemberPricing,
|
|
created_at: new Date().toISOString(),
|
|
updated_at: new Date().toISOString()
|
|
};
|
|
|
|
const newRSVP = await eventsClient.createRSVP(rsvpData);
|
|
|
|
// Update event attendee count using the new force update method
|
|
try {
|
|
const newAttendeeCount = await eventsClient.forceUpdateEventAttendeeCount(eventId);
|
|
console.log('[RSVP] ✅ Force updated event attendee count for event:', eventId, 'to:', newAttendeeCount);
|
|
} catch (countError) {
|
|
console.log('[RSVP] ⚠️ Failed to force update attendee count:', countError);
|
|
// Don't fail the RSVP creation if count update fails
|
|
}
|
|
|
|
// Include payment information in response for paid events
|
|
let responseData: any = newRSVP;
|
|
|
|
if (eventDetails.is_paid === 'true' && paymentStatus === 'pending') {
|
|
const registrationConfig = await getRegistrationConfig();
|
|
|
|
responseData = {
|
|
...newRSVP,
|
|
payment_info: {
|
|
cost: isMemberPricing === 'true' ?
|
|
eventDetails.cost_members :
|
|
eventDetails.cost_non_members,
|
|
iban: registrationConfig.iban,
|
|
recipient: registrationConfig.accountHolder,
|
|
reference: paymentReference,
|
|
member_pricing: isMemberPricing === 'true'
|
|
}
|
|
};
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
data: responseData,
|
|
message: (body.rsvp_status as string) === 'waitlist' ?
|
|
'Added to waitlist - event is full' :
|
|
'RSVP submitted successfully'
|
|
};
|
|
|
|
} catch (error: any) {
|
|
console.error('Error processing RSVP:', error);
|
|
|
|
if (error.statusCode) {
|
|
throw error;
|
|
}
|
|
|
|
throw createError({
|
|
statusCode: 500,
|
|
statusMessage: 'Failed to process RSVP'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Helper functions
|
|
|
|
function generatePaymentReference(memberId: string): string {
|
|
const date = new Date().toISOString().split('T')[0];
|
|
return `EVT-${memberId}-${date}`;
|
|
}
|
|
|
|
async function getRegistrationConfig() {
|
|
// This should fetch from your admin config system
|
|
return {
|
|
iban: 'FR76 1234 5678 9012 3456 7890 123',
|
|
accountHolder: 'MonacoUSA Association'
|
|
};
|
|
}
|
|
|
|
async function updateEventAttendeeCount(eventId: string) {
|
|
console.log('[updateEventAttendeeCount] Updating attendee count for event:', eventId);
|
|
|
|
try {
|
|
const eventsClient = createNocoDBEventsClient();
|
|
|
|
// Get all confirmed RSVPs for this event
|
|
const confirmedRSVPs = await eventsClient.getEventRSVPs(eventId, 'confirmed');
|
|
|
|
// Calculate total attendees (confirmed RSVPs + their guests)
|
|
let totalAttendees = 0;
|
|
|
|
for (const rsvp of confirmedRSVPs) {
|
|
totalAttendees += 1; // The member themselves
|
|
const guestCount = parseInt(rsvp.extra_guests || '0');
|
|
totalAttendees += guestCount; // Add their guests
|
|
}
|
|
|
|
console.log('[updateEventAttendeeCount] Calculated total attendees:', totalAttendees, 'from', confirmedRSVPs.length, 'RSVPs');
|
|
|
|
// Update the event's current_attendees field
|
|
await eventsClient.update(eventId, {
|
|
current_attendees: totalAttendees.toString()
|
|
});
|
|
|
|
console.log('[updateEventAttendeeCount] ✅ Successfully updated event attendee count to:', totalAttendees);
|
|
|
|
return totalAttendees;
|
|
} catch (error) {
|
|
console.error('[updateEventAttendeeCount] ❌ Error updating attendee count:', error);
|
|
throw error;
|
|
}
|
|
}
|