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:
TimoUttenweiler
2026-04-01 10:52:50 +02:00
parent 89a176700d
commit 929d5ab3a1
2 changed files with 159 additions and 104 deletions

View File

@@ -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 },

View File

@@ -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({