fix: Leads ohne E-Mails wenn Anymailfinder-Guthaben leer
Anymailfinder-Fehler (z.B. 402) markiert Job nicht mehr als failed. Maps- und SERP-Jobs schließen als complete ab und liefern die gefundenen Unternehmen ohne Kontaktdaten — SERP-Supplement triggert danach normal. - maps-enrich: Anymailfinder in eigenem try-catch, Fehler → complete - serp-enrich: SERP-Rohdaten zuerst speichern, dann Enrichment versuchen Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -110,69 +110,83 @@ async function runMapsEnrich(
|
|||||||
// 3. Optionally enrich with Anymailfinder
|
// 3. Optionally enrich with Anymailfinder
|
||||||
if (params.enrichEmails && places.length > 0) {
|
if (params.enrichEmails && places.length > 0) {
|
||||||
const anymailKey = await getApiKey("anymailfinder");
|
const anymailKey = await getApiKey("anymailfinder");
|
||||||
if (!anymailKey) throw new Error("Anymailfinder API-Key fehlt — bitte in den Einstellungen eintragen");
|
if (!anymailKey) {
|
||||||
const domains = places.filter(p => p.domain).map(p => p.domain!);
|
// No key configured — complete with Maps-only results (no emails)
|
||||||
|
await prisma.job.update({ where: { id: jobId }, data: { status: "complete", totalLeads: places.length } });
|
||||||
// Map domain → placeId for updating results
|
return;
|
||||||
const domainToResultId = new Map<string, string>();
|
|
||||||
const existingResults = await prisma.leadResult.findMany({
|
|
||||||
where: { jobId },
|
|
||||||
select: { id: true, domain: true },
|
|
||||||
});
|
|
||||||
for (const r of existingResults) {
|
|
||||||
if (r.domain) domainToResultId.set(r.domain, r.id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let emailsFound = 0;
|
try {
|
||||||
const enrichResults = await bulkSearchDomains(
|
const domains = places.filter(p => p.domain).map(p => p.domain!);
|
||||||
domains,
|
|
||||||
params.categories,
|
// Map domain → leadResult id for updating
|
||||||
anymailKey,
|
const domainToResultId = new Map<string, string>();
|
||||||
async (_completed, total) => {
|
const existingResults = await prisma.leadResult.findMany({
|
||||||
await prisma.job.update({ where: { id: jobId }, data: { totalLeads: total } });
|
where: { jobId },
|
||||||
|
select: { id: true, domain: true },
|
||||||
|
});
|
||||||
|
for (const r of existingResults) {
|
||||||
|
if (r.domain) domainToResultId.set(r.domain, r.id);
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
for (const result of enrichResults) {
|
let emailsFound = 0;
|
||||||
const hasEmail = !!result.email;
|
const enrichResults = await bulkSearchDomains(
|
||||||
if (hasEmail) emailsFound++;
|
domains,
|
||||||
|
params.categories,
|
||||||
|
anymailKey,
|
||||||
|
async (_completed, total) => {
|
||||||
|
await prisma.job.update({ where: { id: jobId }, data: { totalLeads: total } });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const resultId = domainToResultId.get(result.domain || "");
|
for (const result of enrichResults) {
|
||||||
if (!resultId) continue;
|
const hasEmail = !!result.email;
|
||||||
|
if (hasEmail) emailsFound++;
|
||||||
|
|
||||||
await prisma.leadResult.update({
|
const resultId = domainToResultId.get(result.domain || "");
|
||||||
where: { id: resultId },
|
if (!resultId) continue;
|
||||||
data: {
|
|
||||||
contactName: result.person_full_name || null,
|
await prisma.leadResult.update({
|
||||||
contactTitle: result.person_job_title || null,
|
where: { id: resultId },
|
||||||
email: result.email || null,
|
data: {
|
||||||
linkedinUrl: result.person_linkedin_url || null,
|
contactName: result.person_full_name || null,
|
||||||
},
|
contactTitle: result.person_job_title || null,
|
||||||
|
email: result.email || null,
|
||||||
|
linkedinUrl: result.person_linkedin_url || null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.job.update({ where: { id: jobId }, data: { emailsFound } });
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.job.update({
|
||||||
|
where: { id: jobId },
|
||||||
|
data: { status: "complete", emailsFound, totalLeads: places.length },
|
||||||
});
|
});
|
||||||
|
|
||||||
await prisma.job.update({ where: { id: jobId }, data: { emailsFound } });
|
// Update vault entries with enrichment results
|
||||||
|
await sinkLeadsToVault(
|
||||||
|
enrichResults
|
||||||
|
.filter(r => r.email)
|
||||||
|
.map(r => ({
|
||||||
|
domain: r.domain,
|
||||||
|
contactName: r.person_full_name || null,
|
||||||
|
contactTitle: r.person_job_title || null,
|
||||||
|
email: r.email || null,
|
||||||
|
linkedinUrl: r.person_linkedin_url || null,
|
||||||
|
})),
|
||||||
|
"maps",
|
||||||
|
params.queries.join(", "),
|
||||||
|
jobId,
|
||||||
|
);
|
||||||
|
} catch (enrichErr) {
|
||||||
|
// Anymailfinder failed (e.g. 402 quota) — complete with Maps-only results
|
||||||
|
console.warn(`[maps-enrich] Anymailfinder failed for job ${jobId}:`, enrichErr);
|
||||||
|
await prisma.job.update({
|
||||||
|
where: { id: jobId },
|
||||||
|
data: { status: "complete", totalLeads: places.length },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.job.update({
|
|
||||||
where: { id: jobId },
|
|
||||||
data: { status: "complete", emailsFound, totalLeads: places.length },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update vault entries with enrichment results
|
|
||||||
await sinkLeadsToVault(
|
|
||||||
enrichResults
|
|
||||||
.filter(r => r.email)
|
|
||||||
.map(r => ({
|
|
||||||
domain: r.domain,
|
|
||||||
contactName: r.person_full_name || null,
|
|
||||||
contactTitle: r.person_job_title || null,
|
|
||||||
email: r.email || null,
|
|
||||||
linkedinUrl: r.person_linkedin_url || null,
|
|
||||||
})),
|
|
||||||
"maps",
|
|
||||||
params.queries.join(", "),
|
|
||||||
jobId,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
await prisma.job.update({
|
await prisma.job.update({
|
||||||
where: { id: jobId },
|
where: { id: jobId },
|
||||||
|
|||||||
@@ -97,68 +97,109 @@ async function runSerpEnrich(
|
|||||||
data: { totalLeads: domains.length },
|
data: { totalLeads: domains.length },
|
||||||
});
|
});
|
||||||
|
|
||||||
// 7. Enrich with Anymailfinder Bulk API
|
// 7. Store raw SERP results first (so we have leads even if enrichment fails)
|
||||||
const enrichResults = await bulkSearchDomains(
|
for (const r of filteredResults) {
|
||||||
domains,
|
|
||||||
params.categories,
|
|
||||||
anymailKey,
|
|
||||||
async (_completed, total) => {
|
|
||||||
await prisma.job.update({ where: { id: jobId }, data: { totalLeads: total } });
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 8. Store results
|
|
||||||
let emailsFound = 0;
|
|
||||||
for (const result of enrichResults) {
|
|
||||||
const serpData = serpMap.get(result.domain || "");
|
|
||||||
const hasEmail = !!result.valid_email;
|
|
||||||
if (hasEmail) emailsFound++;
|
|
||||||
|
|
||||||
await prisma.leadResult.create({
|
await prisma.leadResult.create({
|
||||||
data: {
|
data: {
|
||||||
jobId,
|
jobId,
|
||||||
companyName: serpData?.title || null,
|
companyName: r.title || null,
|
||||||
domain: result.domain || null,
|
domain: r.domain || null,
|
||||||
contactName: result.person_full_name || null,
|
source: JSON.stringify({ url: r.url, description: r.description, position: r.position }),
|
||||||
contactTitle: result.person_job_title || null,
|
|
||||||
email: result.email || null,
|
|
||||||
linkedinUrl: result.person_linkedin_url || null,
|
|
||||||
source: JSON.stringify({
|
|
||||||
url: serpData?.url,
|
|
||||||
description: serpData?.description,
|
|
||||||
position: serpData?.position,
|
|
||||||
email_status: result.email_status,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.job.update({
|
// 8. Sink raw results to vault immediately (no contact info yet)
|
||||||
where: { id: jobId },
|
|
||||||
data: { status: "complete", emailsFound, totalLeads: enrichResults.length },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Sync to LeadVault
|
|
||||||
await sinkLeadsToVault(
|
await sinkLeadsToVault(
|
||||||
enrichResults.map(r => {
|
filteredResults.map(r => ({
|
||||||
const serpData = serpMap.get(r.domain || "");
|
domain: r.domain || null,
|
||||||
return {
|
companyName: r.title || null,
|
||||||
domain: r.domain || null,
|
serpTitle: r.title || null,
|
||||||
companyName: serpData?.title || null,
|
serpSnippet: r.description || null,
|
||||||
contactName: r.person_full_name || null,
|
serpRank: r.position ?? null,
|
||||||
contactTitle: r.person_job_title || null,
|
serpUrl: r.url || null,
|
||||||
email: r.email || null,
|
})),
|
||||||
linkedinUrl: r.person_linkedin_url || null,
|
|
||||||
serpTitle: serpData?.title || null,
|
|
||||||
serpSnippet: serpData?.description || null,
|
|
||||||
serpRank: serpData?.position ?? null,
|
|
||||||
serpUrl: serpData?.url || null,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
"serp",
|
"serp",
|
||||||
params.query,
|
params.query,
|
||||||
jobId,
|
jobId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 9. Enrich with Anymailfinder (best-effort — failure still completes the job)
|
||||||
|
try {
|
||||||
|
const enrichResults = await bulkSearchDomains(
|
||||||
|
domains,
|
||||||
|
params.categories,
|
||||||
|
anymailKey,
|
||||||
|
async (_completed, total) => {
|
||||||
|
await prisma.job.update({ where: { id: jobId }, data: { totalLeads: total } });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Build domain → leadResult id map for updating
|
||||||
|
const domainToResultId = new Map<string, string>();
|
||||||
|
const storedResults = await prisma.leadResult.findMany({
|
||||||
|
where: { jobId },
|
||||||
|
select: { id: true, domain: true },
|
||||||
|
});
|
||||||
|
for (const r of storedResults) {
|
||||||
|
if (r.domain) domainToResultId.set(r.domain, r.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
let emailsFound = 0;
|
||||||
|
for (const result of enrichResults) {
|
||||||
|
const hasEmail = !!result.valid_email;
|
||||||
|
if (hasEmail) emailsFound++;
|
||||||
|
|
||||||
|
const resultId = domainToResultId.get(result.domain || "");
|
||||||
|
if (resultId) {
|
||||||
|
await prisma.leadResult.update({
|
||||||
|
where: { id: resultId },
|
||||||
|
data: {
|
||||||
|
contactName: result.person_full_name || null,
|
||||||
|
contactTitle: result.person_job_title || null,
|
||||||
|
email: result.email || null,
|
||||||
|
linkedinUrl: result.person_linkedin_url || null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.job.update({
|
||||||
|
where: { id: jobId },
|
||||||
|
data: { status: "complete", emailsFound, totalLeads: filteredResults.length },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update vault entries with contact info
|
||||||
|
await sinkLeadsToVault(
|
||||||
|
enrichResults
|
||||||
|
.filter(r => r.email)
|
||||||
|
.map(r => {
|
||||||
|
const serpData = serpMap.get(r.domain || "");
|
||||||
|
return {
|
||||||
|
domain: r.domain || null,
|
||||||
|
companyName: serpData?.title || null,
|
||||||
|
contactName: r.person_full_name || null,
|
||||||
|
contactTitle: r.person_job_title || null,
|
||||||
|
email: r.email || null,
|
||||||
|
linkedinUrl: r.person_linkedin_url || null,
|
||||||
|
serpTitle: serpData?.title || null,
|
||||||
|
serpSnippet: serpData?.description || null,
|
||||||
|
serpRank: serpData?.position ?? null,
|
||||||
|
serpUrl: serpData?.url || null,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
"serp",
|
||||||
|
params.query,
|
||||||
|
jobId,
|
||||||
|
);
|
||||||
|
} catch (enrichErr) {
|
||||||
|
// Anymailfinder failed — complete with SERP-only results (no emails)
|
||||||
|
console.warn(`[serp-enrich] Anymailfinder failed for job ${jobId}:`, enrichErr);
|
||||||
|
await prisma.job.update({
|
||||||
|
where: { id: jobId },
|
||||||
|
data: { status: "complete", totalLeads: filteredResults.length },
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
await prisma.job.update({
|
await prisma.job.update({
|
||||||
|
|||||||
Reference in New Issue
Block a user