feat: add lead qualification workflow
This commit is contained in:
@@ -1,4 +1,10 @@
|
||||
import type { GooglePlaceCandidate } from "./lead-discovery-google";
|
||||
import {
|
||||
normalizePhone,
|
||||
normalizeText,
|
||||
getUsableContactEmail,
|
||||
type GooglePlaceCandidate,
|
||||
} from "./lead-discovery-google";
|
||||
import type { Id } from "../convex/_generated/dataModel";
|
||||
|
||||
type AgentRunLike = {
|
||||
status: string;
|
||||
@@ -12,8 +18,16 @@ type LeadDiscoveryCounterInput = {
|
||||
};
|
||||
|
||||
type LeadDiscoveryContactInput = {
|
||||
websiteDomain?: string | null;
|
||||
phone?: string | null;
|
||||
usableEmail?: string | null;
|
||||
};
|
||||
|
||||
export type LeadDiscoveryPriority = "high" | "medium" | "low" | "defer" | "blocked";
|
||||
|
||||
type LeadDiscoveryPriorityInput = {
|
||||
isBlacklisted?: boolean;
|
||||
isDuplicate?: boolean;
|
||||
hasWebsite?: boolean;
|
||||
hasWebsiteSignal?: boolean;
|
||||
};
|
||||
|
||||
type LeadDiscoveryLeadRecordInput<TCampaignId extends string, TRunId extends string> = {
|
||||
@@ -70,7 +84,7 @@ export function buildLeadDiscoveryCounters(input: LeadDiscoveryCounterInput) {
|
||||
export function getLeadDiscoveryContactStatus(
|
||||
input: LeadDiscoveryContactInput,
|
||||
) {
|
||||
if (input.websiteDomain || input.phone) {
|
||||
if (input.usableEmail) {
|
||||
return "new";
|
||||
}
|
||||
|
||||
@@ -81,6 +95,14 @@ export function buildLeadDiscoveryLeadRecord<
|
||||
TCampaignId extends string,
|
||||
TRunId extends string,
|
||||
>(input: LeadDiscoveryLeadRecordInput<TCampaignId, TRunId>) {
|
||||
type LeadDiscoveryDuplicateStatus =
|
||||
| "unchecked"
|
||||
| "unique"
|
||||
| "possible_duplicate"
|
||||
| "duplicate";
|
||||
|
||||
const usableEmail = getUsableContactEmail(input.candidate);
|
||||
|
||||
const lead: {
|
||||
campaignId: TCampaignId;
|
||||
discoveryRunId: TRunId;
|
||||
@@ -100,9 +122,21 @@ export function buildLeadDiscoveryLeadRecord<
|
||||
websiteUrl?: string;
|
||||
websiteDomain?: string;
|
||||
phone?: string;
|
||||
priority: "medium";
|
||||
normalizedGooglePlaceId?: 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: "unique";
|
||||
duplicateStatus: LeadDiscoveryDuplicateStatus;
|
||||
blacklistStatus: "clear";
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
@@ -119,8 +153,7 @@ export function buildLeadDiscoveryLeadRecord<
|
||||
sourceFetchedAt: input.candidate.sourceFetchedAt,
|
||||
priority: "medium",
|
||||
contactStatus: getLeadDiscoveryContactStatus({
|
||||
websiteDomain: input.candidate.websiteDomain,
|
||||
phone: input.candidate.phone,
|
||||
usableEmail: usableEmail?.email,
|
||||
}),
|
||||
duplicateStatus: "unique",
|
||||
blacklistStatus: "clear",
|
||||
@@ -136,6 +169,21 @@ export function buildLeadDiscoveryLeadRecord<
|
||||
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;
|
||||
@@ -161,6 +209,55 @@ export function buildLeadDiscoveryLeadRecord<
|
||||
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.",
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user