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:
239
models/Room.js
Normal file
239
models/Room.js
Normal file
@@ -0,0 +1,239 @@
|
||||
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);
|
||||
Reference in New Issue
Block a user