import { normalizePhone, normalizeText, getUsableContactEmail, type GooglePlaceCandidate, } from "./lead-discovery-google"; import type { Id } from "../convex/_generated/dataModel"; type AgentRunLike = { status: string; updatedAt?: number; }; type LeadDiscoveryCounterInput = { leadsFound: number; leadsCreated: number; errors: number; }; type LeadDiscoveryContactInput = { usableEmail?: string | null; }; export type LeadDiscoveryContactStatus = | "new" | "missing_contact" | "audit_ready" | "outreach_ready" | "contacted" | "replied" | "do_not_contact"; type WebsiteEnrichmentScheduleInput = { websiteUrl?: string | null; websiteDomain?: string | null; contactStatus: LeadDiscoveryContactStatus; }; export type LeadDiscoveryPriority = "high" | "medium" | "low" | "defer" | "blocked"; type LeadDiscoveryPriorityInput = { isBlacklisted?: boolean; isDuplicate?: boolean; hasWebsite?: boolean; hasWebsiteSignal?: boolean; }; type LeadDiscoveryLeadRecordInput = { campaignId: TCampaignId; runId: TRunId; niche: string; postalCode: string; candidate: GooglePlaceCandidate; now: number; }; function optionalString(value: string | null | undefined) { return value && value.trim().length > 0 ? value : undefined; } function optionalNumber(value: number | null) { return typeof value === "number" && Number.isFinite(value) ? value : undefined; } export const PENDING_AGENT_RUN_GRACE_MS = 10 * 60 * 1000; export function isStalePendingAgentRun(run: AgentRunLike, now: number) { const updatedAt = typeof run.updatedAt === "number" ? run.updatedAt : 0; return ( run.status === "pending" && updatedAt > 0 && now - updatedAt > PENDING_AGENT_RUN_GRACE_MS ); } export function canStartAgentRun(runs: AgentRunLike[], now = Date.now()) { return !runs.some((run) => { if (run.status === "running") { return true; } return run.status === "pending" && !isStalePendingAgentRun(run, now); }); } export function buildLeadDiscoveryCounters(input: LeadDiscoveryCounterInput) { return { leadsFound: input.leadsFound, leadsCreated: input.leadsCreated, auditsCreated: 0, outreachPrepared: 0, errors: input.errors, }; } export function getLeadDiscoveryContactStatus( input: LeadDiscoveryContactInput, ) { if (input.usableEmail) { return "new"; } return "missing_contact"; } export function shouldScheduleWebsiteEnrichment( input: WebsiteEnrichmentScheduleInput, ) { const hasWebsiteData = optionalString(input.websiteUrl) !== undefined || optionalString(input.websiteDomain) !== undefined; return input.contactStatus === "missing_contact" && hasWebsiteData; } export function buildLeadDiscoveryLeadRecord< TCampaignId extends string, TRunId extends string, >(input: LeadDiscoveryLeadRecordInput) { type LeadDiscoveryDuplicateStatus = | "unchecked" | "unique" | "possible_duplicate" | "duplicate"; const usableEmail = getUsableContactEmail(input.candidate); const lead: { campaignId: TCampaignId; discoveryRunId: TRunId; companyName: string; niche: string; address: string; postalCode: string; googlePlaceId: string; googleMapsUrl?: string; googlePrimaryType?: string; googleTypes: string[]; googleRating?: number; googleUserRatingCount?: number; googleBusinessStatus?: string; sourceProvider: "google_places" | "local_business_data"; sourceBusinessId?: string; sourceFetchedAt: number; websiteUrl?: string; websiteDomain?: string; phone?: string; normalizedGooglePlaceId?: string; normalizedSourceBusinessId?: string; normalizedEmail?: string; normalizedPhone?: string; normalizedCompanyName?: string; normalizedAddress?: string; email?: string; emailSource?: string; contactPerson?: string; priorityReason?: string; duplicateReason?: string; duplicateOfLeadId?: Id<"leads">; blacklistReason?: string; priority: LeadDiscoveryPriority; contactStatus: "new" | "missing_contact"; duplicateStatus: LeadDiscoveryDuplicateStatus; blacklistStatus: "clear"; createdAt: number; updatedAt: number; } = { campaignId: input.campaignId, discoveryRunId: input.runId, companyName: input.candidate.businessName, niche: input.niche, address: input.candidate.address, postalCode: input.postalCode, googlePlaceId: input.candidate.placeId, googleTypes: input.candidate.googleTypes, sourceProvider: input.candidate.sourceProvider, sourceFetchedAt: input.candidate.sourceFetchedAt, priority: "medium", contactStatus: getLeadDiscoveryContactStatus({ usableEmail: usableEmail?.email, }), duplicateStatus: "unique", blacklistStatus: "clear", createdAt: input.now, updatedAt: input.now, }; const googleMapsUrl = optionalString(input.candidate.googleMapsUrl); const googlePrimaryType = optionalString(input.candidate.googlePrimaryType); const googleRating = optionalNumber(input.candidate.rating); const googleUserRatingCount = optionalNumber(input.candidate.userRatingCount); const googleBusinessStatus = optionalString(input.candidate.businessStatus); const sourceBusinessId = optionalString(input.candidate.sourceBusinessId); const websiteUrl = optionalString(input.candidate.websiteUrl); const websiteDomain = optionalString(input.candidate.websiteDomain); const phone = optionalString(input.candidate.phone); const normalizedPhone = normalizePhone(phone); const normalizedCompanyName = normalizeText(input.candidate.businessName); const normalizedAddress = normalizeText(input.candidate.address); if (normalizedCompanyName !== "") { lead.normalizedCompanyName = normalizedCompanyName; } if (normalizedAddress !== "") { lead.normalizedAddress = normalizedAddress; } if (normalizedPhone !== "") { lead.normalizedPhone = normalizedPhone; } if (googleMapsUrl !== undefined) { lead.googleMapsUrl = googleMapsUrl; } if (googlePrimaryType !== undefined) { lead.googlePrimaryType = googlePrimaryType; } if (googleRating !== undefined) { lead.googleRating = googleRating; } if (googleUserRatingCount !== undefined) { lead.googleUserRatingCount = googleUserRatingCount; } if (googleBusinessStatus !== undefined) { lead.googleBusinessStatus = googleBusinessStatus; } if (sourceBusinessId !== undefined) { lead.sourceBusinessId = sourceBusinessId; } if (websiteUrl !== undefined) { lead.websiteUrl = websiteUrl; } if (websiteDomain !== undefined) { lead.websiteDomain = websiteDomain; } if (phone !== undefined) { lead.phone = phone; } if (usableEmail) { lead.normalizedEmail = usableEmail.email; lead.email = usableEmail.email; if (usableEmail.emailSource !== null) { lead.emailSource = usableEmail.emailSource; } if (usableEmail.contactPerson !== null) { lead.contactPerson = usableEmail.contactPerson; } } else { lead.contactStatus = "missing_contact"; } return lead; } export function getLeadDiscoveryPriority( input: LeadDiscoveryPriorityInput, ): { priority: LeadDiscoveryPriority; reason: string } { if (input.isBlacklisted) { return { priority: "blocked", reason: "Lead ist auf der Sperrliste.", }; } if (input.isDuplicate) { return { priority: "defer", reason: "Dublettenprüfung oder Reviewpause.", }; } if (!input.hasWebsite) { return { priority: "high", reason: "Kein Website-Indikator vorhanden.", }; } if (input.hasWebsiteSignal) { return { priority: "low", reason: "Website vorhanden: geringer Kontaktaufwand.", }; } return { priority: "medium", reason: "Standardpriorität.", }; }