fix validation error
This commit is contained in:
@@ -12,20 +12,31 @@ const bookingSchema = new mongoose.Schema({
|
||||
required: true,
|
||||
unique: true
|
||||
},
|
||||
|
||||
|
||||
// Guest information
|
||||
guest: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Guest',
|
||||
required: true
|
||||
},
|
||||
|
||||
|
||||
// Room and dates
|
||||
room: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Room',
|
||||
required: true
|
||||
required: false
|
||||
},
|
||||
|
||||
roomCategoryId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'RoomCategory',
|
||||
required: false
|
||||
},
|
||||
requestedRoomType: {
|
||||
type: String,
|
||||
maxlength: 200
|
||||
},
|
||||
|
||||
checkInDate: {
|
||||
type: Date,
|
||||
required: true
|
||||
@@ -34,7 +45,7 @@ const bookingSchema = new mongoose.Schema({
|
||||
type: Date,
|
||||
required: true
|
||||
},
|
||||
|
||||
|
||||
// Guest details
|
||||
numberOfGuests: {
|
||||
adults: {
|
||||
@@ -48,7 +59,7 @@ const bookingSchema = new mongoose.Schema({
|
||||
min: 0
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// Pricing
|
||||
roomRate: {
|
||||
type: Number,
|
||||
@@ -78,7 +89,7 @@ const bookingSchema = new mongoose.Schema({
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
|
||||
|
||||
// Payment information
|
||||
paymentStatus: {
|
||||
type: String,
|
||||
@@ -90,14 +101,14 @@ const bookingSchema = new mongoose.Schema({
|
||||
enum: ['Credit Card', 'Debit Card', 'Bank Transfer', 'Cash', 'Online Payment']
|
||||
},
|
||||
stripePaymentIntentId: String,
|
||||
|
||||
|
||||
// Booking status
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['Pending', 'Confirmed', 'Checked In', 'Checked Out', 'Cancelled', 'No Show'],
|
||||
default: 'Pending'
|
||||
},
|
||||
|
||||
|
||||
// Special requests and notes
|
||||
specialRequests: {
|
||||
type: String,
|
||||
@@ -107,7 +118,7 @@ const bookingSchema = new mongoose.Schema({
|
||||
type: String,
|
||||
maxlength: 1000
|
||||
},
|
||||
|
||||
|
||||
// Check-in/out details
|
||||
actualCheckInTime: Date,
|
||||
actualCheckOutTime: Date,
|
||||
@@ -119,18 +130,18 @@ const bookingSchema = new mongoose.Schema({
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
|
||||
// Booking source
|
||||
bookingSource: {
|
||||
type: String,
|
||||
enum: ['Direct', 'Booking.com', 'Expedia', 'Trip.com', 'Phone', 'Walk-in', 'Travel Agent'],
|
||||
default: 'Direct'
|
||||
},
|
||||
|
||||
|
||||
// External system IDs
|
||||
operaBookingId: String,
|
||||
externalBookingId: String, // ID from booking platforms
|
||||
|
||||
|
||||
// Cancellation
|
||||
cancellationReason: String,
|
||||
cancellationDate: Date,
|
||||
@@ -142,7 +153,7 @@ const bookingSchema = new mongoose.Schema({
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
|
||||
|
||||
// Communication
|
||||
emailConfirmationSent: {
|
||||
type: Boolean,
|
||||
@@ -152,7 +163,7 @@ const bookingSchema = new mongoose.Schema({
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
|
||||
// Additional services
|
||||
addOns: [{
|
||||
service: String,
|
||||
@@ -161,7 +172,7 @@ const bookingSchema = new mongoose.Schema({
|
||||
unitPrice: Number,
|
||||
totalPrice: Number
|
||||
}],
|
||||
|
||||
|
||||
// Group booking
|
||||
isGroupBooking: {
|
||||
type: Boolean,
|
||||
@@ -169,7 +180,7 @@ const bookingSchema = new mongoose.Schema({
|
||||
},
|
||||
groupSize: Number,
|
||||
groupLeader: String,
|
||||
|
||||
|
||||
// Loyalty program
|
||||
loyaltyPointsEarned: {
|
||||
type: Number,
|
||||
@@ -213,7 +224,7 @@ bookingSchema.pre('validate', function(next) {
|
||||
const random = Math.floor(100000 + Math.random() * 900000);
|
||||
this.bookingNumber = `OVH${year}${random}`;
|
||||
}
|
||||
|
||||
|
||||
if (!this.confirmationCode) {
|
||||
// Generate confirmation code: 8 character alphanumeric
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
@@ -223,13 +234,13 @@ bookingSchema.pre('validate', function(next) {
|
||||
}
|
||||
this.confirmationCode = code;
|
||||
}
|
||||
|
||||
|
||||
// Calculate number of nights if not set
|
||||
if (!this.numberOfNights && this.checkInDate && this.checkOutDate) {
|
||||
const diffTime = Math.abs(this.checkOutDate - this.checkInDate);
|
||||
this.numberOfNights = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
}
|
||||
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
@@ -265,9 +276,9 @@ bookingSchema.methods.canBeCancelled = function() {
|
||||
const now = new Date();
|
||||
const checkInDate = new Date(this.checkInDate);
|
||||
const hoursUntilCheckIn = (checkInDate - now) / (1000 * 60 * 60);
|
||||
|
||||
|
||||
return (
|
||||
this.status === 'Confirmed' &&
|
||||
this.status === 'Confirmed' &&
|
||||
hoursUntilCheckIn > 24 // Can cancel up to 24 hours before check-in
|
||||
);
|
||||
};
|
||||
@@ -277,7 +288,7 @@ bookingSchema.methods.calculateCancellationFee = function() {
|
||||
const now = new Date();
|
||||
const checkInDate = new Date(this.checkInDate);
|
||||
const hoursUntilCheckIn = (checkInDate - now) / (1000 * 60 * 60);
|
||||
|
||||
|
||||
if (hoursUntilCheckIn > 48) {
|
||||
return 0; // Free cancellation
|
||||
} else if (hoursUntilCheckIn > 24) {
|
||||
|
||||
@@ -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