import { normalizeEmailAddress, type GooglePlaceCandidate, } from "./lead-discovery-google"; export const LOCAL_BUSINESS_DATA_HOST = "local-business-data.p.rapidapi.com"; export const LOCAL_BUSINESS_DATA_SEARCH_ENDPOINT = `https://${LOCAL_BUSINESS_DATA_HOST}/search`; export const LOCAL_BUSINESS_DATA_MAX_RESULTS = 500; type CampaignLike = { categoryMode?: "preset" | "custom"; category?: string | null; customSearchTerm?: string | null; postalCode: string; maxNewLeads: number; }; type LocalBusinessDataError = { message?: string; code?: number; }; type LocalBusinessDataBusiness = Record; export type LocalBusinessDataResponse = { status?: string; request_id?: string; error?: LocalBusinessDataError; data?: unknown; }; function normalizeSearchTerm(value?: string | null) { return value?.trim() ?? ""; } function clampSearchLimit(limit: number) { if (!Number.isFinite(limit)) { return 1; } return Math.min(Math.max(Math.floor(limit), 1), LOCAL_BUSINESS_DATA_MAX_RESULTS); } function stringValue(...values: unknown[]) { for (const value of values) { if (typeof value === "string" && value.trim().length > 0) { return value.trim(); } } return null; } function numberValue(...values: unknown[]) { for (const value of values) { if (typeof value === "number" && Number.isFinite(value)) { return value; } if (typeof value === "string" && value.trim().length > 0) { const parsed = Number.parseFloat(value); if (Number.isFinite(parsed)) { return parsed; } } } return null; } function stringArrayValue(...values: unknown[]) { const result: string[] = []; for (const value of values) { if (typeof value === "string" && value.trim().length > 0) { result.push(value.trim()); continue; } if (Array.isArray(value)) { for (const item of value) { if (typeof item === "string" && item.trim().length > 0) { result.push(item.trim()); } else if (item && typeof item === "object") { const email = stringValue( (item as Record).email, (item as Record).value, ); if (email) { result.push(email); } } } } } return [...new Set(result)]; } function objectValue(value: unknown) { return value && typeof value === "object" ? (value as Record) : null; } function normalizeWebsiteDomain(input?: string | null) { if (!input) { return null; } try { const url = new URL(input); return url.hostname.toLowerCase().replace(/^www\./, ""); } catch { try { const url = new URL(`https://${input}`); return url.hostname.toLowerCase().replace(/^www\./, ""); } catch { return null; } } } function normalizeWebsiteUrl(value?: string | null) { if (!value) { return null; } try { return new URL(value).toString(); } catch { try { return new URL(`https://${value}`).toString(); } catch { return null; } } } function extractBusinesses(data: unknown): LocalBusinessDataBusiness[] { if (Array.isArray(data)) { return data.filter( (item): item is LocalBusinessDataBusiness => item !== null && typeof item === "object", ); } if (data && typeof data === "object") { const record = data as Record; for (const key of ["businesses", "results", "items"]) { const value = record[key]; if (Array.isArray(value)) { return value.filter( (item): item is LocalBusinessDataBusiness => item !== null && typeof item === "object", ); } } } return []; } export function buildLocalBusinessSearchUrl({ query, limit, language = "de", region = "de", }: { query: string; limit: number; language?: string; region?: string; }) { const url = new URL(LOCAL_BUSINESS_DATA_SEARCH_ENDPOINT); url.searchParams.set("query", query); url.searchParams.set("limit", String(clampSearchLimit(limit))); url.searchParams.set("language", language); url.searchParams.set("region", region); url.searchParams.set("extract_emails_and_contacts", "true"); return url.toString(); } export function getLocalBusinessSearchSpec(campaignLike: CampaignLike) { const category = normalizeSearchTerm(campaignLike.category); const baseTerm = campaignLike.categoryMode === "custom" || category === "Anderes" ? normalizeSearchTerm(campaignLike.customSearchTerm) : category; const query = `${baseTerm || "Unternehmen"} in ${campaignLike.postalCode} Deutschland`; const limit = clampSearchLimit(campaignLike.maxNewLeads); return { query, limit, url: buildLocalBusinessSearchUrl({ query, limit }), }; } export function normalizeLocalBusinessSearchResponse( response: LocalBusinessDataResponse, fetchedAt: number, ): GooglePlaceCandidate[] { if (!response || response.status === "ERROR") { const code = response?.error?.code ?? "unknown"; const message = response?.error?.message ?? "Unknown Local Business Data error."; throw new Error(`Local Business Data API error ${code}: ${message}`); } const businesses = extractBusinesses(response.data); return businesses.flatMap((business) => { const sourceBusinessId = stringValue( business.business_id, business.businessId, business.id, ); const placeId = stringValue( business.place_id, business.google_place_id, business.google_id, business.googleId, sourceBusinessId, ); const businessName = stringValue( business.name, business.business_name, business.title, ); if (!placeId || !businessName) { return []; } const websiteUrl = normalizeWebsiteUrl( stringValue(business.website, business.site, business.url), ); const emailsAndContacts = objectValue(business.emails_and_contacts); const directEmails = stringArrayValue( business.email, business.emails, business.contact_emails, business.contacts, emailsAndContacts?.emails, ); const seenEmails = new Set(); const contactEmails = directEmails.flatMap((email) => { const normalizedEmail = normalizeEmailAddress(email); if (!normalizedEmail || seenEmails.has(normalizedEmail)) { return []; } seenEmails.add(normalizedEmail); return [ { email: normalizedEmail, emailSource: "local_business_data", isBusinessContactAddress: normalizedEmail.split("@")[0]?.includes(".") === false, }, ]; }); return [ { placeId, sourceBusinessId: sourceBusinessId ?? placeId, businessName, address: stringValue( business.full_address, business.address, business.formatted_address, ) ?? "", websiteUrl, websiteDomain: normalizeWebsiteDomain(websiteUrl), phone: stringValue( business.phone_number, business.phone, business.telephone, ) ?? null, email: contactEmails[0]?.email ?? null, emailSource: contactEmails[0]?.emailSource ?? null, contactEmails, rating: numberValue(business.rating, business.google_rating), userRatingCount: numberValue( business.review_count, business.reviews_count, business.user_ratings_total, ), businessStatus: stringValue(business.business_status, business.status) ?? null, googleTypes: stringArrayValue( business.types, business.subtypes, business.category, ), googlePrimaryType: stringValue(business.type, business.primary_type, business.category) ?? null, googleMapsUrl: stringValue( business.google_maps_url, business.google_maps_link, business.maps_url, ) ?? null, sourceProvider: "local_business_data", sourceFetchedAt: fetchedAt, }, ]; }); }