salesprompter-cli 0.1.17 → 0.1.18

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.
@@ -0,0 +1,310 @@
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 ["DE", "AT", "CH", "NL", "GB", "FR", "SE", "DK", "US"];
10
+ }
11
+ function sqlStringList(values) {
12
+ return values.map((value) => `'${value.replaceAll("'", "\\'")}'`).join(", ");
13
+ }
14
+ function broadTitlePredicateSql() {
15
+ return [
16
+ "LOWER(c.jobTitle) LIKE '%head of hr%'",
17
+ "LOWER(c.jobTitle) LIKE '%head of human resources%'",
18
+ "LOWER(c.jobTitle) LIKE '%head of people%'",
19
+ "LOWER(c.jobTitle) LIKE '%head of payroll%'",
20
+ "LOWER(c.jobTitle) LIKE '%people services%'",
21
+ "LOWER(c.jobTitle) LIKE '%shared services%'",
22
+ "LOWER(c.jobTitle) LIKE '%total rewards%'",
23
+ "LOWER(c.jobTitle) LIKE '%compensation%'",
24
+ "LOWER(c.jobTitle) LIKE '%benefits%'",
25
+ "LOWER(c.jobTitle) LIKE '%hr business partner%'",
26
+ "LOWER(c.jobTitle) LIKE '%hrbp%'",
27
+ "LOWER(c.jobTitle) LIKE '%people operations%'",
28
+ "LOWER(c.jobTitle) LIKE '%people ops%'",
29
+ "LOWER(c.jobTitle) LIKE '%hr operations%'",
30
+ "LOWER(c.jobTitle) LIKE '%hr administration%'",
31
+ "LOWER(c.jobTitle) LIKE '%hr director%'",
32
+ "LOWER(c.jobTitle) LIKE '%director hr%'",
33
+ "LOWER(c.jobTitle) LIKE '%chief people officer%'",
34
+ "LOWER(c.jobTitle) LIKE '%chro%'",
35
+ "LOWER(c.jobTitle) LIKE '%leiter personal%'",
36
+ "LOWER(c.jobTitle) LIKE '%leiter der personalabteilung%'",
37
+ "LOWER(c.jobTitle) LIKE '%personalleitung%'",
38
+ "LOWER(c.jobTitle) LIKE '%bereichsleiter personal%'",
39
+ "LOWER(c.jobTitle) LIKE '%geschäftsbereichsleiter personal%'",
40
+ "LOWER(c.jobTitle) LIKE '%geschaeftsbereichsleiter personal%'"
41
+ ].join("\n OR ");
42
+ }
43
+ function titleExclusionPredicateSql() {
44
+ return [
45
+ "LOWER(c.jobTitle) NOT LIKE '%assistant%'",
46
+ "LOWER(c.jobTitle) NOT LIKE '%assistent%'",
47
+ "LOWER(c.jobTitle) NOT LIKE '%assistentin%'",
48
+ "LOWER(c.jobTitle) NOT LIKE '%recruitment%'",
49
+ "LOWER(c.jobTitle) NOT LIKE '%recruiter%'",
50
+ "LOWER(c.jobTitle) NOT LIKE '%talent acquisition specialist%'"
51
+ ].join("\n AND ");
52
+ }
53
+ export function buildDirectPathLeadExportSql(vendor, market, limit) {
54
+ if (vendor !== "deel") {
55
+ throw new Error(`Unsupported direct-path vendor: ${vendor}`);
56
+ }
57
+ const countries = sqlStringList(marketCountries(market));
58
+ const broadTitlePredicate = broadTitlePredicateSql();
59
+ const titleExclusions = titleExclusionPredicateSql();
60
+ return `WITH hr_leadlists AS (
61
+ SELECT
62
+ CAST(leadListId AS STRING) AS leadListId,
63
+ CAST(query AS STRING) AS query,
64
+ leadList_container_ts
65
+ FROM \`icpidentifier.SalesPrompter.leadLists_raw\`
66
+ WHERE query IS NOT NULL
67
+ AND LOWER(CAST(query AS STRING)) LIKE '%/sales/search/people%'
68
+ AND LOWER(CAST(query AS STRING)) LIKE '%type%3afunction%2cvalues%3alist((id%3a12%'
69
+ QUALIFY ROW_NUMBER() OVER (PARTITION BY CAST(leadListId AS STRING) ORDER BY leadList_container_ts DESC) = 1
70
+ ), contacts AS (
71
+ SELECT
72
+ CAST(leadListId AS STRING) AS leadListId,
73
+ CAST(contactId AS STRING) AS contactId,
74
+ CAST(companyId AS STRING) AS companyId,
75
+ firstName,
76
+ lastName,
77
+ jobTitle,
78
+ contact_companyName,
79
+ contact_location,
80
+ contact_companyLocation,
81
+ tenureAtCompany,
82
+ tenureAtPosition,
83
+ contact_summary,
84
+ contact_titleDescription
85
+ FROM \`icpidentifier.SalesPrompter.linkedin_contacts\`
86
+ ), companies AS (
87
+ SELECT
88
+ CAST(id AS STRING) AS companyId,
89
+ handle,
90
+ name,
91
+ countryCode,
92
+ companySize,
93
+ headquarters,
94
+ domain,
95
+ domain_linkedin,
96
+ website_linkedin,
97
+ industry,
98
+ timestamp AS company_ts
99
+ FROM \`icpidentifier.SalesPrompter.linkedin_companies\`
100
+ )
101
+ SELECT
102
+ c.leadListId,
103
+ c.contactId,
104
+ c.companyId,
105
+ CAST(NULL AS STRING) AS profileUrl,
106
+ c.firstName,
107
+ c.lastName,
108
+ CONCAT(COALESCE(c.firstName, ''), IF(c.firstName IS NOT NULL AND c.lastName IS NOT NULL, ' ', ''), COALESCE(c.lastName, '')) AS fullName,
109
+ c.jobTitle,
110
+ COALESCE(co.name, c.contact_companyName) AS companyName,
111
+ co.handle AS linkedin_companies_handle,
112
+ CONCAT('https://www.linkedin.com/company/', c.companyId) AS companyUrl,
113
+ co.companySize,
114
+ co.countryCode,
115
+ co.headquarters,
116
+ co.domain,
117
+ co.domain_linkedin,
118
+ co.website_linkedin,
119
+ co.industry,
120
+ c.contact_location,
121
+ c.contact_companyLocation,
122
+ c.tenureAtCompany,
123
+ c.tenureAtPosition,
124
+ c.contact_summary,
125
+ c.contact_titleDescription,
126
+ CAST(l.leadList_container_ts AS STRING) AS leadList_container_ts,
127
+ CAST(co.company_ts AS STRING) AS company_ts,
128
+ l.query AS leadListQuery
129
+ FROM contacts c
130
+ JOIN hr_leadlists l ON c.leadListId = l.leadListId
131
+ LEFT JOIN companies co ON c.companyId = co.companyId
132
+ WHERE co.countryCode IN (${countries})
133
+ AND co.domain IS NOT NULL
134
+ AND co.companySize IN ('200-499', '501-1000', '1001-5000', '5001-10.000', '10.000+')
135
+ AND (
136
+ ${broadTitlePredicate}
137
+ )
138
+ AND ${titleExclusions}
139
+ QUALIFY ROW_NUMBER() OVER (PARTITION BY c.contactId ORDER BY co.company_ts DESC) = 1
140
+ ORDER BY co.company_ts DESC
141
+ LIMIT ${limit}`;
142
+ }
143
+ function classifySegment(title) {
144
+ const normalized = title.toLowerCase();
145
+ if (normalized.includes("payroll") ||
146
+ normalized.includes("people services") ||
147
+ normalized.includes("shared services") ||
148
+ normalized.includes("total rewards") ||
149
+ normalized.includes("compensation") ||
150
+ normalized.includes("benefits")) {
151
+ return {
152
+ segment: "payroll-people-services",
153
+ reason: "payroll, shared services, benefits, or people services scope"
154
+ };
155
+ }
156
+ if (normalized.includes("head of hr") ||
157
+ normalized.includes("head of human resources") ||
158
+ normalized.includes("hr director") ||
159
+ normalized.includes("director hr") ||
160
+ normalized.includes("chief people officer") ||
161
+ normalized.includes("chro") ||
162
+ normalized.includes("leiter personal") ||
163
+ normalized.includes("leiter der personalabteilung") ||
164
+ normalized.includes("personalleitung") ||
165
+ normalized.includes("bereichsleiter personal") ||
166
+ normalized.includes("geschäftsbereichsleiter personal") ||
167
+ normalized.includes("geschaeftsbereichsleiter personal")) {
168
+ return {
169
+ segment: "leadership",
170
+ reason: "top-level HR leadership title"
171
+ };
172
+ }
173
+ return {
174
+ segment: "broader-hr",
175
+ reason: "HR, people, or operations role outside the strict leadership/payroll buckets"
176
+ };
177
+ }
178
+ function toNumber(value) {
179
+ if (value === null || value === undefined) {
180
+ return null;
181
+ }
182
+ const numeric = Number(value);
183
+ return Number.isFinite(numeric) ? numeric : null;
184
+ }
185
+ function toNullableString(value) {
186
+ if (value === null || value === undefined) {
187
+ return null;
188
+ }
189
+ const normalized = String(value).trim();
190
+ return normalized.length > 0 ? normalized : null;
191
+ }
192
+ export function normalizeDirectPathRows(rows) {
193
+ return rows.map((row) => ({
194
+ leadListId: toNullableString(row.leadListId),
195
+ contactId: toNullableString(row.contactId),
196
+ companyId: toNullableString(row.companyId),
197
+ profileUrl: toNullableString(row.profileUrl),
198
+ firstName: toNullableString(row.firstName),
199
+ lastName: toNullableString(row.lastName),
200
+ fullName: toNullableString(row.fullName),
201
+ jobTitle: toNullableString(row.jobTitle),
202
+ companyName: toNullableString(row.companyName),
203
+ linkedin_companies_handle: toNullableString(row.linkedin_companies_handle),
204
+ companyUrl: toNullableString(row.companyUrl),
205
+ companySize: toNullableString(row.companySize),
206
+ countryCode: toNullableString(row.countryCode),
207
+ headquarters: toNullableString(row.headquarters),
208
+ domain: toNullableString(row.domain),
209
+ domain_linkedin: toNullableString(row.domain_linkedin),
210
+ website_linkedin: toNullableString(row.website_linkedin),
211
+ industry: toNullableString(row.industry),
212
+ contact_location: toNullableString(row.contact_location),
213
+ contact_companyLocation: toNullableString(row.contact_companyLocation),
214
+ tenureAtCompany: toNumber(row.tenureAtCompany),
215
+ tenureAtPosition: toNumber(row.tenureAtPosition),
216
+ contact_summary: toNullableString(row.contact_summary),
217
+ contact_titleDescription: toNullableString(row.contact_titleDescription),
218
+ leadList_container_ts: toNullableString(row.leadList_container_ts),
219
+ company_ts: toNullableString(row.company_ts),
220
+ leadListQuery: toNullableString(row.leadListQuery),
221
+ leadListQueryDecoded: (() => {
222
+ const rawQuery = toNullableString(row.leadListQuery);
223
+ return rawQuery === null ? null : parseHistoricalQuery(rawQuery).decodedQuery;
224
+ })(),
225
+ leadListFunctionFilters: (() => {
226
+ const rawQuery = toNullableString(row.leadListQuery);
227
+ return rawQuery === null
228
+ ? []
229
+ : parseHistoricalQuery(rawQuery).functionFilters
230
+ .filter((filter) => filter.selection === "INCLUDED")
231
+ .map((filter) => filter.text);
232
+ })(),
233
+ leadListTitleFilters: (() => {
234
+ const rawQuery = toNullableString(row.leadListQuery);
235
+ return rawQuery === null
236
+ ? []
237
+ : parseHistoricalQuery(rawQuery).titleFilters
238
+ .filter((filter) => filter.selection === "INCLUDED")
239
+ .map((filter) => filter.text);
240
+ })(),
241
+ leadListRegionFilters: (() => {
242
+ const rawQuery = toNullableString(row.leadListQuery);
243
+ return rawQuery === null
244
+ ? []
245
+ : parseHistoricalQuery(rawQuery).regionFilters
246
+ .filter((filter) => filter.selection === "INCLUDED")
247
+ .map((filter) => filter.text);
248
+ })(),
249
+ leadListHeadquartersFilters: (() => {
250
+ const rawQuery = toNullableString(row.leadListQuery);
251
+ return rawQuery === null
252
+ ? []
253
+ : parseHistoricalQuery(rawQuery).headquartersFilters
254
+ .filter((filter) => filter.selection === "INCLUDED")
255
+ .map((filter) => filter.text);
256
+ })(),
257
+ leadListHeadcountFilters: (() => {
258
+ const rawQuery = toNullableString(row.leadListQuery);
259
+ return rawQuery === null
260
+ ? []
261
+ : parseHistoricalQuery(rawQuery).headcountFilters
262
+ .filter((filter) => filter.selection === "INCLUDED")
263
+ .map((filter) => filter.text);
264
+ })()
265
+ }));
266
+ }
267
+ export function segmentDirectPathRows(vendor, market, rows) {
268
+ if (vendor !== "deel") {
269
+ throw new Error(`Unsupported direct-path vendor: ${vendor}`);
270
+ }
271
+ const segments = {
272
+ leadership: [],
273
+ "payroll-people-services": [],
274
+ "broader-hr": []
275
+ };
276
+ for (const row of rows) {
277
+ const title = row.jobTitle ?? "";
278
+ const { segment, reason } = classifySegment(title);
279
+ segments[segment].push({
280
+ ...row,
281
+ segment,
282
+ segmentReason: reason
283
+ });
284
+ }
285
+ const countries = new Map();
286
+ const titles = new Map();
287
+ for (const row of rows) {
288
+ const country = row.countryCode ?? "unknown";
289
+ countries.set(country, (countries.get(country) ?? 0) + 1);
290
+ const title = row.jobTitle ?? "unknown";
291
+ titles.set(title, (titles.get(title) ?? 0) + 1);
292
+ }
293
+ return {
294
+ vendor,
295
+ market,
296
+ total: rows.length,
297
+ distinctContacts: new Set(rows.map((row) => row.contactId).filter((value) => value !== null)).size,
298
+ distinctCompanies: new Set(rows.map((row) => row.companyId).filter((value) => value !== null)).size,
299
+ segments,
300
+ summary: {
301
+ segmentCounts: {
302
+ leadership: segments.leadership.length,
303
+ "payroll-people-services": segments["payroll-people-services"].length,
304
+ "broader-hr": segments["broader-hr"].length
305
+ },
306
+ countries: [...countries.entries()].sort((a, b) => a[0].localeCompare(b[0])),
307
+ topTitles: [...titles.entries()].sort((a, b) => b[1] - a[1]).slice(0, 20)
308
+ }
309
+ };
310
+ }