salesprompter-cli 0.1.22 → 0.1.23
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/README.md +6 -0
- package/dist/cli.js +379 -0
- package/dist/deel-outreach.js +454 -0
- package/dist/deel-salesnav.js +368 -0
- package/dist/direct-path.js +6 -5
- package/dist/domain.js +3 -0
- package/dist/instantly.js +2 -1
- package/dist/leadlists-funnel.js +30 -13
- package/package.json +10 -17
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
import { classifyDirectPathSegment } from "./direct-path.js";
|
|
2
|
+
import { parseHistoricalQuery } from "./historical-queries.js";
|
|
3
|
+
function marketCountries(market) {
|
|
4
|
+
if (market === "dach") {
|
|
5
|
+
return ["DE", "AT", "CH"];
|
|
6
|
+
}
|
|
7
|
+
if (market === "europe") {
|
|
8
|
+
return ["DE", "AT", "CH", "NL", "GB", "FR", "SE", "DK"];
|
|
9
|
+
}
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
function sqlStringList(values) {
|
|
13
|
+
return values.map((value) => `'${value.replaceAll("'", "\\'")}'`).join(", ");
|
|
14
|
+
}
|
|
15
|
+
function broadTitlePredicateSql(field) {
|
|
16
|
+
return [
|
|
17
|
+
`LOWER(${field}) LIKE '%head of hr%'`,
|
|
18
|
+
`LOWER(${field}) LIKE '%head of human resources%'`,
|
|
19
|
+
`LOWER(${field}) LIKE '%head of people%'`,
|
|
20
|
+
`LOWER(${field}) LIKE '%head of payroll%'`,
|
|
21
|
+
`LOWER(${field}) LIKE '%people services%'`,
|
|
22
|
+
`LOWER(${field}) LIKE '%shared services%'`,
|
|
23
|
+
`LOWER(${field}) LIKE '%total rewards%'`,
|
|
24
|
+
`LOWER(${field}) LIKE '%compensation%'`,
|
|
25
|
+
`LOWER(${field}) LIKE '%benefits%'`,
|
|
26
|
+
`LOWER(${field}) LIKE '%hr business partner%'`,
|
|
27
|
+
`LOWER(${field}) LIKE '%hrbp%'`,
|
|
28
|
+
`LOWER(${field}) LIKE '%people operations%'`,
|
|
29
|
+
`LOWER(${field}) LIKE '%people ops%'`,
|
|
30
|
+
`LOWER(${field}) LIKE '%hr operations%'`,
|
|
31
|
+
`LOWER(${field}) LIKE '%hr administration%'`,
|
|
32
|
+
`LOWER(${field}) LIKE '%hr director%'`,
|
|
33
|
+
`LOWER(${field}) LIKE '%director hr%'`,
|
|
34
|
+
`LOWER(${field}) LIKE '%chief people officer%'`,
|
|
35
|
+
`LOWER(${field}) LIKE '%chro%'`,
|
|
36
|
+
`LOWER(${field}) LIKE '%leiter personal%'`,
|
|
37
|
+
`LOWER(${field}) LIKE '%leiter der personalabteilung%'`,
|
|
38
|
+
`LOWER(${field}) LIKE '%personalleitung%'`,
|
|
39
|
+
`LOWER(${field}) LIKE '%bereichsleiter personal%'`,
|
|
40
|
+
`LOWER(${field}) LIKE '%geschäftsbereichsleiter personal%'`,
|
|
41
|
+
`LOWER(${field}) LIKE '%geschaeftsbereichsleiter personal%'`
|
|
42
|
+
].join("\n OR ");
|
|
43
|
+
}
|
|
44
|
+
function titleExclusionPredicateSql(field) {
|
|
45
|
+
return [
|
|
46
|
+
`LOWER(${field}) NOT LIKE '%assistant%'`,
|
|
47
|
+
`LOWER(${field}) NOT LIKE '%assistent%'`,
|
|
48
|
+
`LOWER(${field}) NOT LIKE '%assistentin%'`,
|
|
49
|
+
`LOWER(${field}) NOT LIKE '%recruitment%'`,
|
|
50
|
+
`LOWER(${field}) NOT LIKE '%recruiter%'`,
|
|
51
|
+
`LOWER(${field}) NOT LIKE '%talent acquisition specialist%'`
|
|
52
|
+
].join("\n AND ");
|
|
53
|
+
}
|
|
54
|
+
export function buildDeelOutreachExportSql(options) {
|
|
55
|
+
const countries = marketCountries(options.market);
|
|
56
|
+
const countryClause = countries.length > 0 ? `UPPER(CAST(p.company_countryCode AS STRING)) IN (${sqlStringList(countries)})` : "TRUE";
|
|
57
|
+
return `WITH hr_leadlists AS (
|
|
58
|
+
SELECT
|
|
59
|
+
CAST(leadListId AS STRING) AS leadListId,
|
|
60
|
+
CAST(query AS STRING) AS query,
|
|
61
|
+
leadList_container_ts
|
|
62
|
+
FROM \`icpidentifier.SalesPrompter.leadLists_raw\`
|
|
63
|
+
WHERE query IS NOT NULL
|
|
64
|
+
AND LOWER(CAST(query AS STRING)) LIKE '%/sales/search/people%'
|
|
65
|
+
AND REGEXP_CONTAINS(LOWER(CAST(query AS STRING)), r'type%3afunction[^)]*id%3a12[^)]*selectiontype%3aincluded')
|
|
66
|
+
AND NOT REGEXP_CONTAINS(LOWER(CAST(query AS STRING)), r'id%3a12[^)]*selectiontype%3aexcluded')
|
|
67
|
+
QUALIFY ROW_NUMBER() OVER (
|
|
68
|
+
PARTITION BY CAST(leadListId AS STRING)
|
|
69
|
+
ORDER BY leadList_container_ts DESC
|
|
70
|
+
) = 1
|
|
71
|
+
)
|
|
72
|
+
SELECT
|
|
73
|
+
CAST(p.leadListId AS STRING) AS leadListId,
|
|
74
|
+
CAST(p.contactId AS STRING) AS contactId,
|
|
75
|
+
CAST(p.linkedin_contacts_companyId AS STRING) AS companyId,
|
|
76
|
+
CAST(p.firstName_cleaned AS STRING) AS firstName,
|
|
77
|
+
CAST(p.lastName_cleaned AS STRING) AS lastName,
|
|
78
|
+
CONCAT(
|
|
79
|
+
COALESCE(CAST(p.firstName_cleaned AS STRING), ''),
|
|
80
|
+
IF(p.firstName_cleaned IS NOT NULL AND p.lastName_cleaned IS NOT NULL, ' ', ''),
|
|
81
|
+
COALESCE(CAST(p.lastName_cleaned AS STRING), '')
|
|
82
|
+
) AS fullName,
|
|
83
|
+
CAST(p.jobTitle AS STRING) AS jobTitle,
|
|
84
|
+
CAST(p.companyName AS STRING) AS companyName,
|
|
85
|
+
CAST(p.linkedin_companies_handle AS STRING) AS linkedin_companies_handle,
|
|
86
|
+
CONCAT('https://www.linkedin.com/company/', CAST(p.linkedin_contacts_companyId AS STRING)) AS companyUrl,
|
|
87
|
+
CAST(p.companySize AS STRING) AS companySize,
|
|
88
|
+
CAST(p.company_countryCode AS STRING) AS countryCode,
|
|
89
|
+
CAST(p.headquarters AS STRING) AS headquarters,
|
|
90
|
+
CAST(p.domain AS STRING) AS domain,
|
|
91
|
+
CAST(p.domain_linkedin AS STRING) AS domain_linkedin,
|
|
92
|
+
CAST(p.industry AS STRING) AS industry,
|
|
93
|
+
CAST(p.contact_location AS STRING) AS contact_location,
|
|
94
|
+
CAST(p.contact_companyLocation AS STRING) AS contact_companyLocation,
|
|
95
|
+
SAFE_CAST(p.tenureAtCompany AS FLOAT64) AS tenureAtCompany,
|
|
96
|
+
SAFE_CAST(p.tenureAtPosition AS FLOAT64) AS tenureAtPosition,
|
|
97
|
+
CAST(p.contact_summary AS STRING) AS contact_summary,
|
|
98
|
+
CAST(p.contact_titleDescription AS STRING) AS contact_titleDescription,
|
|
99
|
+
CAST(p.leadList_container_ts AS STRING) AS leadList_container_ts,
|
|
100
|
+
CAST(p.timestamp AS STRING) AS company_ts,
|
|
101
|
+
l.query AS leadListQuery,
|
|
102
|
+
CAST(p.email AS STRING) AS email,
|
|
103
|
+
SAFE_CAST(p.emailScore AS INT64) AS emailScore,
|
|
104
|
+
SAFE_CAST(p.gender AS INT64) AS gender,
|
|
105
|
+
COALESCE(p.emails_accept_all, FALSE) AS emails_accept_all
|
|
106
|
+
FROM \`icpidentifier.SalesGPT.leadPool_new\` p
|
|
107
|
+
JOIN hr_leadlists l ON CAST(p.leadListId AS STRING) = l.leadListId
|
|
108
|
+
WHERE CAST(p.functionId AS STRING) = '12'
|
|
109
|
+
AND ${countryClause}
|
|
110
|
+
AND CAST(p.email AS STRING) IS NOT NULL
|
|
111
|
+
AND COALESCE(p.email_invalid, FALSE) = FALSE
|
|
112
|
+
AND COALESCE(p.email_bounced, FALSE) = FALSE
|
|
113
|
+
AND COALESCE(p.company_blacklisted, FALSE) = FALSE
|
|
114
|
+
AND COALESCE(p.company_inSequence, FALSE) = FALSE
|
|
115
|
+
AND COALESCE(p.contact_replied, FALSE) = FALSE
|
|
116
|
+
AND COALESCE(p.contact_bounced, FALSE) = FALSE
|
|
117
|
+
AND COALESCE(p.jobTitle_blacklisted, FALSE) = FALSE
|
|
118
|
+
AND COALESCE(p.domainBlacklisted, FALSE) = FALSE
|
|
119
|
+
AND CAST(p.domain AS STRING) IS NOT NULL
|
|
120
|
+
AND CAST(p.firstName_cleaned AS STRING) IS NOT NULL
|
|
121
|
+
AND CAST(p.lastName_cleaned AS STRING) IS NOT NULL
|
|
122
|
+
AND SAFE_CAST(p.emailScore AS INT64) >= ${Math.trunc(options.minEmailScore)}
|
|
123
|
+
AND CAST(p.companySize AS STRING) IN ('200-499', '501-1000', '1001-5000', '5001-10.000', '10.000+')
|
|
124
|
+
AND (
|
|
125
|
+
${broadTitlePredicateSql("CAST(p.jobTitle AS STRING)")}
|
|
126
|
+
)
|
|
127
|
+
AND ${titleExclusionPredicateSql("CAST(p.jobTitle AS STRING)")}
|
|
128
|
+
QUALIFY ROW_NUMBER() OVER (
|
|
129
|
+
PARTITION BY LOWER(CAST(p.email AS STRING))
|
|
130
|
+
ORDER BY SAFE_CAST(p.emailScore AS INT64) DESC NULLS LAST, p.timestamp DESC, p.contact_ts DESC
|
|
131
|
+
) = 1
|
|
132
|
+
ORDER BY SAFE_CAST(p.emailScore AS INT64) DESC NULLS LAST, p.timestamp DESC
|
|
133
|
+
LIMIT ${Math.trunc(options.limit)}`;
|
|
134
|
+
}
|
|
135
|
+
function toNullableString(value) {
|
|
136
|
+
if (value === null || value === undefined) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
const normalized = String(value).trim();
|
|
140
|
+
return normalized.length > 0 ? normalized : null;
|
|
141
|
+
}
|
|
142
|
+
function toNumber(value) {
|
|
143
|
+
if (value === null || value === undefined) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
const numeric = Number(value);
|
|
147
|
+
return Number.isFinite(numeric) ? numeric : null;
|
|
148
|
+
}
|
|
149
|
+
function toBoolean(value) {
|
|
150
|
+
if (value === null || value === undefined) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
if (typeof value === "boolean") {
|
|
154
|
+
return value;
|
|
155
|
+
}
|
|
156
|
+
const normalized = String(value).trim().toLowerCase();
|
|
157
|
+
if (normalized === "true") {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
if (normalized === "false") {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
function normalizeCountryCode(value) {
|
|
166
|
+
return value === null ? null : value.trim().toUpperCase();
|
|
167
|
+
}
|
|
168
|
+
function isDachCountry(countryCode) {
|
|
169
|
+
return ["DE", "AT", "CH"].includes(countryCode ?? "");
|
|
170
|
+
}
|
|
171
|
+
function employeeCountFromBucket(bucket) {
|
|
172
|
+
switch ((bucket ?? "").trim()) {
|
|
173
|
+
case "200-499":
|
|
174
|
+
return 350;
|
|
175
|
+
case "501-1000":
|
|
176
|
+
return 750;
|
|
177
|
+
case "1001-5000":
|
|
178
|
+
return 2500;
|
|
179
|
+
case "5001-10.000":
|
|
180
|
+
return 7500;
|
|
181
|
+
case "10.000+":
|
|
182
|
+
return 10000;
|
|
183
|
+
default:
|
|
184
|
+
return 500;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
export function buildDeelLeadPoolContactSql(options) {
|
|
188
|
+
const normalizedIds = options.contactIds
|
|
189
|
+
.map((contactId) => contactId.trim())
|
|
190
|
+
.filter((contactId) => contactId.length > 0);
|
|
191
|
+
if (normalizedIds.length === 0) {
|
|
192
|
+
throw new Error("At least one contactId is required to build the leadPool contact SQL.");
|
|
193
|
+
}
|
|
194
|
+
const contactList = normalizedIds.map((id) => `'${id.replaceAll("'", "\\'")}'`).join(", ");
|
|
195
|
+
const minScore = options.minEmailScore ?? 0;
|
|
196
|
+
return `SELECT
|
|
197
|
+
CAST(p.leadListId AS STRING) AS leadListId,
|
|
198
|
+
CAST(p.contactId AS STRING) AS contactId,
|
|
199
|
+
CAST(p.linkedin_contacts_companyId AS STRING) AS companyId,
|
|
200
|
+
CAST(p.firstName_cleaned AS STRING) AS firstName,
|
|
201
|
+
CAST(p.lastName_cleaned AS STRING) AS lastName,
|
|
202
|
+
CONCAT(
|
|
203
|
+
COALESCE(CAST(p.firstName_cleaned AS STRING), ''),
|
|
204
|
+
IF(p.firstName_cleaned IS NOT NULL AND p.lastName_cleaned IS NOT NULL, ' ', ''),
|
|
205
|
+
COALESCE(CAST(p.lastName_cleaned AS STRING), '')
|
|
206
|
+
) AS fullName,
|
|
207
|
+
CAST(p.jobTitle AS STRING) AS jobTitle,
|
|
208
|
+
CAST(p.companyName AS STRING) AS companyName,
|
|
209
|
+
CAST(p.linkedin_companies_handle AS STRING) AS linkedin_companies_handle,
|
|
210
|
+
CONCAT('https://www.linkedin.com/company/', CAST(p.linkedin_contacts_companyId AS STRING)) AS companyUrl,
|
|
211
|
+
CAST(p.companySize AS STRING) AS companySize,
|
|
212
|
+
CAST(p.company_countryCode AS STRING) AS countryCode,
|
|
213
|
+
CAST(p.headquarters AS STRING) AS headquarters,
|
|
214
|
+
CAST(p.domain AS STRING) AS domain,
|
|
215
|
+
CAST(p.domain_linkedin AS STRING) AS domain_linkedin,
|
|
216
|
+
CAST(p.industry AS STRING) AS industry,
|
|
217
|
+
CAST(p.contact_location AS STRING) AS contact_location,
|
|
218
|
+
CAST(p.contact_companyLocation AS STRING) AS contact_companyLocation,
|
|
219
|
+
SAFE_CAST(p.tenureAtCompany AS FLOAT64) AS tenureAtCompany,
|
|
220
|
+
SAFE_CAST(p.tenureAtPosition AS FLOAT64) AS tenureAtPosition,
|
|
221
|
+
CAST(p.contact_summary AS STRING) AS contact_summary,
|
|
222
|
+
CAST(p.contact_titleDescription AS STRING) AS contact_titleDescription,
|
|
223
|
+
CAST(p.leadList_container_ts AS STRING) AS leadList_container_ts,
|
|
224
|
+
CAST(p.timestamp AS STRING) AS company_ts,
|
|
225
|
+
CAST(p.email AS STRING) AS email,
|
|
226
|
+
SAFE_CAST(p.emailScore AS INT64) AS emailScore,
|
|
227
|
+
SAFE_CAST(p.gender AS INT64) AS gender,
|
|
228
|
+
COALESCE(p.emails_accept_all, FALSE) AS emails_accept_all,
|
|
229
|
+
CAST(p.jobTitle AS STRING) AS jobTitleRaw,
|
|
230
|
+
CAST(p.domain AS STRING) AS domainRaw
|
|
231
|
+
FROM \`icpidentifier.SalesGPT.leadPool_new\` p
|
|
232
|
+
WHERE CAST(p.contactId AS STRING) IN (${contactList})
|
|
233
|
+
AND CAST(p.email AS STRING) IS NOT NULL
|
|
234
|
+
AND COALESCE(p.email_invalid, FALSE) = FALSE
|
|
235
|
+
AND COALESCE(p.email_bounced, FALSE) = FALSE
|
|
236
|
+
AND COALESCE(p.company_blacklisted, FALSE) = FALSE
|
|
237
|
+
AND COALESCE(p.company_inSequence, FALSE) = FALSE
|
|
238
|
+
AND COALESCE(p.contact_replied, FALSE) = FALSE
|
|
239
|
+
AND COALESCE(p.contact_bounced, FALSE) = FALSE
|
|
240
|
+
AND COALESCE(p.jobTitle_blacklisted, FALSE) = FALSE
|
|
241
|
+
AND COALESCE(p.domainBlacklisted, FALSE) = FALSE
|
|
242
|
+
AND CAST(p.domain AS STRING) IS NOT NULL
|
|
243
|
+
AND CAST(p.firstName_cleaned AS STRING) IS NOT NULL
|
|
244
|
+
AND CAST(p.lastName_cleaned AS STRING) IS NOT NULL
|
|
245
|
+
AND SAFE_CAST(p.emailScore AS INT64) >= ${Math.trunc(minScore)}
|
|
246
|
+
AND CAST(p.companySize AS STRING) IN ('200-499', '501-1000', '1001-5000', '5001-10.000', '10.000+')
|
|
247
|
+
QUALIFY ROW_NUMBER() OVER (
|
|
248
|
+
PARTITION BY LOWER(CAST(p.email AS STRING))
|
|
249
|
+
ORDER BY SAFE_CAST(p.emailScore AS INT64) DESC NULLS LAST, p.timestamp DESC, p.contact_ts DESC
|
|
250
|
+
) = 1
|
|
251
|
+
ORDER BY RANDOM()`;
|
|
252
|
+
}
|
|
253
|
+
function buildGermanSalutation(row) {
|
|
254
|
+
const lastName = row.lastName?.trim() ?? "";
|
|
255
|
+
const firstName = row.firstName?.trim() ?? "";
|
|
256
|
+
if (row.gender === 1 && lastName.length > 0) {
|
|
257
|
+
return `Sehr geehrter Herr ${lastName},`;
|
|
258
|
+
}
|
|
259
|
+
if (row.gender === 2 && lastName.length > 0) {
|
|
260
|
+
return `Sehr geehrte Frau ${lastName},`;
|
|
261
|
+
}
|
|
262
|
+
if (firstName.length > 0 || lastName.length > 0) {
|
|
263
|
+
return `Guten Tag ${[firstName, lastName].filter((value) => value.length > 0).join(" ")},`;
|
|
264
|
+
}
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
function deriveLanguage(countryCode) {
|
|
268
|
+
return isDachCountry(countryCode) ? "de" : "en";
|
|
269
|
+
}
|
|
270
|
+
function deriveRegion(countryCode) {
|
|
271
|
+
if (isDachCountry(countryCode)) {
|
|
272
|
+
return "DACH";
|
|
273
|
+
}
|
|
274
|
+
return countryCode ?? "GLOBAL";
|
|
275
|
+
}
|
|
276
|
+
function computeLeadScore(row) {
|
|
277
|
+
const emailScore = row.emailScore ?? 70;
|
|
278
|
+
const segment = row.segment ?? "broader-hr";
|
|
279
|
+
const segmentBonus = segment === "leadership" ? 10 : segment === "payroll-people-services" ? 8 : 4;
|
|
280
|
+
const acceptAllPenalty = row.emails_accept_all ? 4 : 0;
|
|
281
|
+
const tenureBonus = (row.tenureAtCompany ?? 0) >= 1 ? 2 : 0;
|
|
282
|
+
return Math.max(60, Math.min(99, emailScore + segmentBonus + tenureBonus - acceptAllPenalty));
|
|
283
|
+
}
|
|
284
|
+
function gradeFromScore(score) {
|
|
285
|
+
if (score >= 90) {
|
|
286
|
+
return "A";
|
|
287
|
+
}
|
|
288
|
+
if (score >= 80) {
|
|
289
|
+
return "B";
|
|
290
|
+
}
|
|
291
|
+
if (score >= 70) {
|
|
292
|
+
return "C";
|
|
293
|
+
}
|
|
294
|
+
return "D";
|
|
295
|
+
}
|
|
296
|
+
export function normalizeDeelOutreachRows(rows) {
|
|
297
|
+
return rows.map((row) => {
|
|
298
|
+
const leadListQuery = toNullableString(row.leadListQuery);
|
|
299
|
+
const parsedQuery = leadListQuery === null ? null : parseHistoricalQuery(leadListQuery);
|
|
300
|
+
const countryCode = normalizeCountryCode(toNullableString(row.countryCode));
|
|
301
|
+
return {
|
|
302
|
+
leadListId: toNullableString(row.leadListId),
|
|
303
|
+
contactId: toNullableString(row.contactId),
|
|
304
|
+
companyId: toNullableString(row.companyId),
|
|
305
|
+
firstName: toNullableString(row.firstName),
|
|
306
|
+
lastName: toNullableString(row.lastName),
|
|
307
|
+
fullName: toNullableString(row.fullName),
|
|
308
|
+
jobTitle: toNullableString(row.jobTitle),
|
|
309
|
+
companyName: toNullableString(row.companyName),
|
|
310
|
+
linkedin_companies_handle: toNullableString(row.linkedin_companies_handle),
|
|
311
|
+
companyUrl: toNullableString(row.companyUrl),
|
|
312
|
+
companySize: toNullableString(row.companySize),
|
|
313
|
+
countryCode,
|
|
314
|
+
headquarters: toNullableString(row.headquarters),
|
|
315
|
+
domain: toNullableString(row.domain),
|
|
316
|
+
domain_linkedin: toNullableString(row.domain_linkedin),
|
|
317
|
+
industry: toNullableString(row.industry),
|
|
318
|
+
contact_location: toNullableString(row.contact_location),
|
|
319
|
+
contact_companyLocation: toNullableString(row.contact_companyLocation),
|
|
320
|
+
tenureAtCompany: toNumber(row.tenureAtCompany),
|
|
321
|
+
tenureAtPosition: toNumber(row.tenureAtPosition),
|
|
322
|
+
contact_summary: toNullableString(row.contact_summary),
|
|
323
|
+
contact_titleDescription: toNullableString(row.contact_titleDescription),
|
|
324
|
+
leadList_container_ts: toNullableString(row.leadList_container_ts),
|
|
325
|
+
company_ts: toNullableString(row.company_ts),
|
|
326
|
+
leadListQuery,
|
|
327
|
+
leadListQueryDecoded: parsedQuery?.decodedQuery ?? null,
|
|
328
|
+
leadListFunctionFilters: parsedQuery?.functionFilters.filter((filter) => filter.selection === "INCLUDED").map((filter) => filter.text) ?? [],
|
|
329
|
+
leadListTitleFilters: parsedQuery?.titleFilters.filter((filter) => filter.selection === "INCLUDED").map((filter) => filter.text) ?? [],
|
|
330
|
+
leadListRegionFilters: parsedQuery?.regionFilters.filter((filter) => filter.selection === "INCLUDED").map((filter) => filter.text) ?? [],
|
|
331
|
+
leadListHeadquartersFilters: parsedQuery?.headquartersFilters
|
|
332
|
+
.filter((filter) => filter.selection === "INCLUDED")
|
|
333
|
+
.map((filter) => filter.text) ?? [],
|
|
334
|
+
leadListHeadcountFilters: parsedQuery?.headcountFilters.filter((filter) => filter.selection === "INCLUDED").map((filter) => filter.text) ?? [],
|
|
335
|
+
email: toNullableString(row.email),
|
|
336
|
+
emailScore: toNumber(row.emailScore),
|
|
337
|
+
gender: toNumber(row.gender),
|
|
338
|
+
emails_accept_all: toBoolean(row.emails_accept_all),
|
|
339
|
+
language: deriveLanguage(countryCode),
|
|
340
|
+
marketSegment: isDachCountry(countryCode) ? "dach" : "non-dach"
|
|
341
|
+
};
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
function rowToScoredLead(row) {
|
|
345
|
+
const contactName = row.fullName?.trim() ||
|
|
346
|
+
[row.firstName, row.lastName].filter((value) => value !== null && value.length > 0).join(" ").trim();
|
|
347
|
+
const title = row.jobTitle?.trim() ?? "";
|
|
348
|
+
const { segment, reason } = classifyDirectPathSegment(title);
|
|
349
|
+
const score = computeLeadScore({ ...row, segment, segmentReason: reason });
|
|
350
|
+
return {
|
|
351
|
+
companyName: row.companyName ?? row.domain ?? "Unknown Company",
|
|
352
|
+
domain: row.domain ?? row.domain_linkedin ?? "unknown.example",
|
|
353
|
+
industry: row.industry ?? "Unknown",
|
|
354
|
+
region: deriveRegion(row.countryCode),
|
|
355
|
+
employeeCount: employeeCountFromBucket(row.companySize),
|
|
356
|
+
contactName: contactName.length > 0 ? contactName : row.email ?? "Unknown Contact",
|
|
357
|
+
title: title.length > 0 ? title : "Human Resources",
|
|
358
|
+
email: row.email ?? "unknown@example.com",
|
|
359
|
+
source: "salesprompter-deel-leadlists",
|
|
360
|
+
signals: [
|
|
361
|
+
`language:${row.language}`,
|
|
362
|
+
`market:${row.marketSegment}`,
|
|
363
|
+
`segment:${segment}`,
|
|
364
|
+
row.countryCode ? `country:${row.countryCode}` : null
|
|
365
|
+
].filter((value) => value !== null),
|
|
366
|
+
techStack: [],
|
|
367
|
+
crmFit: segment === "broader-hr" ? "medium" : "high",
|
|
368
|
+
outreachFit: score >= 85 ? "high" : "medium",
|
|
369
|
+
buyingStage: segment === "payroll-people-services" ? "solution-aware" : "problem-aware",
|
|
370
|
+
notes: [
|
|
371
|
+
row.leadListId ? `leadListId:${row.leadListId}` : null,
|
|
372
|
+
row.leadListRegionFilters.length > 0 ? `regions:${row.leadListRegionFilters.join(", ")}` : null
|
|
373
|
+
].filter((value) => value !== null),
|
|
374
|
+
score,
|
|
375
|
+
grade: gradeFromScore(score),
|
|
376
|
+
rationale: [
|
|
377
|
+
`segment ${segment}: ${reason}`,
|
|
378
|
+
row.emailScore !== null ? `email score ${row.emailScore}` : "email score unavailable",
|
|
379
|
+
row.language === "de" ? "DACH lead routed to German outreach" : "non-DACH lead routed to English outreach"
|
|
380
|
+
],
|
|
381
|
+
customVariables: {
|
|
382
|
+
language: row.language,
|
|
383
|
+
salesprompter_language: row.language,
|
|
384
|
+
salesprompter_market_scope: row.marketSegment,
|
|
385
|
+
salesprompter_country_code: row.countryCode,
|
|
386
|
+
salesprompter_segment: segment,
|
|
387
|
+
salesprompter_segment_reason: reason,
|
|
388
|
+
salesprompter_email_score: row.emailScore,
|
|
389
|
+
salesprompter_emails_accept_all: row.emails_accept_all,
|
|
390
|
+
salesprompter_leadlist_id: row.leadListId,
|
|
391
|
+
salesprompter_leadlist_regions: row.leadListRegionFilters.join(", "),
|
|
392
|
+
salesprompter_leadlist_titles: row.leadListTitleFilters.join(", "),
|
|
393
|
+
salesprompter_leadlist_functions: row.leadListFunctionFilters.join(", "),
|
|
394
|
+
salesprompter_linkedin_company_handle: row.linkedin_companies_handle,
|
|
395
|
+
gender: row.gender,
|
|
396
|
+
anrede_zeile: row.language === "de" ? buildGermanSalutation(row) : null
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function average(values) {
|
|
401
|
+
if (values.length === 0) {
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
const total = values.reduce((sum, value) => sum + value, 0);
|
|
405
|
+
return Number((total / values.length).toFixed(2));
|
|
406
|
+
}
|
|
407
|
+
export function buildDeelOutreachPack(market, rows) {
|
|
408
|
+
const locales = {
|
|
409
|
+
de: [],
|
|
410
|
+
en: []
|
|
411
|
+
};
|
|
412
|
+
const countries = new Map();
|
|
413
|
+
const titles = new Map();
|
|
414
|
+
const segmentCounts = {
|
|
415
|
+
leadership: 0,
|
|
416
|
+
"payroll-people-services": 0,
|
|
417
|
+
"broader-hr": 0
|
|
418
|
+
};
|
|
419
|
+
const emailScores = {
|
|
420
|
+
de: [],
|
|
421
|
+
en: []
|
|
422
|
+
};
|
|
423
|
+
for (const row of rows) {
|
|
424
|
+
const { segment } = classifyDirectPathSegment(row.jobTitle ?? "");
|
|
425
|
+
segmentCounts[segment] += 1;
|
|
426
|
+
const country = row.countryCode ?? "unknown";
|
|
427
|
+
countries.set(country, (countries.get(country) ?? 0) + 1);
|
|
428
|
+
const title = row.jobTitle ?? "unknown";
|
|
429
|
+
titles.set(title, (titles.get(title) ?? 0) + 1);
|
|
430
|
+
if (row.emailScore !== null) {
|
|
431
|
+
emailScores[row.language].push(row.emailScore);
|
|
432
|
+
}
|
|
433
|
+
locales[row.language].push(rowToScoredLead({ ...row, segment }));
|
|
434
|
+
}
|
|
435
|
+
return {
|
|
436
|
+
vendor: "deel",
|
|
437
|
+
market,
|
|
438
|
+
total: rows.length,
|
|
439
|
+
locales,
|
|
440
|
+
summary: {
|
|
441
|
+
localeCounts: {
|
|
442
|
+
de: locales.de.length,
|
|
443
|
+
en: locales.en.length
|
|
444
|
+
},
|
|
445
|
+
segmentCounts,
|
|
446
|
+
countries: [...countries.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])),
|
|
447
|
+
averageEmailScoreByLocale: {
|
|
448
|
+
de: average(emailScores.de),
|
|
449
|
+
en: average(emailScores.en)
|
|
450
|
+
},
|
|
451
|
+
topTitles: [...titles.entries()].sort((a, b) => b[1] - a[1]).slice(0, 20)
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
}
|