Initial commit: LeadFlow lead generation platform
Full-stack Next.js 16 app with three scraping pipelines: - AirScale CSV → Anymailfinder Bulk Decision Maker search - LinkedIn Sales Navigator → Vayne → Anymailfinder email enrichment - Apify Google SERP → domain extraction → Anymailfinder bulk enrichment Includes Docker multi-stage build + docker-compose for Coolify deployment. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
106
app/api/jobs/airscale-enrich/route.ts
Normal file
106
app/api/jobs/airscale-enrich/route.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { decrypt } from "@/lib/utils/encryption";
|
||||
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 cred = await prisma.apiCredential.findUnique({ where: { service: "anymailfinder" } });
|
||||
if (!cred?.value) {
|
||||
return NextResponse.json({ error: "Anymailfinder API key not configured" }, { status: 400 });
|
||||
}
|
||||
const apiKey = decrypt(cred.value);
|
||||
|
||||
// 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 },
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user