fix validation error
This commit is contained in:
@@ -24,8 +24,19 @@ const bookingSchema = new mongoose.Schema({
|
|||||||
room: {
|
room: {
|
||||||
type: mongoose.Schema.Types.ObjectId,
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
ref: 'Room',
|
ref: 'Room',
|
||||||
required: true
|
required: false
|
||||||
},
|
},
|
||||||
|
|
||||||
|
roomCategoryId: {
|
||||||
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
|
ref: 'RoomCategory',
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
requestedRoomType: {
|
||||||
|
type: String,
|
||||||
|
maxlength: 200
|
||||||
|
},
|
||||||
|
|
||||||
checkInDate: {
|
checkInDate: {
|
||||||
type: Date,
|
type: Date,
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -10,6 +10,50 @@ const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
|
|||||||
const sendEmail = require('../utils/sendEmail');
|
const sendEmail = require('../utils/sendEmail');
|
||||||
const logger = require('../utils/logger');
|
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
|
// @route POST /api/bookings/request
|
||||||
// @desc Create a booking request (no online payment)
|
// @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.lastName').notEmpty().withMessage('Last name is required'),
|
||||||
body('guestInfo.email').isEmail().withMessage('Valid email is required'),
|
body('guestInfo.email').isEmail().withMessage('Valid email is required'),
|
||||||
body('guestInfo.phone').notEmpty().withMessage('Phone number 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('checkInDate').isISO8601().withMessage('Valid check-in date is required'),
|
||||||
body('checkOutDate').isISO8601().withMessage('Valid check-out 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'),
|
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 checkIn = new Date(checkInDate);
|
||||||
const checkOut = new Date(checkOutDate);
|
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' });
|
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);
|
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 roomId provided, keep old behavior
|
||||||
if (!isAvailable) {
|
let room = null;
|
||||||
return res.status(400).json({
|
if (roomId) {
|
||||||
success: false,
|
room = await Room.findById(roomId);
|
||||||
message: 'Room is not available for the selected dates'
|
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
|
// Find or create guest
|
||||||
@@ -80,14 +171,23 @@ router.post('/request', [
|
|||||||
|
|
||||||
// Pricing
|
// Pricing
|
||||||
const numberOfNights = Math.ceil((checkOut - checkIn) / (1000 * 60 * 60 * 24));
|
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 subtotal = roomRate * numberOfNights;
|
||||||
const taxes = subtotal * 0.12;
|
const taxes = subtotal * 0.12;
|
||||||
const totalAmount = subtotal + taxes;
|
const totalAmount = subtotal + taxes;
|
||||||
|
|
||||||
const booking = new Booking({
|
const booking = new Booking({
|
||||||
guest: guest._id,
|
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,
|
checkInDate: checkIn,
|
||||||
checkOutDate: checkOut,
|
checkOutDate: checkOut,
|
||||||
numberOfGuests,
|
numberOfGuests,
|
||||||
@@ -332,7 +432,6 @@ router.get('/:bookingNumber', async (req, res) => {
|
|||||||
confirmationCode
|
confirmationCode
|
||||||
}).populate(['guest', 'room']);
|
}).populate(['guest', 'room']);
|
||||||
} else {
|
} else {
|
||||||
// Otherwise require authentication (implement auth middleware check here)
|
|
||||||
booking = await Booking.findOne({ bookingNumber })
|
booking = await Booking.findOne({ bookingNumber })
|
||||||
.populate(['guest', 'room']);
|
.populate(['guest', 'room']);
|
||||||
}
|
}
|
||||||
@@ -544,12 +643,9 @@ router.get('/', adminAuth, async (req, res) => {
|
|||||||
as: 'room'
|
as: 'room'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{ $unwind: '$guest' },
|
||||||
$unwind: '$guest'
|
// ✅ preserve bookings even if room is null
|
||||||
},
|
{ $unwind: { path: '$room', preserveNullAndEmptyArrays: true } }
|
||||||
{
|
|
||||||
$unwind: '$room'
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const [bookings, totalCount] = await Promise.all([
|
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') {
|
if (booking.status !== 'Confirmed') {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
@@ -603,12 +707,10 @@ router.put('/:id/checkin', adminAuth, async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update booking status
|
|
||||||
booking.status = 'Checked In';
|
booking.status = 'Checked In';
|
||||||
booking.actualCheckInTime = new Date();
|
booking.actualCheckInTime = new Date();
|
||||||
await booking.save();
|
await booking.save();
|
||||||
|
|
||||||
// Update room status
|
|
||||||
const room = await Room.findById(booking.room._id);
|
const room = await Room.findById(booking.room._id);
|
||||||
room.status = 'Occupied';
|
room.status = 'Occupied';
|
||||||
await room.save();
|
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') {
|
if (booking.status !== 'Checked In') {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
@@ -649,12 +759,10 @@ router.put('/:id/checkout', adminAuth, async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update booking status
|
|
||||||
booking.status = 'Checked Out';
|
booking.status = 'Checked Out';
|
||||||
booking.actualCheckOutTime = new Date();
|
booking.actualCheckOutTime = new Date();
|
||||||
await booking.save();
|
await booking.save();
|
||||||
|
|
||||||
// Update room status
|
|
||||||
const room = await Room.findById(booking.room._id);
|
const room = await Room.findById(booking.room._id);
|
||||||
room.status = 'Available';
|
room.status = 'Available';
|
||||||
room.cleaningStatus = 'Dirty';
|
room.cleaningStatus = 'Dirty';
|
||||||
@@ -687,7 +795,6 @@ router.get('/analytics/revenue', adminAuth, async (req, res) => {
|
|||||||
|
|
||||||
const revenueData = await Booking.generateRevenueReport(start, end);
|
const revenueData = await Booking.generateRevenueReport(start, end);
|
||||||
|
|
||||||
// Calculate total metrics
|
|
||||||
const totalRevenue = revenueData.reduce((sum, day) => sum + day.totalRevenue, 0);
|
const totalRevenue = revenueData.reduce((sum, day) => sum + day.totalRevenue, 0);
|
||||||
const totalBookings = revenueData.reduce((sum, day) => sum + day.bookingsCount, 0);
|
const totalBookings = revenueData.reduce((sum, day) => sum + day.bookingsCount, 0);
|
||||||
const averageBookingValue = totalBookings > 0 ? totalRevenue / totalBookings : 0;
|
const averageBookingValue = totalBookings > 0 ? totalRevenue / totalBookings : 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user