feat: integrate google lead discovery
This commit is contained in:
334
tests/lead-discovery-google.test.ts
Normal file
334
tests/lead-discovery-google.test.ts
Normal file
@@ -0,0 +1,334 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
GOOGLE_PLACES_FIELD_MASK,
|
||||
buildGeocodingUrl,
|
||||
getBlacklistMatches,
|
||||
getBlacklistLookupValues,
|
||||
getPlacesSearchSpec,
|
||||
isDuplicateCandidate,
|
||||
normalizePlacesResponse,
|
||||
parseGeocodingResponse,
|
||||
} from "../lib/lead-discovery-google";
|
||||
|
||||
test("places search spec maps known presets to nearby search and converts radius to meters", () => {
|
||||
const nearbySpec = getPlacesSearchSpec({
|
||||
categoryMode: "preset",
|
||||
category: "Anwalt",
|
||||
postalCode: "10115",
|
||||
latitude: 52.52,
|
||||
longitude: 13.405,
|
||||
radiusKm: 12,
|
||||
});
|
||||
|
||||
assert.equal(nearbySpec.searchType, "nearby");
|
||||
assert.equal(nearbySpec.endpoint, "searchNearby");
|
||||
if (nearbySpec.searchType !== "nearby" || nearbySpec.endpoint !== "searchNearby") {
|
||||
throw new Error("Expected nearby search spec for preset category.");
|
||||
}
|
||||
|
||||
const nearbyBody = nearbySpec.body as {
|
||||
includedTypes: string[];
|
||||
locationRestriction: {
|
||||
circle: {
|
||||
center: {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
};
|
||||
radius: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
assert.deepEqual(nearbyBody.includedTypes, ["lawyer"]);
|
||||
assert.equal(
|
||||
nearbyBody.locationRestriction.circle.radius,
|
||||
12_000,
|
||||
);
|
||||
assert.equal(nearbyBody.locationRestriction.circle.center.latitude, 52.52);
|
||||
assert.equal(nearbyBody.locationRestriction.circle.center.longitude, 13.405);
|
||||
});
|
||||
|
||||
test("places search spec uses text search for custom/Anderes and includes query context", () => {
|
||||
const customSpec = getPlacesSearchSpec({
|
||||
categoryMode: "custom",
|
||||
category: "Anderes",
|
||||
customSearchTerm: "Barber Shop für Hunde",
|
||||
postalCode: "80331",
|
||||
latitude: 48.137,
|
||||
longitude: 11.575,
|
||||
radiusKm: 5,
|
||||
});
|
||||
|
||||
assert.equal(customSpec.searchType, "text");
|
||||
assert.equal(customSpec.endpoint, "searchText");
|
||||
if (customSpec.searchType !== "text" || customSpec.endpoint !== "searchText") {
|
||||
throw new Error("Expected text search spec for custom/Anderes.");
|
||||
}
|
||||
const customBody = customSpec.body as {
|
||||
textQuery: string;
|
||||
locationBias?: {
|
||||
circle: {
|
||||
center: { latitude: number; longitude: number };
|
||||
radius: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
assert.equal(
|
||||
customBody.textQuery,
|
||||
"Barber Shop für Hunde in 80331 Deutschland",
|
||||
);
|
||||
assert.deepEqual(customBody.locationBias, {
|
||||
circle: {
|
||||
center: { latitude: 48.137, longitude: 11.575 },
|
||||
radius: 5_000,
|
||||
},
|
||||
});
|
||||
|
||||
const handwerkSpec = getPlacesSearchSpec({
|
||||
categoryMode: "preset",
|
||||
category: "Handwerk",
|
||||
customSearchTerm: "ignored",
|
||||
postalCode: "80331",
|
||||
radiusKm: 5,
|
||||
});
|
||||
|
||||
assert.equal(handwerkSpec.searchType, "text");
|
||||
assert.equal(handwerkSpec.endpoint, "searchText");
|
||||
if (handwerkSpec.searchType !== "text" || handwerkSpec.endpoint !== "searchText") {
|
||||
throw new Error("Expected text search spec for unmapped preset category.");
|
||||
}
|
||||
const handwerkBody = handwerkSpec.body as { textQuery: string };
|
||||
assert.equal(handwerkBody.textQuery, "Handwerk in 80331 Deutschland");
|
||||
});
|
||||
|
||||
test("geocoding URL includes API key, DE region, and components filter", () => {
|
||||
const url = new URL(
|
||||
buildGeocodingUrl({ postalCode: "40210", apiKey: "geocode-key-123" }),
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
url.origin + url.pathname,
|
||||
"https://maps.googleapis.com/maps/api/geocode/json",
|
||||
);
|
||||
assert.equal(url.searchParams.get("address"), "40210, Deutschland");
|
||||
assert.equal(url.searchParams.get("components"), "country:DE|postal_code:40210");
|
||||
assert.equal(url.searchParams.get("language"), "de");
|
||||
assert.equal(url.searchParams.get("region"), "de");
|
||||
assert.equal(url.searchParams.get("key"), "geocode-key-123");
|
||||
});
|
||||
|
||||
test("geocoding parser extracts location from OK response and rejects ZERO_RESULTS", () => {
|
||||
const ok = parseGeocodingResponse(
|
||||
{
|
||||
status: "OK",
|
||||
results: [
|
||||
{
|
||||
formatted_address: "Berlin, 10115 Berlin, Deutschland",
|
||||
place_id: "place-id-1",
|
||||
geometry: {
|
||||
location: {
|
||||
lat: 52.5170365,
|
||||
lng: 13.3888599,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
1717480000000,
|
||||
);
|
||||
|
||||
assert.equal(ok.latitude, 52.5170365);
|
||||
assert.equal(ok.longitude, 13.3888599);
|
||||
assert.equal(ok.formattedAddress, "Berlin, 10115 Berlin, Deutschland");
|
||||
assert.equal(ok.placeId, "place-id-1");
|
||||
assert.equal(ok.fetchedAt, 1717480000000);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
parseGeocodingResponse(
|
||||
{ status: "ZERO_RESULTS", results: [] },
|
||||
1717480000123,
|
||||
),
|
||||
(error: unknown) =>
|
||||
error instanceof Error &&
|
||||
/ZERO_RESULTS|no geocoding results/i.test(error.message),
|
||||
);
|
||||
});
|
||||
|
||||
test("places normalization maps source metadata and normalizes website domain", () => {
|
||||
const normalized = normalizePlacesResponse(
|
||||
{
|
||||
places: [
|
||||
{
|
||||
id: "place-1",
|
||||
displayName: { text: "Beispiel Café" },
|
||||
formattedAddress: "Hauptstraße 1, 60311 Frankfurt am Main, Deutschland",
|
||||
websiteUri: "https://www.Example.De/some-path",
|
||||
nationalPhoneNumber: "+49 30 123456",
|
||||
internationalPhoneNumber: "+49 49 654321",
|
||||
rating: 4.6,
|
||||
userRatingCount: 42,
|
||||
businessStatus: "OPERATIONAL",
|
||||
types: ["restaurant", "cafe"],
|
||||
primaryType: "restaurant",
|
||||
googleMapsUri: "https://maps.google.com/place-id-1",
|
||||
},
|
||||
],
|
||||
},
|
||||
1717480001000,
|
||||
);
|
||||
|
||||
assert.equal(normalized.length, 1);
|
||||
assert.deepEqual(normalized[0], {
|
||||
placeId: "place-1",
|
||||
businessName: "Beispiel Café",
|
||||
address: "Hauptstraße 1, 60311 Frankfurt am Main, Deutschland",
|
||||
websiteUrl: "https://www.Example.De/some-path",
|
||||
websiteDomain: "example.de",
|
||||
phone: "+49 30 123456",
|
||||
rating: 4.6,
|
||||
userRatingCount: 42,
|
||||
businessStatus: "OPERATIONAL",
|
||||
googleTypes: ["restaurant", "cafe"],
|
||||
googlePrimaryType: "restaurant",
|
||||
googleMapsUrl: "https://maps.google.com/place-id-1",
|
||||
sourceProvider: "google_places",
|
||||
sourceFetchedAt: 1717480001000,
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
GOOGLE_PLACES_FIELD_MASK,
|
||||
"places.id,places.displayName,places.formattedAddress,places.websiteUri,places.nationalPhoneNumber,places.internationalPhoneNumber,places.rating,places.userRatingCount,places.businessStatus,places.types,places.primaryType,places.googleMapsUri",
|
||||
);
|
||||
});
|
||||
|
||||
test("duplicate detection uses placeId and websiteDomain", () => {
|
||||
const existingLeads = [
|
||||
{ googlePlaceId: "dup-1", websiteDomain: "other.de" },
|
||||
{ googlePlaceId: "other-2", websiteDomain: "example.de" },
|
||||
];
|
||||
|
||||
assert.equal(
|
||||
isDuplicateCandidate(
|
||||
{
|
||||
placeId: "dup-1",
|
||||
businessName: "Test",
|
||||
address: "A",
|
||||
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(
|
||||
isDuplicateCandidate(
|
||||
{
|
||||
placeId: "none",
|
||||
businessName: "Test",
|
||||
address: "A",
|
||||
websiteUrl: "https://www.example.de",
|
||||
websiteDomain: "example.de",
|
||||
phone: null,
|
||||
rating: null,
|
||||
userRatingCount: null,
|
||||
businessStatus: null,
|
||||
googleTypes: [],
|
||||
googlePrimaryType: null,
|
||||
googleMapsUrl: null,
|
||||
sourceProvider: "google_places",
|
||||
sourceFetchedAt: 0,
|
||||
},
|
||||
existingLeads,
|
||||
),
|
||||
true,
|
||||
);
|
||||
|
||||
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,
|
||||
},
|
||||
existingLeads,
|
||||
),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("blacklist matches include google_place_id, domain, company and phone", () => {
|
||||
const candidate = {
|
||||
placeId: "place-blacklisted",
|
||||
businessName: "Muster GmbH",
|
||||
address: "A",
|
||||
websiteUrl: "https://www.Blocked.de",
|
||||
websiteDomain: "blocked.de",
|
||||
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-blacklisted" },
|
||||
{ type: "domain", normalizedValue: "blocked.de" },
|
||||
{ type: "company", normalizedValue: "muster gmbh" },
|
||||
{ type: "phone", normalizedValue: "4930555123" },
|
||||
{ type: "phone", normalizedValue: "+49 30 555 123" },
|
||||
]);
|
||||
|
||||
const matches = getBlacklistMatches(
|
||||
candidate,
|
||||
[
|
||||
{
|
||||
type: "google_place_id",
|
||||
value: "place-blacklisted",
|
||||
normalizedValue: "place-blacklisted",
|
||||
},
|
||||
{ type: "domain", value: "blocked.de", normalizedValue: "blocked.de" },
|
||||
{ type: "company", value: "Muster GmbH", normalizedValue: "muster gmbh" },
|
||||
{ type: "phone", value: "+49 30 555 123", normalizedValue: "4930555123" },
|
||||
{
|
||||
type: "phone",
|
||||
value: "+49 30 555 123",
|
||||
normalizedValue: "+49 30 555 123",
|
||||
},
|
||||
{ type: "email", value: "x@example.de", normalizedValue: "x@example.de" },
|
||||
{ type: "phone", value: "+49 30 999 999", normalizedValue: "4930999999" },
|
||||
],
|
||||
);
|
||||
|
||||
const matchTypes = matches.map((match) => match.type).sort();
|
||||
assert.deepEqual(
|
||||
matchTypes,
|
||||
["company", "domain", "google_place_id", "phone", "phone"].sort(),
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user