indiecrm-cli 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +447 -0
- package/CODE_OF_CONDUCT.md +35 -0
- package/CONTRIBUTING.md +7 -0
- package/README.md +14 -2
- package/RESEARCH.md +346 -0
- package/SECURITY.md +35 -0
- package/dist/affiliate-copy.js +49 -0
- package/dist/auth.js +453 -0
- package/dist/bigquery.js +199 -0
- package/dist/chrome-browser.js +231 -0
- package/dist/cli.js +19652 -0
- package/dist/company-identity-review.js +78 -0
- package/dist/company-leads.js +422 -0
- package/dist/company-recovery.js +84 -0
- package/dist/deel-outreach.js +469 -0
- package/dist/deel-salesnav.js +368 -0
- package/dist/direct-path.js +326 -0
- package/dist/domain.js +53 -0
- package/dist/domainfinder.js +764 -0
- package/dist/engine.js +216 -0
- package/dist/historical-queries.js +189 -0
- package/dist/hunter-emailfinder.js +252 -0
- package/dist/icp-templates.js +171 -0
- package/dist/indiecrm/commands.js +108 -0
- package/dist/indiecrm-cli.js +2 -104
- package/dist/instantly.js +136 -0
- package/dist/io.js +21 -0
- package/dist/leadlists-funnel.js +148 -0
- package/dist/linkedin-companies.js +562 -0
- package/dist/linkedin-product-details.js +1203 -0
- package/dist/linkedin-product-search.js +1081 -0
- package/dist/linkedin-products.js +786 -0
- package/dist/linkedin-session-contracts.js +3 -0
- package/dist/linkedin-session.js +846 -0
- package/dist/providers.js +1 -0
- package/dist/research-browser-preference.js +37 -0
- package/dist/sales-navigator.js +1231 -0
- package/dist/salesnav-backfill.js +710 -0
- package/dist/sample-data.js +34 -0
- package/dist/session-recovery.js +62 -0
- package/dist/vendor/salesprompter-shared/extension-session-contracts.js +29 -0
- package/dist/vendor/salesprompter-shared/linkedin-session.js +22 -0
- package/dist/vendor/salesprompter-shared/phantombuster-contracts.js +16 -0
- package/dist/vendor/salesprompter-shared/session-vault-contracts.js +17 -0
- package/package.json +73 -14
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
const COMPANY_SUFFIX_PATTERNS = [
|
|
2
|
+
/\s+GmbH\s*&\s*Co\.?\s+KG\.?$/i,
|
|
3
|
+
/\s+GmbH\.?$/i,
|
|
4
|
+
/\s+AG\.?$/i,
|
|
5
|
+
/\s+SE\.?$/i,
|
|
6
|
+
/\s+KG\.?$/i,
|
|
7
|
+
/\s+OHG\.?$/i,
|
|
8
|
+
/\s+GbR\.?$/i,
|
|
9
|
+
/\s+S\.A\.?$/i,
|
|
10
|
+
/\s+SA\.?$/i,
|
|
11
|
+
/\s+SARL\.?$/i,
|
|
12
|
+
/\s+LLC\.?$/i,
|
|
13
|
+
/\s+Inc\.?$/i,
|
|
14
|
+
/\s+Corp\.?$/i,
|
|
15
|
+
/\s+Ltd\.?$/i,
|
|
16
|
+
/\s+Group$/i
|
|
17
|
+
];
|
|
18
|
+
const DACH_SIGNAL_MATCHERS = [
|
|
19
|
+
{ label: "Germany", pattern: /(^|[^a-z])(germany|deutschland|bundesrepublik)([^a-z]|$)/i },
|
|
20
|
+
{ label: "Austria", pattern: /(^|[^a-z])(austria|österreich|oesterreich)([^a-z]|$)/i },
|
|
21
|
+
{ label: "Switzerland", pattern: /(^|[^a-z])(switzerland|schweiz|suisse|svizzera)([^a-z]|$)/i },
|
|
22
|
+
{ label: "Berlin", pattern: /(^|[^a-z])berlin([^a-z]|$)/i },
|
|
23
|
+
{ label: "Hamburg", pattern: /(^|[^a-z])hamburg([^a-z]|$)/i },
|
|
24
|
+
{ label: "Munich", pattern: /(^|[^a-z])(munich|münchen|muenchen)([^a-z]|$)/i },
|
|
25
|
+
{ label: "Frankfurt", pattern: /(^|[^a-z])frankfurt([^a-z]|$)/i },
|
|
26
|
+
{ label: "Cologne", pattern: /(^|[^a-z])(cologne|köln|koeln)([^a-z]|$)/i },
|
|
27
|
+
{ label: "Stuttgart", pattern: /(^|[^a-z])stuttgart([^a-z]|$)/i },
|
|
28
|
+
{ label: "Düsseldorf", pattern: /(^|[^a-z])(düsseldorf|duesseldorf)([^a-z]|$)/i },
|
|
29
|
+
{ label: "Vienna", pattern: /(^|[^a-z])(vienna|wien)([^a-z]|$)/i },
|
|
30
|
+
{ label: "Graz", pattern: /(^|[^a-z])graz([^a-z]|$)/i },
|
|
31
|
+
{ label: "Linz", pattern: /(^|[^a-z])linz([^a-z]|$)/i },
|
|
32
|
+
{ label: "Salzburg", pattern: /(^|[^a-z])salzburg([^a-z]|$)/i },
|
|
33
|
+
{ label: "Zurich", pattern: /(^|[^a-z])(zurich|zürich|zuerich)([^a-z]|$)/i },
|
|
34
|
+
{ label: "Geneva", pattern: /(^|[^a-z])(geneva|genf)([^a-z]|$)/i },
|
|
35
|
+
{ label: "Basel", pattern: /(^|[^a-z])basel([^a-z]|$)/i },
|
|
36
|
+
{ label: "Bern", pattern: /(^|[^a-z])bern([^a-z]|$)/i },
|
|
37
|
+
{ label: "Lausanne", pattern: /(^|[^a-z])lausanne([^a-z]|$)/i },
|
|
38
|
+
{ label: "Zug", pattern: /(^|[^a-z])zug([^a-z]|$)/i }
|
|
39
|
+
];
|
|
40
|
+
const DEEL_RELEVANT_TITLE_MATCHERS = [
|
|
41
|
+
/\bhead of (hr|human resources|people|people operations|people ops|people services|payroll|total rewards)\b/i,
|
|
42
|
+
/\b(head of compensation|head of benefits|head of people and culture)\b/i,
|
|
43
|
+
/\b(chief people officer|chro)\b/i,
|
|
44
|
+
/\b(vp|vice president)\b.*\b(hr|human resources|people|payroll|total rewards|compensation|benefits)\b/i,
|
|
45
|
+
/\bdirector\b.*\b(hr|human resources|people|payroll|total rewards|compensation|benefits|global mobility)\b/i,
|
|
46
|
+
/\b(hr|human resources|people|payroll)\b.*\bdirector\b/i,
|
|
47
|
+
/\b(hr business partner|hrbp)\b/i,
|
|
48
|
+
/\b(people operations manager|people ops manager|hr operations manager|payroll manager|benefits manager|compensation manager|global mobility manager)\b/i,
|
|
49
|
+
/\b(global payroll|global mobility|compensation and benefits)\b/i,
|
|
50
|
+
/\b(leiter personal|leiter hr|leiter human resources|personalleitung|personalleitung|bereichsleiter personal)\b/i
|
|
51
|
+
];
|
|
52
|
+
const DEEL_TITLE_EXCLUSION_MATCHERS = [
|
|
53
|
+
/\bassistant\b/i,
|
|
54
|
+
/\bassistent/i,
|
|
55
|
+
/\brecruit/i,
|
|
56
|
+
/\btalent acquisition\b/i,
|
|
57
|
+
/\bhead chef\b/i,
|
|
58
|
+
/\bceo\b/i,
|
|
59
|
+
/\bchief executive officer\b/i,
|
|
60
|
+
/\bfounder\b/i,
|
|
61
|
+
/\bco-founder\b/i,
|
|
62
|
+
/\bpresident\b/i,
|
|
63
|
+
/\bhead of sales\b/i,
|
|
64
|
+
/\bmarketing\b/i
|
|
65
|
+
];
|
|
66
|
+
function normalizeNullableString(value) {
|
|
67
|
+
const trimmed = value?.trim() ?? "";
|
|
68
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
69
|
+
}
|
|
70
|
+
export function cleanDeelCompanyName(value) {
|
|
71
|
+
let next = normalizeNullableString(value);
|
|
72
|
+
if (!next) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
for (const pattern of COMPANY_SUFFIX_PATTERNS) {
|
|
76
|
+
next = next.replace(pattern, "").trim();
|
|
77
|
+
}
|
|
78
|
+
return next.length > 0 ? next : normalizeNullableString(value);
|
|
79
|
+
}
|
|
80
|
+
export function isDeelRelevantSalesNavTitle(value) {
|
|
81
|
+
const normalized = normalizeNullableString(value);
|
|
82
|
+
if (!normalized) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
if (DEEL_TITLE_EXCLUSION_MATCHERS.some((pattern) => pattern.test(normalized))) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
return DEEL_RELEVANT_TITLE_MATCHERS.some((pattern) => pattern.test(normalized));
|
|
89
|
+
}
|
|
90
|
+
export function extractLinkedInCompanyHandleFromUrl(value) {
|
|
91
|
+
const normalized = normalizeNullableString(value);
|
|
92
|
+
if (!normalized) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
const url = new URL(normalized);
|
|
97
|
+
if (!/(^|\.)linkedin\.com$/i.test(url.hostname)) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
const segments = url.pathname.split("/").filter((segment) => segment.length > 0);
|
|
101
|
+
const companyIndex = segments.findIndex((segment) => segment.toLowerCase() === "company");
|
|
102
|
+
const handle = companyIndex >= 0 ? segments[companyIndex + 1] ?? "" : "";
|
|
103
|
+
return handle.trim().length > 0 ? handle.trim().toLowerCase() : null;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function preferredProfileUrl(row) {
|
|
110
|
+
return (normalizeNullableString(row.linkedin_profile_url) ??
|
|
111
|
+
normalizeNullableString(row.default_profile_url) ??
|
|
112
|
+
row.sales_nav_profile_url);
|
|
113
|
+
}
|
|
114
|
+
function collectDachSignals(value) {
|
|
115
|
+
if (!value) {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
const matches = [];
|
|
119
|
+
for (const matcher of DACH_SIGNAL_MATCHERS) {
|
|
120
|
+
if (matcher.pattern.test(value)) {
|
|
121
|
+
matches.push(matcher.label);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return matches;
|
|
125
|
+
}
|
|
126
|
+
export function classifyDeelSalesNavLanguage(row) {
|
|
127
|
+
const signalSources = [
|
|
128
|
+
{ field: "location", value: normalizeNullableString(row.location) },
|
|
129
|
+
{ field: "companyLocation", value: normalizeNullableString(row.company_location) },
|
|
130
|
+
{ field: "searchQuery", value: normalizeNullableString(row.search_query) }
|
|
131
|
+
];
|
|
132
|
+
const languageSignals = [];
|
|
133
|
+
const signalFields = [];
|
|
134
|
+
for (const source of signalSources) {
|
|
135
|
+
const sourceSignals = collectDachSignals(source.value);
|
|
136
|
+
if (sourceSignals.length > 0) {
|
|
137
|
+
languageSignals.push(...sourceSignals);
|
|
138
|
+
signalFields.push(source.field);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const uniqueSignals = [...new Set(languageSignals)];
|
|
142
|
+
const uniqueFields = [...new Set(signalFields)];
|
|
143
|
+
if (uniqueSignals.length > 0) {
|
|
144
|
+
return {
|
|
145
|
+
language: "de",
|
|
146
|
+
marketSegment: "dach",
|
|
147
|
+
languageReason: `clear_dach_signal:${uniqueSignals.join("|")}`,
|
|
148
|
+
languageSignals: uniqueSignals,
|
|
149
|
+
signalFields: uniqueFields
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
language: "en",
|
|
154
|
+
marketSegment: "non-dach",
|
|
155
|
+
languageReason: "fallback_non_dach",
|
|
156
|
+
languageSignals: [],
|
|
157
|
+
signalFields: []
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
export function normalizeDeelSalesNavRow(row) {
|
|
161
|
+
const classification = classifyDeelSalesNavLanguage(row);
|
|
162
|
+
return {
|
|
163
|
+
id: row.id,
|
|
164
|
+
orgId: row.org_id,
|
|
165
|
+
runId: normalizeNullableString(row.run_id),
|
|
166
|
+
language: classification.language,
|
|
167
|
+
marketSegment: classification.marketSegment,
|
|
168
|
+
languageReason: classification.languageReason,
|
|
169
|
+
languageSignals: classification.languageSignals,
|
|
170
|
+
signalFields: classification.signalFields,
|
|
171
|
+
fullName: normalizeNullableString(row.full_name),
|
|
172
|
+
firstName: normalizeNullableString(row.first_name),
|
|
173
|
+
lastName: normalizeNullableString(row.last_name),
|
|
174
|
+
title: normalizeNullableString(row.title),
|
|
175
|
+
industry: normalizeNullableString(row.industry),
|
|
176
|
+
companyName: normalizeNullableString(row.company_name),
|
|
177
|
+
companyNameCleaned: cleanDeelCompanyName(row.company_name),
|
|
178
|
+
location: normalizeNullableString(row.location),
|
|
179
|
+
companyLocation: normalizeNullableString(row.company_location),
|
|
180
|
+
preferredProfileUrl: preferredProfileUrl(row),
|
|
181
|
+
linkedinProfileUrl: normalizeNullableString(row.linkedin_profile_url),
|
|
182
|
+
defaultProfileUrl: normalizeNullableString(row.default_profile_url),
|
|
183
|
+
salesNavProfileUrl: row.sales_nav_profile_url,
|
|
184
|
+
companyUrl: normalizeNullableString(row.company_url),
|
|
185
|
+
regularCompanyUrl: normalizeNullableString(row.regular_company_url),
|
|
186
|
+
companyLinkedInHandle: extractLinkedInCompanyHandleFromUrl(row.company_url) ??
|
|
187
|
+
extractLinkedInCompanyHandleFromUrl(row.regular_company_url),
|
|
188
|
+
searchQuery: normalizeNullableString(row.search_query),
|
|
189
|
+
scrapedAt: normalizeNullableString(row.scraped_at)
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function percentage(count, total) {
|
|
193
|
+
if (total <= 0) {
|
|
194
|
+
return 0;
|
|
195
|
+
}
|
|
196
|
+
return Number(((count / total) * 100).toFixed(2));
|
|
197
|
+
}
|
|
198
|
+
export function buildDeelSalesNavPack(orgId, rows) {
|
|
199
|
+
const locales = { de: [], en: [] };
|
|
200
|
+
const titleCounts = new Map();
|
|
201
|
+
const signalFieldCounts = {
|
|
202
|
+
location: 0,
|
|
203
|
+
companyLocation: 0,
|
|
204
|
+
searchQuery: 0,
|
|
205
|
+
none: 0
|
|
206
|
+
};
|
|
207
|
+
const fieldCounts = {
|
|
208
|
+
firstName: 0,
|
|
209
|
+
lastName: 0,
|
|
210
|
+
fullName: 0,
|
|
211
|
+
companyName: 0,
|
|
212
|
+
companyNameCleaned: 0,
|
|
213
|
+
preferredProfileUrl: 0,
|
|
214
|
+
linkedinProfileUrl: 0,
|
|
215
|
+
companyLinkedInHandle: 0,
|
|
216
|
+
location: 0,
|
|
217
|
+
companyLocation: 0,
|
|
218
|
+
searchQuery: 0
|
|
219
|
+
};
|
|
220
|
+
for (const row of rows) {
|
|
221
|
+
const prepared = normalizeDeelSalesNavRow(row);
|
|
222
|
+
locales[prepared.language].push(prepared);
|
|
223
|
+
if (prepared.title) {
|
|
224
|
+
titleCounts.set(prepared.title, (titleCounts.get(prepared.title) ?? 0) + 1);
|
|
225
|
+
}
|
|
226
|
+
if (prepared.signalFields.length === 0) {
|
|
227
|
+
signalFieldCounts.none += 1;
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
for (const field of prepared.signalFields) {
|
|
231
|
+
if (field === "location") {
|
|
232
|
+
signalFieldCounts.location += 1;
|
|
233
|
+
}
|
|
234
|
+
else if (field === "companyLocation") {
|
|
235
|
+
signalFieldCounts.companyLocation += 1;
|
|
236
|
+
}
|
|
237
|
+
else if (field === "searchQuery") {
|
|
238
|
+
signalFieldCounts.searchQuery += 1;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (prepared.firstName)
|
|
243
|
+
fieldCounts.firstName += 1;
|
|
244
|
+
if (prepared.lastName)
|
|
245
|
+
fieldCounts.lastName += 1;
|
|
246
|
+
if (prepared.fullName)
|
|
247
|
+
fieldCounts.fullName += 1;
|
|
248
|
+
if (prepared.companyName)
|
|
249
|
+
fieldCounts.companyName += 1;
|
|
250
|
+
if (prepared.companyNameCleaned)
|
|
251
|
+
fieldCounts.companyNameCleaned += 1;
|
|
252
|
+
if (prepared.preferredProfileUrl)
|
|
253
|
+
fieldCounts.preferredProfileUrl += 1;
|
|
254
|
+
if (prepared.linkedinProfileUrl)
|
|
255
|
+
fieldCounts.linkedinProfileUrl += 1;
|
|
256
|
+
if (prepared.companyLinkedInHandle)
|
|
257
|
+
fieldCounts.companyLinkedInHandle += 1;
|
|
258
|
+
if (prepared.location)
|
|
259
|
+
fieldCounts.location += 1;
|
|
260
|
+
if (prepared.companyLocation)
|
|
261
|
+
fieldCounts.companyLocation += 1;
|
|
262
|
+
if (prepared.searchQuery)
|
|
263
|
+
fieldCounts.searchQuery += 1;
|
|
264
|
+
}
|
|
265
|
+
const total = rows.length;
|
|
266
|
+
const localeCounts = {
|
|
267
|
+
de: locales.de.length,
|
|
268
|
+
en: locales.en.length
|
|
269
|
+
};
|
|
270
|
+
const fieldCoverage = {
|
|
271
|
+
firstName: { count: fieldCounts.firstName, percentage: percentage(fieldCounts.firstName, total) },
|
|
272
|
+
lastName: { count: fieldCounts.lastName, percentage: percentage(fieldCounts.lastName, total) },
|
|
273
|
+
fullName: { count: fieldCounts.fullName, percentage: percentage(fieldCounts.fullName, total) },
|
|
274
|
+
companyName: { count: fieldCounts.companyName, percentage: percentage(fieldCounts.companyName, total) },
|
|
275
|
+
companyNameCleaned: { count: fieldCounts.companyNameCleaned, percentage: percentage(fieldCounts.companyNameCleaned, total) },
|
|
276
|
+
preferredProfileUrl: { count: fieldCounts.preferredProfileUrl, percentage: percentage(fieldCounts.preferredProfileUrl, total) },
|
|
277
|
+
linkedinProfileUrl: { count: fieldCounts.linkedinProfileUrl, percentage: percentage(fieldCounts.linkedinProfileUrl, total) },
|
|
278
|
+
companyLinkedInHandle: { count: fieldCounts.companyLinkedInHandle, percentage: percentage(fieldCounts.companyLinkedInHandle, total) },
|
|
279
|
+
location: { count: fieldCounts.location, percentage: percentage(fieldCounts.location, total) },
|
|
280
|
+
companyLocation: { count: fieldCounts.companyLocation, percentage: percentage(fieldCounts.companyLocation, total) },
|
|
281
|
+
searchQuery: { count: fieldCounts.searchQuery, percentage: percentage(fieldCounts.searchQuery, total) }
|
|
282
|
+
};
|
|
283
|
+
return {
|
|
284
|
+
vendor: "deel",
|
|
285
|
+
source: "salesnav-supabase",
|
|
286
|
+
orgId,
|
|
287
|
+
total,
|
|
288
|
+
locales,
|
|
289
|
+
summary: {
|
|
290
|
+
localeCounts,
|
|
291
|
+
localePercentages: {
|
|
292
|
+
de: percentage(localeCounts.de, total),
|
|
293
|
+
en: percentage(localeCounts.en, total)
|
|
294
|
+
},
|
|
295
|
+
fieldCoverage,
|
|
296
|
+
signalFieldCounts,
|
|
297
|
+
topTitles: [...titleCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 20)
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
function csvEscape(value) {
|
|
302
|
+
const normalized = value ?? "";
|
|
303
|
+
return `"${normalized.replaceAll('"', '""')}"`;
|
|
304
|
+
}
|
|
305
|
+
export function buildDeelSalesNavCsvHeader() {
|
|
306
|
+
return [
|
|
307
|
+
"id",
|
|
308
|
+
"orgId",
|
|
309
|
+
"runId",
|
|
310
|
+
"language",
|
|
311
|
+
"marketSegment",
|
|
312
|
+
"languageReason",
|
|
313
|
+
"languageSignals",
|
|
314
|
+
"signalFields",
|
|
315
|
+
"fullName",
|
|
316
|
+
"firstName",
|
|
317
|
+
"lastName",
|
|
318
|
+
"title",
|
|
319
|
+
"industry",
|
|
320
|
+
"companyName",
|
|
321
|
+
"companyNameCleaned",
|
|
322
|
+
"location",
|
|
323
|
+
"companyLocation",
|
|
324
|
+
"preferredProfileUrl",
|
|
325
|
+
"linkedinProfileUrl",
|
|
326
|
+
"defaultProfileUrl",
|
|
327
|
+
"salesNavProfileUrl",
|
|
328
|
+
"companyUrl",
|
|
329
|
+
"regularCompanyUrl",
|
|
330
|
+
"companyLinkedInHandle",
|
|
331
|
+
"searchQuery",
|
|
332
|
+
"scrapedAt"
|
|
333
|
+
].join(",");
|
|
334
|
+
}
|
|
335
|
+
export function buildDeelSalesNavCsvLines(rows) {
|
|
336
|
+
return rows
|
|
337
|
+
.map((row) => [
|
|
338
|
+
row.id,
|
|
339
|
+
row.orgId,
|
|
340
|
+
row.runId ?? "",
|
|
341
|
+
row.language,
|
|
342
|
+
row.marketSegment,
|
|
343
|
+
row.languageReason,
|
|
344
|
+
row.languageSignals.join("|"),
|
|
345
|
+
row.signalFields.join("|"),
|
|
346
|
+
row.fullName ?? "",
|
|
347
|
+
row.firstName ?? "",
|
|
348
|
+
row.lastName ?? "",
|
|
349
|
+
row.title ?? "",
|
|
350
|
+
row.industry ?? "",
|
|
351
|
+
row.companyName ?? "",
|
|
352
|
+
row.companyNameCleaned ?? "",
|
|
353
|
+
row.location ?? "",
|
|
354
|
+
row.companyLocation ?? "",
|
|
355
|
+
row.preferredProfileUrl,
|
|
356
|
+
row.linkedinProfileUrl ?? "",
|
|
357
|
+
row.defaultProfileUrl ?? "",
|
|
358
|
+
row.salesNavProfileUrl,
|
|
359
|
+
row.companyUrl ?? "",
|
|
360
|
+
row.regularCompanyUrl ?? "",
|
|
361
|
+
row.companyLinkedInHandle ?? "",
|
|
362
|
+
row.searchQuery ?? "",
|
|
363
|
+
row.scrapedAt ?? ""
|
|
364
|
+
]
|
|
365
|
+
.map(csvEscape)
|
|
366
|
+
.join(","))
|
|
367
|
+
.join("\n");
|
|
368
|
+
}
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { parseHistoricalQuery } from "./historical-queries.js";
|
|
2
|
+
function marketCountries(market) {
|
|
3
|
+
if (market === "dach") {
|
|
4
|
+
return ["DE", "AT", "CH"];
|
|
5
|
+
}
|
|
6
|
+
if (market === "europe") {
|
|
7
|
+
return ["DE", "AT", "CH", "NL", "GB", "FR", "SE", "DK"];
|
|
8
|
+
}
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
function sqlStringList(values) {
|
|
12
|
+
return values.map((value) => `'${value.replaceAll("'", "\\'")}'`).join(", ");
|
|
13
|
+
}
|
|
14
|
+
function companySizeEmployeeCountSql(field) {
|
|
15
|
+
return `CASE TRIM(${field})
|
|
16
|
+
WHEN '1-10' THEN 10
|
|
17
|
+
WHEN '11-50' THEN 30
|
|
18
|
+
WHEN '51-200' THEN 125
|
|
19
|
+
WHEN '201-500' THEN 350
|
|
20
|
+
WHEN '501-1000' THEN 750
|
|
21
|
+
WHEN '1001-5000' THEN 2500
|
|
22
|
+
WHEN '5001-10.000' THEN 7500
|
|
23
|
+
WHEN '10.000+' THEN 10000
|
|
24
|
+
ELSE NULL
|
|
25
|
+
END`;
|
|
26
|
+
}
|
|
27
|
+
function broadTitlePredicateSql() {
|
|
28
|
+
return [
|
|
29
|
+
"LOWER(c.jobTitle) LIKE '%head of hr%'",
|
|
30
|
+
"LOWER(c.jobTitle) LIKE '%head of human resources%'",
|
|
31
|
+
"LOWER(c.jobTitle) LIKE '%head of people%'",
|
|
32
|
+
"LOWER(c.jobTitle) LIKE '%head of payroll%'",
|
|
33
|
+
"LOWER(c.jobTitle) LIKE '%people services%'",
|
|
34
|
+
"LOWER(c.jobTitle) LIKE '%shared services%'",
|
|
35
|
+
"LOWER(c.jobTitle) LIKE '%total rewards%'",
|
|
36
|
+
"LOWER(c.jobTitle) LIKE '%compensation%'",
|
|
37
|
+
"LOWER(c.jobTitle) LIKE '%benefits%'",
|
|
38
|
+
"LOWER(c.jobTitle) LIKE '%hr business partner%'",
|
|
39
|
+
"LOWER(c.jobTitle) LIKE '%hrbp%'",
|
|
40
|
+
"LOWER(c.jobTitle) LIKE '%people operations%'",
|
|
41
|
+
"LOWER(c.jobTitle) LIKE '%people ops%'",
|
|
42
|
+
"LOWER(c.jobTitle) LIKE '%hr operations%'",
|
|
43
|
+
"LOWER(c.jobTitle) LIKE '%hr administration%'",
|
|
44
|
+
"LOWER(c.jobTitle) LIKE '%hr director%'",
|
|
45
|
+
"LOWER(c.jobTitle) LIKE '%director hr%'",
|
|
46
|
+
"LOWER(c.jobTitle) LIKE '%chief people officer%'",
|
|
47
|
+
"LOWER(c.jobTitle) LIKE '%chro%'",
|
|
48
|
+
"LOWER(c.jobTitle) LIKE '%leiter personal%'",
|
|
49
|
+
"LOWER(c.jobTitle) LIKE '%leiter der personalabteilung%'",
|
|
50
|
+
"LOWER(c.jobTitle) LIKE '%personalleitung%'",
|
|
51
|
+
"LOWER(c.jobTitle) LIKE '%bereichsleiter personal%'",
|
|
52
|
+
"LOWER(c.jobTitle) LIKE '%geschäftsbereichsleiter personal%'",
|
|
53
|
+
"LOWER(c.jobTitle) LIKE '%geschaeftsbereichsleiter personal%'"
|
|
54
|
+
].join("\n OR ");
|
|
55
|
+
}
|
|
56
|
+
function titleExclusionPredicateSql() {
|
|
57
|
+
return [
|
|
58
|
+
"LOWER(c.jobTitle) NOT LIKE '%assistant%'",
|
|
59
|
+
"LOWER(c.jobTitle) NOT LIKE '%assistent%'",
|
|
60
|
+
"LOWER(c.jobTitle) NOT LIKE '%assistentin%'",
|
|
61
|
+
"LOWER(c.jobTitle) NOT LIKE '%recruitment%'",
|
|
62
|
+
"LOWER(c.jobTitle) NOT LIKE '%recruiter%'",
|
|
63
|
+
"LOWER(c.jobTitle) NOT LIKE '%talent acquisition specialist%'"
|
|
64
|
+
].join("\n AND ");
|
|
65
|
+
}
|
|
66
|
+
export function buildDirectPathLeadExportSql(vendor, market, limit) {
|
|
67
|
+
if (vendor !== "deel") {
|
|
68
|
+
throw new Error(`Unsupported direct-path vendor: ${vendor}`);
|
|
69
|
+
}
|
|
70
|
+
const countries = marketCountries(market);
|
|
71
|
+
const countryClause = countries.length > 0 ? `co.countryCode IN (${sqlStringList(countries)})` : "TRUE";
|
|
72
|
+
const broadTitlePredicate = broadTitlePredicateSql();
|
|
73
|
+
const titleExclusions = titleExclusionPredicateSql();
|
|
74
|
+
const companySizeEmployeeCount = companySizeEmployeeCountSql("co.companySize");
|
|
75
|
+
return `WITH hr_leadlists AS (
|
|
76
|
+
SELECT
|
|
77
|
+
CAST(leadListId AS STRING) AS leadListId,
|
|
78
|
+
CAST(query AS STRING) AS query,
|
|
79
|
+
leadList_container_ts
|
|
80
|
+
FROM \`icpidentifier.SalesPrompter.leadLists_raw\`
|
|
81
|
+
WHERE query IS NOT NULL
|
|
82
|
+
AND LOWER(CAST(query AS STRING)) LIKE '%/sales/search/people%'
|
|
83
|
+
AND LOWER(CAST(query AS STRING)) LIKE '%type%3afunction%2cvalues%3alist((id%3a12%'
|
|
84
|
+
QUALIFY ROW_NUMBER() OVER (PARTITION BY CAST(leadListId AS STRING) ORDER BY leadList_container_ts DESC) = 1
|
|
85
|
+
), contacts AS (
|
|
86
|
+
SELECT
|
|
87
|
+
CAST(leadListId AS STRING) AS leadListId,
|
|
88
|
+
CAST(contactId AS STRING) AS contactId,
|
|
89
|
+
CAST(companyId AS STRING) AS companyId,
|
|
90
|
+
firstName,
|
|
91
|
+
lastName,
|
|
92
|
+
jobTitle,
|
|
93
|
+
contact_companyName,
|
|
94
|
+
contact_location,
|
|
95
|
+
contact_companyLocation,
|
|
96
|
+
tenureAtCompany,
|
|
97
|
+
tenureAtPosition,
|
|
98
|
+
contact_summary,
|
|
99
|
+
contact_titleDescription
|
|
100
|
+
FROM \`icpidentifier.SalesPrompter.linkedin_contacts\`
|
|
101
|
+
), companies AS (
|
|
102
|
+
SELECT
|
|
103
|
+
CAST(id AS STRING) AS companyId,
|
|
104
|
+
handle,
|
|
105
|
+
name,
|
|
106
|
+
countryCode,
|
|
107
|
+
companySize,
|
|
108
|
+
headquarters,
|
|
109
|
+
domain,
|
|
110
|
+
domain_linkedin,
|
|
111
|
+
website_linkedin,
|
|
112
|
+
industry,
|
|
113
|
+
timestamp AS company_ts
|
|
114
|
+
FROM \`icpidentifier.SalesPrompter.linkedin_companies\`
|
|
115
|
+
)
|
|
116
|
+
SELECT
|
|
117
|
+
c.leadListId,
|
|
118
|
+
c.contactId,
|
|
119
|
+
c.companyId,
|
|
120
|
+
CAST(NULL AS STRING) AS profileUrl,
|
|
121
|
+
c.firstName,
|
|
122
|
+
c.lastName,
|
|
123
|
+
CONCAT(COALESCE(c.firstName, ''), IF(c.firstName IS NOT NULL AND c.lastName IS NOT NULL, ' ', ''), COALESCE(c.lastName, '')) AS fullName,
|
|
124
|
+
c.jobTitle,
|
|
125
|
+
COALESCE(co.name, c.contact_companyName) AS companyName,
|
|
126
|
+
co.handle AS linkedin_companies_handle,
|
|
127
|
+
CONCAT('https://www.linkedin.com/company/', c.companyId) AS companyUrl,
|
|
128
|
+
co.companySize,
|
|
129
|
+
co.countryCode,
|
|
130
|
+
co.headquarters,
|
|
131
|
+
co.domain,
|
|
132
|
+
co.domain_linkedin,
|
|
133
|
+
co.website_linkedin,
|
|
134
|
+
co.industry,
|
|
135
|
+
c.contact_location,
|
|
136
|
+
c.contact_companyLocation,
|
|
137
|
+
c.tenureAtCompany,
|
|
138
|
+
c.tenureAtPosition,
|
|
139
|
+
c.contact_summary,
|
|
140
|
+
c.contact_titleDescription,
|
|
141
|
+
CAST(l.leadList_container_ts AS STRING) AS leadList_container_ts,
|
|
142
|
+
CAST(co.company_ts AS STRING) AS company_ts,
|
|
143
|
+
l.query AS leadListQuery
|
|
144
|
+
FROM contacts c
|
|
145
|
+
JOIN hr_leadlists l ON c.leadListId = l.leadListId
|
|
146
|
+
LEFT JOIN companies co ON c.companyId = co.companyId
|
|
147
|
+
WHERE ${countryClause}
|
|
148
|
+
AND co.domain IS NOT NULL
|
|
149
|
+
AND ${companySizeEmployeeCount} >= 100
|
|
150
|
+
AND ${companySizeEmployeeCount} <= 2500
|
|
151
|
+
AND (
|
|
152
|
+
${broadTitlePredicate}
|
|
153
|
+
)
|
|
154
|
+
AND ${titleExclusions}
|
|
155
|
+
QUALIFY ROW_NUMBER() OVER (PARTITION BY c.contactId ORDER BY co.company_ts DESC) = 1
|
|
156
|
+
ORDER BY co.company_ts DESC
|
|
157
|
+
LIMIT ${limit}`;
|
|
158
|
+
}
|
|
159
|
+
export function classifyDirectPathSegment(title) {
|
|
160
|
+
const normalized = title.toLowerCase();
|
|
161
|
+
if (normalized.includes("payroll") ||
|
|
162
|
+
normalized.includes("people services") ||
|
|
163
|
+
normalized.includes("shared services") ||
|
|
164
|
+
normalized.includes("total rewards") ||
|
|
165
|
+
normalized.includes("compensation") ||
|
|
166
|
+
normalized.includes("benefits")) {
|
|
167
|
+
return {
|
|
168
|
+
segment: "payroll-people-services",
|
|
169
|
+
reason: "payroll, shared services, benefits, or people services scope"
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
if (normalized.includes("head of hr") ||
|
|
173
|
+
normalized.includes("head of human resources") ||
|
|
174
|
+
normalized.includes("hr director") ||
|
|
175
|
+
normalized.includes("director hr") ||
|
|
176
|
+
normalized.includes("chief people officer") ||
|
|
177
|
+
normalized.includes("chro") ||
|
|
178
|
+
normalized.includes("leiter personal") ||
|
|
179
|
+
normalized.includes("leiter der personalabteilung") ||
|
|
180
|
+
normalized.includes("personalleitung") ||
|
|
181
|
+
normalized.includes("bereichsleiter personal") ||
|
|
182
|
+
normalized.includes("geschäftsbereichsleiter personal") ||
|
|
183
|
+
normalized.includes("geschaeftsbereichsleiter personal")) {
|
|
184
|
+
return {
|
|
185
|
+
segment: "leadership",
|
|
186
|
+
reason: "top-level HR leadership title"
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
segment: "broader-hr",
|
|
191
|
+
reason: "HR, people, or operations role outside the strict leadership/payroll buckets"
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function toNumber(value) {
|
|
195
|
+
if (value === null || value === undefined) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
const numeric = Number(value);
|
|
199
|
+
return Number.isFinite(numeric) ? numeric : null;
|
|
200
|
+
}
|
|
201
|
+
function toNullableString(value) {
|
|
202
|
+
if (value === null || value === undefined) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
const normalized = String(value).trim();
|
|
206
|
+
return normalized.length > 0 ? normalized : null;
|
|
207
|
+
}
|
|
208
|
+
export function normalizeDirectPathRows(rows) {
|
|
209
|
+
return rows.map((row) => ({
|
|
210
|
+
leadListId: toNullableString(row.leadListId),
|
|
211
|
+
contactId: toNullableString(row.contactId),
|
|
212
|
+
companyId: toNullableString(row.companyId),
|
|
213
|
+
profileUrl: toNullableString(row.profileUrl),
|
|
214
|
+
firstName: toNullableString(row.firstName),
|
|
215
|
+
lastName: toNullableString(row.lastName),
|
|
216
|
+
fullName: toNullableString(row.fullName),
|
|
217
|
+
jobTitle: toNullableString(row.jobTitle),
|
|
218
|
+
companyName: toNullableString(row.companyName),
|
|
219
|
+
linkedin_companies_handle: toNullableString(row.linkedin_companies_handle),
|
|
220
|
+
companyUrl: toNullableString(row.companyUrl),
|
|
221
|
+
companySize: toNullableString(row.companySize),
|
|
222
|
+
countryCode: toNullableString(row.countryCode),
|
|
223
|
+
headquarters: toNullableString(row.headquarters),
|
|
224
|
+
domain: toNullableString(row.domain),
|
|
225
|
+
domain_linkedin: toNullableString(row.domain_linkedin),
|
|
226
|
+
website_linkedin: toNullableString(row.website_linkedin),
|
|
227
|
+
industry: toNullableString(row.industry),
|
|
228
|
+
contact_location: toNullableString(row.contact_location),
|
|
229
|
+
contact_companyLocation: toNullableString(row.contact_companyLocation),
|
|
230
|
+
tenureAtCompany: toNumber(row.tenureAtCompany),
|
|
231
|
+
tenureAtPosition: toNumber(row.tenureAtPosition),
|
|
232
|
+
contact_summary: toNullableString(row.contact_summary),
|
|
233
|
+
contact_titleDescription: toNullableString(row.contact_titleDescription),
|
|
234
|
+
leadList_container_ts: toNullableString(row.leadList_container_ts),
|
|
235
|
+
company_ts: toNullableString(row.company_ts),
|
|
236
|
+
leadListQuery: toNullableString(row.leadListQuery),
|
|
237
|
+
leadListQueryDecoded: (() => {
|
|
238
|
+
const rawQuery = toNullableString(row.leadListQuery);
|
|
239
|
+
return rawQuery === null ? null : parseHistoricalQuery(rawQuery).decodedQuery;
|
|
240
|
+
})(),
|
|
241
|
+
leadListFunctionFilters: (() => {
|
|
242
|
+
const rawQuery = toNullableString(row.leadListQuery);
|
|
243
|
+
return rawQuery === null
|
|
244
|
+
? []
|
|
245
|
+
: parseHistoricalQuery(rawQuery).functionFilters
|
|
246
|
+
.filter((filter) => filter.selection === "INCLUDED")
|
|
247
|
+
.map((filter) => filter.text);
|
|
248
|
+
})(),
|
|
249
|
+
leadListTitleFilters: (() => {
|
|
250
|
+
const rawQuery = toNullableString(row.leadListQuery);
|
|
251
|
+
return rawQuery === null
|
|
252
|
+
? []
|
|
253
|
+
: parseHistoricalQuery(rawQuery).titleFilters
|
|
254
|
+
.filter((filter) => filter.selection === "INCLUDED")
|
|
255
|
+
.map((filter) => filter.text);
|
|
256
|
+
})(),
|
|
257
|
+
leadListRegionFilters: (() => {
|
|
258
|
+
const rawQuery = toNullableString(row.leadListQuery);
|
|
259
|
+
return rawQuery === null
|
|
260
|
+
? []
|
|
261
|
+
: parseHistoricalQuery(rawQuery).regionFilters
|
|
262
|
+
.filter((filter) => filter.selection === "INCLUDED")
|
|
263
|
+
.map((filter) => filter.text);
|
|
264
|
+
})(),
|
|
265
|
+
leadListHeadquartersFilters: (() => {
|
|
266
|
+
const rawQuery = toNullableString(row.leadListQuery);
|
|
267
|
+
return rawQuery === null
|
|
268
|
+
? []
|
|
269
|
+
: parseHistoricalQuery(rawQuery).headquartersFilters
|
|
270
|
+
.filter((filter) => filter.selection === "INCLUDED")
|
|
271
|
+
.map((filter) => filter.text);
|
|
272
|
+
})(),
|
|
273
|
+
leadListHeadcountFilters: (() => {
|
|
274
|
+
const rawQuery = toNullableString(row.leadListQuery);
|
|
275
|
+
return rawQuery === null
|
|
276
|
+
? []
|
|
277
|
+
: parseHistoricalQuery(rawQuery).headcountFilters
|
|
278
|
+
.filter((filter) => filter.selection === "INCLUDED")
|
|
279
|
+
.map((filter) => filter.text);
|
|
280
|
+
})()
|
|
281
|
+
}));
|
|
282
|
+
}
|
|
283
|
+
export function segmentDirectPathRows(vendor, market, rows) {
|
|
284
|
+
if (vendor !== "deel") {
|
|
285
|
+
throw new Error(`Unsupported direct-path vendor: ${vendor}`);
|
|
286
|
+
}
|
|
287
|
+
const segments = {
|
|
288
|
+
leadership: [],
|
|
289
|
+
"payroll-people-services": [],
|
|
290
|
+
"broader-hr": []
|
|
291
|
+
};
|
|
292
|
+
for (const row of rows) {
|
|
293
|
+
const title = row.jobTitle ?? "";
|
|
294
|
+
const { segment, reason } = classifyDirectPathSegment(title);
|
|
295
|
+
segments[segment].push({
|
|
296
|
+
...row,
|
|
297
|
+
segment,
|
|
298
|
+
segmentReason: reason
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
const countries = new Map();
|
|
302
|
+
const titles = new Map();
|
|
303
|
+
for (const row of rows) {
|
|
304
|
+
const country = row.countryCode ?? "unknown";
|
|
305
|
+
countries.set(country, (countries.get(country) ?? 0) + 1);
|
|
306
|
+
const title = row.jobTitle ?? "unknown";
|
|
307
|
+
titles.set(title, (titles.get(title) ?? 0) + 1);
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
vendor,
|
|
311
|
+
market,
|
|
312
|
+
total: rows.length,
|
|
313
|
+
distinctContacts: new Set(rows.map((row) => row.contactId).filter((value) => value !== null)).size,
|
|
314
|
+
distinctCompanies: new Set(rows.map((row) => row.companyId).filter((value) => value !== null)).size,
|
|
315
|
+
segments,
|
|
316
|
+
summary: {
|
|
317
|
+
segmentCounts: {
|
|
318
|
+
leadership: segments.leadership.length,
|
|
319
|
+
"payroll-people-services": segments["payroll-people-services"].length,
|
|
320
|
+
"broader-hr": segments["broader-hr"].length
|
|
321
|
+
},
|
|
322
|
+
countries: [...countries.entries()].sort((a, b) => a[0].localeCompare(b[0])),
|
|
323
|
+
topTitles: [...titles.entries()].sort((a, b) => b[1] - a[1]).slice(0, 20)
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
}
|