Initial commit: CMS backend for Old Vine Hotel
- Complete Express.js API server - MongoDB integration with Mongoose - Admin authentication and authorization - Room management (CRUD operations) - Booking management system - Guest management - Payment processing (Stripe integration) - Content management (pages, blog, gallery) - Media upload and management - Integration services (Booking.com, Expedia, Opera PMS, Trip.com) - Email notifications - Comprehensive logging and error handling
This commit is contained in:
521
routes/rooms.js
Normal file
521
routes/rooms.js
Normal file
@@ -0,0 +1,521 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const Room = require('../models/Room');
|
||||
const RoomCategory = require('../models/RoomCategory');
|
||||
const { body, validationResult, query } = require('express-validator');
|
||||
const auth = require('../middleware/auth');
|
||||
const adminAuth = require('../middleware/adminAuth');
|
||||
|
||||
// @route GET /api/rooms
|
||||
// @desc Get all rooms with filtering and pagination
|
||||
// @access Public
|
||||
router.get('/', [
|
||||
query('page').optional().isInt({ min: 1 }),
|
||||
query('limit').optional().isInt({ min: 1, max: 100 }),
|
||||
query('type').optional().isIn(['Standard', 'Deluxe', 'Suite', 'Executive Suite', 'Presidential Suite']),
|
||||
query('maxPrice').optional().isFloat({ min: 0 }),
|
||||
query('minPrice').optional().isFloat({ min: 0 }),
|
||||
query('guests').optional().isInt({ min: 1 }),
|
||||
query('checkIn').optional().isISO8601(),
|
||||
query('checkOut').optional().isISO8601(),
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Validation errors',
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const {
|
||||
page = 1,
|
||||
limit = 12,
|
||||
type,
|
||||
category, // category slug or ID
|
||||
maxPrice,
|
||||
minPrice,
|
||||
guests,
|
||||
checkIn,
|
||||
checkOut,
|
||||
sortBy = 'name',
|
||||
sortOrder = 'asc'
|
||||
} = req.query;
|
||||
|
||||
// Build filter object
|
||||
let filter = {
|
||||
isActive: true,
|
||||
status: 'Available'
|
||||
};
|
||||
|
||||
if (type) filter.type = type;
|
||||
if (guests) filter.maxOccupancy = { $gte: parseInt(guests) };
|
||||
|
||||
// Handle category filtering
|
||||
if (category) {
|
||||
// Try to find category by slug first, then by ID
|
||||
const categoryDoc = await RoomCategory.findOne({
|
||||
$or: [
|
||||
{ slug: category },
|
||||
{ _id: category }
|
||||
]
|
||||
});
|
||||
|
||||
if (categoryDoc) {
|
||||
filter.category = categoryDoc._id;
|
||||
} else {
|
||||
// If category not found, return empty results
|
||||
return res.json({
|
||||
success: true,
|
||||
data: {
|
||||
rooms: [],
|
||||
pagination: {
|
||||
currentPage: parseInt(page),
|
||||
totalPages: 0,
|
||||
totalCount: 0,
|
||||
hasNextPage: false,
|
||||
hasPrevPage: false
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Price filtering (using virtual currentPrice would require aggregation)
|
||||
if (minPrice || maxPrice) {
|
||||
filter.basePrice = {};
|
||||
if (minPrice) filter.basePrice.$gte = parseFloat(minPrice);
|
||||
if (maxPrice) filter.basePrice.$lte = parseFloat(maxPrice);
|
||||
}
|
||||
|
||||
// If dates are provided, find available rooms
|
||||
let roomQuery;
|
||||
if (checkIn && checkOut) {
|
||||
const checkInDate = new Date(checkIn);
|
||||
const checkOutDate = new Date(checkOut);
|
||||
|
||||
if (checkInDate >= checkOutDate) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Check-out date must be after check-in date'
|
||||
});
|
||||
}
|
||||
|
||||
roomQuery = Room.findAvailable(checkInDate, checkOutDate, guests ? parseInt(guests) : 1);
|
||||
} else {
|
||||
roomQuery = Room.find(filter);
|
||||
}
|
||||
|
||||
// Apply sorting
|
||||
const sortOptions = {};
|
||||
sortOptions[sortBy] = sortOrder === 'desc' ? -1 : 1;
|
||||
|
||||
// Execute query with pagination
|
||||
const skip = (parseInt(page) - 1) * parseInt(limit);
|
||||
|
||||
const [rooms, totalCount] = await Promise.all([
|
||||
roomQuery
|
||||
.sort(sortOptions)
|
||||
.skip(skip)
|
||||
.limit(parseInt(limit)),
|
||||
|
||||
checkIn && checkOut ?
|
||||
Room.findAvailable(new Date(checkIn), new Date(checkOut), guests ? parseInt(guests) : 1).countDocuments() :
|
||||
Room.countDocuments(filter)
|
||||
]);
|
||||
|
||||
const totalPages = Math.ceil(totalCount / parseInt(limit));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
rooms,
|
||||
pagination: {
|
||||
currentPage: parseInt(page),
|
||||
totalPages,
|
||||
totalCount,
|
||||
hasNextPage: parseInt(page) < totalPages,
|
||||
hasPrevPage: parseInt(page) > 1
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching rooms:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error while fetching rooms'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route GET /api/rooms/:id
|
||||
// @desc Get single room by ID
|
||||
// @access Public
|
||||
router.get('/:id', async (req, res) => {
|
||||
try {
|
||||
const room = await Room.findById(req.params.id);
|
||||
|
||||
if (!room || !room.isActive) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Room not found'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: room
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching room:', error);
|
||||
|
||||
if (error.name === 'CastError') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Invalid room ID format'
|
||||
});
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error while fetching room'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route GET /api/rooms/slug/:slug
|
||||
// @desc Get single room by slug
|
||||
// @access Public
|
||||
router.get('/slug/:slug', async (req, res) => {
|
||||
try {
|
||||
const room = await Room.findOne({
|
||||
slug: req.params.slug,
|
||||
isActive: true
|
||||
});
|
||||
|
||||
if (!room) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Room not found'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: room
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching room by slug:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error while fetching room'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route POST /api/rooms/:id/availability
|
||||
// @desc Check room availability for specific dates
|
||||
// @access Public
|
||||
router.post('/:id/availability', [
|
||||
body('checkIn').isISO8601().withMessage('Valid check-in date is required'),
|
||||
body('checkOut').isISO8601().withMessage('Valid check-out date is required'),
|
||||
body('guests').optional().isInt({ min: 1 }).withMessage('Number of guests must be at least 1')
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Validation errors',
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const { checkIn, checkOut, guests = 1 } = req.body;
|
||||
const checkInDate = new Date(checkIn);
|
||||
const checkOutDate = new Date(checkOut);
|
||||
|
||||
if (checkInDate >= checkOutDate) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Check-out date must be after check-in date'
|
||||
});
|
||||
}
|
||||
|
||||
if (checkInDate < new Date()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Check-in date cannot be in the past'
|
||||
});
|
||||
}
|
||||
|
||||
const room = await Room.findById(req.params.id);
|
||||
|
||||
if (!room || !room.isActive) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Room not found'
|
||||
});
|
||||
}
|
||||
|
||||
if (room.maxOccupancy < guests) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: `Room can accommodate maximum ${room.maxOccupancy} guests`
|
||||
});
|
||||
}
|
||||
|
||||
const isAvailable = await room.isAvailable(checkInDate, checkOutDate);
|
||||
|
||||
// Calculate pricing
|
||||
const numberOfNights = Math.ceil((checkOutDate - checkInDate) / (1000 * 60 * 60 * 24));
|
||||
const roomRate = room.currentPrice;
|
||||
const subtotal = roomRate * numberOfNights;
|
||||
const taxes = subtotal * 0.12; // 12% tax
|
||||
const total = subtotal + taxes;
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
available: isAvailable,
|
||||
room: {
|
||||
id: room._id,
|
||||
name: room.name,
|
||||
type: room.type,
|
||||
maxOccupancy: room.maxOccupancy
|
||||
},
|
||||
pricing: {
|
||||
roomRate,
|
||||
numberOfNights,
|
||||
subtotal,
|
||||
taxes,
|
||||
total
|
||||
},
|
||||
dates: {
|
||||
checkIn: checkInDate,
|
||||
checkOut: checkOutDate
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error checking availability:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error while checking availability'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route GET /api/rooms/types/available
|
||||
// @desc Get available room types with counts
|
||||
// @access Public
|
||||
router.get('/types/available', [
|
||||
query('checkIn').optional().isISO8601(),
|
||||
query('checkOut').optional().isISO8601(),
|
||||
query('guests').optional().isInt({ min: 1 })
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const { checkIn, checkOut, guests = 1 } = req.query;
|
||||
|
||||
let aggregationPipeline = [
|
||||
{
|
||||
$match: {
|
||||
isActive: true,
|
||||
status: 'Available',
|
||||
maxOccupancy: { $gte: parseInt(guests) }
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// If dates provided, filter by availability
|
||||
if (checkIn && checkOut) {
|
||||
const checkInDate = new Date(checkIn);
|
||||
const checkOutDate = new Date(checkOut);
|
||||
|
||||
aggregationPipeline.push(
|
||||
{
|
||||
$lookup: {
|
||||
from: 'bookings',
|
||||
let: { roomId: '$_id' },
|
||||
pipeline: [
|
||||
{
|
||||
$match: {
|
||||
$expr: { $eq: ['$room', '$$roomId'] },
|
||||
status: { $in: ['Confirmed', 'Checked In'] },
|
||||
$or: [
|
||||
{
|
||||
checkInDate: { $lt: checkOutDate },
|
||||
checkOutDate: { $gt: checkInDate }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
as: 'conflictingBookings'
|
||||
}
|
||||
},
|
||||
{
|
||||
$match: {
|
||||
conflictingBookings: { $size: 0 }
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
aggregationPipeline.push(
|
||||
{
|
||||
$group: {
|
||||
_id: '$type',
|
||||
count: { $sum: 1 },
|
||||
minPrice: { $min: '$basePrice' },
|
||||
maxPrice: { $max: '$basePrice' },
|
||||
avgPrice: { $avg: '$basePrice' },
|
||||
rooms: {
|
||||
$push: {
|
||||
id: '$_id',
|
||||
name: '$name',
|
||||
price: '$basePrice',
|
||||
amenities: '$amenities',
|
||||
images: { $slice: ['$images', 1] }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$sort: { minPrice: 1 }
|
||||
}
|
||||
);
|
||||
|
||||
const roomTypes = await Room.aggregate(aggregationPipeline);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: roomTypes
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching room types:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error while fetching room types'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Protected routes below (require authentication)
|
||||
|
||||
// @route POST /api/rooms
|
||||
// @desc Create a new room (Admin only)
|
||||
// @access Private/Admin
|
||||
router.post('/', adminAuth, [
|
||||
body('name').notEmpty().withMessage('Room name is required'),
|
||||
body('type').isIn(['Standard', 'Deluxe', 'Suite', 'Executive Suite', 'Presidential Suite']),
|
||||
body('description').notEmpty().withMessage('Description is required'),
|
||||
body('roomNumber').notEmpty().withMessage('Room number is required'),
|
||||
body('floor').isInt({ min: 1 }).withMessage('Floor must be a positive integer'),
|
||||
body('size').isFloat({ min: 1 }).withMessage('Size must be a positive number'),
|
||||
body('maxOccupancy').isInt({ min: 1, max: 8 }).withMessage('Max occupancy must be between 1 and 8'),
|
||||
body('basePrice').isFloat({ min: 0 }).withMessage('Base price must be a positive number'),
|
||||
body('bedType').isIn(['Single', 'Double', 'Queen', 'King', 'Twin', 'Sofa Bed']),
|
||||
body('bedCount').isInt({ min: 1 }).withMessage('Bed count must be at least 1')
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Validation errors',
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
// Check if room number already exists
|
||||
const existingRoom = await Room.findOne({ roomNumber: req.body.roomNumber });
|
||||
if (existingRoom) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Room number already exists'
|
||||
});
|
||||
}
|
||||
|
||||
const room = new Room(req.body);
|
||||
await room.save();
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: 'Room created successfully',
|
||||
data: room
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error creating room:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error while creating room'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route PUT /api/rooms/:id
|
||||
// @desc Update room (Admin only)
|
||||
// @access Private/Admin
|
||||
router.put('/:id', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const room = await Room.findByIdAndUpdate(
|
||||
req.params.id,
|
||||
req.body,
|
||||
{ new: true, runValidators: true }
|
||||
);
|
||||
|
||||
if (!room) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Room not found'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Room updated successfully',
|
||||
data: room
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error updating room:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error while updating room'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @route DELETE /api/rooms/:id
|
||||
// @desc Delete room (Admin only)
|
||||
// @access Private/Admin
|
||||
router.delete('/:id', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const room = await Room.findByIdAndUpdate(
|
||||
req.params.id,
|
||||
{ isActive: false },
|
||||
{ new: true }
|
||||
);
|
||||
|
||||
if (!room) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Room not found'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Room deleted successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error deleting room:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error while deleting room'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user