Files
oldvine/docs/DEVELOPMENT.md

19 KiB

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

# 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

# Create development database
mongo
use oldvinehotel_dev
db.createUser({
  user: "hotel_admin",
  pwd: "secure_password",
  roles: ["readWrite"]
})

Environment Variables

Server (.env)

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

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

{
  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

{
  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

{
  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

// 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

// 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

// 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

// 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

// 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

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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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 for backend
FROM node:16-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .

EXPOSE 5000

CMD ["npm", "start"]
# 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

# 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

# Check MongoDB status
sudo systemctl status mongod

# View MongoDB logs
sudo journalctl -u mongod

# Test connection
mongo --eval "db.adminCommand('ismaster')"

API Issues

# 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

# Clear React cache
npm start -- --reset-cache

# Check bundle size
npm run build -- --analyze

# Test production build locally
npx serve -s build

Performance Debugging

// 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.