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.
Files changed (45) hide show
  1. package/CHANGELOG.md +447 -0
  2. package/CODE_OF_CONDUCT.md +35 -0
  3. package/CONTRIBUTING.md +7 -0
  4. package/README.md +14 -2
  5. package/RESEARCH.md +346 -0
  6. package/SECURITY.md +35 -0
  7. package/dist/affiliate-copy.js +49 -0
  8. package/dist/auth.js +453 -0
  9. package/dist/bigquery.js +199 -0
  10. package/dist/chrome-browser.js +231 -0
  11. package/dist/cli.js +19652 -0
  12. package/dist/company-identity-review.js +78 -0
  13. package/dist/company-leads.js +422 -0
  14. package/dist/company-recovery.js +84 -0
  15. package/dist/deel-outreach.js +469 -0
  16. package/dist/deel-salesnav.js +368 -0
  17. package/dist/direct-path.js +326 -0
  18. package/dist/domain.js +53 -0
  19. package/dist/domainfinder.js +764 -0
  20. package/dist/engine.js +216 -0
  21. package/dist/historical-queries.js +189 -0
  22. package/dist/hunter-emailfinder.js +252 -0
  23. package/dist/icp-templates.js +171 -0
  24. package/dist/indiecrm/commands.js +108 -0
  25. package/dist/indiecrm-cli.js +2 -104
  26. package/dist/instantly.js +136 -0
  27. package/dist/io.js +21 -0
  28. package/dist/leadlists-funnel.js +148 -0
  29. package/dist/linkedin-companies.js +562 -0
  30. package/dist/linkedin-product-details.js +1203 -0
  31. package/dist/linkedin-product-search.js +1081 -0
  32. package/dist/linkedin-products.js +786 -0
  33. package/dist/linkedin-session-contracts.js +3 -0
  34. package/dist/linkedin-session.js +846 -0
  35. package/dist/providers.js +1 -0
  36. package/dist/research-browser-preference.js +37 -0
  37. package/dist/sales-navigator.js +1231 -0
  38. package/dist/salesnav-backfill.js +710 -0
  39. package/dist/sample-data.js +34 -0
  40. package/dist/session-recovery.js +62 -0
  41. package/dist/vendor/salesprompter-shared/extension-session-contracts.js +29 -0
  42. package/dist/vendor/salesprompter-shared/linkedin-session.js +22 -0
  43. package/dist/vendor/salesprompter-shared/phantombuster-contracts.js +16 -0
  44. package/dist/vendor/salesprompter-shared/session-vault-contracts.js +17 -0
  45. 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
+ }