/** * Hotel Search and Booking Tools * Implements searchHotels and bookHotel MCP tools */ import { getHotelsByCity, getHotelById, generateHotelPrice } from '../data/hotels.js'; import { generatePNR } from '../data/pnr.js'; import { storage } from '../session/storage.js'; import { logger } from '../utils/logger.js'; import { ValidationError, BookingError } from '../utils/errors.js'; import { validateRequired, validateString, validateNumber, validateFutureDate } from '../validation/validators.js'; /** * Search for available hotels * @param {Object} params - Search parameters * @param {string} sessionId - Session identifier * @returns {Promise} Search results */ export async function searchHotels(params: any, sessionId: any) { try { // Validate required fields validateRequired(params.cityCode, 'cityCode'); validateRequired(params.checkInDate, 'checkInDate'); validateRequired(params.checkOutDate, 'checkOutDate'); validateString(params.cityCode, 'cityCode', { minLength: 3, maxLength: 3 }); validateFutureDate(params.checkInDate, 'checkInDate'); validateFutureDate(params.checkOutDate, 'checkOutDate'); if (params.guests) { validateNumber(params.guests, 'guests', { min: 1, max: 10 }); } const cityCode = params.cityCode.toUpperCase(); const checkInDate = new Date(params.checkInDate); const checkOutDate = new Date(params.checkOutDate); const guests = params.guests || 1; // Validate date range if (checkOutDate <= checkInDate) { throw new ValidationError('checkOutDate must be after checkInDate'); } const checkInDateObj = new Date(checkInDate); const checkOutDateObj = new Date(checkOutDate); const nights = Math.ceil((checkOutDateObj.getTime() - checkInDateObj.getTime()) / (1000 * 60 * 60 * 24)); if (nights < 1) { throw new ValidationError('Minimum stay is 1 night'); } // Get hotels for city const cityHotels = getHotelsByCity(cityCode); if (cityHotels.length === 0) { return { cityCode, checkInDate: params.checkInDate, checkOutDate: params.checkOutDate, nights, results: [], message: `No hotels found for city code ${cityCode}` }; } // Select 5-10 hotels and generate pricing const numResults = Math.min(cityHotels.length, Math.floor(Math.random() * 6) + 5); const selectedHotels = cityHotels .sort(() => Math.random() - 0.5) .slice(0, numResults); const results = selectedHotels.map(hotel => { const pricing = generateHotelPrice(hotel.basePrice, nights, params.checkInDate); return { hotelId: hotel.id, name: hotel.name, chain: hotel.chain, stars: hotel.stars, address: hotel.address, amenities: hotel.amenities, pricing: { pricePerNight: pricing.pricePerNight, nights: pricing.nights, totalPrice: pricing.totalPrice, currency: 'USD' }, availability: 'available' // Mock: always available }; }); logger.info({ sessionId, cityCode, nights, resultCount: results.length }, 'Hotel search completed'); return { cityCode, checkInDate: params.checkInDate, checkOutDate: params.checkOutDate, nights, guests, results }; } catch (error) { logger.error({ error, sessionId }, 'Hotel search failed'); throw error; } } /** * Book a hotel * @param {Object} params - Booking parameters * @param {string} sessionId - Session identifier * @returns {Promise} Booking confirmation */ export async function bookHotel(params: any, sessionId: any) { try { // Validate required fields validateRequired(params.hotelId, 'hotelId'); validateRequired(params.checkInDate, 'checkInDate'); validateRequired(params.checkOutDate, 'checkOutDate'); validateRequired(params.guestName, 'guestName'); validateRequired(params.guestEmail, 'guestEmail'); const { hotelId, checkInDate, checkOutDate, guestName, guestEmail, guestPhone, pnr: existingPnr, roomType } = params; // Validate hotel exists const hotel = getHotelById(hotelId); if (!hotel) { throw new ValidationError(`Hotel not found: ${hotelId}`); } // Validate dates validateFutureDate(checkInDate, 'checkInDate'); validateFutureDate(checkOutDate, 'checkOutDate'); const checkIn = new Date(checkInDate); const checkOut = new Date(checkOutDate); if (checkOut <= checkIn) { throw new ValidationError('checkOutDate must be after checkInDate'); } const checkInDateObj = new Date(params.checkInDate); const checkOutDateObj = new Date(params.checkOutDate); const nights = Math.ceil((checkOutDateObj.getTime() - checkInDateObj.getTime()) / (1000 * 60 * 60 * 24)); // Calculate pricing const pricing = generateHotelPrice(hotel.basePrice, nights, checkInDate); // Generate or use existing PNR const pnr = existingPnr || generatePNR(); const client = storage.client; // Check if this is adding to existing booking let booking; if (existingPnr) { const existingKey = `gds:session:${sessionId}:booking:${existingPnr}`; const existingData = await client.get(existingKey); if (!existingData) { throw new BookingError(`Booking not found: ${existingPnr}`); } booking = JSON.parse(existingData); // Add hotel segment if (!booking.hotels) { booking.hotels = []; } booking.hotels.push({ hotelId: hotel.id, name: hotel.name, chain: hotel.chain, stars: hotel.stars, address: hotel.address, checkInDate, checkOutDate, nights, roomType: roomType || 'Standard', guestName, guestEmail, guestPhone, pricing: { pricePerNight: pricing.pricePerNight, nights: pricing.nights, totalPrice: pricing.totalPrice, currency: 'USD' }, status: 'confirmed', bookedAt: new Date().toISOString() }); // Recalculate total price let totalPrice = 0; if (booking.flights) { totalPrice += booking.flights.reduce((sum, f) => sum + (f.pricing?.totalPrice || 0), 0); } if (booking.hotels) { totalPrice += booking.hotels.reduce((sum, h) => sum + (h.pricing?.totalPrice || 0), 0); } if (booking.cars) { totalPrice += booking.cars.reduce((sum, c) => sum + (c.pricing?.totalPrice || 0), 0); } booking.totalPrice = totalPrice; booking.updatedAt = new Date().toISOString(); } else { // Create new hotel-only booking booking = { pnr, type: 'hotel', status: 'confirmed', hotels: [{ hotelId: hotel.id, name: hotel.name, chain: hotel.chain, stars: hotel.stars, address: hotel.address, checkInDate, checkOutDate, nights, roomType: roomType || 'Standard', guestName, guestEmail, guestPhone, pricing: { pricePerNight: pricing.pricePerNight, nights: pricing.nights, totalPrice: pricing.totalPrice, currency: 'USD' }, status: 'confirmed', bookedAt: new Date().toISOString() }], totalPrice: pricing.totalPrice, currency: 'USD', createdAt: new Date().toISOString(), sessionId }; } // Store booking const bookingKey = `gds:session:${sessionId}:booking:${pnr}`; await client.set(bookingKey, JSON.stringify(booking), 'EX', 3600); // Update session bookings set await client.sadd(`gds:session:${sessionId}:bookings`, pnr); logger.info({ sessionId, pnr, hotelId, nights }, 'Hotel booking created'); return { pnr, status: 'confirmed', hotel: { hotelId: hotel.id, name: hotel.name, chain: hotel.chain, stars: hotel.stars, checkInDate, checkOutDate, nights, roomType: roomType || 'Standard' }, pricing: { pricePerNight: pricing.pricePerNight, nights: pricing.nights, totalPrice: pricing.totalPrice, currency: 'USD' }, guestName, guestEmail, message: existingPnr ? `Hotel added to existing booking ${pnr}` : `Hotel booking confirmed with PNR ${pnr}` }; } catch (error) { logger.error({ error, sessionId }, 'Hotel booking failed'); throw error; } } export default { searchHotels, bookHotel };