Compare commits

..

1 Commits

Author SHA1 Message Date
yotakii
9e5919fb6c fix validation error 2026-03-05 11:01:49 +03:00
2 changed files with 184 additions and 66 deletions

View File

@@ -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

View File

@@ -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;