fix validation error
This commit is contained in:
@@ -10,6 +10,50 @@ const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
|
||||
const sendEmail = require('../utils/sendEmail');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
const escapeRegex = (s = '') => String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
|
||||
async function resolveRoomFromCategory({ roomCategoryId, requestedRoomType, checkIn, checkOut, totalGuests }) {
|
||||
const or = [];
|
||||
|
||||
if (roomCategoryId) {
|
||||
or.push(
|
||||
{ category: roomCategoryId },
|
||||
{ categoryId: roomCategoryId },
|
||||
{ roomCategory: roomCategoryId },
|
||||
{ roomCategoryId: roomCategoryId },
|
||||
{ typeId: roomCategoryId }
|
||||
);
|
||||
}
|
||||
|
||||
if (requestedRoomType) {
|
||||
const rx = new RegExp(`^${escapeRegex(requestedRoomType)}$`, 'i');
|
||||
or.push(
|
||||
{ type: rx },
|
||||
{ roomType: rx },
|
||||
{ categoryName: rx },
|
||||
{ name: rx }
|
||||
);
|
||||
}
|
||||
|
||||
if (or.length === 0) return null;
|
||||
|
||||
const candidates = await Room.find({
|
||||
isActive: true,
|
||||
$or: or
|
||||
}).limit(50);
|
||||
|
||||
for (const room of candidates) {
|
||||
try {
|
||||
if (room.maxOccupancy != null && room.maxOccupancy < totalGuests) continue;
|
||||
const ok = await room.isAvailable(checkIn, checkOut);
|
||||
if (ok) return room;
|
||||
} catch (e) {
|
||||
// ignore and try next
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// @route POST /api/bookings/request
|
||||
// @desc Create a booking request (no online payment)
|
||||
@@ -19,7 +63,21 @@ router.post('/request', [
|
||||
body('guestInfo.lastName').notEmpty().withMessage('Last name is required'),
|
||||
body('guestInfo.email').isEmail().withMessage('Valid email is required'),
|
||||
body('guestInfo.phone').notEmpty().withMessage('Phone number is required'),
|
||||
body('roomId').isMongoId().withMessage('Valid room ID is required'),
|
||||
|
||||
// accept roomId OR roomCategoryId / requestedRoomType
|
||||
body('roomId').optional().isMongoId().withMessage('Valid room ID is required'),
|
||||
body('roomCategoryId').optional().isMongoId().withMessage('Valid room category ID is required'),
|
||||
body('requestedRoomType').optional().isString().isLength({ max: 200 }),
|
||||
body('roomCategory').optional().isString().isLength({ max: 200 }),
|
||||
|
||||
body().custom((_, { req }) => {
|
||||
if (req.body.roomId) return true;
|
||||
if (req.body.roomCategoryId) return true;
|
||||
if (req.body.requestedRoomType) return true;
|
||||
if (req.body.roomCategory) return true;
|
||||
throw new Error('roomId or roomCategoryId/requestedRoomType is required');
|
||||
}),
|
||||
|
||||
body('checkInDate').isISO8601().withMessage('Valid check-in date is required'),
|
||||
body('checkOutDate').isISO8601().withMessage('Valid check-out date is required'),
|
||||
body('numberOfGuests.adults').isInt({ min: 1 }).withMessage('At least 1 adult required'),
|
||||
@@ -35,7 +93,17 @@ router.post('/request', [
|
||||
});
|
||||
}
|
||||
|
||||
const { guestInfo, roomId, checkInDate, checkOutDate, numberOfGuests, specialRequests } = req.body;
|
||||
const {
|
||||
guestInfo,
|
||||
roomId,
|
||||
roomCategoryId,
|
||||
roomCategory,
|
||||
requestedRoomType,
|
||||
checkInDate,
|
||||
checkOutDate,
|
||||
numberOfGuests,
|
||||
specialRequests
|
||||
} = req.body;
|
||||
|
||||
const checkIn = new Date(checkInDate);
|
||||
const checkOut = new Date(checkOutDate);
|
||||
@@ -47,25 +115,48 @@ router.post('/request', [
|
||||
return res.status(400).json({ success: false, message: 'Check-in date cannot be in the past' });
|
||||
}
|
||||
|
||||
const room = await Room.findById(roomId);
|
||||
if (!room || !room.isActive) {
|
||||
return res.status(404).json({ success: false, message: 'Room not found' });
|
||||
}
|
||||
|
||||
const totalGuests = Number(numberOfGuests.adults) + Number(numberOfGuests.children || 0);
|
||||
if (room.maxOccupancy < totalGuests) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: `Room can accommodate maximum ${room.maxOccupancy} guests`
|
||||
});
|
||||
}
|
||||
|
||||
const isAvailable = await room.isAvailable(checkIn, checkOut);
|
||||
if (!isAvailable) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Room is not available for the selected dates'
|
||||
// If roomId provided, keep old behavior
|
||||
let room = null;
|
||||
if (roomId) {
|
||||
room = await Room.findById(roomId);
|
||||
if (!room || !room.isActive) {
|
||||
return res.status(404).json({ success: false, message: 'Room not found' });
|
||||
}
|
||||
|
||||
if (room.maxOccupancy < totalGuests) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: `Room can accommodate maximum ${room.maxOccupancy} guests`
|
||||
});
|
||||
}
|
||||
|
||||
const isAvailable = await room.isAvailable(checkIn, checkOut);
|
||||
if (!isAvailable) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Room is not available for the selected dates'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// No roomId => try to resolve an actual room from category/type
|
||||
const typeName = requestedRoomType || roomCategory || null;
|
||||
room = await resolveRoomFromCategory({
|
||||
roomCategoryId,
|
||||
requestedRoomType: typeName,
|
||||
checkIn,
|
||||
checkOut,
|
||||
totalGuests
|
||||
});
|
||||
|
||||
// If we found a room, enforce occupancy like before
|
||||
if (room && room.maxOccupancy != null && room.maxOccupancy < totalGuests) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: `Room can accommodate maximum ${room.maxOccupancy} guests`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Find or create guest
|
||||
@@ -80,14 +171,23 @@ router.post('/request', [
|
||||
|
||||
// Pricing
|
||||
const numberOfNights = Math.ceil((checkOut - checkIn) / (1000 * 60 * 60 * 24));
|
||||
const roomRate = room.currentPrice;
|
||||
|
||||
// If room exists => use its currentPrice (old behavior)
|
||||
// If room doesn't exist => set roomRate to 0 (or you can later adjust manually in admin)
|
||||
const roomRate = room ? Number(room.currentPrice || 0) : 0;
|
||||
|
||||
const subtotal = roomRate * numberOfNights;
|
||||
const taxes = subtotal * 0.12;
|
||||
const totalAmount = subtotal + taxes;
|
||||
|
||||
const booking = new Booking({
|
||||
guest: guest._id,
|
||||
room: roomId,
|
||||
room: room ? room._id : undefined,
|
||||
|
||||
// store category/type for category-only requests
|
||||
roomCategoryId: room ? undefined : (roomCategoryId || undefined),
|
||||
requestedRoomType: room ? undefined : (requestedRoomType || roomCategory || undefined),
|
||||
|
||||
checkInDate: checkIn,
|
||||
checkOutDate: checkOut,
|
||||
numberOfGuests,
|
||||
@@ -155,7 +255,7 @@ router.post('/', [
|
||||
|
||||
const checkIn = new Date(checkInDate);
|
||||
const checkOut = new Date(checkOutDate);
|
||||
|
||||
|
||||
// Validate dates
|
||||
if (checkIn >= checkOut) {
|
||||
return res.status(400).json({
|
||||
@@ -163,7 +263,7 @@ router.post('/', [
|
||||
message: 'Check-out date must be after check-in date'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (checkIn < new Date()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
@@ -284,7 +384,7 @@ router.post('/', [
|
||||
room: room
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
booking.emailConfirmationSent = true;
|
||||
await booking.save();
|
||||
} catch (emailError) {
|
||||
@@ -294,8 +394,8 @@ router.post('/', [
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: paymentIntent.status === 'succeeded'
|
||||
? 'Booking confirmed successfully'
|
||||
message: paymentIntent.status === 'succeeded'
|
||||
? 'Booking confirmed successfully'
|
||||
: 'Booking created, payment processing',
|
||||
data: {
|
||||
booking,
|
||||
@@ -324,7 +424,7 @@ router.get('/:bookingNumber', async (req, res) => {
|
||||
const { confirmationCode } = req.query;
|
||||
|
||||
let booking;
|
||||
|
||||
|
||||
// If confirmation code provided, allow public access
|
||||
if (confirmationCode) {
|
||||
booking = await Booking.findOne({
|
||||
@@ -332,7 +432,6 @@ router.get('/:bookingNumber', async (req, res) => {
|
||||
confirmationCode
|
||||
}).populate(['guest', 'room']);
|
||||
} else {
|
||||
// Otherwise require authentication (implement auth middleware check here)
|
||||
booking = await Booking.findOne({ bookingNumber })
|
||||
.populate(['guest', 'room']);
|
||||
}
|
||||
@@ -486,13 +585,13 @@ router.get('/', adminAuth, async (req, res) => {
|
||||
|
||||
// Build filter
|
||||
let filter = {};
|
||||
|
||||
|
||||
if (status) filter.status = status;
|
||||
if (guestEmail) {
|
||||
const guest = await Guest.findOne({ email: guestEmail });
|
||||
if (guest) filter.guest = guest._id;
|
||||
}
|
||||
|
||||
|
||||
if (checkInDate || checkOutDate) {
|
||||
filter.checkInDate = {};
|
||||
if (checkInDate) filter.checkInDate.$gte = new Date(checkInDate);
|
||||
@@ -501,7 +600,7 @@ router.get('/', adminAuth, async (req, res) => {
|
||||
|
||||
// Build aggregation pipeline for room type filter
|
||||
let aggregationPipeline = [{ $match: filter }];
|
||||
|
||||
|
||||
if (roomType) {
|
||||
aggregationPipeline.push(
|
||||
{
|
||||
@@ -523,7 +622,7 @@ router.get('/', adminAuth, async (req, res) => {
|
||||
// Add pagination and sorting
|
||||
const sortOptions = {};
|
||||
sortOptions[sortBy] = sortOrder === 'desc' ? -1 : 1;
|
||||
|
||||
|
||||
aggregationPipeline.push(
|
||||
{ $sort: sortOptions },
|
||||
{ $skip: (parseInt(page) - 1) * parseInt(limit) },
|
||||
@@ -544,12 +643,9 @@ router.get('/', adminAuth, async (req, res) => {
|
||||
as: 'room'
|
||||
}
|
||||
},
|
||||
{
|
||||
$unwind: '$guest'
|
||||
},
|
||||
{
|
||||
$unwind: '$room'
|
||||
}
|
||||
{ $unwind: '$guest' },
|
||||
// ✅ preserve bookings even if room is null
|
||||
{ $unwind: { path: '$room', preserveNullAndEmptyArrays: true } }
|
||||
);
|
||||
|
||||
const [bookings, totalCount] = await Promise.all([
|
||||
@@ -596,6 +692,14 @@ router.put('/:id/checkin', adminAuth, async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
// guard if no room assigned
|
||||
if (!booking.room) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Booking has no room assigned'
|
||||
});
|
||||
}
|
||||
|
||||
if (booking.status !== 'Confirmed') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
@@ -603,12 +707,10 @@ router.put('/:id/checkin', adminAuth, async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
// Update booking status
|
||||
booking.status = 'Checked In';
|
||||
booking.actualCheckInTime = new Date();
|
||||
await booking.save();
|
||||
|
||||
// Update room status
|
||||
const room = await Room.findById(booking.room._id);
|
||||
room.status = 'Occupied';
|
||||
await room.save();
|
||||
@@ -642,6 +744,14 @@ router.put('/:id/checkout', adminAuth, async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
// guard if no room assigned
|
||||
if (!booking.room) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Booking has no room assigned'
|
||||
});
|
||||
}
|
||||
|
||||
if (booking.status !== 'Checked In') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
@@ -649,12 +759,10 @@ router.put('/:id/checkout', adminAuth, async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
// Update booking status
|
||||
booking.status = 'Checked Out';
|
||||
booking.actualCheckOutTime = new Date();
|
||||
await booking.save();
|
||||
|
||||
// Update room status
|
||||
const room = await Room.findById(booking.room._id);
|
||||
room.status = 'Available';
|
||||
room.cleaningStatus = 'Dirty';
|
||||
@@ -681,13 +789,12 @@ router.put('/:id/checkout', adminAuth, async (req, res) => {
|
||||
router.get('/analytics/revenue', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const { startDate, endDate, groupBy = 'day' } = req.query;
|
||||
|
||||
|
||||
const start = startDate ? new Date(startDate) : new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
|
||||
const end = endDate ? new Date(endDate) : new Date();
|
||||
|
||||
const revenueData = await Booking.generateRevenueReport(start, end);
|
||||
|
||||
// Calculate total metrics
|
||||
|
||||
const totalRevenue = revenueData.reduce((sum, day) => sum + day.totalRevenue, 0);
|
||||
const totalBookings = revenueData.reduce((sum, day) => sum + day.bookingsCount, 0);
|
||||
const averageBookingValue = totalBookings > 0 ? totalRevenue / totalBookings : 0;
|
||||
|
||||
Reference in New Issue
Block a user