Files
oldvine_cms/models/Room.js
Talal Sharabi a3308a26e2 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
2026-01-06 12:21:56 +04:00

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