799 lines
19 KiB
Markdown
799 lines
19 KiB
Markdown
# The Old Vine Hotel - Development Documentation
|
|
|
|
## 🏗️ Architecture Overview
|
|
|
|
This hotel management system follows a modern, scalable architecture designed for enterprise-level operations.
|
|
|
|
### System Architecture
|
|
|
|
```
|
|
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
|
│ React Client │────│ Express API │────│ MongoDB │
|
|
│ (Port 3000) │ │ (Port 5000) │ │ Database │
|
|
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
|
│ │ │
|
|
│ ┌─────────────────┐ │
|
|
│ │ Integrations │ │
|
|
│ └─────────────────┘ │
|
|
│ │ │
|
|
│ ┌─────────────────┐ │
|
|
└──────────────│ External APIs │──────────────┘
|
|
│ • Opera PMS │
|
|
│ • Booking.com │
|
|
│ • Expedia │
|
|
│ • Trip.com │
|
|
│ • Stripe │
|
|
└─────────────────┘
|
|
```
|
|
|
|
## 🛠️ Development Setup
|
|
|
|
### Prerequisites
|
|
|
|
```bash
|
|
# Install Node.js (v16+)
|
|
curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash -
|
|
sudo apt-get install -y nodejs
|
|
|
|
# Install MongoDB
|
|
wget -qO - https://www.mongodb.org/static/pgp/server-5.0.asc | sudo apt-key add -
|
|
echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/5.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-5.0.list
|
|
sudo apt-get update
|
|
sudo apt-get install -y mongodb-org
|
|
sudo systemctl start mongod
|
|
sudo systemctl enable mongod
|
|
```
|
|
|
|
### Environment Setup
|
|
|
|
#### Development Database
|
|
```bash
|
|
# Create development database
|
|
mongo
|
|
use oldvinehotel_dev
|
|
db.createUser({
|
|
user: "hotel_admin",
|
|
pwd: "secure_password",
|
|
roles: ["readWrite"]
|
|
})
|
|
```
|
|
|
|
#### Environment Variables
|
|
|
|
**Server (.env)**
|
|
```bash
|
|
# Development configuration
|
|
NODE_ENV=development
|
|
PORT=5000
|
|
CLIENT_URL=http://localhost:3000
|
|
|
|
# Database
|
|
MONGODB_URI=mongodb://hotel_admin:secure_password@localhost:27017/oldvinehotel_dev
|
|
|
|
# Security
|
|
JWT_SECRET=your_super_secret_jwt_key_for_development
|
|
JWT_EXPIRES_IN=7d
|
|
|
|
# External APIs (Development keys)
|
|
STRIPE_SECRET_KEY=sk_test_your_development_key
|
|
GOOGLE_MAPS_API_KEY=your_development_maps_key
|
|
|
|
# Email (Development)
|
|
EMAIL_HOST=smtp.mailtrap.io
|
|
EMAIL_PORT=2525
|
|
EMAIL_USER=your_mailtrap_user
|
|
EMAIL_PASS=your_mailtrap_pass
|
|
```
|
|
|
|
**Client (.env)**
|
|
```bash
|
|
REACT_APP_API_URL=http://localhost:5000
|
|
REACT_APP_STRIPE_PUBLISHABLE_KEY=pk_test_your_development_key
|
|
REACT_APP_GOOGLE_MAPS_API_KEY=your_development_maps_key
|
|
```
|
|
|
|
## 📊 Database Schema
|
|
|
|
### Core Models
|
|
|
|
#### Room Model
|
|
```javascript
|
|
{
|
|
name: String, // "Deluxe Ocean Suite"
|
|
type: String, // "Suite", "Deluxe", etc.
|
|
roomNumber: String, // "101", "201A"
|
|
basePrice: Number, // 299.99
|
|
maxOccupancy: Number, // 4
|
|
amenities: [String], // ["WiFi", "Ocean View"]
|
|
status: String, // "Available", "Occupied"
|
|
operaRoomId: String, // PMS integration ID
|
|
images: [{
|
|
url: String,
|
|
alt: String,
|
|
isPrimary: Boolean
|
|
}]
|
|
}
|
|
```
|
|
|
|
#### Booking Model
|
|
```javascript
|
|
{
|
|
bookingNumber: String, // "OVH202512345"
|
|
confirmationCode: String, // "ABC12345"
|
|
guest: ObjectId, // Reference to Guest
|
|
room: ObjectId, // Reference to Room
|
|
checkInDate: Date,
|
|
checkOutDate: Date,
|
|
numberOfGuests: {
|
|
adults: Number,
|
|
children: Number
|
|
},
|
|
totalAmount: Number,
|
|
paymentStatus: String, // "Paid", "Pending"
|
|
status: String, // "Confirmed", "Cancelled"
|
|
stripePaymentIntentId: String
|
|
}
|
|
```
|
|
|
|
#### Guest Model
|
|
```javascript
|
|
{
|
|
firstName: String,
|
|
lastName: String,
|
|
email: String,
|
|
phone: String,
|
|
password: String, // Hashed
|
|
loyaltyProgram: {
|
|
tier: String, // "Bronze", "Silver", "Gold"
|
|
points: Number
|
|
},
|
|
preferences: {
|
|
roomType: String,
|
|
language: String
|
|
},
|
|
totalStays: Number,
|
|
isVIP: Boolean
|
|
}
|
|
```
|
|
|
|
### Database Indexes
|
|
|
|
```javascript
|
|
// Room indexes for performance
|
|
db.rooms.createIndex({ "roomNumber": 1 }, { unique: true })
|
|
db.rooms.createIndex({ "type": 1, "status": 1 })
|
|
db.rooms.createIndex({ "operaRoomId": 1 })
|
|
|
|
// Booking indexes
|
|
db.bookings.createIndex({ "bookingNumber": 1 }, { unique: true })
|
|
db.bookings.createIndex({ "confirmationCode": 1 })
|
|
db.bookings.createIndex({ "checkInDate": 1, "checkOutDate": 1 })
|
|
db.bookings.createIndex({ "guest": 1 })
|
|
db.bookings.createIndex({ "room": 1 })
|
|
|
|
// Guest indexes
|
|
db.guests.createIndex({ "email": 1 }, { unique: true })
|
|
db.guests.createIndex({ "loyaltyProgram.tier": 1 })
|
|
```
|
|
|
|
## 🔌 API Design
|
|
|
|
### RESTful API Structure
|
|
|
|
```
|
|
GET /api/rooms # List rooms with filters
|
|
GET /api/rooms/:id # Get specific room
|
|
POST /api/rooms/:id/availability # Check availability
|
|
|
|
POST /api/bookings # Create booking
|
|
GET /api/bookings/:bookingNumber # Get booking details
|
|
PUT /api/bookings/:id/cancel # Cancel booking
|
|
|
|
POST /api/auth/register # Guest registration
|
|
POST /api/auth/login # Guest login
|
|
GET /api/auth/me # Current user info
|
|
|
|
POST /api/contact # Send contact message
|
|
GET /api/contact/info # Hotel information
|
|
```
|
|
|
|
### Response Format
|
|
|
|
```javascript
|
|
// Success Response
|
|
{
|
|
"success": true,
|
|
"data": {
|
|
// Response data
|
|
},
|
|
"message": "Operation successful"
|
|
}
|
|
|
|
// Error Response
|
|
{
|
|
"success": false,
|
|
"message": "Error description",
|
|
"errors": [
|
|
{
|
|
"field": "email",
|
|
"message": "Invalid email format"
|
|
}
|
|
]
|
|
}
|
|
|
|
// Paginated Response
|
|
{
|
|
"success": true,
|
|
"data": {
|
|
"items": [...],
|
|
"pagination": {
|
|
"currentPage": 1,
|
|
"totalPages": 10,
|
|
"totalCount": 100,
|
|
"hasNextPage": true,
|
|
"hasPrevPage": false
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## 🔐 Authentication Flow
|
|
|
|
### JWT Implementation
|
|
|
|
```javascript
|
|
// Token Generation
|
|
const generateToken = (payload) => {
|
|
return jwt.sign(payload, process.env.JWT_SECRET, {
|
|
expiresIn: process.env.JWT_EXPIRES_IN || '7d'
|
|
});
|
|
};
|
|
|
|
// Token Verification Middleware
|
|
const auth = async (req, res, next) => {
|
|
const token = req.header('Authorization')?.replace('Bearer ', '');
|
|
|
|
if (!token) {
|
|
return res.status(401).json({ success: false, message: 'Access denied' });
|
|
}
|
|
|
|
try {
|
|
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
|
req.user = await User.findById(decoded.id);
|
|
next();
|
|
} catch (error) {
|
|
res.status(401).json({ success: false, message: 'Invalid token' });
|
|
}
|
|
};
|
|
```
|
|
|
|
### Password Security
|
|
|
|
```javascript
|
|
// Password Hashing (in model)
|
|
guestSchema.pre('save', async function(next) {
|
|
if (!this.isModified('password')) return next();
|
|
|
|
this.password = await bcrypt.hash(this.password, 12);
|
|
next();
|
|
});
|
|
|
|
// Password Comparison
|
|
guestSchema.methods.comparePassword = async function(candidatePassword) {
|
|
return bcrypt.compare(candidatePassword, this.password);
|
|
};
|
|
```
|
|
|
|
## 💳 Payment Integration
|
|
|
|
### Stripe Implementation
|
|
|
|
```javascript
|
|
// Create Payment Intent
|
|
const createPayment = async (amount, bookingData) => {
|
|
const paymentIntent = await stripe.paymentIntents.create({
|
|
amount: Math.round(amount * 100), // Convert to cents
|
|
currency: 'usd',
|
|
metadata: {
|
|
bookingNumber: bookingData.bookingNumber,
|
|
guestEmail: bookingData.guest.email
|
|
}
|
|
});
|
|
|
|
return paymentIntent;
|
|
};
|
|
|
|
// Handle Webhook
|
|
const handleStripeWebhook = (req, res) => {
|
|
const sig = req.headers['stripe-signature'];
|
|
let event;
|
|
|
|
try {
|
|
event = stripe.webhooks.constructEvent(
|
|
req.body,
|
|
sig,
|
|
process.env.STRIPE_WEBHOOK_SECRET
|
|
);
|
|
} catch (err) {
|
|
return res.status(400).send(`Webhook signature verification failed.`);
|
|
}
|
|
|
|
switch (event.type) {
|
|
case 'payment_intent.succeeded':
|
|
// Handle successful payment
|
|
break;
|
|
case 'payment_intent.payment_failed':
|
|
// Handle failed payment
|
|
break;
|
|
}
|
|
|
|
res.json({ received: true });
|
|
};
|
|
```
|
|
|
|
## 🔗 Integration Services
|
|
|
|
### Opera PMS Integration
|
|
|
|
```javascript
|
|
class OperaPMSService {
|
|
async createReservation(booking) {
|
|
const xmlRequest = this.generateXMLRequest('OTA_HotelResRQ', {
|
|
// XML structure for Opera PMS
|
|
});
|
|
|
|
const response = await this.client.post('/reservations', xmlRequest);
|
|
return this.parseXMLResponse(response.data);
|
|
}
|
|
|
|
async syncAvailability() {
|
|
// Sync room availability with Opera PMS
|
|
}
|
|
|
|
async updateRates(roomType, rates) {
|
|
// Update room rates in Opera PMS
|
|
}
|
|
}
|
|
```
|
|
|
|
### Booking Platform APIs
|
|
|
|
```javascript
|
|
// Booking.com Integration
|
|
class BookingComService {
|
|
async updateAvailability(roomType, dates, availability) {
|
|
const xmlRequest = this.buildAvailabilityXML(roomType, dates, availability);
|
|
return this.sendRequest(xmlRequest);
|
|
}
|
|
|
|
async processWebhook(webhookData) {
|
|
switch (webhookData.event_type) {
|
|
case 'booking_created':
|
|
await this.importBooking(webhookData);
|
|
break;
|
|
case 'booking_cancelled':
|
|
await this.cancelBooking(webhookData);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## 🎨 Frontend Architecture
|
|
|
|
### React Component Structure
|
|
|
|
```
|
|
src/
|
|
├── components/
|
|
│ ├── common/ # Shared components
|
|
│ │ ├── Button/
|
|
│ │ ├── Modal/
|
|
│ │ └── Loading/
|
|
│ ├── layout/ # Layout components
|
|
│ │ ├── Header/
|
|
│ │ ├── Footer/
|
|
│ │ └── Sidebar/
|
|
│ └── booking/ # Booking-specific components
|
|
│ ├── RoomCard/
|
|
│ ├── BookingForm/
|
|
│ └── PaymentForm/
|
|
├── pages/ # Page components
|
|
├── services/ # API services
|
|
├── hooks/ # Custom React hooks
|
|
├── utils/ # Utility functions
|
|
└── context/ # React contexts
|
|
```
|
|
|
|
### State Management
|
|
|
|
```javascript
|
|
// React Query for server state
|
|
const { data: rooms, isLoading } = useQuery(
|
|
['rooms', filters],
|
|
() => roomsAPI.getRooms(filters),
|
|
{
|
|
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
cacheTime: 10 * 60 * 1000, // 10 minutes
|
|
}
|
|
);
|
|
|
|
// Context for global state
|
|
const AuthContext = createContext();
|
|
|
|
export const AuthProvider = ({ children }) => {
|
|
const [user, setUser] = useState(null);
|
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
|
|
|
return (
|
|
<AuthContext.Provider value={{ user, isAuthenticated, setUser }}>
|
|
{children}
|
|
</AuthContext.Provider>
|
|
);
|
|
};
|
|
```
|
|
|
|
### API Service Layer
|
|
|
|
```javascript
|
|
// API service structure
|
|
class RoomsAPI {
|
|
async getRooms(filters = {}) {
|
|
const params = new URLSearchParams(filters);
|
|
const response = await api.get(`/rooms?${params}`);
|
|
return response.data;
|
|
}
|
|
|
|
async checkAvailability(roomId, dates) {
|
|
const response = await api.post(`/rooms/${roomId}/availability`, dates);
|
|
return response.data;
|
|
}
|
|
|
|
async createBooking(bookingData) {
|
|
const response = await api.post('/bookings', bookingData);
|
|
return response.data;
|
|
}
|
|
}
|
|
|
|
export const roomsAPI = new RoomsAPI();
|
|
```
|
|
|
|
## 🧪 Testing Strategy
|
|
|
|
### Backend Testing
|
|
|
|
```javascript
|
|
// Unit Tests (Jest)
|
|
describe('Room Model', () => {
|
|
test('should calculate current price with seasonal pricing', () => {
|
|
const room = new Room({
|
|
basePrice: 200,
|
|
seasonalPricing: [{
|
|
season: 'summer',
|
|
startDate: new Date('2024-06-01'),
|
|
endDate: new Date('2024-08-31'),
|
|
priceMultiplier: 1.5
|
|
}]
|
|
});
|
|
|
|
expect(room.currentPrice).toBe(300);
|
|
});
|
|
});
|
|
|
|
// Integration Tests
|
|
describe('Booking API', () => {
|
|
test('POST /api/bookings should create booking', async () => {
|
|
const bookingData = {
|
|
roomId: 'room123',
|
|
checkInDate: '2024-06-01',
|
|
checkOutDate: '2024-06-03',
|
|
guestInfo: { /* guest data */ }
|
|
};
|
|
|
|
const response = await request(app)
|
|
.post('/api/bookings')
|
|
.send(bookingData)
|
|
.expect(201);
|
|
|
|
expect(response.body.success).toBe(true);
|
|
expect(response.body.data.booking).toBeDefined();
|
|
});
|
|
});
|
|
```
|
|
|
|
### Frontend Testing
|
|
|
|
```javascript
|
|
// Component Tests (React Testing Library)
|
|
import { render, screen, fireEvent } from '@testing-library/react';
|
|
import BookingForm from '../BookingForm';
|
|
|
|
test('should submit booking form with valid data', async () => {
|
|
render(<BookingForm />);
|
|
|
|
fireEvent.change(screen.getByLabelText(/check-in date/i), {
|
|
target: { value: '2024-06-01' }
|
|
});
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /book now/i }));
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/booking confirmed/i)).toBeInTheDocument();
|
|
});
|
|
});
|
|
```
|
|
|
|
## 📊 Performance Optimization
|
|
|
|
### Database Optimization
|
|
|
|
```javascript
|
|
// Efficient aggregation for room availability
|
|
const getAvailableRooms = async (checkIn, checkOut) => {
|
|
return Room.aggregate([
|
|
{
|
|
$match: {
|
|
status: 'Available',
|
|
isActive: true
|
|
}
|
|
},
|
|
{
|
|
$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 }
|
|
}
|
|
}
|
|
]);
|
|
};
|
|
```
|
|
|
|
### Frontend Optimization
|
|
|
|
```javascript
|
|
// Code splitting and lazy loading
|
|
const Booking = lazy(() => import('./pages/Booking'));
|
|
const AdminDashboard = lazy(() => import('./pages/admin/Dashboard'));
|
|
|
|
// Memoization for expensive calculations
|
|
const ExpensiveComponent = memo(({ data }) => {
|
|
const processedData = useMemo(() => {
|
|
return expensiveCalculation(data);
|
|
}, [data]);
|
|
|
|
return <div>{processedData}</div>;
|
|
});
|
|
|
|
// Image optimization
|
|
const OptimizedImage = ({ src, alt, ...props }) => {
|
|
const [imageSrc, setImageSrc] = useState(src + '?w=50&q=10'); // Placeholder
|
|
|
|
useEffect(() => {
|
|
const img = new Image();
|
|
img.onload = () => setImageSrc(src);
|
|
img.src = src;
|
|
}, [src]);
|
|
|
|
return <img src={imageSrc} alt={alt} {...props} />;
|
|
};
|
|
```
|
|
|
|
## 🔍 Monitoring & Logging
|
|
|
|
### Application Logging
|
|
|
|
```javascript
|
|
// Winston logger configuration
|
|
const logger = winston.createLogger({
|
|
level: process.env.LOG_LEVEL || 'info',
|
|
format: winston.format.combine(
|
|
winston.format.timestamp(),
|
|
winston.format.errors({ stack: true }),
|
|
winston.format.json()
|
|
),
|
|
transports: [
|
|
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
|
|
new winston.transports.File({ filename: 'logs/combined.log' }),
|
|
new winston.transports.File({
|
|
filename: 'logs/bookings.log',
|
|
format: winston.format((info) => {
|
|
return info.type === 'booking' ? info : false;
|
|
})()
|
|
})
|
|
]
|
|
});
|
|
|
|
// Usage in application
|
|
logger.info('Booking created', {
|
|
type: 'booking',
|
|
bookingId: booking._id,
|
|
guestEmail: booking.guest.email,
|
|
amount: booking.totalAmount
|
|
});
|
|
```
|
|
|
|
### Health Monitoring
|
|
|
|
```javascript
|
|
// Health check endpoint
|
|
app.get('/health', async (req, res) => {
|
|
const healthCheck = {
|
|
status: 'OK',
|
|
timestamp: new Date().toISOString(),
|
|
uptime: process.uptime(),
|
|
checks: {
|
|
database: await checkDatabaseHealth(),
|
|
redis: await checkRedisHealth(),
|
|
external_apis: await checkExternalAPIs()
|
|
}
|
|
};
|
|
|
|
const isHealthy = Object.values(healthCheck.checks).every(check => check.status === 'OK');
|
|
|
|
res.status(isHealthy ? 200 : 503).json(healthCheck);
|
|
});
|
|
```
|
|
|
|
## 🚀 Deployment
|
|
|
|
### Docker Configuration
|
|
|
|
```dockerfile
|
|
# Dockerfile for backend
|
|
FROM node:16-alpine
|
|
|
|
WORKDIR /app
|
|
|
|
COPY package*.json ./
|
|
RUN npm ci --only=production
|
|
|
|
COPY . .
|
|
|
|
EXPOSE 5000
|
|
|
|
CMD ["npm", "start"]
|
|
```
|
|
|
|
```yaml
|
|
# docker-compose.yml
|
|
version: '3.8'
|
|
|
|
services:
|
|
frontend:
|
|
build: ./client
|
|
ports:
|
|
- "3000:3000"
|
|
environment:
|
|
- REACT_APP_API_URL=http://backend:5000
|
|
depends_on:
|
|
- backend
|
|
|
|
backend:
|
|
build: ./server
|
|
ports:
|
|
- "5000:5000"
|
|
environment:
|
|
- NODE_ENV=production
|
|
- MONGODB_URI=mongodb://mongodb:27017/oldvinehotel
|
|
depends_on:
|
|
- mongodb
|
|
|
|
mongodb:
|
|
image: mongo:5.0
|
|
ports:
|
|
- "27017:27017"
|
|
volumes:
|
|
- mongodb_data:/data/db
|
|
|
|
volumes:
|
|
mongodb_data:
|
|
```
|
|
|
|
### Production Deployment
|
|
|
|
```bash
|
|
# Build and deploy script
|
|
#!/bin/bash
|
|
|
|
# Build frontend
|
|
cd client
|
|
npm run build
|
|
|
|
# Deploy to CDN (e.g., AWS S3 + CloudFront)
|
|
aws s3 sync build/ s3://hotel-website-bucket --delete
|
|
aws cloudfront create-invalidation --distribution-id ABCD1234 --paths "/*"
|
|
|
|
# Deploy backend
|
|
cd ../server
|
|
|
|
# Build Docker image
|
|
docker build -t hotel-api:latest .
|
|
|
|
# Deploy to container registry
|
|
docker tag hotel-api:latest your-registry/hotel-api:latest
|
|
docker push your-registry/hotel-api:latest
|
|
|
|
# Deploy to production environment
|
|
kubectl apply -f k8s/
|
|
```
|
|
|
|
## 🔧 Troubleshooting
|
|
|
|
### Common Issues
|
|
|
|
#### Database Connection
|
|
```bash
|
|
# Check MongoDB status
|
|
sudo systemctl status mongod
|
|
|
|
# View MongoDB logs
|
|
sudo journalctl -u mongod
|
|
|
|
# Test connection
|
|
mongo --eval "db.adminCommand('ismaster')"
|
|
```
|
|
|
|
#### API Issues
|
|
```bash
|
|
# Check API health
|
|
curl http://localhost:5000/health
|
|
|
|
# Test specific endpoint
|
|
curl -X POST http://localhost:5000/api/rooms/availability \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"checkIn":"2024-06-01","checkOut":"2024-06-03"}'
|
|
```
|
|
|
|
#### Frontend Issues
|
|
```bash
|
|
# Clear React cache
|
|
npm start -- --reset-cache
|
|
|
|
# Check bundle size
|
|
npm run build -- --analyze
|
|
|
|
# Test production build locally
|
|
npx serve -s build
|
|
```
|
|
|
|
### Performance Debugging
|
|
|
|
```javascript
|
|
// Database query profiling
|
|
db.setProfilingLevel(2); // Profile all operations
|
|
db.system.profile.find().limit(5).sort({ts:-1}).pretty();
|
|
|
|
// API response time monitoring
|
|
const responseTime = require('response-time');
|
|
app.use(responseTime((req, res, time) => {
|
|
logger.info('API Response Time', {
|
|
method: req.method,
|
|
url: req.originalUrl,
|
|
responseTime: time,
|
|
statusCode: res.statusCode
|
|
});
|
|
}));
|
|
```
|
|
|
|
---
|
|
|
|
This documentation provides a comprehensive guide for developers working on The Old Vine Hotel project. For specific implementation details, refer to the code comments and individual component documentation. |