import { NextRequest } from "next/server"; import { z } from "zod"; import { findResort, calculateResortScore, type Resort } from "@/lib/resorts"; import { format, addDays } from "date-fns"; const schema = z.object({ resorts: z.array(z.string()).nonempty(), departureDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), origin: z.string().min(3).max(4).default("YOW"), tripLength: z.number().int().positive().default(7), budget: z.number().positive().optional(), contingencyPercent: z.number().min(0).max(50).optional().default(15), // Allow up to 15% over budget for high scores preferences: z.object({ beach: z.number().min(0).max(10).optional(), pool: z.number().min(0).max(10).optional(), golf: z.number().min(0).max(10).optional(), spa: z.number().min(0).max(10).optional(), food: z.number().min(0).max(10).optional(), nightlife: z.number().min(0).max(10).optional(), shopping: z.number().min(0).max(10).optional(), culture: z.number().min(0).max(10).optional(), outdoor: z.number().min(0).max(10).optional(), family: z.number().min(0).max(10).optional(), }).optional() }); type ResortComparison = { resort: Resort; matchScore: number; flightLinks: { skyscanner: string; googleFlights: string; airCanada: string; }; estimatedFlightPrice?: string; estimatedFlightPriceMin?: number; estimatedFlightPriceMax?: number; estimatedTotalMin?: number; estimatedTotalMax?: number; withinBudget?: boolean; budgetStatus?: string; overBudgetAmount?: number; }; function buildFlightLinks(origin: string, destination: string, departureDate: string, returnDate: string) { const depFormatted = departureDate.replace(/-/g, ""); const retFormatted = returnDate.replace(/-/g, ""); return { skyscanner: `https://www.skyscanner.ca/transport/flights/${origin.toLowerCase()}/${destination.toLowerCase()}/${depFormatted}/${retFormatted}/`, googleFlights: `https://www.google.com/travel/flights?q=${encodeURIComponent(`flights from ${origin} to ${destination} ${departureDate} to ${returnDate}`)}`, airCanada: `https://www.aircanada.com/ca/en/aco/home/book/travel.html?${new URLSearchParams({ org1: origin.toUpperCase(), dest1: destination.toUpperCase(), departureDate1: departureDate, returnDate1: returnDate, tripType: "2", lang: "en-CA" }).toString()}` }; } function estimateResortCost(priceRange: string, nights: number): { min: number; max: number } { // CANADIAN ALL-INCLUSIVE PACKAGE PRICING (per person, CAD) // Based on real travel agent quotes for 7-night packages from Ottawa // Includes: flights, accommodations, transfers, taxes const packageRanges: Record = { "$": { min: 1200, max: 1600 }, // Budget packages "$$": { min: 1600, max: 2000 }, // Mid-range packages "$$$": { min: 2000, max: 2400 }, // Upper mid-range "$$$$": { min: 2400, max: 2800 }, // Premium (Dreams Onyx: $2509) "$$$$$": { min: 2800, max: 3500 } // Luxury packages }; // Adjust for trip length (base is 7 nights) const baseDays = 7; const lengthMultiplier = nights / baseDays; const range = packageRanges[priceRange] || packageRanges["$$$"]; return { min: Math.round(range.min * lengthMultiplier), max: Math.round(range.max * lengthMultiplier) }; } export async function POST(req: NextRequest) { try { const body = await req.json(); const parsed = schema.parse(body); const comparisons: ResortComparison[] = []; const notFound: string[] = []; // Calculate return date const returnDate = format(addDays(new Date(parsed.departureDate), parsed.tripLength), "yyyy-MM-dd"); for (const resortName of parsed.resorts) { const resort = findResort(resortName); if (!resort) { notFound.push(resortName); continue; } const matchScore = calculateResortScore(resort, parsed.preferences); const flightLinks = buildFlightLinks(parsed.origin, resort.airportCode, parsed.departureDate, returnDate); // Package pricing (per person, includes flights) const packageCost = estimateResortCost(resort.priceRange || "$$$", parsed.tripLength); // Total cost for 2 people (standard package assumption) const estimatedTotalMin = packageCost.min * 2; const estimatedTotalMax = packageCost.max * 2; // Per person cost for display const estimatedPerPerson = `$${packageCost.min}-$${packageCost.max} per person`; // Budget analysis let withinBudget = true; let budgetStatus = "Within Budget"; let overBudgetAmount = 0; if (parsed.budget) { const contingencyAmount = parsed.budget * ((parsed.contingencyPercent || 15) / 100); const budgetWithContingency = parsed.budget + contingencyAmount; if (estimatedTotalMin > parsed.budget) { overBudgetAmount = estimatedTotalMin - parsed.budget; // Check if high score justifies going over budget if (matchScore >= 1800 && estimatedTotalMin <= budgetWithContingency) { withinBudget = true; budgetStatus = `Over budget by $${overBudgetAmount.toFixed(0)} but HIGH MATCH (within ${parsed.contingencyPercent}% contingency)`; } else if (estimatedTotalMin <= budgetWithContingency) { withinBudget = true; budgetStatus = `Within ${parsed.contingencyPercent}% contingency (+$${overBudgetAmount.toFixed(0)})`; } else { withinBudget = false; budgetStatus = `Over budget by $${overBudgetAmount.toFixed(0)} (exceeds contingency)`; } } else { budgetStatus = `Under budget by $${(parsed.budget - estimatedTotalMin).toFixed(0)}`; } } comparisons.push({ resort, matchScore, flightLinks, estimatedFlightPrice: estimatedPerPerson, estimatedFlightPriceMin: packageCost.min, estimatedFlightPriceMax: packageCost.max, estimatedTotalMin, estimatedTotalMax, withinBudget, budgetStatus, overBudgetAmount: overBudgetAmount > 0 ? overBudgetAmount : undefined }); } // Sort by match score (highest first) comparisons.sort((a, b) => b.matchScore - a.matchScore); // Filter by budget if specified (but include high-scoring resorts within contingency) let budgetFiltered = comparisons; if (parsed.budget) { budgetFiltered = comparisons.filter(c => c.withinBudget); } return new Response(JSON.stringify({ comparisons: budgetFiltered, allComparisons: comparisons, // Include all for reference notFound, budgetInfo: parsed.budget ? { budget: parsed.budget, contingencyPercent: parsed.contingencyPercent || 15, contingencyAmount: parsed.budget * ((parsed.contingencyPercent || 15) / 100), maxBudget: parsed.budget + (parsed.budget * ((parsed.contingencyPercent || 15) / 100)), withinBudgetCount: budgetFiltered.length, totalCount: comparisons.length, overBudgetCount: comparisons.length - budgetFiltered.length } : null, flightInfo: { origin: parsed.origin, departureDate: parsed.departureDate, returnDate, tripLength: parsed.tripLength }, packageSites: [ "Sunwing Vacations", "Air Canada Vacations", "WestJet Vacations", "Air Transat Holidays", "RedTag.ca" ], pricingNote: "Estimates are for individual bookings (flight + resort). Package deals are typically 30-40% cheaper." }), { headers: { "content-type": "application/json" } }); } catch (e: any) { return new Response(JSON.stringify({ error: e?.message || "Invalid payload" }), { status: 400, headers: { "content-type": "application/json" } }); } }