forked from UKSOURCE/cms.hailearning.edu.vn
first commit
This commit is contained in:
558
controllers/bookingSubmissionController.js
Normal file
558
controllers/bookingSubmissionController.js
Normal file
@@ -0,0 +1,558 @@
|
||||
const BookingSubmission = require('../models/bookingSubmission');
|
||||
const Activity = require('../models/activity');
|
||||
|
||||
// API endpoint để tạo booking submission mới
|
||||
exports.submitBooking = async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
activityId,
|
||||
sessionId,
|
||||
parentFirstName,
|
||||
parentLastName,
|
||||
email,
|
||||
phone,
|
||||
address,
|
||||
city,
|
||||
country,
|
||||
postalCode,
|
||||
participantFirstName,
|
||||
participantLastName,
|
||||
participantBirthDate,
|
||||
participantGender,
|
||||
numberOfParticipants,
|
||||
medicalConditions,
|
||||
dietaryRestrictions,
|
||||
specialRequests,
|
||||
emergencyContact,
|
||||
emergencyPhone,
|
||||
agreeTerms,
|
||||
agreeNewsletter
|
||||
} = req.body;
|
||||
|
||||
// Validate required fields
|
||||
if (!activityId || !sessionId || !parentFirstName || !parentLastName ||
|
||||
!email || !phone || !address || !city || !country || !postalCode ||
|
||||
!participantFirstName || !participantLastName || !participantBirthDate ||
|
||||
!participantGender || !emergencyContact || !emergencyPhone || !agreeTerms) {
|
||||
return res.status(400).json({
|
||||
error: 'Missing required fields',
|
||||
message: 'Please fill in all required fields'
|
||||
});
|
||||
}
|
||||
|
||||
// Verify activity exists
|
||||
const activity = await Activity.findById(activityId);
|
||||
if (!activity) {
|
||||
return res.status(404).json({
|
||||
error: 'Activity not found',
|
||||
message: 'The selected activity does not exist'
|
||||
});
|
||||
}
|
||||
|
||||
// Verify session exists and is active
|
||||
const session = activity.bookingSessions?.find(s => s.sessionId === sessionId);
|
||||
if (!session) {
|
||||
return res.status(404).json({
|
||||
error: 'Session not found',
|
||||
message: 'The selected session does not exist'
|
||||
});
|
||||
}
|
||||
|
||||
if (!session.isActive) {
|
||||
return res.status(400).json({
|
||||
error: 'Session not available',
|
||||
message: 'The selected session is no longer available for booking'
|
||||
});
|
||||
}
|
||||
|
||||
// Check availability based on participant gender
|
||||
const currentBookings = await BookingSubmission.countDocuments({
|
||||
activityId,
|
||||
sessionId,
|
||||
participantGender,
|
||||
status: { $in: ['pending', 'confirmed'] }
|
||||
});
|
||||
|
||||
const availableSpots = participantGender === 'male'
|
||||
? session.totalMaleSpots - session.bookedMaleSpots
|
||||
: session.totalFemaleSpots - session.bookedFemaleSpots;
|
||||
|
||||
if (currentBookings >= availableSpots) {
|
||||
return res.status(400).json({
|
||||
error: 'Session full',
|
||||
message: `No more spots available for ${participantGender} participants in this session`
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate total amount based on activity price and number of participants
|
||||
const totalAmount = (activity.price || 0) * (parseInt(numberOfParticipants) || 1);
|
||||
|
||||
// Create booking submission
|
||||
const bookingSubmission = new BookingSubmission({
|
||||
activityId,
|
||||
sessionId,
|
||||
parentFirstName: parentFirstName.trim(),
|
||||
parentLastName: parentLastName.trim(),
|
||||
email: email.toLowerCase().trim(),
|
||||
phone: phone.trim(),
|
||||
address: address.trim(),
|
||||
city: city.trim(),
|
||||
country: country.trim(),
|
||||
postalCode: postalCode.trim(),
|
||||
participantFirstName: participantFirstName.trim(),
|
||||
participantLastName: participantLastName.trim(),
|
||||
participantBirthDate: new Date(participantBirthDate),
|
||||
participantGender,
|
||||
numberOfParticipants: parseInt(numberOfParticipants) || 1,
|
||||
medicalConditions: (medicalConditions || '').trim(),
|
||||
dietaryRestrictions: dietaryRestrictions || 'none',
|
||||
specialRequests: (specialRequests || '').trim(),
|
||||
emergencyContact: emergencyContact.trim(),
|
||||
emergencyPhone: emergencyPhone.trim(),
|
||||
agreeTerms: Boolean(agreeTerms),
|
||||
agreeNewsletter: Boolean(agreeNewsletter),
|
||||
totalAmount,
|
||||
status: 'pending',
|
||||
paymentStatus: 'pending'
|
||||
});
|
||||
|
||||
await bookingSubmission.save();
|
||||
|
||||
// Update session booked spots
|
||||
const updateField = participantGender === 'male' ? 'bookingSessions.$.bookedMaleSpots' : 'bookingSessions.$.bookedFemaleSpots';
|
||||
await Activity.updateOne(
|
||||
{ _id: activityId, 'bookingSessions.sessionId': sessionId },
|
||||
{ $inc: { [updateField]: 1 } }
|
||||
);
|
||||
|
||||
// Populate activity info for response
|
||||
await bookingSubmission.populate('activityId', 'name price');
|
||||
|
||||
return res.status(201).json({
|
||||
success: true,
|
||||
message: 'Booking submitted successfully',
|
||||
booking: {
|
||||
id: bookingSubmission._id,
|
||||
activityName: bookingSubmission.activityId.name,
|
||||
sessionId: bookingSubmission.sessionId,
|
||||
participantName: `${bookingSubmission.participantFirstName} ${bookingSubmission.participantLastName}`,
|
||||
totalAmount: bookingSubmission.totalAmount,
|
||||
status: bookingSubmission.status
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('submitBooking error:', error);
|
||||
|
||||
// Handle validation errors
|
||||
if (error.name === 'ValidationError') {
|
||||
const validationErrors = Object.values(error.errors).map(err => err.message);
|
||||
return res.status(400).json({
|
||||
error: 'Validation failed',
|
||||
message: validationErrors.join(', ')
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(500).json({
|
||||
error: 'Server error',
|
||||
message: 'An error occurred while processing your booking. Please try again.'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// API endpoint để lấy thông tin session availability
|
||||
exports.getSessionAvailability = async (req, res) => {
|
||||
try {
|
||||
const { activityId, sessionId } = req.params;
|
||||
|
||||
const activity = await Activity.findById(activityId);
|
||||
if (!activity) {
|
||||
return res.status(404).json({ error: 'Activity not found' });
|
||||
}
|
||||
|
||||
const session = activity.bookingSessions?.find(s => s.sessionId === sessionId);
|
||||
if (!session) {
|
||||
return res.status(404).json({ error: 'Session not found' });
|
||||
}
|
||||
|
||||
// Get current booking counts
|
||||
const maleBookings = await BookingSubmission.countDocuments({
|
||||
activityId,
|
||||
sessionId,
|
||||
participantGender: 'male',
|
||||
status: { $in: ['pending', 'confirmed'] }
|
||||
});
|
||||
|
||||
const femaleBookings = await BookingSubmission.countDocuments({
|
||||
activityId,
|
||||
sessionId,
|
||||
participantGender: 'female',
|
||||
status: { $in: ['pending', 'confirmed'] }
|
||||
});
|
||||
|
||||
return res.json({
|
||||
sessionId,
|
||||
isActive: session.isActive,
|
||||
startDate: session.startDate,
|
||||
endDate: session.endDate,
|
||||
overnightStays: session.overnightStays,
|
||||
price: session.price || activity.price,
|
||||
availability: {
|
||||
male: {
|
||||
total: session.totalMaleSpots,
|
||||
booked: maleBookings,
|
||||
available: Math.max(0, session.totalMaleSpots - maleBookings)
|
||||
},
|
||||
female: {
|
||||
total: session.totalFemaleSpots,
|
||||
booked: femaleBookings,
|
||||
available: Math.max(0, session.totalFemaleSpots - femaleBookings)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('getSessionAvailability error:', error);
|
||||
return res.status(500).json({ error: 'Error loading session availability' });
|
||||
}
|
||||
};
|
||||
|
||||
// API endpoint để lấy tất cả sessions có sẵn cho một activity
|
||||
exports.getAvailableSessions = async (req, res) => {
|
||||
try {
|
||||
const { activityId } = req.params;
|
||||
|
||||
const activity = await Activity.findById(activityId);
|
||||
if (!activity) {
|
||||
return res.status(404).json({ error: 'Activity not found' });
|
||||
}
|
||||
|
||||
const sessions = activity.bookingSessions || [];
|
||||
const availableSessions = [];
|
||||
|
||||
for (const session of sessions) {
|
||||
if (!session.isActive) continue;
|
||||
|
||||
// Get current booking counts
|
||||
const maleBookings = await BookingSubmission.countDocuments({
|
||||
activityId,
|
||||
sessionId: session.sessionId,
|
||||
participantGender: 'male',
|
||||
status: { $in: ['pending', 'confirmed'] }
|
||||
});
|
||||
|
||||
const femaleBookings = await BookingSubmission.countDocuments({
|
||||
activityId,
|
||||
sessionId: session.sessionId,
|
||||
participantGender: 'female',
|
||||
status: { $in: ['pending', 'confirmed'] }
|
||||
});
|
||||
|
||||
const maleAvailable = Math.max(0, session.totalMaleSpots - maleBookings);
|
||||
const femaleAvailable = Math.max(0, session.totalFemaleSpots - femaleBookings);
|
||||
|
||||
// Only include sessions that have available spots
|
||||
if (maleAvailable > 0 || femaleAvailable > 0) {
|
||||
availableSessions.push({
|
||||
sessionId: session.sessionId,
|
||||
startDate: session.startDate,
|
||||
endDate: session.endDate,
|
||||
overnightStays: session.overnightStays,
|
||||
price: session.price || activity.price,
|
||||
availability: {
|
||||
male: {
|
||||
total: session.totalMaleSpots,
|
||||
booked: maleBookings,
|
||||
available: maleAvailable
|
||||
},
|
||||
female: {
|
||||
total: session.totalFemaleSpots,
|
||||
booked: femaleBookings,
|
||||
available: femaleAvailable
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return res.json({
|
||||
activityId,
|
||||
activityName: activity.name,
|
||||
sessions: availableSessions
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('getAvailableSessions error:', error);
|
||||
return res.status(500).json({ error: 'Error loading available sessions' });
|
||||
}
|
||||
};
|
||||
|
||||
// API endpoint để cập nhật booking submission
|
||||
exports.updateBookingSubmission = async (req, res) => {
|
||||
try {
|
||||
const { bookingId } = req.params;
|
||||
const updateData = req.body;
|
||||
|
||||
// Find the booking
|
||||
let booking = await BookingSubmission.findById(bookingId);
|
||||
|
||||
// If not found as a separate document, try to find it as an embedded booking in Activity.bookingSessions
|
||||
let activityContaining = null;
|
||||
let sessionIndex = -1;
|
||||
let bookingIndex = -1;
|
||||
if (!booking) {
|
||||
activityContaining = await Activity.findOne({ 'bookingSessions.bookingList._id': bookingId });
|
||||
if (!activityContaining) {
|
||||
return res.status(404).json({
|
||||
error: 'Booking not found',
|
||||
message: 'The booking submission does not exist'
|
||||
});
|
||||
}
|
||||
|
||||
// locate the exact session and booking positions
|
||||
for (let si = 0; si < activityContaining.bookingSessions.length; si++) {
|
||||
const bl = activityContaining.bookingSessions[si].bookingList || [];
|
||||
const bi = bl.findIndex(b => b._id && b._id.toString() === bookingId.toString());
|
||||
if (bi !== -1) {
|
||||
sessionIndex = si;
|
||||
bookingIndex = bi;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (sessionIndex === -1 || bookingIndex === -1) {
|
||||
return res.status(404).json({ error: 'Booking not found', message: 'The booking submission does not exist' });
|
||||
}
|
||||
|
||||
booking = activityContaining.bookingSessions[sessionIndex].bookingList[bookingIndex];
|
||||
}
|
||||
|
||||
// Define allowed fields to update
|
||||
const allowedUpdates = [
|
||||
'status',
|
||||
'paymentStatus',
|
||||
'paidAmount',
|
||||
'totalAmount',
|
||||
'adminNotes',
|
||||
'emergencyContact',
|
||||
'emergencyPhone',
|
||||
'medicalConditions',
|
||||
'dietaryRestrictions',
|
||||
'specialRequests'
|
||||
];
|
||||
|
||||
// Build update object with only allowed fields
|
||||
const updateFields = {};
|
||||
for (const field of allowedUpdates) {
|
||||
if (updateData[field] !== undefined) {
|
||||
updateFields[field] = updateData[field];
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(updateFields).length === 0) {
|
||||
return res.status(400).json({
|
||||
error: 'No valid fields to update',
|
||||
message: 'Please provide at least one valid field to update'
|
||||
});
|
||||
}
|
||||
|
||||
// If booking is a separate document, update the BookingSubmission collection
|
||||
if (activityContaining === null) {
|
||||
const updatedBooking = await BookingSubmission.findByIdAndUpdate(
|
||||
bookingId,
|
||||
updateFields,
|
||||
{ new: true, runValidators: true }
|
||||
).populate('activityId', 'name price');
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: 'Booking updated successfully',
|
||||
booking: updatedBooking
|
||||
});
|
||||
}
|
||||
|
||||
// Otherwise update the embedded booking in the Activity document
|
||||
const currentBooking = activityContaining.bookingSessions[sessionIndex].bookingList[bookingIndex];
|
||||
|
||||
// Handle status updates and spot adjustments
|
||||
const newStatus = updateData.status || updateData.bookingStatus;
|
||||
const currentStatus = currentBooking.status || currentBooking.bookingStatus;
|
||||
|
||||
// Apply allowed updates to the embedded booking
|
||||
const allowedEmbeddedUpdates = [
|
||||
'status', 'bookingStatus', 'paymentStatus', 'paidAmount', 'totalAmount', 'adminNotes',
|
||||
'emergencyContact', 'emergencyPhone', 'medicalConditions', 'dietaryRestrictions', 'specialRequests'
|
||||
];
|
||||
|
||||
for (const field of allowedEmbeddedUpdates) {
|
||||
if (updateData[field] !== undefined) {
|
||||
if (field === 'status') {
|
||||
activityContaining.bookingSessions[sessionIndex].bookingList[bookingIndex].status = updateData.status;
|
||||
activityContaining.bookingSessions[sessionIndex].bookingList[bookingIndex].bookingStatus = updateData.status;
|
||||
} else {
|
||||
activityContaining.bookingSessions[sessionIndex].bookingList[bookingIndex][field] = updateData[field];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If status change affects spots, adjust counts
|
||||
if (newStatus && newStatus !== currentStatus) {
|
||||
const numberOfParticipants = currentBooking.numberOfParticipants || 1;
|
||||
const participantGender = currentBooking.participantGender;
|
||||
|
||||
// If booking is being cancelled, free up spots
|
||||
if (newStatus === 'cancelled' && currentStatus !== 'cancelled') {
|
||||
if (participantGender === 'male') {
|
||||
activityContaining.bookingSessions[sessionIndex].bookedMaleSpots = Math.max(0, activityContaining.bookingSessions[sessionIndex].bookedMaleSpots - numberOfParticipants);
|
||||
} else if (participantGender === 'female') {
|
||||
activityContaining.bookingSessions[sessionIndex].bookedFemaleSpots = Math.max(0, activityContaining.bookingSessions[sessionIndex].bookedFemaleSpots - numberOfParticipants);
|
||||
}
|
||||
}
|
||||
|
||||
// If restoring from cancelled, ensure capacity then book spots
|
||||
if (currentStatus === 'cancelled' && newStatus !== 'cancelled') {
|
||||
if (participantGender === 'male') {
|
||||
const totalMale = activityContaining.bookingSessions[sessionIndex].totalMaleSpots;
|
||||
const currentMale = activityContaining.bookingSessions[sessionIndex].bookedMaleSpots;
|
||||
if (currentMale + numberOfParticipants > totalMale) {
|
||||
return res.status(400).json({ error: "Not enough male spots available to restore this booking" });
|
||||
}
|
||||
activityContaining.bookingSessions[sessionIndex].bookedMaleSpots += numberOfParticipants;
|
||||
} else if (participantGender === 'female') {
|
||||
const totalFemale = activityContaining.bookingSessions[sessionIndex].totalFemaleSpots;
|
||||
const currentFemale = activityContaining.bookingSessions[sessionIndex].bookedFemaleSpots;
|
||||
if (currentFemale + numberOfParticipants > totalFemale) {
|
||||
return res.status(400).json({ error: "Not enough female spots available to restore this booking" });
|
||||
}
|
||||
activityContaining.bookingSessions[sessionIndex].bookedFemaleSpots += numberOfParticipants;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await activityContaining.save();
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: 'Embedded booking updated successfully',
|
||||
booking: activityContaining.bookingSessions[sessionIndex].bookingList[bookingIndex]
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('updateBookingSubmission error:', error);
|
||||
|
||||
// Handle validation errors
|
||||
if (error.name === 'ValidationError') {
|
||||
const validationErrors = Object.values(error.errors).map(err => err.message);
|
||||
return res.status(400).json({
|
||||
error: 'Validation failed',
|
||||
message: validationErrors.join(', ')
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(500).json({
|
||||
error: 'Server error',
|
||||
message: 'An error occurred while updating the booking'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// API endpoint để xóa booking submission
|
||||
exports.deleteBookingSubmission = async (req, res) => {
|
||||
try {
|
||||
const { bookingId } = req.params;
|
||||
|
||||
// Find and delete the booking
|
||||
let booking = await BookingSubmission.findById(bookingId);
|
||||
|
||||
// If not found in separate collection, try to delete embedded booking in Activity
|
||||
if (!booking) {
|
||||
const activityContaining = await Activity.findOne({ 'bookingSessions.bookingList._id': bookingId });
|
||||
if (!activityContaining) {
|
||||
return res.status(404).json({
|
||||
error: 'Booking not found',
|
||||
message: 'The booking submission does not exist'
|
||||
});
|
||||
}
|
||||
|
||||
// locate session and booking
|
||||
let sessionIndex = -1;
|
||||
let bookingIndex = -1;
|
||||
for (let si = 0; si < activityContaining.bookingSessions.length; si++) {
|
||||
const bl = activityContaining.bookingSessions[si].bookingList || [];
|
||||
const bi = bl.findIndex(b => b._id && b._id.toString() === bookingId.toString());
|
||||
if (bi !== -1) {
|
||||
sessionIndex = si;
|
||||
bookingIndex = bi;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (sessionIndex === -1 || bookingIndex === -1) {
|
||||
return res.status(404).json({ error: 'Booking not found', message: 'The booking submission does not exist' });
|
||||
}
|
||||
|
||||
const bookingToDelete = activityContaining.bookingSessions[sessionIndex].bookingList[bookingIndex];
|
||||
|
||||
// Free up spots if booking is not cancelled
|
||||
if ((bookingToDelete.bookingStatus || bookingToDelete.status) !== 'cancelled') {
|
||||
const numberOfParticipants = bookingToDelete.numberOfParticipants || 1;
|
||||
const participantGender = bookingToDelete.participantGender;
|
||||
|
||||
if (participantGender === 'male') {
|
||||
activityContaining.bookingSessions[sessionIndex].bookedMaleSpots = Math.max(0, activityContaining.bookingSessions[sessionIndex].bookedMaleSpots - numberOfParticipants);
|
||||
} else if (participantGender === 'female') {
|
||||
activityContaining.bookingSessions[sessionIndex].bookedFemaleSpots = Math.max(0, activityContaining.bookingSessions[sessionIndex].bookedFemaleSpots - numberOfParticipants);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove booking and save
|
||||
activityContaining.bookingSessions[sessionIndex].bookingList.splice(bookingIndex, 1);
|
||||
await activityContaining.save();
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: 'Embedded booking deleted successfully',
|
||||
booking: {
|
||||
id: bookingId,
|
||||
participantName: `${bookingToDelete.participantFirstName} ${bookingToDelete.participantLastName}`,
|
||||
email: bookingToDelete.email
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Store info for session spot adjustment
|
||||
const { activityId, sessionId, participantGender, numberOfParticipants } = booking;
|
||||
|
||||
// Delete the booking
|
||||
await BookingSubmission.findByIdAndDelete(bookingId);
|
||||
|
||||
// Update session booked spots (decrease the count)
|
||||
if (booking.status !== 'cancelled') {
|
||||
const updateField = participantGender === 'male'
|
||||
? 'bookingSessions.$.bookedMaleSpots'
|
||||
: 'bookingSessions.$.bookedFemaleSpots';
|
||||
|
||||
await Activity.updateOne(
|
||||
{ _id: activityId, 'bookingSessions.sessionId': sessionId },
|
||||
{ $inc: { [updateField]: -numberOfParticipants } }
|
||||
);
|
||||
}
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: 'Booking deleted successfully',
|
||||
booking: {
|
||||
id: bookingId,
|
||||
participantName: `${booking.participantFirstName} ${booking.participantLastName}`,
|
||||
email: booking.email
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('deleteBookingSubmission error:', error);
|
||||
return res.status(500).json({
|
||||
error: 'Server error',
|
||||
message: 'An error occurred while deleting the booking'
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user