feat: integrate google lead discovery
This commit is contained in:
166
lib/lead-discovery-run.ts
Normal file
166
lib/lead-discovery-run.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import type { GooglePlaceCandidate } from "./lead-discovery-google";
|
||||
|
||||
type AgentRunLike = {
|
||||
status: string;
|
||||
updatedAt?: number;
|
||||
};
|
||||
|
||||
type LeadDiscoveryCounterInput = {
|
||||
leadsFound: number;
|
||||
leadsCreated: number;
|
||||
errors: number;
|
||||
};
|
||||
|
||||
type LeadDiscoveryContactInput = {
|
||||
websiteDomain?: string | null;
|
||||
phone?: string | null;
|
||||
};
|
||||
|
||||
type LeadDiscoveryLeadRecordInput<TCampaignId extends string, TRunId extends string> = {
|
||||
campaignId: TCampaignId;
|
||||
runId: TRunId;
|
||||
niche: string;
|
||||
postalCode: string;
|
||||
candidate: GooglePlaceCandidate;
|
||||
now: number;
|
||||
};
|
||||
|
||||
function optionalString(value: string | null) {
|
||||
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.websiteDomain || input.phone) {
|
||||
return "new";
|
||||
}
|
||||
|
||||
return "missing_contact";
|
||||
}
|
||||
|
||||
export function buildLeadDiscoveryLeadRecord<
|
||||
TCampaignId extends string,
|
||||
TRunId extends string,
|
||||
>(input: LeadDiscoveryLeadRecordInput<TCampaignId, TRunId>) {
|
||||
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";
|
||||
sourceFetchedAt: number;
|
||||
websiteUrl?: string;
|
||||
websiteDomain?: string;
|
||||
phone?: string;
|
||||
priority: "medium";
|
||||
contactStatus: "new" | "missing_contact";
|
||||
duplicateStatus: "unique";
|
||||
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({
|
||||
websiteDomain: input.candidate.websiteDomain,
|
||||
phone: input.candidate.phone,
|
||||
}),
|
||||
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 websiteUrl = optionalString(input.candidate.websiteUrl);
|
||||
const websiteDomain = optionalString(input.candidate.websiteDomain);
|
||||
const phone = optionalString(input.candidate.phone);
|
||||
|
||||
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 (websiteUrl !== undefined) {
|
||||
lead.websiteUrl = websiteUrl;
|
||||
}
|
||||
if (websiteDomain !== undefined) {
|
||||
lead.websiteDomain = websiteDomain;
|
||||
}
|
||||
if (phone !== undefined) {
|
||||
lead.phone = phone;
|
||||
}
|
||||
|
||||
return lead;
|
||||
}
|
||||
Reference in New Issue
Block a user