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,764 @@
|
|
|
1
|
+
const DOMAIN_BLACKLIST = new Set(["linkedin.com", "bit.ly", "linktr.ee", "facebook.com"]);
|
|
2
|
+
const COMPANY_NAME_STOPWORDS = new Set([
|
|
3
|
+
"group",
|
|
4
|
+
"gmbh",
|
|
5
|
+
"ag",
|
|
6
|
+
"kg",
|
|
7
|
+
"co",
|
|
8
|
+
"company",
|
|
9
|
+
"holding",
|
|
10
|
+
"holdings",
|
|
11
|
+
"solutions",
|
|
12
|
+
"systems",
|
|
13
|
+
"services",
|
|
14
|
+
"international",
|
|
15
|
+
"global",
|
|
16
|
+
"the",
|
|
17
|
+
"und",
|
|
18
|
+
"and",
|
|
19
|
+
"de",
|
|
20
|
+
"of"
|
|
21
|
+
]);
|
|
22
|
+
function marketCountries(market) {
|
|
23
|
+
if (market === "dach") {
|
|
24
|
+
return ["DE", "AT", "CH"];
|
|
25
|
+
}
|
|
26
|
+
if (market === "europe") {
|
|
27
|
+
return ["DE", "AT", "CH", "NL", "GB", "FR", "SE", "DK"];
|
|
28
|
+
}
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
function sqlCountryList(market) {
|
|
32
|
+
const countries = marketCountries(market);
|
|
33
|
+
if (countries.length === 0) {
|
|
34
|
+
return "";
|
|
35
|
+
}
|
|
36
|
+
return countries.map((country) => `"${country}"`).join(", ");
|
|
37
|
+
}
|
|
38
|
+
function normalizeDomain(value) {
|
|
39
|
+
if (!value) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
let normalized = value.trim().toLowerCase();
|
|
43
|
+
if (normalized.length === 0) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
normalized = normalized.replace(/^https?:\/\//, "");
|
|
47
|
+
normalized = normalized.replace(/^www\./, "");
|
|
48
|
+
normalized = normalized.replace(/\/.*$/, "");
|
|
49
|
+
return normalized.length > 0 ? normalized : null;
|
|
50
|
+
}
|
|
51
|
+
function rootDomain(value) {
|
|
52
|
+
const normalized = normalizeDomain(value);
|
|
53
|
+
if (!normalized) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
const parts = normalized.split(".");
|
|
57
|
+
if (parts.length <= 2) {
|
|
58
|
+
return normalized;
|
|
59
|
+
}
|
|
60
|
+
return parts.slice(-2).join(".");
|
|
61
|
+
}
|
|
62
|
+
function domainLabel(value) {
|
|
63
|
+
const root = rootDomain(value);
|
|
64
|
+
if (!root) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
return root.split(".")[0] ?? null;
|
|
68
|
+
}
|
|
69
|
+
function companyNameTokens(companyName) {
|
|
70
|
+
return (companyName ?? "")
|
|
71
|
+
.toLowerCase()
|
|
72
|
+
.replace(/[^a-z0-9]+/g, " ")
|
|
73
|
+
.split(/\s+/)
|
|
74
|
+
.map((token) => token.trim())
|
|
75
|
+
.filter((token) => token.length >= 3 && !COMPANY_NAME_STOPWORDS.has(token));
|
|
76
|
+
}
|
|
77
|
+
function domainCompanyMatchScore(domain, companyName) {
|
|
78
|
+
const label = domainLabel(domain);
|
|
79
|
+
if (!label) {
|
|
80
|
+
return -1;
|
|
81
|
+
}
|
|
82
|
+
const normalizedLabel = label.replace(/[^a-z0-9]/g, "");
|
|
83
|
+
if (normalizedLabel.length === 0) {
|
|
84
|
+
return -1;
|
|
85
|
+
}
|
|
86
|
+
const tokens = companyNameTokens(companyName);
|
|
87
|
+
if (tokens.length === 0) {
|
|
88
|
+
return normalizedLabel.length;
|
|
89
|
+
}
|
|
90
|
+
let score = 0;
|
|
91
|
+
for (const token of tokens) {
|
|
92
|
+
const normalizedToken = token.replace(/[^a-z0-9]/g, "");
|
|
93
|
+
if (normalizedToken.length === 0) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (normalizedLabel === normalizedToken) {
|
|
97
|
+
score += 100 + normalizedToken.length;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (normalizedLabel.startsWith(normalizedToken)) {
|
|
101
|
+
score += 60 + normalizedToken.length;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (normalizedLabel.includes(normalizedToken)) {
|
|
105
|
+
score += 40 + normalizedToken.length;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (normalizedToken.startsWith(normalizedLabel) && normalizedLabel.length >= 4) {
|
|
109
|
+
score += 20 + normalizedLabel.length;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return score;
|
|
113
|
+
}
|
|
114
|
+
function chooseBetterCompanyMatchCandidate(candidates, baselineDomain) {
|
|
115
|
+
const baselineScore = domainCompanyMatchScore(baselineDomain, candidates[0]?.companyName);
|
|
116
|
+
const ranked = [...candidates]
|
|
117
|
+
.filter((candidate) => normalizeDomain(candidate.domain) !== null)
|
|
118
|
+
.filter((candidate) => !isBlacklistedDomain(candidate.domain))
|
|
119
|
+
.filter((candidate) => candidate.source !== "linkedin")
|
|
120
|
+
.sort((a, b) => {
|
|
121
|
+
const scoreDelta = domainCompanyMatchScore(b.domain, b.companyName) -
|
|
122
|
+
domainCompanyMatchScore(a.domain, a.companyName);
|
|
123
|
+
if (scoreDelta !== 0) {
|
|
124
|
+
return scoreDelta;
|
|
125
|
+
}
|
|
126
|
+
const hunterDelta = (b.hunterEmailCount ?? -1) - (a.hunterEmailCount ?? -1);
|
|
127
|
+
if (hunterDelta !== 0) {
|
|
128
|
+
return hunterDelta;
|
|
129
|
+
}
|
|
130
|
+
return (a.source ?? "").localeCompare(b.source ?? "");
|
|
131
|
+
});
|
|
132
|
+
const best = ranked[0] ?? null;
|
|
133
|
+
if (!best) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
const bestScore = domainCompanyMatchScore(best.domain, best.companyName);
|
|
137
|
+
const hasPositiveHunterSignal = (best.hunterEmailCount ?? 0) > 0;
|
|
138
|
+
if (hasPositiveHunterSignal && bestScore > baselineScore) {
|
|
139
|
+
return {
|
|
140
|
+
...best,
|
|
141
|
+
domain: normalizeDomain(best.domain)
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
function isBlacklistedDomain(value) {
|
|
147
|
+
const normalized = normalizeDomain(value);
|
|
148
|
+
if (!normalized) {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
return DOMAIN_BLACKLIST.has(normalized);
|
|
152
|
+
}
|
|
153
|
+
function getCompanyKey(candidate) {
|
|
154
|
+
if (candidate.companyId !== null) {
|
|
155
|
+
return `company:${candidate.companyId}`;
|
|
156
|
+
}
|
|
157
|
+
if (candidate.crmCompanyId !== null) {
|
|
158
|
+
return `crm:${candidate.crmCompanyId}`;
|
|
159
|
+
}
|
|
160
|
+
return `name:${candidate.companyName.trim().toLowerCase()}`;
|
|
161
|
+
}
|
|
162
|
+
function chooseBestDomainPipedreamLegacy(candidates) {
|
|
163
|
+
if (candidates.length === 0) {
|
|
164
|
+
return {
|
|
165
|
+
companyKey: "unknown",
|
|
166
|
+
selected: null,
|
|
167
|
+
reason: "no-domain",
|
|
168
|
+
candidates: []
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const companyKey = getCompanyKey(candidates[0]);
|
|
172
|
+
const sourceOrder = { hunter: 0, openai: 1 };
|
|
173
|
+
const best = [...candidates].sort((a, b) => {
|
|
174
|
+
const hunterDelta = (b.hunterEmailCount ?? 0) - (a.hunterEmailCount ?? 0);
|
|
175
|
+
if (hunterDelta !== 0) {
|
|
176
|
+
return hunterDelta;
|
|
177
|
+
}
|
|
178
|
+
const aHasDomain = normalizeDomain(a.domain) !== null ? 0 : 1;
|
|
179
|
+
const bHasDomain = normalizeDomain(b.domain) !== null ? 0 : 1;
|
|
180
|
+
if (aHasDomain !== bHasDomain) {
|
|
181
|
+
return aHasDomain - bHasDomain;
|
|
182
|
+
}
|
|
183
|
+
return (sourceOrder[a.source] ?? 99) - (sourceOrder[b.source] ?? 99);
|
|
184
|
+
})[0] ?? null;
|
|
185
|
+
if (best && normalizeDomain(best.domain) !== null) {
|
|
186
|
+
return {
|
|
187
|
+
companyKey,
|
|
188
|
+
selected: {
|
|
189
|
+
...best,
|
|
190
|
+
domain: normalizeDomain(best.domain)
|
|
191
|
+
},
|
|
192
|
+
reason: "highest-hunter-count",
|
|
193
|
+
candidates
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
companyKey,
|
|
198
|
+
selected: null,
|
|
199
|
+
reason: "no-domain",
|
|
200
|
+
candidates
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
export function chooseBestDomain(candidates) {
|
|
204
|
+
if (candidates.length === 0) {
|
|
205
|
+
return {
|
|
206
|
+
companyKey: "unknown",
|
|
207
|
+
selected: null,
|
|
208
|
+
reason: "no-domain",
|
|
209
|
+
candidates: []
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
const companyKey = getCompanyKey(candidates[0]);
|
|
213
|
+
const nonNullCandidates = candidates.filter((candidate) => normalizeDomain(candidate.domain) !== null);
|
|
214
|
+
const linkedinDomain = normalizeDomain(candidates[0].linkedinDomain);
|
|
215
|
+
const linkedinWebsite = normalizeDomain(candidates[0].linkedinWebsite);
|
|
216
|
+
const betterThanLinkedinDomain = linkedinDomain
|
|
217
|
+
? chooseBetterCompanyMatchCandidate(nonNullCandidates, linkedinDomain)
|
|
218
|
+
: null;
|
|
219
|
+
const betterThanLinkedinWebsite = !linkedinDomain && linkedinWebsite
|
|
220
|
+
? chooseBetterCompanyMatchCandidate(nonNullCandidates, linkedinWebsite)
|
|
221
|
+
: null;
|
|
222
|
+
if (betterThanLinkedinDomain) {
|
|
223
|
+
return {
|
|
224
|
+
companyKey,
|
|
225
|
+
selected: betterThanLinkedinDomain,
|
|
226
|
+
reason: "better-company-match",
|
|
227
|
+
candidates
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
if (linkedinDomain && !isBlacklistedDomain(linkedinDomain)) {
|
|
231
|
+
const selectedCandidate = nonNullCandidates.find((candidate) => normalizeDomain(candidate.domain) === linkedinDomain) ?? {
|
|
232
|
+
...candidates[0],
|
|
233
|
+
domain: linkedinDomain,
|
|
234
|
+
source: "linkedin",
|
|
235
|
+
type: "original",
|
|
236
|
+
hunterEmailCount: candidates[0].hunterEmailCount
|
|
237
|
+
};
|
|
238
|
+
const selected = {
|
|
239
|
+
...selectedCandidate,
|
|
240
|
+
domain: linkedinDomain
|
|
241
|
+
};
|
|
242
|
+
return {
|
|
243
|
+
companyKey,
|
|
244
|
+
selected,
|
|
245
|
+
reason: "linkedin-domain",
|
|
246
|
+
candidates
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
if (betterThanLinkedinWebsite) {
|
|
250
|
+
return {
|
|
251
|
+
companyKey,
|
|
252
|
+
selected: betterThanLinkedinWebsite,
|
|
253
|
+
reason: "better-company-match",
|
|
254
|
+
candidates
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
if (linkedinWebsite && !isBlacklistedDomain(linkedinWebsite)) {
|
|
258
|
+
const websiteRoot = rootDomain(linkedinWebsite);
|
|
259
|
+
const selectedCandidate = nonNullCandidates.find((candidate) => rootDomain(candidate.domain) === websiteRoot) ?? {
|
|
260
|
+
...candidates[0],
|
|
261
|
+
domain: websiteRoot,
|
|
262
|
+
source: "linkedin",
|
|
263
|
+
type: "original",
|
|
264
|
+
hunterEmailCount: candidates[0].hunterEmailCount
|
|
265
|
+
};
|
|
266
|
+
const selected = {
|
|
267
|
+
...selectedCandidate,
|
|
268
|
+
domain: websiteRoot
|
|
269
|
+
};
|
|
270
|
+
return {
|
|
271
|
+
companyKey,
|
|
272
|
+
selected,
|
|
273
|
+
reason: "linkedin-website",
|
|
274
|
+
candidates
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
const bestByHunter = [...nonNullCandidates]
|
|
278
|
+
.filter((candidate) => !isBlacklistedDomain(candidate.domain))
|
|
279
|
+
.sort((a, b) => {
|
|
280
|
+
const hunterDelta = (b.hunterEmailCount ?? -1) - (a.hunterEmailCount ?? -1);
|
|
281
|
+
if (hunterDelta !== 0) {
|
|
282
|
+
return hunterDelta;
|
|
283
|
+
}
|
|
284
|
+
return (a.source ?? "").localeCompare(b.source ?? "");
|
|
285
|
+
})[0];
|
|
286
|
+
if (bestByHunter) {
|
|
287
|
+
return {
|
|
288
|
+
companyKey,
|
|
289
|
+
selected: bestByHunter,
|
|
290
|
+
reason: "highest-hunter-count",
|
|
291
|
+
candidates
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
const firstNonNull = nonNullCandidates.find((candidate) => !isBlacklistedDomain(candidate.domain)) ?? null;
|
|
295
|
+
if (firstNonNull) {
|
|
296
|
+
return {
|
|
297
|
+
companyKey,
|
|
298
|
+
selected: {
|
|
299
|
+
...firstNonNull,
|
|
300
|
+
domain: normalizeDomain(firstNonNull.domain)
|
|
301
|
+
},
|
|
302
|
+
reason: "fallback-first-non-null",
|
|
303
|
+
candidates
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
companyKey,
|
|
308
|
+
selected: null,
|
|
309
|
+
reason: "no-domain",
|
|
310
|
+
candidates
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
export function selectBestDomains(candidates) {
|
|
314
|
+
const groups = new Map();
|
|
315
|
+
for (const candidate of candidates) {
|
|
316
|
+
const key = getCompanyKey(candidate);
|
|
317
|
+
const existing = groups.get(key) ?? [];
|
|
318
|
+
existing.push(candidate);
|
|
319
|
+
groups.set(key, existing);
|
|
320
|
+
}
|
|
321
|
+
return Array.from(groups.values()).map((group) => chooseBestDomain(group));
|
|
322
|
+
}
|
|
323
|
+
export function compareDomainSelectionStrategies(candidates) {
|
|
324
|
+
const groups = new Map();
|
|
325
|
+
for (const candidate of candidates) {
|
|
326
|
+
const key = getCompanyKey(candidate);
|
|
327
|
+
const existing = groups.get(key) ?? [];
|
|
328
|
+
existing.push(candidate);
|
|
329
|
+
groups.set(key, existing);
|
|
330
|
+
}
|
|
331
|
+
const oldByReason = {};
|
|
332
|
+
const newByReason = {};
|
|
333
|
+
const changeFlags = {};
|
|
334
|
+
const changes = [];
|
|
335
|
+
for (const [companyKey, group] of groups.entries()) {
|
|
336
|
+
const legacy = chooseBestDomainPipedreamLegacy(group);
|
|
337
|
+
const improved = chooseBestDomain(group);
|
|
338
|
+
oldByReason[legacy.reason] = (oldByReason[legacy.reason] ?? 0) + 1;
|
|
339
|
+
newByReason[improved.reason] = (newByReason[improved.reason] ?? 0) + 1;
|
|
340
|
+
const oldDomain = normalizeDomain(legacy.selected?.domain);
|
|
341
|
+
const newDomain = normalizeDomain(improved.selected?.domain);
|
|
342
|
+
const oldSource = legacy.selected?.source ?? null;
|
|
343
|
+
const newSource = improved.selected?.source ?? null;
|
|
344
|
+
if (oldDomain === newDomain && oldSource === newSource && legacy.reason === improved.reason) {
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
const flags = [];
|
|
348
|
+
if (oldDomain !== newDomain) {
|
|
349
|
+
flags.push("domain-changed");
|
|
350
|
+
}
|
|
351
|
+
if (oldSource !== newSource) {
|
|
352
|
+
flags.push("source-changed");
|
|
353
|
+
}
|
|
354
|
+
if (oldDomain !== null && isBlacklistedDomain(oldDomain) && newDomain === null) {
|
|
355
|
+
flags.push("rejected-blacklisted-legacy-choice");
|
|
356
|
+
}
|
|
357
|
+
if (legacy.reason === "highest-hunter-count" && improved.reason !== "highest-hunter-count") {
|
|
358
|
+
flags.push("overrode-hunter-preference");
|
|
359
|
+
}
|
|
360
|
+
if (improved.reason === "linkedin-domain" || improved.reason === "linkedin-website") {
|
|
361
|
+
flags.push("preferred-linkedin");
|
|
362
|
+
}
|
|
363
|
+
if (improved.reason === "better-company-match") {
|
|
364
|
+
flags.push("preferred-hunter-company-match");
|
|
365
|
+
}
|
|
366
|
+
for (const flag of flags) {
|
|
367
|
+
changeFlags[flag] = (changeFlags[flag] ?? 0) + 1;
|
|
368
|
+
}
|
|
369
|
+
changes.push({
|
|
370
|
+
companyKey,
|
|
371
|
+
companyName: improved.selected?.companyName ?? legacy.selected?.companyName ?? group[0]?.companyName ?? null,
|
|
372
|
+
oldDomain,
|
|
373
|
+
newDomain,
|
|
374
|
+
oldSource,
|
|
375
|
+
newSource,
|
|
376
|
+
oldReason: legacy.reason,
|
|
377
|
+
newReason: improved.reason,
|
|
378
|
+
flags
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
return {
|
|
382
|
+
summary: {
|
|
383
|
+
companies: groups.size,
|
|
384
|
+
changedSelections: changes.length,
|
|
385
|
+
oldByReason,
|
|
386
|
+
newByReason,
|
|
387
|
+
changeFlags
|
|
388
|
+
},
|
|
389
|
+
changes
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
function escapeSqlString(value) {
|
|
393
|
+
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
394
|
+
}
|
|
395
|
+
function sqlStringLiteral(value) {
|
|
396
|
+
if (value === null) {
|
|
397
|
+
return "NULL";
|
|
398
|
+
}
|
|
399
|
+
return `'${escapeSqlString(value)}'`;
|
|
400
|
+
}
|
|
401
|
+
function sqlIntLiteral(value) {
|
|
402
|
+
return value === null ? "NULL" : `${value}`;
|
|
403
|
+
}
|
|
404
|
+
export function buildDomainfinderWritebackSql(decisions, traceId) {
|
|
405
|
+
const accepted = decisions.filter((decision) => decision.selected !== null && decision.reason !== "no-domain" && !isBlacklistedDomain(decision.selected.domain));
|
|
406
|
+
const rowSql = accepted.map((decision) => {
|
|
407
|
+
const selected = decision.selected;
|
|
408
|
+
return `SELECT
|
|
409
|
+
${sqlIntLiteral(selected.companyId)} AS companyId,
|
|
410
|
+
${sqlStringLiteral(selected.type ?? "selected")} AS type,
|
|
411
|
+
${sqlStringLiteral(selected.companyName)} AS name,
|
|
412
|
+
${sqlStringLiteral(selected.source)} AS source,
|
|
413
|
+
${sqlIntLiteral(selected.hunterEmailCount)} AS total,
|
|
414
|
+
CURRENT_TIMESTAMP() AS ts,
|
|
415
|
+
${sqlStringLiteral(traceId)} AS trace_id,
|
|
416
|
+
${sqlStringLiteral(normalizeDomain(selected.domain))} AS domain,
|
|
417
|
+
${sqlIntLiteral(selected.crmCompanyId)} AS crm_companyId`;
|
|
418
|
+
});
|
|
419
|
+
if (rowSql.length === 0) {
|
|
420
|
+
return `-- No accepted domain decisions to write back for trace_id ${traceId}\nSELECT 0 AS rows_to_insert;`;
|
|
421
|
+
}
|
|
422
|
+
return `INSERT INTO \`icpidentifier.SalesPrompter.domainFinder_output\`
|
|
423
|
+
(companyId, type, name, source, total, ts, trace_id, domain, crm_companyId)
|
|
424
|
+
${rowSql.join("\nUNION ALL\n")};`;
|
|
425
|
+
}
|
|
426
|
+
export function auditDomainDecisions(decisions) {
|
|
427
|
+
const findings = decisions.map((decision) => {
|
|
428
|
+
const selected = decision.selected;
|
|
429
|
+
const selectedDomain = normalizeDomain(selected?.domain);
|
|
430
|
+
const linkedinDomain = normalizeDomain(selected?.linkedinDomain ?? decision.candidates[0]?.linkedinDomain);
|
|
431
|
+
const linkedinWebsiteRoot = rootDomain(selected?.linkedinWebsite ?? decision.candidates[0]?.linkedinWebsite);
|
|
432
|
+
const flags = [];
|
|
433
|
+
if (selected === null || selectedDomain === null) {
|
|
434
|
+
flags.push("no-selected-domain");
|
|
435
|
+
}
|
|
436
|
+
if (selectedDomain !== null && isBlacklistedDomain(selectedDomain)) {
|
|
437
|
+
flags.push("selected-blacklisted");
|
|
438
|
+
}
|
|
439
|
+
if (decision.reason === "fallback-first-non-null") {
|
|
440
|
+
flags.push("fallback-selected");
|
|
441
|
+
}
|
|
442
|
+
if (decision.reason === "better-company-match") {
|
|
443
|
+
flags.push("better-company-match-selected");
|
|
444
|
+
}
|
|
445
|
+
if (decision.reason === "highest-hunter-count") {
|
|
446
|
+
flags.push("hunter-selected");
|
|
447
|
+
}
|
|
448
|
+
if (selected?.source === "openai") {
|
|
449
|
+
flags.push("selected-openai");
|
|
450
|
+
}
|
|
451
|
+
if (selected?.source === "hunter") {
|
|
452
|
+
flags.push("selected-hunter");
|
|
453
|
+
}
|
|
454
|
+
if (selectedDomain !== null && linkedinDomain !== null && rootDomain(selectedDomain) !== rootDomain(linkedinDomain)) {
|
|
455
|
+
flags.push("mismatch-linkedin-domain");
|
|
456
|
+
}
|
|
457
|
+
if (selectedDomain !== null && linkedinWebsiteRoot !== null && rootDomain(selectedDomain) !== linkedinWebsiteRoot) {
|
|
458
|
+
flags.push("mismatch-linkedin-website");
|
|
459
|
+
}
|
|
460
|
+
if (selectedDomain !== null && selectedDomain.startsWith("www.")) {
|
|
461
|
+
flags.push("selected-not-root-domain");
|
|
462
|
+
}
|
|
463
|
+
return {
|
|
464
|
+
companyKey: decision.companyKey,
|
|
465
|
+
companyName: selected?.companyName ?? decision.candidates[0]?.companyName ?? null,
|
|
466
|
+
reason: decision.reason,
|
|
467
|
+
selectedDomain,
|
|
468
|
+
selectedSource: selected?.source ?? null,
|
|
469
|
+
flags
|
|
470
|
+
};
|
|
471
|
+
});
|
|
472
|
+
const byReason = {};
|
|
473
|
+
const byFlag = {};
|
|
474
|
+
let acceptedForWriteback = 0;
|
|
475
|
+
for (const decision of decisions) {
|
|
476
|
+
byReason[decision.reason] = (byReason[decision.reason] ?? 0) + 1;
|
|
477
|
+
}
|
|
478
|
+
for (const finding of findings) {
|
|
479
|
+
if (finding.flags.length === 0) {
|
|
480
|
+
acceptedForWriteback += 1;
|
|
481
|
+
}
|
|
482
|
+
for (const flag of finding.flags) {
|
|
483
|
+
byFlag[flag] = (byFlag[flag] ?? 0) + 1;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return {
|
|
487
|
+
summary: {
|
|
488
|
+
decisions: decisions.length,
|
|
489
|
+
acceptedForWriteback,
|
|
490
|
+
rejectedForWriteback: decisions.length - acceptedForWriteback,
|
|
491
|
+
byReason,
|
|
492
|
+
byFlag
|
|
493
|
+
},
|
|
494
|
+
findings: findings.filter((finding) => finding.flags.length > 0)
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
export function buildDomainfinderInputSql(market) {
|
|
498
|
+
const countries = sqlCountryList(market);
|
|
499
|
+
const countryFilter = countries.length > 0 ? `AND countryCode IN (${countries})` : "";
|
|
500
|
+
return `CREATE OR REPLACE VIEW \`icpidentifier.SalesPrompter.domainFinder_input_v2\` AS
|
|
501
|
+
SELECT
|
|
502
|
+
CAST(NULL AS INT64) AS clientId,
|
|
503
|
+
id AS companyId,
|
|
504
|
+
CAST(NULL AS INT64) AS crm_companyId,
|
|
505
|
+
name AS companyName,
|
|
506
|
+
headquarters,
|
|
507
|
+
domain_linkedin AS domain
|
|
508
|
+
FROM \`icpidentifier.SalesPrompter.linkedin_companies\`
|
|
509
|
+
WHERE company_emailDomainFinder_toBeProcessed = TRUE
|
|
510
|
+
${countryFilter}
|
|
511
|
+
AND COALESCE(blacklisted_bySalesPrompter, FALSE) = FALSE
|
|
512
|
+
AND (linkedin_companies_companyUnavailable IS NULL OR linkedin_companies_companyUnavailable = FALSE)
|
|
513
|
+
AND (domain_linkedin IS NOT NULL OR website_linkedin IS NOT NULL)
|
|
514
|
+
AND COALESCE(domainBlacklisted, FALSE) = FALSE;`;
|
|
515
|
+
}
|
|
516
|
+
export function buildDomainfinderCandidatesSql(market, limit) {
|
|
517
|
+
const countries = sqlCountryList(market);
|
|
518
|
+
const countryFilter = countries.length > 0 ? `AND comp.countryCode IN (${countries})` : "";
|
|
519
|
+
return `WITH backlog AS (
|
|
520
|
+
SELECT
|
|
521
|
+
comp.id AS companyId,
|
|
522
|
+
comp.name AS companyName,
|
|
523
|
+
comp.domain_linkedin AS linkedinDomain,
|
|
524
|
+
comp.website_linkedin AS linkedinWebsite,
|
|
525
|
+
comp.hunter_emailCount AS hunterEmailCount
|
|
526
|
+
FROM \`icpidentifier.SalesPrompter.linkedin_companies\` comp
|
|
527
|
+
WHERE comp.company_emailDomainFinder_toBeProcessed = TRUE
|
|
528
|
+
${countryFilter}
|
|
529
|
+
AND COALESCE(comp.blacklisted_bySalesPrompter, FALSE) = FALSE
|
|
530
|
+
AND (comp.linkedin_companies_companyUnavailable IS NULL OR comp.linkedin_companies_companyUnavailable = FALSE)
|
|
531
|
+
AND (comp.domain_linkedin IS NOT NULL OR comp.website_linkedin IS NOT NULL)
|
|
532
|
+
),
|
|
533
|
+
existing_candidates AS (
|
|
534
|
+
SELECT
|
|
535
|
+
o.companyId,
|
|
536
|
+
CAST(NULL AS INT64) AS crmCompanyId,
|
|
537
|
+
b.companyName,
|
|
538
|
+
o.domain,
|
|
539
|
+
o.source,
|
|
540
|
+
o.type,
|
|
541
|
+
SAFE_CAST(o.total AS INT64) AS hunterEmailCount,
|
|
542
|
+
b.linkedinDomain,
|
|
543
|
+
b.linkedinWebsite
|
|
544
|
+
FROM backlog b
|
|
545
|
+
INNER JOIN \`icpidentifier.SalesPrompter.domainFinder_output\` o
|
|
546
|
+
ON o.companyId = b.companyId
|
|
547
|
+
),
|
|
548
|
+
linkedin_candidates AS (
|
|
549
|
+
SELECT
|
|
550
|
+
companyId,
|
|
551
|
+
CAST(NULL AS INT64) AS crmCompanyId,
|
|
552
|
+
companyName,
|
|
553
|
+
linkedinDomain AS domain,
|
|
554
|
+
"linkedin" AS source,
|
|
555
|
+
"original" AS type,
|
|
556
|
+
hunterEmailCount,
|
|
557
|
+
linkedinDomain,
|
|
558
|
+
linkedinWebsite
|
|
559
|
+
FROM backlog
|
|
560
|
+
WHERE linkedinDomain IS NOT NULL
|
|
561
|
+
),
|
|
562
|
+
website_candidates AS (
|
|
563
|
+
SELECT
|
|
564
|
+
companyId,
|
|
565
|
+
CAST(NULL AS INT64) AS crmCompanyId,
|
|
566
|
+
companyName,
|
|
567
|
+
linkedinWebsite AS domain,
|
|
568
|
+
"linkedin" AS source,
|
|
569
|
+
"website" AS type,
|
|
570
|
+
hunterEmailCount,
|
|
571
|
+
linkedinDomain,
|
|
572
|
+
linkedinWebsite
|
|
573
|
+
FROM backlog
|
|
574
|
+
WHERE linkedinDomain IS NULL
|
|
575
|
+
AND linkedinWebsite IS NOT NULL
|
|
576
|
+
)
|
|
577
|
+
SELECT *
|
|
578
|
+
FROM (
|
|
579
|
+
SELECT * FROM existing_candidates
|
|
580
|
+
UNION ALL
|
|
581
|
+
SELECT * FROM linkedin_candidates
|
|
582
|
+
UNION ALL
|
|
583
|
+
SELECT * FROM website_candidates
|
|
584
|
+
)
|
|
585
|
+
LIMIT ${limit}`;
|
|
586
|
+
}
|
|
587
|
+
export function buildDomainfinderBacklogQueries(market) {
|
|
588
|
+
const countries = marketCountries(market);
|
|
589
|
+
const sqlCountries = sqlCountryList(market);
|
|
590
|
+
const marketClause = sqlCountries.length > 0 ? `WHERE countryCode IN (${sqlCountries})` : "";
|
|
591
|
+
return {
|
|
592
|
+
countries,
|
|
593
|
+
stages: [
|
|
594
|
+
{
|
|
595
|
+
key: "linkedin_companies_backlog",
|
|
596
|
+
description: "Backlog in linkedin_companies that still needs domain processing.",
|
|
597
|
+
sql: `SELECT
|
|
598
|
+
COUNT(*) AS companies,
|
|
599
|
+
COUNTIF(company_emailDomainFinder_toBeProcessed = TRUE) AS to_be_processed,
|
|
600
|
+
COUNTIF(company_emailDomainFinder_toBeProcessed = FALSE) AS processed,
|
|
601
|
+
COUNTIF(company_emailDomainFinder_toBeProcessed = TRUE AND domain_linkedin IS NOT NULL AND domain_linkedin != "") AS to_process_has_linkedin_domain,
|
|
602
|
+
COUNTIF(company_emailDomainFinder_toBeProcessed = TRUE AND website_linkedin IS NOT NULL AND website_linkedin != "") AS to_process_has_linkedin_website
|
|
603
|
+
FROM \`icpidentifier.SalesPrompter.linkedin_companies\`
|
|
604
|
+
${marketClause}`
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
key: "domainfinder_input_view",
|
|
608
|
+
description: "Current domainFinder_input view size.",
|
|
609
|
+
sql: `SELECT COUNT(*) AS rows_in_input FROM \`icpidentifier.SalesPrompter.domainFinder_input\``
|
|
610
|
+
},
|
|
611
|
+
{
|
|
612
|
+
key: "leadpool_bridge",
|
|
613
|
+
description: "Coverage of backlog companies inside SalesGPT.leadPool_new.",
|
|
614
|
+
sql: `WITH to_process AS (
|
|
615
|
+
SELECT id
|
|
616
|
+
FROM \`icpidentifier.SalesPrompter.linkedin_companies\`
|
|
617
|
+
WHERE company_emailDomainFinder_toBeProcessed = TRUE
|
|
618
|
+
${sqlCountries.length > 0 ? `AND countryCode IN (${sqlCountries})` : ""}
|
|
619
|
+
), leadpool_companies AS (
|
|
620
|
+
SELECT DISTINCT linkedin_contacts_companyId AS companyId
|
|
621
|
+
FROM \`icpidentifier.SalesGPT.leadPool_new\`
|
|
622
|
+
WHERE linkedin_contacts_companyId IS NOT NULL
|
|
623
|
+
)
|
|
624
|
+
SELECT
|
|
625
|
+
(SELECT COUNT(*) FROM to_process) AS to_process_total,
|
|
626
|
+
COUNTIF(lp.companyId IS NOT NULL) AS represented_in_leadpool,
|
|
627
|
+
COUNTIF(lp.companyId IS NULL) AS missing_from_leadpool
|
|
628
|
+
FROM to_process tp
|
|
629
|
+
LEFT JOIN leadpool_companies lp ON lp.companyId = tp.id`
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
key: "chosen_vs_linkedin",
|
|
633
|
+
description: "Chosen domain versus LinkedIn domain coverage and mismatch.",
|
|
634
|
+
sql: `SELECT
|
|
635
|
+
COUNT(*) AS companies,
|
|
636
|
+
COUNTIF(domain IS NOT NULL AND domain != "") AS with_chosen_domain,
|
|
637
|
+
COUNTIF(domain_linkedin IS NOT NULL AND domain_linkedin != "") AS with_domain_linkedin,
|
|
638
|
+
COUNTIF(website_linkedin IS NOT NULL AND website_linkedin != "") AS with_website_linkedin,
|
|
639
|
+
COUNTIF((domain IS NULL OR domain = "") AND (domain_linkedin IS NOT NULL AND domain_linkedin != "")) AS missing_chosen_has_linkedin,
|
|
640
|
+
COUNTIF(domain IS NOT NULL AND domain != "" AND domain_linkedin IS NOT NULL AND domain_linkedin != "" AND LOWER(domain) != LOWER(domain_linkedin)) AS chosen_differs_from_linkedin
|
|
641
|
+
FROM \`icpidentifier.SalesPrompter.linkedin_companies\`
|
|
642
|
+
${marketClause}`
|
|
643
|
+
}
|
|
644
|
+
],
|
|
645
|
+
notes: [
|
|
646
|
+
"The old Pipedream workflow reads domainFinder_input, which is leadPool-driven and misses most of the linkedin_companies backlog.",
|
|
647
|
+
"A better input source is linkedin_companies where company_emailDomainFinder_toBeProcessed = TRUE.",
|
|
648
|
+
"LinkedIn domain or website should win before OpenAI or Hunter when present and not blacklisted."
|
|
649
|
+
]
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
export function buildExistingDomainAuditQueries(market) {
|
|
653
|
+
const countries = marketCountries(market);
|
|
654
|
+
const sqlCountries = sqlCountryList(market);
|
|
655
|
+
const marketClause = sqlCountries.length > 0 ? `WHERE countryCode IN (${sqlCountries})` : "";
|
|
656
|
+
return {
|
|
657
|
+
countries,
|
|
658
|
+
stages: [
|
|
659
|
+
{
|
|
660
|
+
key: "summary",
|
|
661
|
+
description: "Existing chosen domains versus LinkedIn-derived references.",
|
|
662
|
+
sql: `SELECT
|
|
663
|
+
COUNT(*) AS companies,
|
|
664
|
+
COUNTIF(domain IS NOT NULL AND domain != "") AS with_chosen_domain,
|
|
665
|
+
COUNTIF(domain_linkedin IS NOT NULL AND domain_linkedin != "") AS with_linkedin_domain,
|
|
666
|
+
COUNTIF(website_linkedin IS NOT NULL AND website_linkedin != "") AS with_linkedin_website,
|
|
667
|
+
COUNTIF(LOWER(REGEXP_REPLACE(COALESCE(domain, ''), r'^https?://', '')) LIKE 'www.%') AS chosen_starts_with_www,
|
|
668
|
+
COUNTIF(LOWER(REGEXP_REPLACE(COALESCE(domain, ''), r'^https?://', '')) IN ('linkedin.com','www.linkedin.com','facebook.com','www.facebook.com','linktr.ee','bit.ly')) AS chosen_blacklisted,
|
|
669
|
+
COUNTIF(domain IS NOT NULL AND domain != '' AND domain_linkedin IS NOT NULL AND domain_linkedin != '' AND LOWER(REGEXP_REPLACE(domain, r'^www\\.', '')) != LOWER(REGEXP_REPLACE(domain_linkedin, r'^www\\.', ''))) AS chosen_differs_from_linkedin_domain,
|
|
670
|
+
COUNTIF(domain IS NOT NULL AND domain != '' AND website_linkedin IS NOT NULL AND website_linkedin != '' AND LOWER(REGEXP_REPLACE(REGEXP_EXTRACT(website_linkedin, r'^(?:https?://)?([^/]+)'), r'^www\\.', '')) != LOWER(REGEXP_REPLACE(domain, r'^www\\.', ''))) AS chosen_differs_from_linkedin_website
|
|
671
|
+
FROM \`icpidentifier.SalesPrompter.linkedin_companies\`
|
|
672
|
+
${marketClause}`
|
|
673
|
+
},
|
|
674
|
+
{
|
|
675
|
+
key: "samples",
|
|
676
|
+
description: "Sample problematic chosen domains from the current exposed linkedin_companies view.",
|
|
677
|
+
sql: `SELECT
|
|
678
|
+
id AS companyId,
|
|
679
|
+
name AS companyName,
|
|
680
|
+
countryCode,
|
|
681
|
+
domain,
|
|
682
|
+
domain_linkedin,
|
|
683
|
+
website_linkedin,
|
|
684
|
+
hunter_emailCount
|
|
685
|
+
FROM \`icpidentifier.SalesPrompter.linkedin_companies\`
|
|
686
|
+
${marketClause.length > 0 ? `${marketClause}\n AND` : "WHERE"}
|
|
687
|
+
(
|
|
688
|
+
LOWER(REGEXP_REPLACE(COALESCE(domain, ''), r'^https?://', '')) IN ('linkedin.com','www.linkedin.com','facebook.com','www.facebook.com','linktr.ee','bit.ly')
|
|
689
|
+
OR (
|
|
690
|
+
domain IS NOT NULL AND domain != ''
|
|
691
|
+
AND domain_linkedin IS NOT NULL AND domain_linkedin != ''
|
|
692
|
+
AND LOWER(REGEXP_REPLACE(domain, r'^www\\.', '')) != LOWER(REGEXP_REPLACE(domain_linkedin, r'^www\\.', ''))
|
|
693
|
+
)
|
|
694
|
+
)
|
|
695
|
+
ORDER BY hunter_emailCount DESC NULLS LAST, companyName
|
|
696
|
+
LIMIT 100`
|
|
697
|
+
}
|
|
698
|
+
]
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
export function buildExistingDomainRepairSql(market, traceId, limit, mode) {
|
|
702
|
+
const countries = sqlCountryList(market);
|
|
703
|
+
const marketClause = countries.length > 0 ? `AND comp.countryCode IN (${countries})` : "";
|
|
704
|
+
const repairPredicate = mode === "conservative"
|
|
705
|
+
? `(
|
|
706
|
+
chosenDomain = ''
|
|
707
|
+
OR chosenDomain IN ('linkedin.com','facebook.com','linktr.ee','bit.ly')
|
|
708
|
+
)`
|
|
709
|
+
: mode === "mismatch-only"
|
|
710
|
+
? `(chosenDomain != '' AND chosenDomain != targetDomain)`
|
|
711
|
+
: `(
|
|
712
|
+
chosenDomain = ''
|
|
713
|
+
OR chosenDomain IN ('linkedin.com','facebook.com','linktr.ee','bit.ly')
|
|
714
|
+
OR chosenDomain != targetDomain
|
|
715
|
+
)`;
|
|
716
|
+
return `INSERT INTO \`icpidentifier.SalesPrompter.domainFinder_output\`
|
|
717
|
+
(companyId, type, name, source, total, ts, trace_id, domain, crm_companyId)
|
|
718
|
+
WITH normalized AS (
|
|
719
|
+
SELECT
|
|
720
|
+
comp.id AS companyId,
|
|
721
|
+
comp.name AS companyName,
|
|
722
|
+
comp.hunter_emailCount AS total,
|
|
723
|
+
LOWER(REGEXP_REPLACE(REGEXP_REPLACE(COALESCE(comp.domain, ''), r'^https?://', ''), r'^www\\.', '')) AS chosenDomain,
|
|
724
|
+
LOWER(REGEXP_REPLACE(COALESCE(comp.domain_linkedin, ''), r'^www\\.', '')) AS linkedinDomain,
|
|
725
|
+
LOWER(REGEXP_REPLACE(REGEXP_EXTRACT(COALESCE(comp.website_linkedin, ''), r'^(?:https?://)?([^/]+)'), r'^www\\.', '')) AS linkedinWebsiteHost
|
|
726
|
+
FROM \`icpidentifier.SalesPrompter.linkedin_companies\` comp
|
|
727
|
+
WHERE 1=1
|
|
728
|
+
${marketClause}
|
|
729
|
+
),
|
|
730
|
+
targeted AS (
|
|
731
|
+
SELECT
|
|
732
|
+
companyId,
|
|
733
|
+
companyName,
|
|
734
|
+
total,
|
|
735
|
+
chosenDomain,
|
|
736
|
+
CASE
|
|
737
|
+
WHEN linkedinDomain != '' AND linkedinDomain NOT IN ('linkedin.com','facebook.com','linktr.ee','bit.ly')
|
|
738
|
+
THEN linkedinDomain
|
|
739
|
+
WHEN linkedinWebsiteHost != '' AND linkedinWebsiteHost NOT IN ('linkedin.com','facebook.com','linktr.ee','bit.ly')
|
|
740
|
+
THEN linkedinWebsiteHost
|
|
741
|
+
ELSE NULL
|
|
742
|
+
END AS targetDomain
|
|
743
|
+
FROM normalized
|
|
744
|
+
),
|
|
745
|
+
to_repair AS (
|
|
746
|
+
SELECT *
|
|
747
|
+
FROM targeted
|
|
748
|
+
WHERE targetDomain IS NOT NULL
|
|
749
|
+
AND ${repairPredicate}
|
|
750
|
+
ORDER BY total DESC NULLS LAST, companyName
|
|
751
|
+
LIMIT ${limit}
|
|
752
|
+
)
|
|
753
|
+
SELECT
|
|
754
|
+
companyId,
|
|
755
|
+
'repair-linkedin' AS type,
|
|
756
|
+
companyName AS name,
|
|
757
|
+
'repair' AS source,
|
|
758
|
+
total,
|
|
759
|
+
CURRENT_TIMESTAMP() AS ts,
|
|
760
|
+
'${traceId.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}' AS trace_id,
|
|
761
|
+
targetDomain AS domain,
|
|
762
|
+
CAST(NULL AS INT64) AS crm_companyId
|
|
763
|
+
FROM to_repair;`;
|
|
764
|
+
}
|