import { NextRequest, NextResponse } from "next/server"; import { prisma } from "@/lib/db"; import { getApiKey } from "@/lib/utils/apiKey"; import { submitBulkPersonSearch, getBulkSearchStatus, downloadBulkResults, searchDecisionMakerByDomain, type DecisionMakerCategory, } from "@/lib/services/anymailfinder"; export async function POST(req: NextRequest) { try { const body = await req.json() as { jobId: string; resultIds: string[]; categories: DecisionMakerCategory[]; }; const { jobId, resultIds, categories } = body; const apiKey = await getApiKey("anymailfinder"); if (!apiKey) return NextResponse.json({ error: "Anymailfinder API key not configured" }, { status: 400 }); const results = await prisma.leadResult.findMany({ where: { id: { in: resultIds }, jobId, domain: { not: null } }, }); const enrichJob = await prisma.job.create({ data: { type: "linkedin-enrich", status: "running", config: JSON.stringify({ parentJobId: jobId, categories }), totalLeads: results.length, }, }); runLinkedInEnrich(enrichJob.id, jobId, results, categories, apiKey).catch(console.error); return NextResponse.json({ jobId: enrichJob.id }); } catch (err) { console.error("POST /api/jobs/linkedin-enrich error:", err); return NextResponse.json({ error: "Failed to start enrichment" }, { status: 500 }); } } async function runLinkedInEnrich( enrichJobId: string, parentJobId: string, results: Array<{ id: string; domain: string | null; contactName: string | null; companyName: string | null; contactTitle: string | null; linkedinUrl: string | null; }>, categories: DecisionMakerCategory[], apiKey: string ) { let emailsFound = 0; try { // Separate results into those with names (person search) and those without (decision maker search) const withNames: typeof results = []; const withoutNames: typeof results = []; for (const r of results) { if (r.contactName && r.domain) { withNames.push(r); } else if (r.domain) { withoutNames.push(r); } } // Map to look up results by domain const resultByDomain = new Map(results.map(r => [r.domain!, r])); // 1. Bulk person name search for leads with names if (withNames.length > 0) { const leads = withNames.map(r => { const nameParts = (r.contactName || "").trim().split(/\s+/); return { domain: r.domain!, firstName: nameParts[0] || "", lastName: nameParts.slice(1).join(" ") || "", }; }); try { const searchId = await submitBulkPersonSearch(leads, apiKey, `linkedin-enrich-${enrichJobId}`); // Poll for completion let status; do { await sleep(5000); status = await getBulkSearchStatus(searchId, apiKey); } while (status.status !== "completed" && status.status !== "failed"); if (status.status === "completed") { const rows = await downloadBulkResults(searchId, apiKey); for (const row of rows) { const domain = row["domain"] || row["Domain"] || ""; const result = resultByDomain.get(domain); if (!result) continue; const email = row["email"] || row["Email"] || null; const emailStatus = (row["email_status"] || row["Email Status"] || "not_found").toLowerCase(); const isValid = emailStatus === "valid"; if (isValid) emailsFound++; await prisma.leadResult.update({ where: { id: result.id }, data: { email: email || null, confidence: isValid ? 1.0 : emailStatus === "risky" ? 0.5 : 0, contactName: row["person_full_name"] || row["Full Name"] || result.contactName || null, contactTitle: row["person_job_title"] || row["Job Title"] || result.contactTitle || null, }, }); } } } catch (err) { console.error("Bulk person search error:", err); // Fall through — will attempt decision-maker search below } } // 2. Decision-maker search for leads without names for (const r of withoutNames) { if (!r.domain) continue; try { const found = await searchDecisionMakerByDomain(r.domain, categories, apiKey); const isValid = !!found.valid_email; if (isValid) emailsFound++; await prisma.leadResult.update({ where: { id: r.id }, data: { email: found.email || null, confidence: isValid ? 1.0 : found.email_status === "risky" ? 0.5 : 0, contactName: found.person_full_name || r.contactName || null, contactTitle: found.person_job_title || r.contactTitle || null, }, }); await prisma.job.update({ where: { id: enrichJobId }, data: { emailsFound } }); } catch (err) { console.error(`Decision-maker search error for domain ${r.domain}:`, err); } } await prisma.job.update({ where: { id: enrichJobId }, data: { status: "complete", emailsFound }, }); } catch (err) { const message = err instanceof Error ? err.message : String(err); await prisma.job.update({ where: { id: enrichJobId }, data: { status: "failed", error: message }, }); } } function sleep(ms: number) { return new Promise(r => setTimeout(r, ms)); }