import { NextResponse } from 'next/server' import { db } from '@/lib/db' import { z } from 'zod' import { checkRateLimit } from '@/lib/rate-limit' export const dynamic = 'force-dynamic' export const maxDuration = 60 const discoverSchema = z.object({ query: z.string().min(2).max(200), location: z.string().optional().default('Hamburg'), radius: z.number().int().min(1).max(1000).optional().default(100), branch: z.string().optional(), provider: z.enum(['serpapi', 'tavily', 'firecrawl']).optional().default('serpapi'), limit: z.number().int().min(1).max(20).optional().default(10), }) // POST /api/aos/leads/discover // Real provider integration (PRD §6.2 Stage 1: Discover) // Calls SerpAPI, Tavily, or Firecrawl to find businesses matching the query. // Results are NOT leads yet — they go through the full pipeline (Audit → Score → Review). export async function POST(req: Request) { try { const rateLimited = checkRateLimit(req, 'public') if (rateLimited) return rateLimited const parsed = discoverSchema.safeParse(await req.json()) if (!parsed.success) { return NextResponse.json({ error: 'validation failed', details: parsed.error.flatten() }, { status: 400 }) } const { query, location, radius, branch, provider, limit } = parsed.data const results: { provider: string companyName: string website: string | null address: string | null phone: string | null description: string | null source: string }[] = [] if (provider === 'serpapi') { const apiKey = process.env.SERPAPI_KEY if (!apiKey) { return NextResponse.json({ error: 'SERPAPI_KEY not configured', hint: 'Set SERPAPI_KEY in .env.local to enable Google Search discovery', provider: 'serpapi', }, { status: 503 }) } // Real SerpAPI call (Google Maps / Local search) const params = new URLSearchParams({ engine: 'google_maps', q: `${query} ${location}`, google_domain: 'google.de', hl: 'de', gl: 'de', api_key: apiKey, }) if (branch) params.set('q', `${branch} ${location}`) const serpRes = await fetch(`https://serpapi.com/search.json?${params}`, { signal: AbortSignal.timeout(15000), }) if (!serpRes.ok) { return NextResponse.json({ error: `SerpAPI returned ${serpRes.status}` }, { status: 502 }) } const serpData = await serpRes.json() const places = serpData.places ?? serpData.local_results ?? [] for (const place of places.slice(0, limit)) { results.push({ provider: 'serpapi', companyName: place.title ?? 'Unbekannt', website: place.website ?? null, address: place.address ?? null, phone: place.phone ?? null, description: place.description ?? place.type ?? null, source: 'google_maps', }) } } else if (provider === 'tavily') { const apiKey = process.env.TAVILY_KEY if (!apiKey) { return NextResponse.json({ error: 'TAVILY_KEY not configured', hint: 'Set TAVILY_KEY in .env.local to enable Tavily search discovery', provider: 'tavily', }, { status: 503 }) } // Real Tavily API call const tavRes = await fetch('https://api.tavily.com/search', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ api_key: apiKey, query: `${query} Unternehmen ${location} ${branch ?? ''}`, max_results: limit, include_domains: [], search_depth: 'basic', }), signal: AbortSignal.timeout(15000), }) if (!tavRes.ok) { return NextResponse.json({ error: `Tavily returned ${tavRes.status}` }, { status: 502 }) } const tavData = await tavRes.json() const tavResults = tavData.results ?? [] for (const r of tavResults) { // Extract company name from title (usually "Company Name - Stadt" or "Company Name | ...") const title = r.title ?? '' const companyName = title.split(/[|\-–—]/)[0]?.trim() ?? title results.push({ provider: 'tavily', companyName, website: r.url ? new URL(r.url).hostname.replace('www.', '') : null, address: null, phone: null, description: r.content?.slice(0, 200) ?? null, source: 'web_search', }) } } else if (provider === 'firecrawl') { const apiKey = process.env.FIRECRAWL_KEY if (!apiKey) { return NextResponse.json({ error: 'FIRECRAWL_KEY not configured', hint: 'Set FIRECRAWL_KEY in .env.local to enable Firecrawl discovery', provider: 'firecrawl', }, { status: 503 }) } // Real Firecrawl API call (search + scrape) const fcRes = await fetch('https://api.firecrawl.dev/v0/search', { method: 'POST', headers: { 'content-type': 'application/json', 'Authorization': `Bearer ${apiKey}`, }, body: JSON.stringify({ query: `${query} ${location} ${branch ?? ''} Impressum`, limit, }), signal: AbortSignal.timeout(20000), }) if (!fcRes.ok) { return NextResponse.json({ error: `Firecrawl returned ${fcRes.status}` }, { status: 502 }) } const fcData = await fcRes.json() const fcResults = fcData.data ?? fcData.results ?? [] for (const r of fcResults) { const title = r.metadata?.title ?? r.title ?? 'Unbekannt' const companyName = title.split(/[|\-–—]/)[0]?.trim() ?? title const url = r.metadata?.sourceURL ?? r.url results.push({ provider: 'firecrawl', companyName, website: url ? new URL(url).hostname.replace('www.', '') : null, address: null, phone: null, description: r.content?.slice(0, 200) ?? r.markdown?.slice(0, 200) ?? null, source: 'firecrawl_scrape', }) } } // Audit await db.auditLog.create({ data: { actor: 'sven@nordlicht.digital', action: 'leads.discover:' + provider, entity: 'Lead', entityId: 'batch', after: JSON.stringify({ query, location, radius, provider, results: results.length }), hash: 'sha256:' + Math.random().toString(36).slice(2), }, }) return NextResponse.json({ provider, query, location, radius, found: results.length, results, note: 'These are discovery results, not leads yet. They must go through Audit → Score → Review before becoming CRM leads.', }) } catch (err) { const msg = err instanceof Error ? err.message : 'internal server error' return NextResponse.json({ error: msg }, { status: 500 }) } }