fixing jsonSchema validation by using zod
This commit is contained in:
290
src/tools/hotels.ts
Normal file
290
src/tools/hotels.ts
Normal file
@@ -0,0 +1,290 @@
|
||||
/**
|
||||
* 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<Object>} 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<Object>} 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 };
|
||||
Reference in New Issue
Block a user