- 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
239 lines
5.1 KiB
JavaScript
239 lines
5.1 KiB
JavaScript
const mongoose = require('mongoose');
|
|
|
|
const roomSchema = new mongoose.Schema({
|
|
// Basic room information
|
|
name: {
|
|
type: String,
|
|
required: true,
|
|
trim: true
|
|
},
|
|
type: {
|
|
type: String,
|
|
required: true,
|
|
enum: ['Standard', 'Deluxe', 'Suite', 'Executive Suite', 'Presidential Suite'],
|
|
},
|
|
category: {
|
|
type: mongoose.Schema.Types.ObjectId,
|
|
ref: 'RoomCategory',
|
|
required: false // Optional for backward compatibility
|
|
},
|
|
description: {
|
|
type: String,
|
|
required: true
|
|
},
|
|
shortDescription: {
|
|
type: String,
|
|
required: true,
|
|
maxlength: 200
|
|
},
|
|
|
|
// Room specifications
|
|
roomNumber: {
|
|
type: String,
|
|
required: true,
|
|
unique: true
|
|
},
|
|
floor: {
|
|
type: Number,
|
|
required: true
|
|
},
|
|
size: {
|
|
type: Number, // in square meters
|
|
required: true
|
|
},
|
|
maxOccupancy: {
|
|
type: Number,
|
|
required: true,
|
|
min: 1,
|
|
max: 8
|
|
},
|
|
bedType: {
|
|
type: String,
|
|
required: true,
|
|
enum: ['Single', 'Double', 'Queen', 'King', 'Twin', 'Sofa Bed']
|
|
},
|
|
bedCount: {
|
|
type: Number,
|
|
required: true,
|
|
min: 1
|
|
},
|
|
|
|
// Pricing
|
|
basePrice: {
|
|
type: Number,
|
|
required: true,
|
|
min: 0
|
|
},
|
|
seasonalPricing: [{
|
|
season: String,
|
|
startDate: Date,
|
|
endDate: Date,
|
|
priceMultiplier: Number // 1.2 for 20% increase
|
|
}],
|
|
|
|
// Room features and amenities
|
|
amenities: [{
|
|
type: String,
|
|
enum: [
|
|
'WiFi', 'TV', 'AC', 'Minibar', 'Safe', 'Balcony', 'Ocean View',
|
|
'City View', 'Mountain View', 'Garden View', 'Jacuzzi', 'Fireplace',
|
|
'Kitchen', 'Kitchenette', 'Workspace', 'Butler Service', 'Spa Access',
|
|
'Private Pool', 'Terrace', 'Walk-in Closet', 'Sound System'
|
|
]
|
|
}],
|
|
|
|
// Media
|
|
images: [{
|
|
url: String,
|
|
alt: String,
|
|
isPrimary: { type: Boolean, default: false }
|
|
}],
|
|
virtualTour: {
|
|
url: String,
|
|
provider: String // '360°', 'Matterport', etc.
|
|
},
|
|
|
|
// Availability and status
|
|
status: {
|
|
type: String,
|
|
enum: ['Available', 'Occupied', 'Out of Order', 'Maintenance'],
|
|
default: 'Available'
|
|
},
|
|
isActive: {
|
|
type: Boolean,
|
|
default: true
|
|
},
|
|
|
|
// Integration IDs for external systems
|
|
operaRoomId: String, // Opera PMS room ID
|
|
bookingComRoomId: String,
|
|
expediaRoomId: String,
|
|
tripComRoomId: String,
|
|
|
|
// SEO and metadata
|
|
slug: {
|
|
type: String,
|
|
unique: true,
|
|
lowercase: true
|
|
},
|
|
metaTitle: String,
|
|
metaDescription: String,
|
|
|
|
// Room policies
|
|
smokingAllowed: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
petsAllowed: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
|
|
// Maintenance and housekeeping
|
|
lastMaintenance: Date,
|
|
lastCleaning: Date,
|
|
cleaningStatus: {
|
|
type: String,
|
|
enum: ['Clean', 'Dirty', 'In Progress', 'Inspected'],
|
|
default: 'Clean'
|
|
}
|
|
}, {
|
|
timestamps: true,
|
|
toJSON: { virtuals: true },
|
|
toObject: { virtuals: true }
|
|
});
|
|
|
|
// Indexes
|
|
roomSchema.index({ roomNumber: 1 });
|
|
roomSchema.index({ type: 1, status: 1 });
|
|
roomSchema.index({ slug: 1 });
|
|
roomSchema.index({ category: 1 });
|
|
roomSchema.index({ operaRoomId: 1 });
|
|
|
|
// Virtual for current price (considering seasonal pricing)
|
|
roomSchema.virtual('currentPrice').get(function() {
|
|
const now = new Date();
|
|
const seasonalRate = this.seasonalPricing.find(pricing =>
|
|
pricing.startDate <= now && pricing.endDate >= now
|
|
);
|
|
|
|
return seasonalRate
|
|
? this.basePrice * seasonalRate.priceMultiplier
|
|
: this.basePrice;
|
|
});
|
|
|
|
// Pre-save middleware to generate slug
|
|
roomSchema.pre('save', function(next) {
|
|
if (this.isModified('name') || !this.slug) {
|
|
this.slug = this.name
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]/g, '-')
|
|
.replace(/-+/g, '-')
|
|
.replace(/^-|-$/g, '');
|
|
}
|
|
next();
|
|
});
|
|
|
|
// Static method to find available rooms
|
|
roomSchema.statics.findAvailable = function(checkIn, checkOut, guests = 1) {
|
|
return this.aggregate([
|
|
{
|
|
$match: {
|
|
status: 'Available',
|
|
isActive: true,
|
|
maxOccupancy: { $gte: guests }
|
|
}
|
|
},
|
|
{
|
|
$lookup: {
|
|
from: 'bookings',
|
|
let: { roomId: '$_id' },
|
|
pipeline: [
|
|
{
|
|
$match: {
|
|
$expr: { $eq: ['$room', '$$roomId'] },
|
|
status: { $in: ['Confirmed', 'Checked In'] },
|
|
$or: [
|
|
{
|
|
checkInDate: { $lt: checkOut },
|
|
checkOutDate: { $gt: checkIn }
|
|
}
|
|
]
|
|
}
|
|
}
|
|
],
|
|
as: 'conflictingBookings'
|
|
}
|
|
},
|
|
{
|
|
$match: {
|
|
conflictingBookings: { $size: 0 }
|
|
}
|
|
},
|
|
{
|
|
$project: {
|
|
conflictingBookings: 0
|
|
}
|
|
}
|
|
]);
|
|
};
|
|
|
|
// Instance method to check availability
|
|
roomSchema.methods.isAvailable = async function(checkIn, checkOut) {
|
|
const Booking = mongoose.model('Booking');
|
|
|
|
const conflictingBooking = await Booking.findOne({
|
|
room: this._id,
|
|
status: { $in: ['Confirmed', 'Checked In'] },
|
|
$or: [
|
|
{
|
|
checkInDate: { $lt: checkOut },
|
|
checkOutDate: { $gt: checkIn }
|
|
}
|
|
]
|
|
});
|
|
|
|
return !conflictingBooking && this.status === 'Available' && this.isActive;
|
|
};
|
|
|
|
module.exports = mongoose.model('Room', roomSchema); |