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:
338
models/Guest.js
Normal file
338
models/Guest.js
Normal file
@@ -0,0 +1,338 @@
|
||||
const mongoose = require('mongoose');
|
||||
const bcrypt = require('bcryptjs');
|
||||
|
||||
const guestSchema = new mongoose.Schema({
|
||||
// Personal information
|
||||
firstName: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
},
|
||||
lastName: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
},
|
||||
email: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
lowercase: true,
|
||||
trim: true,
|
||||
match: [/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/, 'Please enter a valid email']
|
||||
},
|
||||
phone: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
},
|
||||
|
||||
// Authentication
|
||||
password: {
|
||||
type: String,
|
||||
minlength: 6,
|
||||
select: false // Don't include in queries by default
|
||||
},
|
||||
isRegistered: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
// Address information
|
||||
address: {
|
||||
street: String,
|
||||
city: String,
|
||||
state: String,
|
||||
country: String,
|
||||
zipCode: String
|
||||
},
|
||||
|
||||
// Personal details
|
||||
dateOfBirth: Date,
|
||||
nationality: String,
|
||||
gender: {
|
||||
type: String,
|
||||
enum: ['Male', 'Female', 'Other', 'Prefer not to say']
|
||||
},
|
||||
|
||||
// Identification
|
||||
idType: {
|
||||
type: String,
|
||||
enum: ['Passport', 'Driver License', 'National ID', 'Other']
|
||||
},
|
||||
idNumber: String,
|
||||
idExpiryDate: Date,
|
||||
|
||||
// Preferences
|
||||
preferences: {
|
||||
roomType: {
|
||||
type: String,
|
||||
enum: ['Standard', 'Deluxe', 'Suite', 'Executive Suite', 'Presidential Suite']
|
||||
},
|
||||
bedPreference: {
|
||||
type: String,
|
||||
enum: ['Single', 'Double', 'Queen', 'King', 'Twin']
|
||||
},
|
||||
smokingPreference: {
|
||||
type: String,
|
||||
enum: ['Non-smoking', 'Smoking'],
|
||||
default: 'Non-smoking'
|
||||
},
|
||||
floorPreference: {
|
||||
type: String,
|
||||
enum: ['Low', 'High', 'No preference'],
|
||||
default: 'No preference'
|
||||
},
|
||||
viewPreference: {
|
||||
type: String,
|
||||
enum: ['Ocean', 'City', 'Garden', 'Mountain', 'No preference'],
|
||||
default: 'No preference'
|
||||
},
|
||||
language: {
|
||||
type: String,
|
||||
default: 'en'
|
||||
},
|
||||
currency: {
|
||||
type: String,
|
||||
default: 'USD'
|
||||
}
|
||||
},
|
||||
|
||||
// Special requirements
|
||||
specialRequirements: {
|
||||
accessibility: {
|
||||
wheelchairAccess: { type: Boolean, default: false },
|
||||
hearingImpaired: { type: Boolean, default: false },
|
||||
visuallyImpaired: { type: Boolean, default: false },
|
||||
other: String
|
||||
},
|
||||
dietaryRestrictions: [{
|
||||
type: String,
|
||||
enum: ['Vegetarian', 'Vegan', 'Gluten-free', 'Halal', 'Kosher', 'Diabetic', 'Other']
|
||||
}],
|
||||
allergies: [String],
|
||||
medicalConditions: String
|
||||
},
|
||||
|
||||
// Loyalty program
|
||||
loyaltyProgram: {
|
||||
memberId: String,
|
||||
tier: {
|
||||
type: String,
|
||||
enum: ['Bronze', 'Silver', 'Gold', 'Platinum'],
|
||||
default: 'Bronze'
|
||||
},
|
||||
points: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
joinDate: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
},
|
||||
|
||||
// Communication preferences
|
||||
communicationPreferences: {
|
||||
email: {
|
||||
marketing: { type: Boolean, default: false },
|
||||
bookingUpdates: { type: Boolean, default: true },
|
||||
specialOffers: { type: Boolean, default: false }
|
||||
},
|
||||
sms: {
|
||||
marketing: { type: Boolean, default: false },
|
||||
bookingUpdates: { type: Boolean, default: false },
|
||||
specialOffers: { type: Boolean, default: false }
|
||||
},
|
||||
phone: {
|
||||
marketing: { type: Boolean, default: false },
|
||||
bookingUpdates: { type: Boolean, default: false }
|
||||
}
|
||||
},
|
||||
|
||||
// Guest history
|
||||
totalStays: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
totalSpent: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
lastStayDate: Date,
|
||||
averageRating: {
|
||||
type: Number,
|
||||
min: 1,
|
||||
max: 5
|
||||
},
|
||||
|
||||
// VIP status
|
||||
isVIP: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
vipNotes: String,
|
||||
|
||||
// External system IDs
|
||||
operaGuestId: String,
|
||||
externalGuestIds: [{
|
||||
system: String, // 'booking.com', 'expedia', etc.
|
||||
id: String
|
||||
}],
|
||||
|
||||
// Emergency contact
|
||||
emergencyContact: {
|
||||
name: String,
|
||||
relationship: String,
|
||||
phone: String,
|
||||
email: String
|
||||
},
|
||||
|
||||
// Account status
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
isBlacklisted: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
blacklistReason: String,
|
||||
|
||||
// Verification
|
||||
emailVerified: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
phoneVerified: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
emailVerificationToken: String,
|
||||
passwordResetToken: String,
|
||||
passwordResetExpires: Date,
|
||||
|
||||
// Notes and comments
|
||||
internalNotes: String,
|
||||
|
||||
// GDPR compliance
|
||||
dataConsent: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
consentDate: Date,
|
||||
|
||||
// Last activity
|
||||
lastLogin: Date,
|
||||
lastActivity: Date
|
||||
}, {
|
||||
timestamps: true,
|
||||
toJSON: { virtuals: true },
|
||||
toObject: { virtuals: true }
|
||||
});
|
||||
|
||||
// Indexes
|
||||
guestSchema.index({ email: 1 });
|
||||
guestSchema.index({ phone: 1 });
|
||||
guestSchema.index({ 'loyaltyProgram.memberId': 1 });
|
||||
guestSchema.index({ operaGuestId: 1 });
|
||||
guestSchema.index({ isVIP: 1 });
|
||||
guestSchema.index({ totalStays: -1 });
|
||||
guestSchema.index({ totalSpent: -1 });
|
||||
|
||||
// Virtual for full name
|
||||
guestSchema.virtual('fullName').get(function() {
|
||||
return `${this.firstName} ${this.lastName}`;
|
||||
});
|
||||
|
||||
// Virtual for formatted address
|
||||
guestSchema.virtual('formattedAddress').get(function() {
|
||||
if (!this.address || !this.address.street) return '';
|
||||
|
||||
const { street, city, state, country, zipCode } = this.address;
|
||||
return `${street}, ${city}, ${state} ${zipCode}, ${country}`;
|
||||
});
|
||||
|
||||
// Pre-save middleware to hash password
|
||||
guestSchema.pre('save', async function(next) {
|
||||
// Only hash password if it's modified and exists
|
||||
if (!this.isModified('password') || !this.password) {
|
||||
return next();
|
||||
}
|
||||
|
||||
try {
|
||||
// Hash password with cost of 12
|
||||
const hashedPassword = await bcrypt.hash(this.password, 12);
|
||||
this.password = hashedPassword;
|
||||
next();
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Method to check password
|
||||
guestSchema.methods.comparePassword = async function(candidatePassword) {
|
||||
if (!this.password) return false;
|
||||
return bcrypt.compare(candidatePassword, this.password);
|
||||
};
|
||||
|
||||
// Method to update loyalty points
|
||||
guestSchema.methods.addLoyaltyPoints = function(points) {
|
||||
this.loyaltyProgram.points += points;
|
||||
|
||||
// Update tier based on points
|
||||
if (this.loyaltyProgram.points >= 10000) {
|
||||
this.loyaltyProgram.tier = 'Platinum';
|
||||
} else if (this.loyaltyProgram.points >= 5000) {
|
||||
this.loyaltyProgram.tier = 'Gold';
|
||||
} else if (this.loyaltyProgram.points >= 1000) {
|
||||
this.loyaltyProgram.tier = 'Silver';
|
||||
}
|
||||
|
||||
return this.save();
|
||||
};
|
||||
|
||||
// Method to update stay statistics
|
||||
guestSchema.methods.updateStayStats = function(stayAmount) {
|
||||
this.totalStays += 1;
|
||||
this.totalSpent += stayAmount;
|
||||
this.lastStayDate = new Date();
|
||||
|
||||
// Check VIP status
|
||||
if (this.totalStays >= 10 && this.totalSpent >= 5000) {
|
||||
this.isVIP = true;
|
||||
}
|
||||
|
||||
return this.save();
|
||||
};
|
||||
|
||||
// Static method to find VIP guests
|
||||
guestSchema.statics.findVIPGuests = function() {
|
||||
return this.find({ isVIP: true, isActive: true })
|
||||
.sort({ totalSpent: -1 })
|
||||
.select('firstName lastName email phone totalStays totalSpent loyaltyProgram');
|
||||
};
|
||||
|
||||
// Static method to find guests by loyalty tier
|
||||
guestSchema.statics.findByLoyaltyTier = function(tier) {
|
||||
return this.find({
|
||||
'loyaltyProgram.tier': tier,
|
||||
isActive: true
|
||||
}).sort({ 'loyaltyProgram.points': -1 });
|
||||
};
|
||||
|
||||
// Method to generate password reset token
|
||||
guestSchema.methods.createPasswordResetToken = function() {
|
||||
const resetToken = require('crypto').randomBytes(32).toString('hex');
|
||||
|
||||
this.passwordResetToken = require('crypto')
|
||||
.createHash('sha256')
|
||||
.update(resetToken)
|
||||
.digest('hex');
|
||||
|
||||
this.passwordResetExpires = Date.now() + 10 * 60 * 1000; // 10 minutes
|
||||
|
||||
return resetToken;
|
||||
};
|
||||
|
||||
module.exports = mongoose.model('Guest', guestSchema);
|
||||
Reference in New Issue
Block a user