Files

83 lines
3.9 KiB
TypeScript

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<Deal[]>[] = [];
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" } });
}
}