- Neuer getApiKey() Helper: prüft zuerst ENV-Vars, dann DB - Alle Job-Routes nutzen getApiKey() statt direktem DB-Lookup - Credentials-Status berücksichtigt ENV-Vars (Sidebar-Haken) - .env.local.example: Platzhalter für alle 4 API Keys Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
104 lines
3.3 KiB
TypeScript
104 lines
3.3 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { prisma } from "@/lib/db";
|
|
import { getApiKey } from "@/lib/utils/apiKey";
|
|
import { cleanDomain } from "@/lib/utils/domains";
|
|
import { bulkSearchDomains, type DecisionMakerCategory } from "@/lib/services/anymailfinder";
|
|
|
|
export async function POST(req: NextRequest) {
|
|
try {
|
|
const body = await req.json() as {
|
|
companies: Array<{ name: string; domain: string }>;
|
|
categories: DecisionMakerCategory[];
|
|
};
|
|
|
|
const { companies, categories } = body;
|
|
if (!companies?.length) {
|
|
return NextResponse.json({ error: "No companies provided" }, { status: 400 });
|
|
}
|
|
|
|
const apiKey = await getApiKey("anymailfinder");
|
|
if (!apiKey) return NextResponse.json({ error: "Anymailfinder API key not configured" }, { status: 400 });
|
|
|
|
// Build domain → company map
|
|
const domainMap = new Map<string, string>();
|
|
for (const c of companies) {
|
|
const d = cleanDomain(c.domain);
|
|
if (d) domainMap.set(d, c.name);
|
|
}
|
|
const domains = Array.from(domainMap.keys());
|
|
|
|
const job = await prisma.job.create({
|
|
data: {
|
|
type: "airscale",
|
|
status: "running",
|
|
config: JSON.stringify({ categories, totalDomains: domains.length }),
|
|
totalLeads: domains.length,
|
|
},
|
|
});
|
|
|
|
// Run enrichment in background
|
|
runEnrichment(job.id, domains, domainMap, categories, apiKey).catch(console.error);
|
|
|
|
return NextResponse.json({ jobId: job.id });
|
|
} catch (err) {
|
|
console.error("POST /api/jobs/airscale-enrich error:", err);
|
|
return NextResponse.json({ error: "Failed to start job" }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
async function runEnrichment(
|
|
jobId: string,
|
|
domains: string[],
|
|
domainMap: Map<string, string>,
|
|
categories: DecisionMakerCategory[],
|
|
apiKey: string
|
|
) {
|
|
try {
|
|
// Use bulk API: submit all domains, poll for completion, then store results.
|
|
const results = await bulkSearchDomains(
|
|
domains,
|
|
categories,
|
|
apiKey,
|
|
async (processed, total) => {
|
|
// Update progress while bulk job is running
|
|
await prisma.job.update({
|
|
where: { id: jobId },
|
|
data: { totalLeads: total },
|
|
});
|
|
}
|
|
);
|
|
|
|
// Store all results
|
|
let emailsFound = 0;
|
|
for (const result of results) {
|
|
const hasEmail = !!result.valid_email;
|
|
if (hasEmail) emailsFound++;
|
|
|
|
await prisma.leadResult.create({
|
|
data: {
|
|
jobId,
|
|
companyName: domainMap.get(result.domain || "") || null,
|
|
domain: result.domain || null,
|
|
contactName: result.person_full_name || null,
|
|
contactTitle: result.person_job_title || null,
|
|
email: result.email || null,
|
|
confidence: result.valid_email ? 1.0 : result.email_status === "risky" ? 0.5 : 0,
|
|
linkedinUrl: result.person_linkedin_url || null,
|
|
source: JSON.stringify({ email_status: result.email_status, category: result.decision_maker_category }),
|
|
},
|
|
});
|
|
}
|
|
|
|
await prisma.job.update({
|
|
where: { id: jobId },
|
|
data: { status: "complete", emailsFound, totalLeads: results.length },
|
|
});
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
await prisma.job.update({
|
|
where: { id: jobId },
|
|
data: { status: "failed", error: message },
|
|
});
|
|
}
|
|
}
|