- 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
290 lines
6.4 KiB
JavaScript
290 lines
6.4 KiB
JavaScript
const mongoose = require('mongoose');
|
|
|
|
const bookingSchema = new mongoose.Schema({
|
|
// Booking identification
|
|
bookingNumber: {
|
|
type: String,
|
|
required: true,
|
|
unique: true
|
|
},
|
|
confirmationCode: {
|
|
type: String,
|
|
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
|
|
},
|
|
checkInDate: {
|
|
type: Date,
|
|
required: true
|
|
},
|
|
checkOutDate: {
|
|
type: Date,
|
|
required: true
|
|
},
|
|
|
|
// Guest details
|
|
numberOfGuests: {
|
|
adults: {
|
|
type: Number,
|
|
required: true,
|
|
min: 1
|
|
},
|
|
children: {
|
|
type: Number,
|
|
default: 0,
|
|
min: 0
|
|
}
|
|
},
|
|
|
|
// Pricing
|
|
roomRate: {
|
|
type: Number,
|
|
required: true
|
|
},
|
|
numberOfNights: {
|
|
type: Number,
|
|
required: true
|
|
},
|
|
subtotal: {
|
|
type: Number,
|
|
required: true
|
|
},
|
|
taxes: {
|
|
type: Number,
|
|
required: true
|
|
},
|
|
fees: {
|
|
type: Number,
|
|
default: 0
|
|
},
|
|
discounts: {
|
|
type: Number,
|
|
default: 0
|
|
},
|
|
totalAmount: {
|
|
type: Number,
|
|
required: true
|
|
},
|
|
|
|
// Payment information
|
|
paymentStatus: {
|
|
type: String,
|
|
enum: ['Pending', 'Paid', 'Partially Paid', 'Refunded', 'Failed'],
|
|
default: 'Pending'
|
|
},
|
|
paymentMethod: {
|
|
type: String,
|
|
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,
|
|
maxlength: 1000
|
|
},
|
|
internalNotes: {
|
|
type: String,
|
|
maxlength: 1000
|
|
},
|
|
|
|
// Check-in/out details
|
|
actualCheckInTime: Date,
|
|
actualCheckOutTime: Date,
|
|
earlyCheckIn: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
lateCheckOut: {
|
|
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,
|
|
cancellationFee: {
|
|
type: Number,
|
|
default: 0
|
|
},
|
|
refundAmount: {
|
|
type: Number,
|
|
default: 0
|
|
},
|
|
|
|
// Communication
|
|
emailConfirmationSent: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
smsConfirmationSent: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
|
|
// Additional services
|
|
addOns: [{
|
|
service: String,
|
|
description: String,
|
|
quantity: Number,
|
|
unitPrice: Number,
|
|
totalPrice: Number
|
|
}],
|
|
|
|
// Group booking
|
|
isGroupBooking: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
groupSize: Number,
|
|
groupLeader: String,
|
|
|
|
// Loyalty program
|
|
loyaltyPointsEarned: {
|
|
type: Number,
|
|
default: 0
|
|
},
|
|
loyaltyPointsRedeemed: {
|
|
type: Number,
|
|
default: 0
|
|
}
|
|
}, {
|
|
timestamps: true,
|
|
toJSON: { virtuals: true },
|
|
toObject: { virtuals: true }
|
|
});
|
|
|
|
// Indexes
|
|
bookingSchema.index({ bookingNumber: 1 });
|
|
bookingSchema.index({ confirmationCode: 1 });
|
|
bookingSchema.index({ guest: 1 });
|
|
bookingSchema.index({ room: 1 });
|
|
bookingSchema.index({ checkInDate: 1, checkOutDate: 1 });
|
|
bookingSchema.index({ status: 1 });
|
|
bookingSchema.index({ bookingSource: 1 });
|
|
bookingSchema.index({ operaBookingId: 1 });
|
|
bookingSchema.index({ createdAt: -1 });
|
|
|
|
// Virtual for booking duration
|
|
bookingSchema.virtual('duration').get(function() {
|
|
if (this.checkInDate && this.checkOutDate) {
|
|
const diffTime = Math.abs(this.checkOutDate - this.checkInDate);
|
|
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
}
|
|
return 0;
|
|
});
|
|
|
|
// Pre-save middleware to generate booking number and confirmation code
|
|
bookingSchema.pre('save', function(next) {
|
|
if (!this.bookingNumber) {
|
|
// Generate booking number: OVH + year + random 6 digits
|
|
const year = new Date().getFullYear();
|
|
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';
|
|
let code = '';
|
|
for (let i = 0; i < 8; i++) {
|
|
code += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
}
|
|
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();
|
|
});
|
|
|
|
// Static method to generate revenue reports
|
|
bookingSchema.statics.generateRevenueReport = function(startDate, endDate) {
|
|
return this.aggregate([
|
|
{
|
|
$match: {
|
|
status: { $in: ['Confirmed', 'Checked In', 'Checked Out'] },
|
|
checkInDate: { $gte: startDate, $lte: endDate }
|
|
}
|
|
},
|
|
{
|
|
$group: {
|
|
_id: {
|
|
year: { $year: '$checkInDate' },
|
|
month: { $month: '$checkInDate' },
|
|
day: { $dayOfMonth: '$checkInDate' }
|
|
},
|
|
totalRevenue: { $sum: '$totalAmount' },
|
|
bookingsCount: { $sum: 1 },
|
|
averageRate: { $avg: '$roomRate' }
|
|
}
|
|
},
|
|
{
|
|
$sort: { '_id.year': 1, '_id.month': 1, '_id.day': 1 }
|
|
}
|
|
]);
|
|
};
|
|
|
|
// Instance method to check if booking can be cancelled
|
|
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' &&
|
|
hoursUntilCheckIn > 24 // Can cancel up to 24 hours before check-in
|
|
);
|
|
};
|
|
|
|
// Instance method to calculate cancellation fee
|
|
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) {
|
|
return this.totalAmount * 0.25; // 25% fee
|
|
} else {
|
|
return this.totalAmount * 0.50; // 50% fee
|
|
}
|
|
};
|
|
|
|
module.exports = mongoose.model('Booking', bookingSchema); |