feat: add lead qualification workflow
This commit is contained in:
@@ -4,6 +4,7 @@ import test from "node:test";
|
||||
import {
|
||||
RUN_STATUSES,
|
||||
SCREENSHOT_VIEWPORTS,
|
||||
LEAD_PRIORITIES,
|
||||
filterSafeSettingsRows,
|
||||
isSafeSettingsKey,
|
||||
normalizeListLimit,
|
||||
@@ -49,6 +50,10 @@ test("run statuses expose observable job lifecycle states", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test("lead priorities include manual blocking option", () => {
|
||||
assert.deepEqual(LEAD_PRIORITIES, ["high", "medium", "low", "defer", "blocked"]);
|
||||
});
|
||||
|
||||
test("list limits are clamped to a positive integer range", () => {
|
||||
assert.equal(normalizeListLimit(undefined), 50);
|
||||
assert.equal(normalizeListLimit(-10), 1);
|
||||
|
||||
@@ -2,6 +2,14 @@ import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
getLeadBlacklistStatusLabel,
|
||||
getLeadContactStatusLabel,
|
||||
getLeadDuplicateStatusLabel,
|
||||
getLeadPriorityLabel,
|
||||
leadBlacklistStatusOptions,
|
||||
leadContactStatusOptions,
|
||||
leadDuplicateStatusOptions,
|
||||
leadPriorityOptions,
|
||||
dashboardKpis,
|
||||
dashboardNavigation,
|
||||
groupLeadFunnelCards,
|
||||
@@ -138,6 +146,49 @@ test("groupLeadFunnelCards derives review, follow-up, and deferred columns witho
|
||||
);
|
||||
});
|
||||
|
||||
test("toLeadFunnelCard maps blocked priority to deferred stage with blocker label", () => {
|
||||
const card = toLeadFunnelCard({
|
||||
id: "lead-blocked",
|
||||
companyName: "Sperr Beispiel",
|
||||
city: "Freiburg",
|
||||
priority: "blocked",
|
||||
contactStatus: "new",
|
||||
blacklistStatus: "blocked",
|
||||
});
|
||||
|
||||
assert.equal(card.stageId, "deferred");
|
||||
assert.equal(card.priorityLabel, "Gesperrt");
|
||||
assert.equal(card.nextAction, "Zurückstellung prüfen");
|
||||
});
|
||||
|
||||
test("dashboard-model exposes stable lead label helpers for UI mapping", () => {
|
||||
assert.deepEqual(leadPriorityOptions, [
|
||||
"high",
|
||||
"medium",
|
||||
"low",
|
||||
"defer",
|
||||
"blocked",
|
||||
]);
|
||||
assert.equal(getLeadPriorityLabel("high"), "Hoch");
|
||||
assert.equal(getLeadContactStatusLabel("missing_contact"), "Kontakt fehlt");
|
||||
assert.equal(getLeadBlacklistStatusLabel("blocked"), "Gesperrt");
|
||||
});
|
||||
|
||||
test("dashboard-model exposes duplicate status options and labels", () => {
|
||||
assert.deepEqual(leadDuplicateStatusOptions, [
|
||||
"unchecked",
|
||||
"unique",
|
||||
"possible_duplicate",
|
||||
"duplicate",
|
||||
]);
|
||||
assert.equal(getLeadDuplicateStatusLabel("duplicate"), "Duplikat");
|
||||
});
|
||||
|
||||
test("dashboard-model exposes contact status options for lead review controls", () => {
|
||||
assert.equal(leadContactStatusOptions[1], "missing_contact");
|
||||
assert.equal(leadBlacklistStatusOptions.length, 2);
|
||||
});
|
||||
|
||||
test("dashboardKpis and reviewQueue expose the above-the-fold dashboard summary", () => {
|
||||
assert.equal(dashboardKpis.length, 4);
|
||||
assert.equal(reviewQueue.length, 3);
|
||||
|
||||
@@ -4,10 +4,14 @@ import test from "node:test";
|
||||
import {
|
||||
GOOGLE_PLACES_FIELD_MASK,
|
||||
buildGeocodingUrl,
|
||||
getUsableContactEmail,
|
||||
getUsableContactEmailFromEntries,
|
||||
getBlacklistMatches,
|
||||
getBlacklistLookupValues,
|
||||
getPlacesSearchSpec,
|
||||
isProbableDuplicateCandidate,
|
||||
isDuplicateCandidate,
|
||||
normalizeEmailAddress,
|
||||
normalizePlacesResponse,
|
||||
parseGeocodingResponse,
|
||||
} from "../lib/lead-discovery-google";
|
||||
@@ -205,8 +209,12 @@ test("places normalization maps source metadata and normalizes website domain",
|
||||
|
||||
test("duplicate detection uses placeId and websiteDomain", () => {
|
||||
const existingLeads = [
|
||||
{ googlePlaceId: "dup-1", websiteDomain: "other.de" },
|
||||
{ googlePlaceId: "other-2", websiteDomain: "example.de" },
|
||||
{
|
||||
googlePlaceId: "dup-1",
|
||||
websiteDomain: "other.de",
|
||||
email: "blocked@example.de",
|
||||
},
|
||||
{ googlePlaceId: "other-2", websiteDomain: "example.de", email: "blocked@example.de" },
|
||||
];
|
||||
|
||||
assert.equal(
|
||||
@@ -277,6 +285,158 @@ test("duplicate detection uses placeId and websiteDomain", () => {
|
||||
),
|
||||
false,
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
isDuplicateCandidate(
|
||||
{
|
||||
placeId: "none",
|
||||
businessName: "Test",
|
||||
address: "A",
|
||||
websiteUrl: "https://www.example.de",
|
||||
websiteDomain: "new.de",
|
||||
phone: null,
|
||||
rating: null,
|
||||
userRatingCount: null,
|
||||
businessStatus: null,
|
||||
googleTypes: [],
|
||||
googlePrimaryType: null,
|
||||
googleMapsUrl: null,
|
||||
sourceProvider: "google_places",
|
||||
sourceFetchedAt: 0,
|
||||
contactEmails: [{ email: "Owner@Example.De", isBusinessContactAddress: false }],
|
||||
},
|
||||
existingLeads,
|
||||
),
|
||||
false,
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
isDuplicateCandidate(
|
||||
{
|
||||
placeId: "none",
|
||||
businessName: "Test",
|
||||
address: "A",
|
||||
websiteUrl: "https://www.new.de",
|
||||
websiteDomain: "new.de",
|
||||
phone: null,
|
||||
rating: null,
|
||||
userRatingCount: null,
|
||||
businessStatus: null,
|
||||
googleTypes: [],
|
||||
googlePrimaryType: null,
|
||||
googleMapsUrl: null,
|
||||
sourceProvider: "google_places",
|
||||
sourceFetchedAt: 0,
|
||||
contactEmails: [{ email: "newlead@new.de" }],
|
||||
},
|
||||
existingLeads,
|
||||
),
|
||||
false,
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
isDuplicateCandidate(
|
||||
{
|
||||
placeId: "none",
|
||||
businessName: "Test",
|
||||
address: "A",
|
||||
websiteUrl: "https://www.example.de",
|
||||
websiteDomain: "new.de",
|
||||
phone: null,
|
||||
rating: null,
|
||||
userRatingCount: null,
|
||||
businessStatus: null,
|
||||
googleTypes: [],
|
||||
googlePrimaryType: null,
|
||||
googleMapsUrl: null,
|
||||
sourceProvider: "google_places",
|
||||
sourceFetchedAt: 0,
|
||||
email: "Blocked@Example.De",
|
||||
},
|
||||
existingLeads,
|
||||
),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("probable duplicates are detected by normalized company+address or normalized phone", () => {
|
||||
const existingLeads = [
|
||||
{
|
||||
googlePlaceId: "dup-1",
|
||||
companyName: "Muster GmbH",
|
||||
address: "Hauptstraße 1, 60311 Frankfurt am Main",
|
||||
phone: "+49 30 123456",
|
||||
},
|
||||
];
|
||||
|
||||
assert.equal(
|
||||
isProbableDuplicateCandidate(
|
||||
{
|
||||
placeId: "none-1",
|
||||
businessName: "Muster GmbH",
|
||||
address: "Hauptstraße 1, 60311 Frankfurt am Main",
|
||||
websiteUrl: null,
|
||||
websiteDomain: null,
|
||||
phone: null,
|
||||
rating: null,
|
||||
userRatingCount: null,
|
||||
businessStatus: null,
|
||||
googleTypes: [],
|
||||
googlePrimaryType: null,
|
||||
googleMapsUrl: null,
|
||||
sourceProvider: "google_places",
|
||||
sourceFetchedAt: 0,
|
||||
},
|
||||
existingLeads,
|
||||
),
|
||||
true,
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
isProbableDuplicateCandidate(
|
||||
{
|
||||
placeId: "none-2",
|
||||
businessName: "Other GmbH",
|
||||
address: "Nebenstraße 9",
|
||||
websiteUrl: null,
|
||||
websiteDomain: null,
|
||||
phone: "0049 30 123456",
|
||||
rating: null,
|
||||
userRatingCount: null,
|
||||
businessStatus: null,
|
||||
googleTypes: [],
|
||||
googlePrimaryType: null,
|
||||
googleMapsUrl: null,
|
||||
sourceProvider: "google_places",
|
||||
sourceFetchedAt: 0,
|
||||
},
|
||||
existingLeads,
|
||||
),
|
||||
true,
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
isProbableDuplicateCandidate(
|
||||
{
|
||||
placeId: "none-3",
|
||||
businessName: "Different GmbH",
|
||||
address: "Musterallee 5",
|
||||
websiteUrl: null,
|
||||
websiteDomain: null,
|
||||
phone: "+49 89 999999",
|
||||
rating: null,
|
||||
userRatingCount: null,
|
||||
businessStatus: null,
|
||||
googleTypes: [],
|
||||
googlePrimaryType: null,
|
||||
googleMapsUrl: null,
|
||||
sourceProvider: "google_places",
|
||||
sourceFetchedAt: 0,
|
||||
},
|
||||
existingLeads,
|
||||
),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("blacklist matches include google_place_id, domain, company and phone", () => {
|
||||
@@ -287,6 +447,8 @@ test("blacklist matches include google_place_id, domain, company and phone", ()
|
||||
websiteUrl: "https://www.Blocked.de",
|
||||
websiteDomain: "blocked.de",
|
||||
phone: "+49 30 555 123",
|
||||
email: "Info@Blocked.De",
|
||||
contactEmails: [{ email: "Hello@blocked.de", isBusinessContactAddress: false }],
|
||||
rating: null,
|
||||
userRatingCount: null,
|
||||
businessStatus: null,
|
||||
@@ -303,6 +465,8 @@ test("blacklist matches include google_place_id, domain, company and phone", ()
|
||||
{ type: "company", normalizedValue: "muster gmbh" },
|
||||
{ type: "phone", normalizedValue: "4930555123" },
|
||||
{ type: "phone", normalizedValue: "+49 30 555 123" },
|
||||
{ type: "email", normalizedValue: "info@blocked.de" },
|
||||
{ type: "email", normalizedValue: "hello@blocked.de" },
|
||||
]);
|
||||
|
||||
const matches = getBlacklistMatches(
|
||||
@@ -323,12 +487,213 @@ test("blacklist matches include google_place_id, domain, company and phone", ()
|
||||
},
|
||||
{ type: "email", value: "x@example.de", normalizedValue: "x@example.de" },
|
||||
{ type: "phone", value: "+49 30 999 999", normalizedValue: "4930999999" },
|
||||
{
|
||||
type: "email",
|
||||
value: "Info@Blocked.De",
|
||||
normalizedValue: "info@blocked.de",
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
const matchTypes = matches.map((match) => match.type).sort();
|
||||
assert.deepEqual(
|
||||
matchTypes,
|
||||
["company", "domain", "google_place_id", "phone", "phone"].sort(),
|
||||
["company", "domain", "google_place_id", "phone", "phone", "email"].sort(),
|
||||
);
|
||||
});
|
||||
|
||||
test("company normalization for blacklist lookup uses text normalization", () => {
|
||||
const candidate = {
|
||||
placeId: "place-company-spaces",
|
||||
businessName: "Muster GmbH",
|
||||
address: "A",
|
||||
websiteUrl: null,
|
||||
websiteDomain: null,
|
||||
phone: "+49 30 555 123",
|
||||
rating: null,
|
||||
userRatingCount: null,
|
||||
businessStatus: null,
|
||||
googleTypes: [],
|
||||
googlePrimaryType: null,
|
||||
googleMapsUrl: null,
|
||||
sourceProvider: "google_places" as const,
|
||||
sourceFetchedAt: 0,
|
||||
};
|
||||
|
||||
assert.deepEqual(getBlacklistLookupValues(candidate), [
|
||||
{ type: "google_place_id", normalizedValue: "place-company-spaces" },
|
||||
{ type: "company", normalizedValue: "muster gmbh" },
|
||||
{ type: "phone", normalizedValue: "4930555123" },
|
||||
{ type: "phone", normalizedValue: "+49 30 555 123" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("company blacklist matching supports whitespace-normalized names", () => {
|
||||
const candidate = {
|
||||
placeId: "place-company-spaces-2",
|
||||
businessName: "Muster GmbH",
|
||||
address: "A",
|
||||
websiteUrl: null,
|
||||
websiteDomain: null,
|
||||
phone: null,
|
||||
rating: null,
|
||||
userRatingCount: null,
|
||||
businessStatus: null,
|
||||
googleTypes: [],
|
||||
googlePrimaryType: null,
|
||||
googleMapsUrl: null,
|
||||
sourceProvider: "google_places" as const,
|
||||
sourceFetchedAt: 0,
|
||||
};
|
||||
|
||||
const matches = getBlacklistMatches(candidate, [
|
||||
{ type: "company", value: "Muster GmbH", normalizedValue: "muster gmbh" },
|
||||
]);
|
||||
|
||||
assert.equal(matches.length, 1);
|
||||
assert.equal(matches[0]!.normalizedValue, "muster gmbh");
|
||||
});
|
||||
|
||||
test("email normalization strips whitespace, lowercases, and rejects malformed addresses", () => {
|
||||
assert.equal(normalizeEmailAddress(" INFO@Example.DE "), "info@example.de");
|
||||
assert.equal(normalizeEmailAddress("hello@domain"), null);
|
||||
assert.equal(normalizeEmailAddress("no-at-symbol"), null);
|
||||
assert.equal(normalizeEmailAddress("@missing-local.com"), null);
|
||||
assert.equal(normalizeEmailAddress("name@"), null);
|
||||
assert.equal(normalizeEmailAddress(""), null);
|
||||
assert.equal(normalizeEmailAddress("näm@beispiel.de"), null);
|
||||
});
|
||||
|
||||
test("usable email helper prefers generic business aliases and requires explicit metadata for named contacts", () => {
|
||||
const genericPreferred = getUsableContactEmail({
|
||||
placeId: "place-1",
|
||||
businessName: "Bäckerei",
|
||||
address: "Musterweg 1",
|
||||
websiteUrl: null,
|
||||
websiteDomain: null,
|
||||
phone: null,
|
||||
rating: null,
|
||||
userRatingCount: null,
|
||||
businessStatus: null,
|
||||
googleTypes: [],
|
||||
googlePrimaryType: null,
|
||||
googleMapsUrl: null,
|
||||
sourceProvider: "google_places",
|
||||
sourceFetchedAt: 0,
|
||||
contactEmails: [
|
||||
{
|
||||
email: "müller@bäckerei.de",
|
||||
isBusinessContactAddress: false,
|
||||
},
|
||||
{
|
||||
email: "Hello@Bäckerei.De",
|
||||
isBusinessContactAddress: false,
|
||||
},
|
||||
{
|
||||
email: "owner@Bäckerei.De",
|
||||
isBusinessContactAddress: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
assert.deepEqual(genericPreferred, {
|
||||
email: "hello@bäckerei.de",
|
||||
emailSource: null,
|
||||
contactPerson: null,
|
||||
});
|
||||
|
||||
const namedWithoutMetadata = getUsableContactEmail({
|
||||
placeId: "place-2",
|
||||
businessName: "Bäckerei",
|
||||
address: "Musterweg 2",
|
||||
websiteUrl: null,
|
||||
websiteDomain: null,
|
||||
phone: null,
|
||||
rating: null,
|
||||
userRatingCount: null,
|
||||
businessStatus: null,
|
||||
googleTypes: [],
|
||||
googlePrimaryType: null,
|
||||
googleMapsUrl: null,
|
||||
sourceProvider: "google_places",
|
||||
sourceFetchedAt: 0,
|
||||
contactEmails: [
|
||||
{
|
||||
email: "owner@Bäckerei.De",
|
||||
isBusinessContactAddress: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
assert.equal(namedWithoutMetadata, null);
|
||||
|
||||
const namedWithMetadata = getUsableContactEmail({
|
||||
placeId: "place-3",
|
||||
businessName: "Bäckerei",
|
||||
address: "Musterweg 3",
|
||||
websiteUrl: null,
|
||||
websiteDomain: null,
|
||||
phone: null,
|
||||
rating: null,
|
||||
userRatingCount: null,
|
||||
businessStatus: null,
|
||||
googleTypes: [],
|
||||
googlePrimaryType: null,
|
||||
googleMapsUrl: null,
|
||||
sourceProvider: "google_places",
|
||||
sourceFetchedAt: 0,
|
||||
contactEmails: [
|
||||
{
|
||||
email: "owner@Bäckerei.De",
|
||||
isBusinessContactAddress: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
assert.deepEqual(namedWithMetadata, {
|
||||
email: "owner@bäckerei.de",
|
||||
emailSource: null,
|
||||
contactPerson: null,
|
||||
});
|
||||
});
|
||||
|
||||
test("standalone contact-email rule helper rejects invalid entries and prefers generic aliases", () => {
|
||||
const validGeneric = getUsableContactEmailFromEntries([
|
||||
{
|
||||
email: "owner@firma.de",
|
||||
isBusinessContactAddress: false,
|
||||
},
|
||||
{
|
||||
email: "support@firma.de",
|
||||
isBusinessContactAddress: false,
|
||||
},
|
||||
{
|
||||
email: "hello@firma.de",
|
||||
isBusinessContactAddress: false,
|
||||
},
|
||||
]);
|
||||
|
||||
assert.deepEqual(validGeneric, {
|
||||
email: "hello@firma.de",
|
||||
emailSource: null,
|
||||
contactPerson: null,
|
||||
});
|
||||
|
||||
const rejectedNamed = getUsableContactEmailFromEntries([
|
||||
{
|
||||
email: "owner@firma.de",
|
||||
isBusinessContactAddress: false,
|
||||
},
|
||||
]);
|
||||
|
||||
assert.equal(rejectedNamed, null);
|
||||
|
||||
const invalid = getUsableContactEmailFromEntries([
|
||||
{
|
||||
email: "no-at-symbol",
|
||||
isBusinessContactAddress: true,
|
||||
},
|
||||
]);
|
||||
|
||||
assert.equal(invalid, null);
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
canStartAgentRun,
|
||||
isStalePendingAgentRun,
|
||||
getLeadDiscoveryContactStatus,
|
||||
getLeadDiscoveryPriority,
|
||||
} from "../lib/lead-discovery-run";
|
||||
|
||||
test("agent run guard ignores stale pending runs but blocks active runs", () => {
|
||||
@@ -62,19 +63,51 @@ test("lead discovery counters preserve audit and outreach counters", () => {
|
||||
|
||||
test("lead discovery contact status separates leads without any contact route", () => {
|
||||
assert.equal(
|
||||
getLeadDiscoveryContactStatus({ websiteDomain: null, phone: null }),
|
||||
getLeadDiscoveryContactStatus({ usableEmail: null }),
|
||||
"missing_contact",
|
||||
);
|
||||
assert.equal(
|
||||
getLeadDiscoveryContactStatus({ websiteDomain: "example.de", phone: null }),
|
||||
getLeadDiscoveryContactStatus({ usableEmail: "info@example.de" }),
|
||||
"new",
|
||||
);
|
||||
assert.equal(
|
||||
getLeadDiscoveryContactStatus({ websiteDomain: null, phone: "030 123" }),
|
||||
"new",
|
||||
getLeadDiscoveryContactStatus({ usableEmail: null }),
|
||||
"missing_contact",
|
||||
);
|
||||
});
|
||||
|
||||
test("lead discovery lead record marks contact missing when no usable email exists", () => {
|
||||
const record = buildLeadDiscoveryLeadRecord({
|
||||
campaignId: "campaign-1",
|
||||
runId: "run-1",
|
||||
niche: "Restaurant",
|
||||
postalCode: "10115",
|
||||
now: 1717480000000,
|
||||
candidate: {
|
||||
placeId: "place-2",
|
||||
businessName: "Kontaktlos GmbH",
|
||||
address: "Hauptstraße 2",
|
||||
websiteUrl: "https://www.beispiel.de",
|
||||
websiteDomain: "example.de",
|
||||
phone: "+49 30 123",
|
||||
rating: 3.9,
|
||||
userRatingCount: 9,
|
||||
businessStatus: "OPERATIONAL",
|
||||
googleTypes: ["consulting"],
|
||||
googlePrimaryType: "consulting",
|
||||
googleMapsUrl: "https://maps.google.com/place-2",
|
||||
sourceProvider: "google_places",
|
||||
sourceFetchedAt: 1717480001000,
|
||||
contactEmails: [{ email: "Herr.Bewerber@Beispiel.de", isBusinessContactAddress: false }],
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(record.contactStatus, "missing_contact");
|
||||
assert.equal(record.phone, "+49 30 123");
|
||||
assert.equal(record.websiteDomain, "example.de");
|
||||
assert.equal(record.email, undefined);
|
||||
});
|
||||
|
||||
test("lead discovery lead record keeps raw website url and normalized domain", () => {
|
||||
const record = buildLeadDiscoveryLeadRecord({
|
||||
campaignId: "campaign-1",
|
||||
@@ -106,3 +139,113 @@ test("lead discovery lead record keeps raw website url and normalized domain", (
|
||||
assert.equal(record.googleUserRatingCount, 12);
|
||||
assert.equal(record.sourceFetchedAt, 1717480001000);
|
||||
});
|
||||
|
||||
test("lead discovery lead record stores valid email and sets contactStatus to new", () => {
|
||||
const record = buildLeadDiscoveryLeadRecord({
|
||||
campaignId: "campaign-1",
|
||||
runId: "run-1",
|
||||
niche: "Restaurant",
|
||||
postalCode: "10115",
|
||||
now: 1717480000000,
|
||||
candidate: {
|
||||
placeId: "place-3",
|
||||
businessName: "Beispiel GmbH",
|
||||
address: "Hauptstraße 1",
|
||||
websiteUrl: "https://www.example.de/path",
|
||||
websiteDomain: "example.de",
|
||||
phone: "+49 30 123",
|
||||
rating: 4.5,
|
||||
userRatingCount: 12,
|
||||
businessStatus: "OPERATIONAL",
|
||||
googleTypes: ["restaurant"],
|
||||
googlePrimaryType: "restaurant",
|
||||
googleMapsUrl: "https://maps.google.com/place-3",
|
||||
sourceProvider: "google_places",
|
||||
sourceFetchedAt: 1717480001000,
|
||||
contactEmails: [
|
||||
{
|
||||
email: "Herr@Beispiel.de",
|
||||
isBusinessContactAddress: false,
|
||||
},
|
||||
{
|
||||
email: "info@beispiel.de",
|
||||
isBusinessContactAddress: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(record.contactStatus, "new");
|
||||
assert.equal(record.email, "info@beispiel.de");
|
||||
assert.equal(record.contactPerson, undefined);
|
||||
});
|
||||
|
||||
test("lead discovery lead record stores normalized matching fields", () => {
|
||||
const record = buildLeadDiscoveryLeadRecord({
|
||||
campaignId: "campaign-1",
|
||||
runId: "run-1",
|
||||
niche: "Restaurant",
|
||||
postalCode: "10115",
|
||||
now: 1717480000000,
|
||||
candidate: {
|
||||
placeId: "place-4",
|
||||
businessName: "Muster GmbH",
|
||||
address: "Hauptstraße 1 60311 Berlin",
|
||||
websiteUrl: "https://www.example.de/",
|
||||
websiteDomain: "Example.de",
|
||||
phone: "+49 30 123 456",
|
||||
rating: 4.5,
|
||||
userRatingCount: 12,
|
||||
businessStatus: "OPERATIONAL",
|
||||
googleTypes: ["restaurant"],
|
||||
googlePrimaryType: "restaurant",
|
||||
googleMapsUrl: "https://maps.google.com/place-4",
|
||||
sourceProvider: "google_places",
|
||||
sourceFetchedAt: 1717480001000,
|
||||
email: "Info@Example.de",
|
||||
contactEmails: [
|
||||
{
|
||||
email: "Info@Example.de",
|
||||
isBusinessContactAddress: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(record.normalizedEmail, "info@example.de");
|
||||
assert.equal(record.normalizedPhone, "4930123456");
|
||||
assert.equal(record.normalizedCompanyName, "muster gmbh");
|
||||
assert.equal(record.normalizedAddress, "hauptstraße 1 60311 berlin");
|
||||
});
|
||||
|
||||
test("lead discovery priority helper classifies blocked, deferred, and low-potential leads", () => {
|
||||
assert.deepEqual(getLeadDiscoveryPriority({ isBlacklisted: true }), {
|
||||
priority: "blocked",
|
||||
reason: "Lead ist auf der Sperrliste.",
|
||||
});
|
||||
|
||||
assert.deepEqual(getLeadDiscoveryPriority({ isDuplicate: true }), {
|
||||
priority: "defer",
|
||||
reason: "Dublettenprüfung oder Reviewpause.",
|
||||
});
|
||||
|
||||
assert.deepEqual(getLeadDiscoveryPriority({ hasWebsite: false }), {
|
||||
priority: "high",
|
||||
reason: "Kein Website-Indikator vorhanden.",
|
||||
});
|
||||
|
||||
assert.deepEqual(getLeadDiscoveryPriority({ hasWebsite: true, hasWebsiteSignal: true }), {
|
||||
priority: "low",
|
||||
reason: "Website vorhanden: geringer Kontaktaufwand.",
|
||||
});
|
||||
|
||||
assert.deepEqual(getLeadDiscoveryPriority({ hasWebsite: true, hasWebsiteSignal: false }), {
|
||||
priority: "medium",
|
||||
reason: "Standardpriorität.",
|
||||
});
|
||||
|
||||
assert.deepEqual(getLeadDiscoveryPriority({ hasWebsite: true }), {
|
||||
priority: "medium",
|
||||
reason: "Standardpriorität.",
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user