import { NextRequest } from "next/server"; import { z } from "zod"; import type { SearchCriteria, Deal } from "@/lib/types"; import { fetchCityDeals } from "@/lib/providers/yowDeals"; import { buildSkyscannerLinks, buildGoogleFlightsLinks, buildAirCanadaLinks, buildAirTransatLinks } from "@/lib/providers/linkBuilders"; import { scoreDeal } from "@/lib/score"; const schema = z.object({ origin: z.string().min(3).max(4), destinations: z.array(z.string().min(3)).nonempty(), startDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), endDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), tripLengthMin: z.number().int().positive(), tripLengthMax: z.number().int().positive(), budget: z.number().int().positive().optional().nullable(), currency: z.string().default("CAD").optional(), nonStopOnly: z.boolean().optional(), sources: z.array(z.string()).optional(), 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() }); export async function POST(req: NextRequest) { try { const body = await req.json(); const parsed = schema.parse(body) as SearchCriteria; const sources = (parsed.sources && parsed.sources.length ? parsed.sources : ["Deals", "Skyscanner", "GoogleFlights", "AirCanada", "AirTransat"]).map(s => s.toLowerCase()); const tasks: Promise[] = []; if (sources.includes("deals")) tasks.push(fetchCityDeals(parsed)); if (sources.includes("skyscanner")) tasks.push(buildSkyscannerLinks(parsed)); if (sources.includes("googleflights")) tasks.push(buildGoogleFlightsLinks(parsed)); if (sources.includes("aircanada")) tasks.push(buildAirCanadaLinks(parsed)); if (sources.includes("airtransat")) tasks.push(buildAirTransatLinks(parsed)); // Demo mode fallback const demo = process.env.DEMO === "true"; let results: Deal[] = []; if (demo) { results = [ { id: "demo1", title: "🔥 Ottawa → Cancun (7 nights)", source: "Demo", link: "#", price: 685, currency: "CAD", startDate: parsed.startDate, endDate: parsed.startDate, nights: 7, origin: parsed.origin, destination: parsed.destinations[0], stops: 0 }, { id: "demo2", title: "Ottawa → Punta Cana (7 nights)", source: "Demo", link: "#", price: 712, currency: "CAD", startDate: parsed.startDate, endDate: parsed.startDate, nights: 7, origin: parsed.origin, destination: parsed.destinations[0], stops: 1 } ]; } else { const settled = await Promise.allSettled(tasks); for (const s of settled) { if (s.status === "fulfilled") results.push(...s.value); } } // Filter and score if (parsed.budget) results = results.filter(r => !r.price || r.price <= (parsed.budget || 0)); if (parsed.nonStopOnly) results = results.filter(r => r.stops == null || r.stops === 0); results.forEach(r => r.score = scoreDeal(r, parsed.preferences)); results.sort((a,b) => (b.score || 0) - (a.score || 0)); // Optional n8n webhook const webhook = process.env.N8N_WEBHOOK_URL; if (webhook) { try { await fetch(webhook, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ criteria: parsed, top10: results.slice(0,10) }) }); } catch(e) { /* ignore */ } } return new Response(JSON.stringify({ results }), { 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" } }); } }