diff --git a/app/api/jobs/maps-enrich/route.ts b/app/api/jobs/maps-enrich/route.ts index 6df6760..5b3718c 100644 --- a/app/api/jobs/maps-enrich/route.ts +++ b/app/api/jobs/maps-enrich/route.ts @@ -110,69 +110,83 @@ async function runMapsEnrich( // 3. Optionally enrich with Anymailfinder if (params.enrichEmails && places.length > 0) { const anymailKey = await getApiKey("anymailfinder"); - if (!anymailKey) throw new Error("Anymailfinder API-Key fehlt — bitte in den Einstellungen eintragen"); - const domains = places.filter(p => p.domain).map(p => p.domain!); - - // Map domain → placeId for updating results - const domainToResultId = new Map(); - 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); + if (!anymailKey) { + // No key configured — complete with Maps-only results (no emails) + await prisma.job.update({ where: { id: jobId }, data: { status: "complete", totalLeads: places.length } }); + return; } - let emailsFound = 0; - const enrichResults = await bulkSearchDomains( - domains, - params.categories, - anymailKey, - async (_completed, total) => { - await prisma.job.update({ where: { id: jobId }, data: { totalLeads: total } }); + try { + const domains = places.filter(p => p.domain).map(p => p.domain!); + + // Map domain → leadResult id for updating + const domainToResultId = new Map(); + 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); } - ); - for (const result of enrichResults) { - const hasEmail = !!result.email; - if (hasEmail) emailsFound++; + let emailsFound = 0; + const enrichResults = await bulkSearchDomains( + domains, + params.categories, + anymailKey, + async (_completed, total) => { + await prisma.job.update({ where: { id: jobId }, data: { totalLeads: total } }); + } + ); - const resultId = domainToResultId.get(result.domain || ""); - if (!resultId) continue; + for (const result of enrichResults) { + const hasEmail = !!result.email; + if (hasEmail) emailsFound++; - 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, - }, + const resultId = domainToResultId.get(result.domain || ""); + if (!resultId) continue; + + 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: { 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 { await prisma.job.update({ where: { id: jobId }, diff --git a/app/api/jobs/serp-enrich/route.ts b/app/api/jobs/serp-enrich/route.ts index 5d02151..526cb49 100644 --- a/app/api/jobs/serp-enrich/route.ts +++ b/app/api/jobs/serp-enrich/route.ts @@ -97,68 +97,109 @@ async function runSerpEnrich( data: { totalLeads: domains.length }, }); - // 7. Enrich with Anymailfinder Bulk API - const enrichResults = await bulkSearchDomains( - 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++; - + // 7. Store raw SERP results first (so we have leads even if enrichment fails) + for (const r of filteredResults) { await prisma.leadResult.create({ data: { jobId, - companyName: serpData?.title || null, - domain: result.domain || null, - contactName: result.person_full_name || null, - 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, - }), + companyName: r.title || null, + domain: r.domain || null, + source: JSON.stringify({ url: r.url, description: r.description, position: r.position }), }, }); } - await prisma.job.update({ - where: { id: jobId }, - data: { status: "complete", emailsFound, totalLeads: enrichResults.length }, - }); - - // Sync to LeadVault + // 8. Sink raw results to vault immediately (no contact info yet) await sinkLeadsToVault( - enrichResults.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, - }; - }), + filteredResults.map(r => ({ + domain: r.domain || null, + companyName: r.title || null, + serpTitle: r.title || null, + serpSnippet: r.description || null, + serpRank: r.position ?? null, + serpUrl: r.url || null, + })), "serp", params.query, 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(); + 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) { const message = err instanceof Error ? err.message : String(err); await prisma.job.update({