271 lines
8.0 KiB
TypeScript
271 lines
8.0 KiB
TypeScript
/**
|
|
* Car Rental Tools
|
|
* Implements searchCars and bookCar MCP tools
|
|
*/
|
|
|
|
import { generateCarOptions, getCarById } from '../data/cars.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,
|
|
validateFutureDate
|
|
} from '../validation/validators.js';
|
|
|
|
/**
|
|
* Search for available car rentals
|
|
* @param {Object} params - Search parameters
|
|
* @param {string} sessionId - Session identifier
|
|
* @returns {Promise<Object>} Search results
|
|
*/
|
|
export async function searchCars(params: any, sessionId: any) {
|
|
try {
|
|
// Validate required fields
|
|
validateRequired(params.pickupLocation, 'pickupLocation');
|
|
validateRequired(params.dropoffLocation, 'dropoffLocation');
|
|
validateRequired(params.pickupDate, 'pickupDate');
|
|
validateRequired(params.dropoffDate, 'dropoffDate');
|
|
|
|
validateString(params.pickupLocation, 'pickupLocation', { minLength: 3, maxLength: 3 });
|
|
validateString(params.dropoffLocation, 'dropoffLocation', { minLength: 3, maxLength: 3 });
|
|
validateFutureDate(params.pickupDate, 'pickupDate');
|
|
validateFutureDate(params.dropoffDate, 'dropoffDate');
|
|
|
|
const pickupLocation = params.pickupLocation.toUpperCase();
|
|
const dropoffLocation = params.dropoffLocation.toUpperCase();
|
|
const pickupDate = new Date(params.pickupDate);
|
|
const dropoffDate = new Date(params.dropoffDate);
|
|
|
|
// Validate date range
|
|
if (dropoffDate <= pickupDate) {
|
|
throw new ValidationError('dropoffDate must be after pickupDate');
|
|
}
|
|
|
|
const dropoffDateObj = new Date(dropoffDate);
|
|
const days = Math.ceil((dropoffDateObj.getTime() - pickupDate.getTime()) / (1000 * 60 * 60 * 24));
|
|
|
|
if (days < 1) {
|
|
throw new ValidationError('Minimum rental period is 1 day');
|
|
}
|
|
|
|
// Generate car options
|
|
const options = generateCarOptions(
|
|
pickupLocation,
|
|
dropoffLocation,
|
|
params.pickupDate,
|
|
params.dropoffDate
|
|
);
|
|
|
|
logger.info({ sessionId, pickupLocation, dropoffLocation, days, resultCount: options.length }, 'Car search completed');
|
|
|
|
return {
|
|
pickupLocation,
|
|
dropoffLocation,
|
|
pickupDate: params.pickupDate,
|
|
dropoffDate: params.dropoffDate,
|
|
days,
|
|
results: options
|
|
};
|
|
|
|
} catch (error) {
|
|
logger.error({ error, sessionId }, 'Car search failed');
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Book a car rental
|
|
* @param {Object} params - Booking parameters
|
|
* @param {string} sessionId - Session identifier
|
|
* @returns {Promise<Object>} Booking confirmation
|
|
*/
|
|
export async function bookCar(params: any, sessionId: any) {
|
|
try {
|
|
// Validate required fields
|
|
validateRequired(params.carId, 'carId');
|
|
validateRequired(params.pickupLocation, 'pickupLocation');
|
|
validateRequired(params.dropoffLocation, 'dropoffLocation');
|
|
validateRequired(params.pickupDate, 'pickupDate');
|
|
validateRequired(params.dropoffDate, 'dropoffDate');
|
|
validateRequired(params.driverName, 'driverName');
|
|
validateRequired(params.driverEmail, 'driverEmail');
|
|
|
|
const { carId, pickupLocation, dropoffLocation, pickupDate, dropoffDate, driverName, driverEmail, driverPhone, driverLicense, pnr: existingPnr } = params;
|
|
|
|
// Validate car exists
|
|
const car = getCarById(carId);
|
|
if (!car) {
|
|
throw new ValidationError(`Car not found: ${carId}`);
|
|
}
|
|
|
|
// Validate dates
|
|
validateFutureDate(pickupDate, 'pickupDate');
|
|
validateFutureDate(dropoffDate, 'dropoffDate');
|
|
|
|
const pickup = new Date(pickupDate);
|
|
const dropoff = new Date(dropoffDate);
|
|
|
|
if (dropoff <= pickup) {
|
|
throw new ValidationError('dropoffDate must be after pickupDate');
|
|
}
|
|
|
|
const pickupDateObj = new Date(params.pickupDate);
|
|
const dropoffDateObj = new Date(params.dropoffDate);
|
|
const days = Math.ceil((dropoffDateObj.getTime() - pickupDateObj.getTime()) / (1000 * 60 * 60 * 24));
|
|
|
|
// Calculate pricing (recalculate with same logic as search)
|
|
const oneWayFee = pickupLocation !== dropoffLocation ? 75 : 0;
|
|
const dailyRate = car.basePrice; // Simplified - real calculation in generateCarOptions
|
|
const totalPrice = (dailyRate * days) + oneWayFee;
|
|
|
|
// 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 car segment
|
|
if (!booking.cars) {
|
|
booking.cars = [];
|
|
}
|
|
|
|
booking.cars.push({
|
|
carId: car.carId,
|
|
company: car.company,
|
|
category: car.category,
|
|
example: car.example,
|
|
passengers: car.passengers,
|
|
bags: car.bags,
|
|
features: car.features,
|
|
pickupLocation,
|
|
dropoffLocation,
|
|
pickupDate,
|
|
dropoffDate,
|
|
days,
|
|
driverName,
|
|
driverEmail,
|
|
driverPhone,
|
|
driverLicense,
|
|
pricing: {
|
|
dailyRate,
|
|
days,
|
|
oneWayFee,
|
|
totalPrice,
|
|
currency: 'USD'
|
|
},
|
|
status: 'confirmed',
|
|
bookedAt: new Date().toISOString()
|
|
});
|
|
|
|
// Recalculate total price
|
|
let totalBookingPrice = 0;
|
|
if (booking.flights) {
|
|
totalBookingPrice += booking.flights.reduce((sum, f) => sum + (f.pricing?.totalPrice || 0), 0);
|
|
}
|
|
if (booking.hotels) {
|
|
totalBookingPrice += booking.hotels.reduce((sum, h) => sum + (h.pricing?.totalPrice || 0), 0);
|
|
}
|
|
if (booking.cars) {
|
|
totalBookingPrice += booking.cars.reduce((sum, c) => sum + (c.pricing?.totalPrice || 0), 0);
|
|
}
|
|
|
|
booking.totalPrice = totalBookingPrice;
|
|
booking.updatedAt = new Date().toISOString();
|
|
|
|
} else {
|
|
// Create new car-only booking
|
|
booking = {
|
|
pnr,
|
|
type: 'car',
|
|
status: 'confirmed',
|
|
cars: [{
|
|
carId: car.carId,
|
|
company: car.company,
|
|
category: car.category,
|
|
example: car.example,
|
|
passengers: car.passengers,
|
|
bags: car.bags,
|
|
features: car.features,
|
|
pickupLocation,
|
|
dropoffLocation,
|
|
pickupDate,
|
|
dropoffDate,
|
|
days,
|
|
driverName,
|
|
driverEmail,
|
|
driverPhone,
|
|
driverLicense,
|
|
pricing: {
|
|
dailyRate,
|
|
days,
|
|
oneWayFee,
|
|
totalPrice,
|
|
currency: 'USD'
|
|
},
|
|
status: 'confirmed',
|
|
bookedAt: new Date().toISOString()
|
|
}],
|
|
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, carId, days }, 'Car rental booking created');
|
|
|
|
return {
|
|
pnr,
|
|
status: 'confirmed',
|
|
car: {
|
|
carId: car.carId,
|
|
company: car.company,
|
|
category: car.category,
|
|
example: car.example,
|
|
pickupLocation,
|
|
dropoffLocation,
|
|
pickupDate,
|
|
dropoffDate,
|
|
days
|
|
},
|
|
pricing: {
|
|
dailyRate,
|
|
days,
|
|
oneWayFee,
|
|
totalPrice,
|
|
currency: 'USD'
|
|
},
|
|
driverName,
|
|
driverEmail,
|
|
message: existingPnr
|
|
? `Car rental added to existing booking ${pnr}`
|
|
: `Car rental confirmed with PNR ${pnr}`
|
|
};
|
|
|
|
} catch (error) {
|
|
logger.error({ error, sessionId }, 'Car booking failed');
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export default { searchCars, bookCar };
|