@aglyn/plugins-outreach 1.0.0-beta.151 → 1.0.0-beta.153
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 +5 -1
- package/package.json +12 -12
- package/src/lib/components/compliance-section.js +1 -1
- package/src/lib/components/compliance-section.js.map +1 -1
- package/src/lib/components/enroll-dialog.js +142 -32
- package/src/lib/components/enroll-dialog.js.map +1 -1
- package/src/lib/components/enrollments-table.js +37 -8
- package/src/lib/components/enrollments-table.js.map +1 -1
- package/src/lib/components/mailbox-card.d.ts +3 -0
- package/src/lib/components/mailbox-card.js +44 -8
- package/src/lib/components/mailbox-card.js.map +1 -1
- package/src/lib/components/use-outreach-crm.d.ts +20 -1
- package/src/lib/components/use-outreach-crm.js +78 -4
- package/src/lib/components/use-outreach-crm.js.map +1 -1
- package/src/lib/components/use-outreach-mailbox-api.d.ts +2 -1
- package/src/lib/components/use-outreach-mailbox-api.js +5 -3
- package/src/lib/components/use-outreach-mailbox-api.js.map +1 -1
- package/src/lib/declarations.console-server.js +14 -0
- package/src/lib/declarations.console-server.js.map +1 -1
- package/src/lib/engine/compose.d.ts +7 -2
- package/src/lib/engine/compose.js +11 -5
- package/src/lib/engine/compose.js.map +1 -1
- package/src/lib/engine/enrollment-state.d.ts +6 -1
- package/src/lib/engine/enrollment-state.js +3 -1
- package/src/lib/engine/enrollment-state.js.map +1 -1
- package/src/lib/engine/opt-out-intent.js +1 -1
- package/src/lib/engine/opt-out-intent.js.map +1 -1
- package/src/lib/enrollment/enroll-people.d.ts +52 -7
- package/src/lib/enrollment/enroll-people.js +109 -12
- package/src/lib/enrollment/enroll-people.js.map +1 -1
- package/src/lib/enrollment/gate-lookups.d.ts +12 -3
- package/src/lib/enrollment/gate-lookups.js +17 -14
- package/src/lib/enrollment/gate-lookups.js.map +1 -1
- package/src/lib/mailboxes/mailbox-api.d.ts +27 -2
- package/src/lib/mailboxes/mailbox-api.js.map +1 -1
- package/src/lib/mailboxes/mailbox-routes.js +52 -8
- package/src/lib/mailboxes/mailbox-routes.js.map +1 -1
- package/src/lib/mailboxes/oauth-redirect.d.ts +15 -4
- package/src/lib/mailboxes/oauth-redirect.js +15 -6
- package/src/lib/mailboxes/oauth-redirect.js.map +1 -1
- package/src/lib/model/outreach-api.d.ts +39 -8
- package/src/lib/model/outreach-api.js.map +1 -1
- package/src/lib/model/outreach.types.d.ts +29 -4
- package/src/lib/model/outreach.types.js +9 -0
- package/src/lib/model/outreach.types.js.map +1 -1
- package/src/lib/model/stored-records.js +7 -0
- package/src/lib/model/stored-records.js.map +1 -1
- package/src/lib/routes/enroll-routes.js +112 -39
- package/src/lib/routes/enroll-routes.js.map +1 -1
- package/src/lib/routes/preview-routes.js +18 -5
- package/src/lib/routes/preview-routes.js.map +1 -1
- package/src/lib/runtime/lead-records.d.ts +26 -0
- package/src/lib/runtime/lead-records.js +84 -0
- package/src/lib/runtime/lead-records.js.map +1 -0
- package/src/lib/runtime/send-job.js +30 -12
- package/src/lib/runtime/send-job.js.map +1 -1
- package/src/lib/runtime/sync-job.js +11 -3
- package/src/lib/runtime/sync-job.js.map +1 -1
- package/src/lib/runtime/timeline.d.ts +6 -4
- package/src/lib/runtime/timeline.js +3 -7
- package/src/lib/runtime/timeline.js.map +1 -1
- package/src/lib/storage/outreach-records.d.ts +21 -2
- package/src/lib/storage/outreach-records.js +31 -2
- package/src/lib/storage/outreach-records.js.map +1 -1
|
@@ -15,7 +15,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
17
|
*/ import { consentGroupForHost } from "@aglyn/aglyn/app-utils/consent-groups";
|
|
18
|
-
import { CRM_COLLECTIONS, crmViewIsListed, normalizeCrmViewFilters } from "@aglyn/aglyn/app-utils/crm";
|
|
18
|
+
import { CRM_COLLECTIONS, crmLeadStatus, crmViewIsListed, isCrmLeadOpen, normalizeCrmViewFilters } from "@aglyn/aglyn/app-utils/crm";
|
|
19
19
|
import { dynamicListDimensionsForCrmView } from "@aglyn/aglyn/app-utils/dynamic-list-rule";
|
|
20
20
|
import { findContactByEmail } from "@aglyn/tenant-data-admin/server/contact-email-index";
|
|
21
21
|
import { buildOutreachEnrollment, planOutreachFirstDue } from "../engine/enrollment-state.js";
|
|
@@ -94,28 +94,45 @@ async function enrollContext(firestore, caller, sequence) {
|
|
|
94
94
|
siteName: host.exists ? String((_host_get = host.get('name')) != null ? _host_get : '') : ''
|
|
95
95
|
};
|
|
96
96
|
}
|
|
97
|
-
/**
|
|
97
|
+
/**
|
|
98
|
+
* The candidates, the enrollments they already have here, and the gates'
|
|
99
|
+
* lookups — every map keyed by the person's own id.
|
|
100
|
+
*
|
|
101
|
+
* "Already in this sequence" is answered two ways (AGL-3234): by the id the
|
|
102
|
+
* person's enrollment would have, and by any enrollment in this sequence
|
|
103
|
+
* that carries the person's ADDRESS. The second is what keeps a contact
|
|
104
|
+
* from being enrolled beside the lead they were converted from: the lead's
|
|
105
|
+
* enrollment followed them, under the lead's key, and still names them.
|
|
106
|
+
*/ async function readPeople(firestore, caller, sequence, context, people) {
|
|
98
107
|
const candidates = await readOutreachEnrollCandidates(firestore, {
|
|
99
108
|
orgId: caller.orgId,
|
|
100
109
|
hostId: sequence.hostId,
|
|
101
110
|
contactGroupId: context.contactGroupId,
|
|
102
|
-
|
|
111
|
+
people
|
|
103
112
|
});
|
|
104
113
|
const enrollments = outreachOrgCollection(firestore, caller.orgId, 'enrollments');
|
|
105
|
-
const existing = candidates.length ? await firestore.getAll(...candidates.map((candidate)=>enrollments.doc(outreachEnrollmentId(sequence.id, candidate.
|
|
114
|
+
const existing = candidates.length ? await firestore.getAll(...candidates.map((candidate)=>enrollments.doc(outreachEnrollmentId(sequence.id, candidate.personId)))) : [];
|
|
106
115
|
const enrolledHere = new Set(candidates.filter((_candidate, index)=>{
|
|
107
116
|
var _existing_index;
|
|
108
117
|
return (_existing_index = existing[index]) == null ? void 0 : _existing_index.exists;
|
|
109
|
-
}).map((candidate)=>candidate.
|
|
118
|
+
}).map((candidate)=>candidate.personId));
|
|
110
119
|
const askable = candidates.filter((candidate)=>candidate.contact && candidate.visible && candidate.email);
|
|
111
120
|
const lookups = askable.length ? await readOutreachGateLookups(firestore, {
|
|
112
121
|
orgId: caller.orgId,
|
|
113
122
|
hostId: sequence.hostId,
|
|
114
123
|
people: askable.map((candidate)=>({
|
|
115
|
-
|
|
124
|
+
personId: candidate.personId,
|
|
125
|
+
contactId: candidate.contactId || null,
|
|
126
|
+
leadId: candidate.leadId,
|
|
116
127
|
email: String(candidate.email)
|
|
117
128
|
}))
|
|
118
129
|
}) : new Map();
|
|
130
|
+
for (const candidate of askable){
|
|
131
|
+
var _ref;
|
|
132
|
+
var _lookups_get;
|
|
133
|
+
const open = (_ref = (_lookups_get = lookups.get(candidate.personId)) == null ? void 0 : _lookups_get.openEnrollments) != null ? _ref : [];
|
|
134
|
+
if (open.some((entry)=>entry.sequenceId === sequence.id)) enrolledHere.add(candidate.personId);
|
|
135
|
+
}
|
|
119
136
|
return {
|
|
120
137
|
candidates,
|
|
121
138
|
enrolledHere,
|
|
@@ -125,18 +142,50 @@ async function enrollContext(firestore, caller, sequence) {
|
|
|
125
142
|
const uniqueIds = (values)=>Array.isArray(values) ? [
|
|
126
143
|
...new Set(values.map(readOutreachDocumentId).filter((id)=>id !== null))
|
|
127
144
|
] : [];
|
|
145
|
+
/** The most leads a saved Leads view reads — the Leads list's own window. */ const LEADS_VIEW_WINDOW = 200;
|
|
146
|
+
/**
|
|
147
|
+
* The people a saved LEADS view selects (AGL-3234): the sequence's site's
|
|
148
|
+
* most recently seen leads, narrowed by the view's status clause the way
|
|
149
|
+
* the Leads list narrows its window — open leads when the view names no
|
|
150
|
+
* status, one status when it does, everything for `all`. A lead is the
|
|
151
|
+
* site's own, so there is no address to resolve: the person key IS the id.
|
|
152
|
+
*/ async function leadsViewPeople(firestore, sequence, filters) {
|
|
153
|
+
var _ref;
|
|
154
|
+
const statusClause = filters.find((clause)=>clause.field === 'status' && clause.op === 'equals');
|
|
155
|
+
const wanted = String((_ref = statusClause == null ? void 0 : statusClause.value) != null ? _ref : 'open');
|
|
156
|
+
const window = await firestore.collection('hosts').doc(sequence.hostId).collection('leads').orderBy('lastSeenAtMs', 'desc').limit(LEADS_VIEW_WINDOW + 1).get();
|
|
157
|
+
const matching = window.docs.slice(0, LEADS_VIEW_WINDOW).filter((doc)=>{
|
|
158
|
+
const lead = doc.data();
|
|
159
|
+
if (wanted === 'all') return true;
|
|
160
|
+
if (wanted === 'open') return isCrmLeadOpen(lead) && !lead['convertedContactId'];
|
|
161
|
+
return crmLeadStatus(lead) === wanted;
|
|
162
|
+
});
|
|
163
|
+
return {
|
|
164
|
+
people: matching.slice(0, OUTREACH_ENROLL_BATCH_MAX).map((doc)=>({
|
|
165
|
+
kind: 'lead',
|
|
166
|
+
id: doc.id
|
|
167
|
+
})),
|
|
168
|
+
total: matching.length,
|
|
169
|
+
truncated: matching.length > OUTREACH_ENROLL_BATCH_MAX || window.docs.length > LEADS_VIEW_WINDOW
|
|
170
|
+
};
|
|
171
|
+
}
|
|
128
172
|
export function createOutreachEnrollRoutes(deps) {
|
|
129
173
|
/**
|
|
130
|
-
* The
|
|
131
|
-
* search picked,
|
|
132
|
-
*
|
|
133
|
-
|
|
174
|
+
* The people a source names, the batch limit applied: every contact a
|
|
175
|
+
* search picked, every lead picked from the sequence's site, or the
|
|
176
|
+
* people a saved Contacts or Leads view selects there — contacts found by
|
|
177
|
+
* address, leads by their own key.
|
|
178
|
+
*/ async function sourcePeople(firestore, caller, sequence, source) {
|
|
134
179
|
var _data_ownerUid;
|
|
135
|
-
if (source['kind'] === 'contacts') {
|
|
136
|
-
const
|
|
180
|
+
if (source['kind'] === 'contacts' || source['kind'] === 'leads') {
|
|
181
|
+
const kind = source['kind'] === 'leads' ? 'lead' : 'contact';
|
|
182
|
+
const ids = uniqueIds(kind === 'lead' ? source['leadIds'] : source['contactIds']);
|
|
137
183
|
if (!ids.length) return outreachRefusal(400, 'invalid-request', 'Pick the people to enroll.');
|
|
138
184
|
return {
|
|
139
|
-
|
|
185
|
+
people: ids.slice(0, OUTREACH_ENROLL_BATCH_MAX).map((id)=>({
|
|
186
|
+
kind,
|
|
187
|
+
id
|
|
188
|
+
})),
|
|
140
189
|
total: ids.length,
|
|
141
190
|
truncated: ids.length > OUTREACH_ENROLL_BATCH_MAX
|
|
142
191
|
};
|
|
@@ -155,8 +204,11 @@ export function createOutreachEnrollRoutes(deps) {
|
|
|
155
204
|
}, caller.uid)) {
|
|
156
205
|
return outreachRefusal(404, 'view-not-found', 'That saved view no longer exists.');
|
|
157
206
|
}
|
|
207
|
+
if (data['section'] === 'leads') {
|
|
208
|
+
return leadsViewPeople(firestore, sequence, normalizeCrmViewFilters(data['filters']));
|
|
209
|
+
}
|
|
158
210
|
if (data['section'] !== 'contacts') {
|
|
159
|
-
return outreachRefusal(400, 'view-unsupported', 'Enroll from a saved view of Contacts.');
|
|
211
|
+
return outreachRefusal(400, 'view-unsupported', 'Enroll from a saved view of Contacts or Leads.');
|
|
160
212
|
}
|
|
161
213
|
const { unsupported } = dynamicListDimensionsForCrmView(normalizeCrmViewFilters(data['filters']));
|
|
162
214
|
if (unsupported.length) {
|
|
@@ -176,9 +228,12 @@ export function createOutreachEnrollRoutes(deps) {
|
|
|
176
228
|
hostId: sequence.hostId
|
|
177
229
|
})));
|
|
178
230
|
return {
|
|
179
|
-
|
|
231
|
+
people: [
|
|
180
232
|
...new Set(found.filter((snapshot)=>snapshot).map((snapshot)=>String(snapshot == null ? void 0 : snapshot.id)))
|
|
181
|
-
]
|
|
233
|
+
].map((id)=>({
|
|
234
|
+
kind: 'contact',
|
|
235
|
+
id
|
|
236
|
+
})),
|
|
182
237
|
total: ordered.length,
|
|
183
238
|
truncated: ordered.length > OUTREACH_ENROLL_BATCH_MAX || !complete
|
|
184
239
|
};
|
|
@@ -194,20 +249,20 @@ export function createOutreachEnrollRoutes(deps) {
|
|
|
194
249
|
const sequence = await loadActiveSequence(firestore, caller.orgId, body['sequenceId']);
|
|
195
250
|
if (sequence instanceof Response) return sequence;
|
|
196
251
|
const source = body['source'] && typeof body['source'] === 'object' ? body['source'] : {};
|
|
197
|
-
const named = await
|
|
252
|
+
const named = await sourcePeople(firestore, caller, sequence, source);
|
|
198
253
|
if (named instanceof Response) return named;
|
|
199
254
|
const context = await enrollContext(firestore, caller, sequence);
|
|
200
|
-
const { candidates, enrolledHere, lookups } = await readPeople(firestore, caller, sequence, context, named.
|
|
255
|
+
const { candidates, enrolledHere, lookups } = await readPeople(firestore, caller, sequence, context, named.people);
|
|
201
256
|
const people = candidates.map((candidate)=>{
|
|
202
257
|
var _lookups_get;
|
|
203
258
|
return previewOutreachPerson({
|
|
204
259
|
candidate,
|
|
205
|
-
lookups: (_lookups_get = lookups.get(candidate.
|
|
260
|
+
lookups: (_lookups_get = lookups.get(candidate.personId)) != null ? _lookups_get : UNCHECKED,
|
|
206
261
|
settings: context.settings,
|
|
207
262
|
contactGroupId: context.contactGroupId
|
|
208
263
|
}, {
|
|
209
264
|
siteName: context.siteName,
|
|
210
|
-
alreadyInSequence: enrolledHere.has(candidate.
|
|
265
|
+
alreadyInSequence: enrolledHere.has(candidate.personId)
|
|
211
266
|
});
|
|
212
267
|
});
|
|
213
268
|
return outreachOk({
|
|
@@ -227,12 +282,23 @@ export function createOutreachEnrollRoutes(deps) {
|
|
|
227
282
|
const firestore = deps.firestore();
|
|
228
283
|
const sequence = await loadActiveSequence(firestore, caller.orgId, body['sequenceId']);
|
|
229
284
|
if (sequence instanceof Response) return sequence;
|
|
285
|
+
// Each person once, by whichever record they are: a contact by id, or a
|
|
286
|
+
// lead by its key (AGL-3234). A person named both ways is a contact.
|
|
230
287
|
const requested = new Map();
|
|
231
288
|
for (const entry of Array.isArray(body['people']) ? body['people'] : []){
|
|
232
289
|
const person = entry && typeof entry === 'object' ? entry : {};
|
|
233
290
|
const contactId = readOutreachDocumentId(person['contactId']);
|
|
234
|
-
|
|
235
|
-
|
|
291
|
+
const leadId = readOutreachDocumentId(person['leadId']);
|
|
292
|
+
const ref = contactId ? {
|
|
293
|
+
kind: 'contact',
|
|
294
|
+
id: contactId
|
|
295
|
+
} : leadId ? {
|
|
296
|
+
kind: 'lead',
|
|
297
|
+
id: leadId
|
|
298
|
+
} : null;
|
|
299
|
+
if (!ref || requested.has(ref.id)) continue;
|
|
300
|
+
requested.set(ref.id, {
|
|
301
|
+
ref,
|
|
236
302
|
personalLine: typeof person['personalLine'] === 'string' ? person['personalLine'] : '',
|
|
237
303
|
attestations: Array.isArray(person['attestations']) ? person['attestations'] : []
|
|
238
304
|
});
|
|
@@ -257,41 +323,50 @@ export function createOutreachEnrollRoutes(deps) {
|
|
|
257
323
|
}
|
|
258
324
|
const context = await enrollContext(firestore, caller, sequence);
|
|
259
325
|
const { candidates, enrolledHere, lookups } = await readPeople(firestore, caller, sequence, context, [
|
|
260
|
-
...requested.
|
|
261
|
-
]);
|
|
326
|
+
...requested.values()
|
|
327
|
+
].map((entry)=>entry.ref));
|
|
262
328
|
const enrollments = outreachOrgCollection(firestore, caller.orgId, 'enrollments');
|
|
263
329
|
const results = await Promise.all(candidates.map(async (candidate)=>{
|
|
264
330
|
var _requested_get, _lookups_get;
|
|
265
|
-
const
|
|
331
|
+
const named = {
|
|
332
|
+
personId: candidate.personId,
|
|
333
|
+
target: candidate.target,
|
|
334
|
+
contactId: candidate.contactId,
|
|
335
|
+
leadId: candidate.leadId
|
|
336
|
+
};
|
|
337
|
+
const supplied = (_requested_get = requested.get(candidate.personId)) != null ? _requested_get : {
|
|
266
338
|
personalLine: '',
|
|
267
339
|
attestations: []
|
|
268
340
|
};
|
|
269
341
|
const decision = decideOutreachEnrollment({
|
|
270
342
|
candidate,
|
|
271
|
-
lookups: (_lookups_get = lookups.get(candidate.
|
|
343
|
+
lookups: (_lookups_get = lookups.get(candidate.personId)) != null ? _lookups_get : UNCHECKED,
|
|
272
344
|
settings: context.settings,
|
|
273
345
|
contactGroupId: context.contactGroupId
|
|
274
346
|
}, {
|
|
275
347
|
siteName: context.siteName,
|
|
276
|
-
alreadyInSequence: enrolledHere.has(candidate.
|
|
277
|
-
},
|
|
348
|
+
alreadyInSequence: enrolledHere.has(candidate.personId)
|
|
349
|
+
}, {
|
|
350
|
+
personalLine: supplied.personalLine,
|
|
351
|
+
attestations: supplied.attestations,
|
|
278
352
|
uid: caller.uid,
|
|
279
353
|
nowMs
|
|
280
|
-
})
|
|
354
|
+
});
|
|
281
355
|
if (!decision.allowed || !decision.email) {
|
|
282
|
-
return {
|
|
283
|
-
contactId: candidate.contactId,
|
|
356
|
+
return _extends({}, named, {
|
|
284
357
|
email: candidate.email,
|
|
285
358
|
outcome: 'blocked',
|
|
286
359
|
blocks: decision.blocks
|
|
287
|
-
};
|
|
360
|
+
});
|
|
288
361
|
}
|
|
289
|
-
const id = outreachEnrollmentId(sequence.id, candidate.
|
|
362
|
+
const id = outreachEnrollmentId(sequence.id, candidate.personId);
|
|
290
363
|
const enrollment = buildOutreachEnrollment({
|
|
291
364
|
id,
|
|
292
365
|
sequence,
|
|
293
366
|
mailbox,
|
|
367
|
+
target: candidate.target,
|
|
294
368
|
contactId: candidate.contactId,
|
|
369
|
+
leadId: candidate.leadId,
|
|
295
370
|
contactName: candidate.name,
|
|
296
371
|
email: decision.email,
|
|
297
372
|
cold: decision.cold,
|
|
@@ -305,8 +380,7 @@ export function createOutreachEnrollRoutes(deps) {
|
|
|
305
380
|
await enrollments.doc(id).create(enrollment);
|
|
306
381
|
} catch (error) {
|
|
307
382
|
if ((error == null ? void 0 : error.code) !== ALREADY_EXISTS) throw error;
|
|
308
|
-
return {
|
|
309
|
-
contactId: candidate.contactId,
|
|
383
|
+
return _extends({}, named, {
|
|
310
384
|
email: candidate.email,
|
|
311
385
|
outcome: 'blocked',
|
|
312
386
|
blocks: [
|
|
@@ -315,14 +389,13 @@ export function createOutreachEnrollRoutes(deps) {
|
|
|
315
389
|
reason: "This person has already been in this sequence, so they won't be sent its emails again."
|
|
316
390
|
}
|
|
317
391
|
]
|
|
318
|
-
};
|
|
392
|
+
});
|
|
319
393
|
}
|
|
320
|
-
return {
|
|
321
|
-
contactId: candidate.contactId,
|
|
394
|
+
return _extends({}, named, {
|
|
322
395
|
email: decision.email,
|
|
323
396
|
outcome: 'enrolled',
|
|
324
397
|
enrollmentId: id
|
|
325
|
-
};
|
|
398
|
+
});
|
|
326
399
|
}));
|
|
327
400
|
const enrolled = results.filter((result)=>result.outcome === 'enrolled').length;
|
|
328
401
|
if (enrolled) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/enroll-routes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { consentGroupForHost } from '@aglyn/aglyn/app-utils/consent-groups'\nimport {\n CRM_COLLECTIONS,\n crmViewIsListed,\n normalizeCrmViewFilters,\n} from '@aglyn/aglyn/app-utils/crm'\nimport { dynamicListDimensionsForCrmView } from '@aglyn/aglyn/app-utils/dynamic-list-rule'\nimport type { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport { findContactByEmail } from '@aglyn/tenant-data-admin/server/contact-email-index'\nimport { buildOutreachEnrollment, planOutreachFirstDue } from '../engine/enrollment-state'\nimport type { OutreachGateLookups } from '../engine/gates'\nimport {\n decideOutreachEnrollment,\n previewOutreachPerson,\n readOutreachEnrollCandidates,\n type OutreachEnrollCandidate,\n type OutreachGateQuestion,\n} from '../enrollment/enroll-people'\nimport { readOutreachGateLookups } from '../enrollment/gate-lookups'\nimport { effectiveOutreachAllowedCountries } from '../model/compliance-settings'\nimport {\n OUTREACH_ENROLL_BATCH_MAX,\n type OutreachEnrollOutcome,\n type OutreachEnrollPreviewResponse,\n type OutreachEnrollResponse,\n} from '../model/outreach-api'\nimport type { OutreachSequence } from '../model/outreach.types'\nimport { readOutreachComplianceSettingsDoc } from '../storage/compliance-settings-store'\nimport {\n outreachEnrollmentId,\n outreachOrgCollection,\n readStoredOutreachMailbox,\n readStoredOutreachSequence,\n} from '../storage/outreach-records'\nimport { OUTREACH_SEQUENCE_ACTIVITY_TARGET, type OutreachRouteDeps } from './route-deps'\nimport { outreachRouteGate, type OutreachRouteCaller, type OutreachRouteExtraPermission } from './route-gate'\nimport {\n outreachMethodNotAllowed,\n outreachOk,\n outreachRefusal,\n readOutreachDocumentId,\n readOutreachJsonBody,\n} from './route-http'\n\n/**\n * ENROLLING PEOPLE IN A SEQUENCE (AGL-2980): `outreach/enroll/preview` and\n * `outreach/enroll`.\n *\n * The preview reads the people a saved Contacts view or a search named —\n * at most {@link OUTREACH_ENROLL_BATCH_MAX} — and says where each stands:\n * eligible, blocked with the gate's own sentence, or waiting on the rep for\n * a personal line and the attestations only they can make. Confirm reads\n * everything again and asks the gates again with what the rep supplied,\n * because a preview is a picture of a moment: someone can unsubscribe\n * between the two, and a request can claim anything. Nothing the preview\n * answered is trusted by the enroll.\n *\n * Both read the CRM's people, so both ask for `data.manage` beside\n * `outreach.use` — the permission the contacts' own rules read with, so\n * Outreach is no side door into the address book.\n *\n * Only an ACTIVE sequence takes people: enrolling schedules the first step\n * in the mailbox's hours, and a draft's steps and a paused sequence's timing\n * are not yet what will be sent.\n */\n\n/** The permission enrolling reads the CRM with. */\nexport const OUTREACH_READS_PEOPLE: OutreachRouteExtraPermission = {\n key: 'data.manage',\n refusal: \"Enrolling reads your CRM's contacts, and your role does not include Manage data.\",\n}\n\n/** Everything enrolling reaches beyond the routes' own dependencies. */\nexport interface OutreachEnrollRouteDeps extends OutreachRouteDeps {\n /**\n * The addresses a saved Contacts view selects among one site's contacts,\n * read the way the dynamic-list sweep reads one, and whether the read\n * reached the whole view before its budget.\n */\n crmViewEmails(input: { hostId: string; viewId: string }): Promise<{ emails: string[]; complete: boolean }>\n}\n\nexport interface OutreachEnrollRoutes {\n preview: PluginWebApiHandler\n confirm: PluginWebApiHandler\n}\n\n/** The activity line an enroll writes. */\nexport function outreachEnrolledActivity(count: number): string {\n return count === 1\n ? 'Enrolled 1 person in a sequence'\n : `Enrolled ${count} people in a sequence`\n}\n\n/** Lookups for a person the gates will never be asked about: every one unchecked. */\nconst UNCHECKED: OutreachGateLookups = {\n platformSuppressed: null,\n hostSuppressed: null,\n salesTopicState: null,\n doNotContact: null,\n workspaceMembers: null,\n openEnrollments: null,\n hasInboundEmail: null,\n}\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** gRPC `ALREADY_EXISTS`, which `create()` rejects with when the document is there. */\nconst ALREADY_EXISTS = 6\n\nasync function loadActiveSequence(\n firestore: Firestore,\n orgId: string,\n rawSequenceId: unknown,\n): Promise<Response | OutreachSequence> {\n const sequenceId = readOutreachDocumentId(rawSequenceId)\n if (!sequenceId) return outreachRefusal(400, 'invalid-request', 'Name the sequence to enroll people in.')\n const snapshot = await outreachOrgCollection(firestore, orgId, 'sequences').doc(sequenceId).get()\n const sequence = readStoredOutreachSequence(sequenceId, snapshot.exists ? snapshot.data() : undefined)\n if (!sequence) return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n if (sequence.status === 'archived') {\n return outreachRefusal(409, 'sequence-archived', 'This sequence is archived and takes no one new.')\n }\n if (sequence.status !== 'active') {\n return outreachRefusal(409, 'sequence-not-active', 'Activate this sequence before enrolling people.')\n }\n return sequence\n}\n\n/** What every person in one request is judged against. */\ninterface EnrollContext {\n settings: OutreachGateQuestion['settings']\n contactGroupId: string\n siteName: string\n}\n\nasync function enrollContext(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n sequence: OutreachSequence,\n): Promise<EnrollContext> {\n const [orgSettings, host] = await Promise.all([\n readOutreachComplianceSettingsDoc(firestore, caller.orgId),\n firestore.collection('hosts').doc(sequence.hostId).get(),\n ])\n return {\n // The organization's countries are a ceiling over the sequence's.\n settings: {\n ...sequence.settings,\n allowedCountries: effectiveOutreachAllowedCountries(\n sequence.settings.allowedCountries,\n orgSettings.allowedCountries,\n ),\n },\n contactGroupId: consentGroupForHost(caller.org, sequence.hostId).groupId,\n siteName: host.exists ? String(host.get('name') ?? '') : '',\n }\n}\n\n/** The candidates, the enrollments they already have here, and the gates' lookups. */\nasync function readPeople(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n sequence: OutreachSequence,\n context: EnrollContext,\n contactIds: readonly string[],\n): Promise<{\n candidates: OutreachEnrollCandidate[]\n enrolledHere: Set<string>\n lookups: Map<string, OutreachGateLookups>\n}> {\n const candidates = await readOutreachEnrollCandidates(firestore, {\n orgId: caller.orgId,\n hostId: sequence.hostId,\n contactGroupId: context.contactGroupId,\n contactIds,\n })\n const enrollments = outreachOrgCollection(firestore, caller.orgId, 'enrollments')\n const existing = candidates.length\n ? await firestore.getAll(\n ...candidates.map((candidate) => enrollments.doc(outreachEnrollmentId(sequence.id, candidate.contactId))),\n )\n : []\n const enrolledHere = new Set(\n candidates.filter((_candidate, index) => existing[index]?.exists).map((candidate) => candidate.contactId),\n )\n const askable = candidates.filter((candidate) => candidate.contact && candidate.visible && candidate.email)\n const lookups = askable.length\n ? await readOutreachGateLookups(firestore, {\n orgId: caller.orgId,\n hostId: sequence.hostId,\n people: askable.map((candidate) => ({ contactId: candidate.contactId, email: String(candidate.email) })),\n })\n : new Map<string, OutreachGateLookups>()\n return { candidates, enrolledHere, lookups }\n}\n\nconst uniqueIds = (values: unknown): string[] =>\n Array.isArray(values)\n ? [...new Set(values.map(readOutreachDocumentId).filter((id): id is string => id !== null))]\n : []\n\nexport function createOutreachEnrollRoutes(deps: OutreachEnrollRouteDeps): OutreachEnrollRoutes {\n /**\n * The contacts a source names, the batch limit applied: every contact a\n * search picked, or the people a saved Contacts view selects at the\n * sequence's site, found by address.\n */\n async function sourceContactIds(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n sequence: OutreachSequence,\n source: Record<string, unknown>,\n ): Promise<Response | { contactIds: string[]; total: number; truncated: boolean }> {\n if (source['kind'] === 'contacts') {\n const ids = uniqueIds(source['contactIds'])\n if (!ids.length) return outreachRefusal(400, 'invalid-request', 'Pick the people to enroll.')\n return {\n contactIds: ids.slice(0, OUTREACH_ENROLL_BATCH_MAX),\n total: ids.length,\n truncated: ids.length > OUTREACH_ENROLL_BATCH_MAX,\n }\n }\n if (source['kind'] !== 'view') {\n return outreachRefusal(400, 'invalid-request', 'Enroll people from a saved view or a search.')\n }\n const viewId = readOutreachDocumentId(source['viewId'])\n const view = viewId\n ? await firestore.collection('orgs').doc(caller.orgId).collection(CRM_COLLECTIONS.views).doc(viewId).get()\n : null\n const data = view?.exists ? (view.data() as Record<string, unknown>) : null\n // A colleague's private view is theirs: it is not listed for this\n // reader, so it does not exist for them here either.\n if (\n !viewId ||\n !data ||\n !crmViewIsListed({ shared: data['shared'] === true, ownerUid: String(data['ownerUid'] ?? '') }, caller.uid)\n ) {\n return outreachRefusal(404, 'view-not-found', 'That saved view no longer exists.')\n }\n if (data['section'] !== 'contacts') {\n return outreachRefusal(400, 'view-unsupported', 'Enroll from a saved view of Contacts.')\n }\n const { unsupported } = dynamicListDimensionsForCrmView(normalizeCrmViewFilters(data['filters']))\n if (unsupported.length) {\n // Dropping the filter enrolling cannot read would enroll more people\n // than the view shows, so the view is refused whole.\n return outreachRefusal(\n 400,\n 'view-unsupported',\n `This view filters on ${unsupported.map((clause) => clause.label || clause.field).join(', ')}, which enrolling can't apply. Pick another view, or search.`,\n )\n }\n const { emails, complete } = await deps.crmViewEmails({ hostId: sequence.hostId, viewId })\n const ordered = [...new Set(emails)].sort()\n const contacts = firestore.collection('orgs').doc(caller.orgId).collection('contacts')\n const found = await Promise.all(\n ordered\n .slice(0, OUTREACH_ENROLL_BATCH_MAX)\n .map((email) => findContactByEmail(contacts, email, { hostId: sequence.hostId })),\n )\n return {\n contactIds: [...new Set(found.filter((snapshot) => snapshot).map((snapshot) => String(snapshot?.id)))],\n total: ordered.length,\n truncated: ordered.length > OUTREACH_ENROLL_BATCH_MAX || !complete,\n }\n }\n\n const preview: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate, [OUTREACH_READS_PEOPLE])\n if (caller instanceof Response) return caller\n const firestore = deps.firestore()\n const sequence = await loadActiveSequence(firestore, caller.orgId, body['sequenceId'])\n if (sequence instanceof Response) return sequence\n const source = body['source'] && typeof body['source'] === 'object' ? (body['source'] as Record<string, unknown>) : {}\n const named = await sourceContactIds(firestore, caller, sequence, source)\n if (named instanceof Response) return named\n const context = await enrollContext(firestore, caller, sequence)\n const { candidates, enrolledHere, lookups } = await readPeople(\n firestore,\n caller,\n sequence,\n context,\n named.contactIds,\n )\n const people = candidates.map((candidate) =>\n previewOutreachPerson(\n {\n candidate,\n lookups: lookups.get(candidate.contactId) ?? UNCHECKED,\n settings: context.settings,\n contactGroupId: context.contactGroupId,\n },\n { siteName: context.siteName, alreadyInSequence: enrolledHere.has(candidate.contactId) },\n ),\n )\n return outreachOk({\n ok: true,\n people,\n total: named.total,\n truncated: named.truncated,\n } satisfies OutreachEnrollPreviewResponse)\n }\n\n const confirm: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate, [OUTREACH_READS_PEOPLE])\n if (caller instanceof Response) return caller\n const firestore = deps.firestore()\n const sequence = await loadActiveSequence(firestore, caller.orgId, body['sequenceId'])\n if (sequence instanceof Response) return sequence\n\n const requested = new Map<string, { personalLine: string; attestations: unknown[] }>()\n for (const entry of Array.isArray(body['people']) ? body['people'] : []) {\n const person = (entry && typeof entry === 'object' ? entry : {}) as Record<string, unknown>\n const contactId = readOutreachDocumentId(person['contactId'])\n if (!contactId || requested.has(contactId)) continue\n requested.set(contactId, {\n personalLine: typeof person['personalLine'] === 'string' ? person['personalLine'] : '',\n attestations: Array.isArray(person['attestations']) ? person['attestations'] : [],\n })\n }\n if (!requested.size) return outreachRefusal(400, 'invalid-request', 'Pick the people to enroll.')\n if (requested.size > OUTREACH_ENROLL_BATCH_MAX) {\n return outreachRefusal(\n 400,\n 'invalid-request',\n `Enroll at most ${OUTREACH_ENROLL_BATCH_MAX} people at a time.`,\n )\n }\n\n const mailboxSnapshot = sequence.mailboxId\n ? await outreachOrgCollection(firestore, caller.orgId, 'mailboxes').doc(sequence.mailboxId).get()\n : null\n const mailbox = readStoredOutreachMailbox(\n sequence.mailboxId,\n mailboxSnapshot?.exists ? mailboxSnapshot.data() : undefined,\n )\n if (!mailbox || mailbox.status === 'disconnected') {\n return outreachRefusal(\n 409,\n 'mailbox-unavailable',\n \"This sequence's mailbox is no longer connected. Choose another mailbox for it before enrolling people.\",\n )\n }\n const nowMs = deps.now()\n if (planOutreachFirstDue({ sequence, mailbox, enrolledAtMs: nowMs, random: deps.random }) === null) {\n return outreachRefusal(\n 409,\n 'mailbox-unavailable',\n \"This sequence's sending hours never open in its mailbox's timezone. Fix the hours before enrolling people.\",\n )\n }\n\n const context = await enrollContext(firestore, caller, sequence)\n const { candidates, enrolledHere, lookups } = await readPeople(\n firestore,\n caller,\n sequence,\n context,\n [...requested.keys()],\n )\n const enrollments = outreachOrgCollection(firestore, caller.orgId, 'enrollments')\n const results: OutreachEnrollOutcome[] = await Promise.all(\n candidates.map(async (candidate): Promise<OutreachEnrollOutcome> => {\n const supplied = requested.get(candidate.contactId) ?? { personalLine: '', attestations: [] }\n const decision = decideOutreachEnrollment(\n {\n candidate,\n lookups: lookups.get(candidate.contactId) ?? UNCHECKED,\n settings: context.settings,\n contactGroupId: context.contactGroupId,\n },\n { siteName: context.siteName, alreadyInSequence: enrolledHere.has(candidate.contactId) },\n { ...supplied, uid: caller.uid, nowMs },\n )\n if (!decision.allowed || !decision.email) {\n return { contactId: candidate.contactId, email: candidate.email, outcome: 'blocked', blocks: decision.blocks }\n }\n const id = outreachEnrollmentId(sequence.id, candidate.contactId)\n const enrollment = buildOutreachEnrollment({\n id,\n sequence,\n mailbox,\n contactId: candidate.contactId,\n contactName: candidate.name,\n email: decision.email,\n cold: decision.cold,\n personalLine: supplied.personalLine,\n attestations: decision.attestations,\n enrolledByUid: caller.uid,\n nowMs,\n random: deps.random,\n })\n try {\n await enrollments.doc(id).create(enrollment)\n } catch (error) {\n if ((error as { code?: unknown })?.code !== ALREADY_EXISTS) throw error\n return {\n contactId: candidate.contactId,\n email: candidate.email,\n outcome: 'blocked',\n blocks: [\n {\n code: 'already_in_sequence',\n reason: \"This person has already been in this sequence, so they won't be sent its emails again.\",\n },\n ],\n }\n }\n return { contactId: candidate.contactId, email: decision.email, outcome: 'enrolled', enrollmentId: id }\n }),\n )\n const enrolled = results.filter((result) => result.outcome === 'enrolled').length\n if (enrolled) {\n await deps.logOrgActivity(\n caller.orgId,\n { uid: caller.uid, email: caller.email },\n outreachEnrolledActivity(enrolled),\n { type: OUTREACH_SEQUENCE_ACTIVITY_TARGET, id: sequence.id, name: sequence.name },\n )\n }\n return outreachOk({ ok: true, results, enrolled } satisfies OutreachEnrollResponse)\n }\n\n return { preview, confirm }\n}\n"],"names":["consentGroupForHost","CRM_COLLECTIONS","crmViewIsListed","normalizeCrmViewFilters","dynamicListDimensionsForCrmView","findContactByEmail","buildOutreachEnrollment","planOutreachFirstDue","decideOutreachEnrollment","previewOutreachPerson","readOutreachEnrollCandidates","readOutreachGateLookups","effectiveOutreachAllowedCountries","OUTREACH_ENROLL_BATCH_MAX","readOutreachComplianceSettingsDoc","outreachEnrollmentId","outreachOrgCollection","readStoredOutreachMailbox","readStoredOutreachSequence","OUTREACH_SEQUENCE_ACTIVITY_TARGET","outreachRouteGate","outreachMethodNotAllowed","outreachOk","outreachRefusal","readOutreachDocumentId","readOutreachJsonBody","OUTREACH_READS_PEOPLE","key","refusal","outreachEnrolledActivity","count","UNCHECKED","platformSuppressed","hostSuppressed","salesTopicState","doNotContact","workspaceMembers","openEnrollments","hasInboundEmail","ALREADY_EXISTS","loadActiveSequence","firestore","orgId","rawSequenceId","sequenceId","snapshot","doc","get","sequence","exists","data","undefined","status","enrollContext","caller","host","orgSettings","Promise","all","collection","hostId","settings","allowedCountries","contactGroupId","org","groupId","siteName","String","readPeople","context","contactIds","candidates","enrollments","existing","length","getAll","map","candidate","id","contactId","enrolledHere","Set","filter","_candidate","index","askable","contact","visible","email","lookups","people","Map","uniqueIds","values","Array","isArray","createOutreachEnrollRoutes","deps","sourceContactIds","source","ids","slice","total","truncated","viewId","view","views","shared","ownerUid","uid","unsupported","clause","label","field","join","emails","complete","crmViewEmails","ordered","sort","contacts","found","preview","request","method","body","gate","Response","named","alreadyInSequence","has","ok","confirm","requested","entry","person","set","personalLine","attestations","size","mailboxSnapshot","mailboxId","mailbox","nowMs","now","enrolledAtMs","random","keys","results","supplied","decision","allowed","outcome","blocks","enrollment","contactName","name","cold","enrolledByUid","create","error","code","reason","enrollmentId","enrolled","result","logOrgActivity","type"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/enroll-routes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { consentGroupForHost } from '@aglyn/aglyn/app-utils/consent-groups'\nimport {\n CRM_COLLECTIONS,\n crmLeadStatus,\n crmViewIsListed,\n isCrmLeadOpen,\n normalizeCrmViewFilters,\n} from '@aglyn/aglyn/app-utils/crm'\nimport { dynamicListDimensionsForCrmView } from '@aglyn/aglyn/app-utils/dynamic-list-rule'\nimport type { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport { findContactByEmail } from '@aglyn/tenant-data-admin/server/contact-email-index'\nimport { buildOutreachEnrollment, planOutreachFirstDue } from '../engine/enrollment-state'\nimport type { OutreachGateLookups } from '../engine/gates'\nimport {\n decideOutreachEnrollment,\n previewOutreachPerson,\n readOutreachEnrollCandidates,\n type OutreachEnrollCandidate,\n type OutreachGateQuestion,\n} from '../enrollment/enroll-people'\nimport { readOutreachGateLookups } from '../enrollment/gate-lookups'\nimport { effectiveOutreachAllowedCountries } from '../model/compliance-settings'\nimport {\n OUTREACH_ENROLL_BATCH_MAX,\n type OutreachEnrollOutcome,\n type OutreachEnrollPreviewResponse,\n type OutreachEnrollResponse,\n type OutreachPersonRef,\n} from '../model/outreach-api'\nimport type { OutreachSequence } from '../model/outreach.types'\nimport { readOutreachComplianceSettingsDoc } from '../storage/compliance-settings-store'\nimport {\n outreachEnrollmentId,\n outreachOrgCollection,\n readStoredOutreachMailbox,\n readStoredOutreachSequence,\n} from '../storage/outreach-records'\nimport { OUTREACH_SEQUENCE_ACTIVITY_TARGET, type OutreachRouteDeps } from './route-deps'\nimport { outreachRouteGate, type OutreachRouteCaller, type OutreachRouteExtraPermission } from './route-gate'\nimport {\n outreachMethodNotAllowed,\n outreachOk,\n outreachRefusal,\n readOutreachDocumentId,\n readOutreachJsonBody,\n} from './route-http'\n\n/**\n * ENROLLING PEOPLE IN A SEQUENCE (AGL-2980): `outreach/enroll/preview` and\n * `outreach/enroll`.\n *\n * The preview reads the people a saved Contacts view or a search named —\n * at most {@link OUTREACH_ENROLL_BATCH_MAX} — and says where each stands:\n * eligible, blocked with the gate's own sentence, or waiting on the rep for\n * a personal line and the attestations only they can make. Confirm reads\n * everything again and asks the gates again with what the rep supplied,\n * because a preview is a picture of a moment: someone can unsubscribe\n * between the two, and a request can claim anything. Nothing the preview\n * answered is trusted by the enroll.\n *\n * Both read the CRM's people, so both ask for `data.manage` beside\n * `outreach.use` — the permission the contacts' own rules read with, so\n * Outreach is no side door into the address book.\n *\n * Only an ACTIVE sequence takes people: enrolling schedules the first step\n * in the mailbox's hours, and a draft's steps and a paused sequence's timing\n * are not yet what will be sent.\n */\n\n/** The permission enrolling reads the CRM with. */\nexport const OUTREACH_READS_PEOPLE: OutreachRouteExtraPermission = {\n key: 'data.manage',\n refusal: \"Enrolling reads your CRM's contacts, and your role does not include Manage data.\",\n}\n\n/** Everything enrolling reaches beyond the routes' own dependencies. */\nexport interface OutreachEnrollRouteDeps extends OutreachRouteDeps {\n /**\n * The addresses a saved Contacts view selects among one site's contacts,\n * read the way the dynamic-list sweep reads one, and whether the read\n * reached the whole view before its budget.\n */\n crmViewEmails(input: { hostId: string; viewId: string }): Promise<{ emails: string[]; complete: boolean }>\n}\n\nexport interface OutreachEnrollRoutes {\n preview: PluginWebApiHandler\n confirm: PluginWebApiHandler\n}\n\n/** The activity line an enroll writes. */\nexport function outreachEnrolledActivity(count: number): string {\n return count === 1\n ? 'Enrolled 1 person in a sequence'\n : `Enrolled ${count} people in a sequence`\n}\n\n/** Lookups for a person the gates will never be asked about: every one unchecked. */\nconst UNCHECKED: OutreachGateLookups = {\n platformSuppressed: null,\n hostSuppressed: null,\n salesTopicState: null,\n doNotContact: null,\n workspaceMembers: null,\n openEnrollments: null,\n hasInboundEmail: null,\n}\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** gRPC `ALREADY_EXISTS`, which `create()` rejects with when the document is there. */\nconst ALREADY_EXISTS = 6\n\nasync function loadActiveSequence(\n firestore: Firestore,\n orgId: string,\n rawSequenceId: unknown,\n): Promise<Response | OutreachSequence> {\n const sequenceId = readOutreachDocumentId(rawSequenceId)\n if (!sequenceId) return outreachRefusal(400, 'invalid-request', 'Name the sequence to enroll people in.')\n const snapshot = await outreachOrgCollection(firestore, orgId, 'sequences').doc(sequenceId).get()\n const sequence = readStoredOutreachSequence(sequenceId, snapshot.exists ? snapshot.data() : undefined)\n if (!sequence) return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n if (sequence.status === 'archived') {\n return outreachRefusal(409, 'sequence-archived', 'This sequence is archived and takes no one new.')\n }\n if (sequence.status !== 'active') {\n return outreachRefusal(409, 'sequence-not-active', 'Activate this sequence before enrolling people.')\n }\n return sequence\n}\n\n/** What every person in one request is judged against. */\ninterface EnrollContext {\n settings: OutreachGateQuestion['settings']\n contactGroupId: string\n siteName: string\n}\n\nasync function enrollContext(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n sequence: OutreachSequence,\n): Promise<EnrollContext> {\n const [orgSettings, host] = await Promise.all([\n readOutreachComplianceSettingsDoc(firestore, caller.orgId),\n firestore.collection('hosts').doc(sequence.hostId).get(),\n ])\n return {\n // The organization's countries are a ceiling over the sequence's.\n settings: {\n ...sequence.settings,\n allowedCountries: effectiveOutreachAllowedCountries(\n sequence.settings.allowedCountries,\n orgSettings.allowedCountries,\n ),\n },\n contactGroupId: consentGroupForHost(caller.org, sequence.hostId).groupId,\n siteName: host.exists ? String(host.get('name') ?? '') : '',\n }\n}\n\n/**\n * The candidates, the enrollments they already have here, and the gates'\n * lookups — every map keyed by the person's own id.\n *\n * \"Already in this sequence\" is answered two ways (AGL-3234): by the id the\n * person's enrollment would have, and by any enrollment in this sequence\n * that carries the person's ADDRESS. The second is what keeps a contact\n * from being enrolled beside the lead they were converted from: the lead's\n * enrollment followed them, under the lead's key, and still names them.\n */\nasync function readPeople(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n sequence: OutreachSequence,\n context: EnrollContext,\n people: readonly OutreachPersonRef[],\n): Promise<{\n candidates: OutreachEnrollCandidate[]\n enrolledHere: Set<string>\n lookups: Map<string, OutreachGateLookups>\n}> {\n const candidates = await readOutreachEnrollCandidates(firestore, {\n orgId: caller.orgId,\n hostId: sequence.hostId,\n contactGroupId: context.contactGroupId,\n people,\n })\n const enrollments = outreachOrgCollection(firestore, caller.orgId, 'enrollments')\n const existing = candidates.length\n ? await firestore.getAll(\n ...candidates.map((candidate) => enrollments.doc(outreachEnrollmentId(sequence.id, candidate.personId))),\n )\n : []\n const enrolledHere = new Set(\n candidates.filter((_candidate, index) => existing[index]?.exists).map((candidate) => candidate.personId),\n )\n const askable = candidates.filter((candidate) => candidate.contact && candidate.visible && candidate.email)\n const lookups = askable.length\n ? await readOutreachGateLookups(firestore, {\n orgId: caller.orgId,\n hostId: sequence.hostId,\n people: askable.map((candidate) => ({\n personId: candidate.personId,\n contactId: candidate.contactId || null,\n leadId: candidate.leadId,\n email: String(candidate.email),\n })),\n })\n : new Map<string, OutreachGateLookups>()\n for (const candidate of askable) {\n const open = lookups.get(candidate.personId)?.openEnrollments ?? []\n if (open.some((entry) => entry.sequenceId === sequence.id)) enrolledHere.add(candidate.personId)\n }\n return { candidates, enrolledHere, lookups }\n}\n\nconst uniqueIds = (values: unknown): string[] =>\n Array.isArray(values)\n ? [...new Set(values.map(readOutreachDocumentId).filter((id): id is string => id !== null))]\n : []\n\n/** The most leads a saved Leads view reads — the Leads list's own window. */\nconst LEADS_VIEW_WINDOW = 200\n\n/**\n * The people a saved LEADS view selects (AGL-3234): the sequence's site's\n * most recently seen leads, narrowed by the view's status clause the way\n * the Leads list narrows its window — open leads when the view names no\n * status, one status when it does, everything for `all`. A lead is the\n * site's own, so there is no address to resolve: the person key IS the id.\n */\nasync function leadsViewPeople(\n firestore: Firestore,\n sequence: OutreachSequence,\n filters: ReturnType<typeof normalizeCrmViewFilters>,\n): Promise<{ people: OutreachPersonRef[]; total: number; truncated: boolean }> {\n const statusClause = filters.find((clause) => clause.field === 'status' && clause.op === 'equals')\n const wanted = String(statusClause?.value ?? 'open')\n const window = await firestore\n .collection('hosts')\n .doc(sequence.hostId)\n .collection('leads')\n .orderBy('lastSeenAtMs', 'desc')\n .limit(LEADS_VIEW_WINDOW + 1)\n .get()\n const matching = window.docs.slice(0, LEADS_VIEW_WINDOW).filter((doc) => {\n const lead = doc.data() as Record<string, unknown>\n if (wanted === 'all') return true\n if (wanted === 'open') return isCrmLeadOpen(lead as never) && !lead['convertedContactId']\n return crmLeadStatus(lead as never) === wanted\n })\n return {\n people: matching.slice(0, OUTREACH_ENROLL_BATCH_MAX).map((doc) => ({ kind: 'lead', id: doc.id })),\n total: matching.length,\n truncated: matching.length > OUTREACH_ENROLL_BATCH_MAX || window.docs.length > LEADS_VIEW_WINDOW,\n }\n}\n\nexport function createOutreachEnrollRoutes(deps: OutreachEnrollRouteDeps): OutreachEnrollRoutes {\n /**\n * The people a source names, the batch limit applied: every contact a\n * search picked, every lead picked from the sequence's site, or the\n * people a saved Contacts or Leads view selects there — contacts found by\n * address, leads by their own key.\n */\n async function sourcePeople(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n sequence: OutreachSequence,\n source: Record<string, unknown>,\n ): Promise<Response | { people: OutreachPersonRef[]; total: number; truncated: boolean }> {\n if (source['kind'] === 'contacts' || source['kind'] === 'leads') {\n const kind = source['kind'] === 'leads' ? 'lead' : 'contact'\n const ids = uniqueIds(kind === 'lead' ? source['leadIds'] : source['contactIds'])\n if (!ids.length) return outreachRefusal(400, 'invalid-request', 'Pick the people to enroll.')\n return {\n people: ids.slice(0, OUTREACH_ENROLL_BATCH_MAX).map((id) => ({ kind, id })),\n total: ids.length,\n truncated: ids.length > OUTREACH_ENROLL_BATCH_MAX,\n }\n }\n if (source['kind'] !== 'view') {\n return outreachRefusal(400, 'invalid-request', 'Enroll people from a saved view or a search.')\n }\n const viewId = readOutreachDocumentId(source['viewId'])\n const view = viewId\n ? await firestore.collection('orgs').doc(caller.orgId).collection(CRM_COLLECTIONS.views).doc(viewId).get()\n : null\n const data = view?.exists ? (view.data() as Record<string, unknown>) : null\n // A colleague's private view is theirs: it is not listed for this\n // reader, so it does not exist for them here either.\n if (\n !viewId ||\n !data ||\n !crmViewIsListed({ shared: data['shared'] === true, ownerUid: String(data['ownerUid'] ?? '') }, caller.uid)\n ) {\n return outreachRefusal(404, 'view-not-found', 'That saved view no longer exists.')\n }\n if (data['section'] === 'leads') {\n return leadsViewPeople(firestore, sequence, normalizeCrmViewFilters(data['filters']))\n }\n if (data['section'] !== 'contacts') {\n return outreachRefusal(400, 'view-unsupported', 'Enroll from a saved view of Contacts or Leads.')\n }\n const { unsupported } = dynamicListDimensionsForCrmView(normalizeCrmViewFilters(data['filters']))\n if (unsupported.length) {\n // Dropping the filter enrolling cannot read would enroll more people\n // than the view shows, so the view is refused whole.\n return outreachRefusal(\n 400,\n 'view-unsupported',\n `This view filters on ${unsupported.map((clause) => clause.label || clause.field).join(', ')}, which enrolling can't apply. Pick another view, or search.`,\n )\n }\n const { emails, complete } = await deps.crmViewEmails({ hostId: sequence.hostId, viewId })\n const ordered = [...new Set(emails)].sort()\n const contacts = firestore.collection('orgs').doc(caller.orgId).collection('contacts')\n const found = await Promise.all(\n ordered\n .slice(0, OUTREACH_ENROLL_BATCH_MAX)\n .map((email) => findContactByEmail(contacts, email, { hostId: sequence.hostId })),\n )\n return {\n people: [...new Set(found.filter((snapshot) => snapshot).map((snapshot) => String(snapshot?.id)))].map(\n (id) => ({ kind: 'contact', id }),\n ),\n total: ordered.length,\n truncated: ordered.length > OUTREACH_ENROLL_BATCH_MAX || !complete,\n }\n }\n\n const preview: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate, [OUTREACH_READS_PEOPLE])\n if (caller instanceof Response) return caller\n const firestore = deps.firestore()\n const sequence = await loadActiveSequence(firestore, caller.orgId, body['sequenceId'])\n if (sequence instanceof Response) return sequence\n const source = body['source'] && typeof body['source'] === 'object' ? (body['source'] as Record<string, unknown>) : {}\n const named = await sourcePeople(firestore, caller, sequence, source)\n if (named instanceof Response) return named\n const context = await enrollContext(firestore, caller, sequence)\n const { candidates, enrolledHere, lookups } = await readPeople(\n firestore,\n caller,\n sequence,\n context,\n named.people,\n )\n const people = candidates.map((candidate) =>\n previewOutreachPerson(\n {\n candidate,\n lookups: lookups.get(candidate.personId) ?? UNCHECKED,\n settings: context.settings,\n contactGroupId: context.contactGroupId,\n },\n { siteName: context.siteName, alreadyInSequence: enrolledHere.has(candidate.personId) },\n ),\n )\n return outreachOk({\n ok: true,\n people,\n total: named.total,\n truncated: named.truncated,\n } satisfies OutreachEnrollPreviewResponse)\n }\n\n const confirm: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate, [OUTREACH_READS_PEOPLE])\n if (caller instanceof Response) return caller\n const firestore = deps.firestore()\n const sequence = await loadActiveSequence(firestore, caller.orgId, body['sequenceId'])\n if (sequence instanceof Response) return sequence\n\n // Each person once, by whichever record they are: a contact by id, or a\n // lead by its key (AGL-3234). A person named both ways is a contact.\n const requested = new Map<\n string,\n { ref: OutreachPersonRef; personalLine: string; attestations: unknown[] }\n >()\n for (const entry of Array.isArray(body['people']) ? body['people'] : []) {\n const person = (entry && typeof entry === 'object' ? entry : {}) as Record<string, unknown>\n const contactId = readOutreachDocumentId(person['contactId'])\n const leadId = readOutreachDocumentId(person['leadId'])\n const ref: OutreachPersonRef | null = contactId\n ? { kind: 'contact', id: contactId }\n : leadId\n ? { kind: 'lead', id: leadId }\n : null\n if (!ref || requested.has(ref.id)) continue\n requested.set(ref.id, {\n ref,\n personalLine: typeof person['personalLine'] === 'string' ? person['personalLine'] : '',\n attestations: Array.isArray(person['attestations']) ? person['attestations'] : [],\n })\n }\n if (!requested.size) return outreachRefusal(400, 'invalid-request', 'Pick the people to enroll.')\n if (requested.size > OUTREACH_ENROLL_BATCH_MAX) {\n return outreachRefusal(\n 400,\n 'invalid-request',\n `Enroll at most ${OUTREACH_ENROLL_BATCH_MAX} people at a time.`,\n )\n }\n\n const mailboxSnapshot = sequence.mailboxId\n ? await outreachOrgCollection(firestore, caller.orgId, 'mailboxes').doc(sequence.mailboxId).get()\n : null\n const mailbox = readStoredOutreachMailbox(\n sequence.mailboxId,\n mailboxSnapshot?.exists ? mailboxSnapshot.data() : undefined,\n )\n if (!mailbox || mailbox.status === 'disconnected') {\n return outreachRefusal(\n 409,\n 'mailbox-unavailable',\n \"This sequence's mailbox is no longer connected. Choose another mailbox for it before enrolling people.\",\n )\n }\n const nowMs = deps.now()\n if (planOutreachFirstDue({ sequence, mailbox, enrolledAtMs: nowMs, random: deps.random }) === null) {\n return outreachRefusal(\n 409,\n 'mailbox-unavailable',\n \"This sequence's sending hours never open in its mailbox's timezone. Fix the hours before enrolling people.\",\n )\n }\n\n const context = await enrollContext(firestore, caller, sequence)\n const { candidates, enrolledHere, lookups } = await readPeople(\n firestore,\n caller,\n sequence,\n context,\n [...requested.values()].map((entry) => entry.ref),\n )\n const enrollments = outreachOrgCollection(firestore, caller.orgId, 'enrollments')\n const results: OutreachEnrollOutcome[] = await Promise.all(\n candidates.map(async (candidate): Promise<OutreachEnrollOutcome> => {\n const named = {\n personId: candidate.personId,\n target: candidate.target,\n contactId: candidate.contactId,\n leadId: candidate.leadId,\n }\n const supplied = requested.get(candidate.personId) ?? { personalLine: '', attestations: [] }\n const decision = decideOutreachEnrollment(\n {\n candidate,\n lookups: lookups.get(candidate.personId) ?? UNCHECKED,\n settings: context.settings,\n contactGroupId: context.contactGroupId,\n },\n { siteName: context.siteName, alreadyInSequence: enrolledHere.has(candidate.personId) },\n { personalLine: supplied.personalLine, attestations: supplied.attestations, uid: caller.uid, nowMs },\n )\n if (!decision.allowed || !decision.email) {\n return { ...named, email: candidate.email, outcome: 'blocked', blocks: decision.blocks }\n }\n const id = outreachEnrollmentId(sequence.id, candidate.personId)\n const enrollment = buildOutreachEnrollment({\n id,\n sequence,\n mailbox,\n target: candidate.target,\n contactId: candidate.contactId,\n leadId: candidate.leadId,\n contactName: candidate.name,\n email: decision.email,\n cold: decision.cold,\n personalLine: supplied.personalLine,\n attestations: decision.attestations,\n enrolledByUid: caller.uid,\n nowMs,\n random: deps.random,\n })\n try {\n await enrollments.doc(id).create(enrollment)\n } catch (error) {\n if ((error as { code?: unknown })?.code !== ALREADY_EXISTS) throw error\n return {\n ...named,\n email: candidate.email,\n outcome: 'blocked',\n blocks: [\n {\n code: 'already_in_sequence',\n reason: \"This person has already been in this sequence, so they won't be sent its emails again.\",\n },\n ],\n }\n }\n return { ...named, email: decision.email, outcome: 'enrolled', enrollmentId: id }\n }),\n )\n const enrolled = results.filter((result) => result.outcome === 'enrolled').length\n if (enrolled) {\n await deps.logOrgActivity(\n caller.orgId,\n { uid: caller.uid, email: caller.email },\n outreachEnrolledActivity(enrolled),\n { type: OUTREACH_SEQUENCE_ACTIVITY_TARGET, id: sequence.id, name: sequence.name },\n )\n }\n return outreachOk({ ok: true, results, enrolled } satisfies OutreachEnrollResponse)\n }\n\n return { preview, confirm }\n}\n"],"names":["consentGroupForHost","CRM_COLLECTIONS","crmLeadStatus","crmViewIsListed","isCrmLeadOpen","normalizeCrmViewFilters","dynamicListDimensionsForCrmView","findContactByEmail","buildOutreachEnrollment","planOutreachFirstDue","decideOutreachEnrollment","previewOutreachPerson","readOutreachEnrollCandidates","readOutreachGateLookups","effectiveOutreachAllowedCountries","OUTREACH_ENROLL_BATCH_MAX","readOutreachComplianceSettingsDoc","outreachEnrollmentId","outreachOrgCollection","readStoredOutreachMailbox","readStoredOutreachSequence","OUTREACH_SEQUENCE_ACTIVITY_TARGET","outreachRouteGate","outreachMethodNotAllowed","outreachOk","outreachRefusal","readOutreachDocumentId","readOutreachJsonBody","OUTREACH_READS_PEOPLE","key","refusal","outreachEnrolledActivity","count","UNCHECKED","platformSuppressed","hostSuppressed","salesTopicState","doNotContact","workspaceMembers","openEnrollments","hasInboundEmail","ALREADY_EXISTS","loadActiveSequence","firestore","orgId","rawSequenceId","sequenceId","snapshot","doc","get","sequence","exists","data","undefined","status","enrollContext","caller","host","orgSettings","Promise","all","collection","hostId","settings","allowedCountries","contactGroupId","org","groupId","siteName","String","readPeople","context","people","candidates","enrollments","existing","length","getAll","map","candidate","id","personId","enrolledHere","Set","filter","_candidate","index","askable","contact","visible","email","lookups","contactId","leadId","Map","open","some","entry","add","uniqueIds","values","Array","isArray","LEADS_VIEW_WINDOW","leadsViewPeople","filters","statusClause","find","clause","field","op","wanted","value","window","orderBy","limit","matching","docs","slice","lead","kind","total","truncated","createOutreachEnrollRoutes","deps","sourcePeople","source","ids","viewId","view","views","shared","ownerUid","uid","unsupported","label","join","emails","complete","crmViewEmails","ordered","sort","contacts","found","preview","request","method","body","gate","Response","named","alreadyInSequence","has","ok","confirm","requested","person","ref","set","personalLine","attestations","size","mailboxSnapshot","mailboxId","mailbox","nowMs","now","enrolledAtMs","random","results","target","supplied","decision","allowed","outcome","blocks","enrollment","contactName","name","cold","enrolledByUid","create","error","code","reason","enrollmentId","enrolled","result","logOrgActivity","type"],"mappings":"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"}
|
|
@@ -19,6 +19,7 @@ import { CRM_COLLECTIONS } from "@aglyn/aglyn/app-utils/crm";
|
|
|
19
19
|
import { normalizeCrmEmailTemplate } from "@aglyn/aglyn/app-utils/crm-email-templates";
|
|
20
20
|
import { visibleToHost } from "@aglyn/aglyn/app-utils/scope-tokens";
|
|
21
21
|
import { firstEmailStepIndex } from "../engine/sequence-validation.js";
|
|
22
|
+
import { leadAsContact } from "../enrollment/enroll-people.js";
|
|
22
23
|
import { OUTREACH_SAMPLE_PERSON, outreachSampleMergeContext, previewOutreachStep } from "../model/step-preview.js";
|
|
23
24
|
import { readOutreachComplianceSettingsDoc } from "../storage/compliance-settings-store.js";
|
|
24
25
|
import { outreachOrgCollection, readStoredOutreachMailbox, readStoredOutreachSequence } from "../storage/outreach-records.js";
|
|
@@ -48,7 +49,12 @@ import { outreachMethodNotAllowed, outreachOk, outreachRefusal, readOutreachDocu
|
|
|
48
49
|
if (body['contactId'] !== undefined && !contactId) {
|
|
49
50
|
return outreachRefusal(400, 'invalid-request', 'Name the contact to preview for.');
|
|
50
51
|
}
|
|
51
|
-
|
|
52
|
+
// A lead the sequence's site holds (AGL-3234), previewed as the contact it would be.
|
|
53
|
+
const leadId = body['leadId'] === undefined ? null : readOutreachDocumentId(body['leadId']);
|
|
54
|
+
if (body['leadId'] !== undefined && !leadId) {
|
|
55
|
+
return outreachRefusal(400, 'invalid-request', 'Name the lead to preview for.');
|
|
56
|
+
}
|
|
57
|
+
const caller = await outreachRouteGate(request, body['orgId'], deps.gate, contactId || leadId ? [
|
|
52
58
|
OUTREACH_READS_PEOPLE
|
|
53
59
|
] : []);
|
|
54
60
|
if (caller instanceof Response) return caller;
|
|
@@ -65,14 +71,20 @@ import { outreachMethodNotAllowed, outreachOk, outreachRefusal, readOutreachDocu
|
|
|
65
71
|
if ((step == null ? void 0 : step.kind) !== 'email') {
|
|
66
72
|
return outreachRefusal(400, 'invalid-request', 'That step does not send an email.');
|
|
67
73
|
}
|
|
68
|
-
const [orgSettings, mailboxSnapshot, host, template, contact] = await Promise.all([
|
|
74
|
+
const [orgSettings, mailboxSnapshot, host, template, contact, lead] = await Promise.all([
|
|
69
75
|
readOutreachComplianceSettingsDoc(firestore, caller.orgId),
|
|
70
76
|
sequence.mailboxId ? outreachOrgCollection(firestore, caller.orgId, 'mailboxes').doc(sequence.mailboxId).get() : null,
|
|
71
77
|
sequence.hostId ? firestore.collection('hosts').doc(sequence.hostId).get() : null,
|
|
72
78
|
step.templateId ? org.collection(CRM_COLLECTIONS.emailTemplates).doc(step.templateId).get() : null,
|
|
73
|
-
contactId ? org.collection('contacts').doc(contactId).get() : null
|
|
79
|
+
contactId ? org.collection('contacts').doc(contactId).get() : null,
|
|
80
|
+
leadId && sequence.hostId ? firestore.collection('hosts').doc(sequence.hostId).collection('leads').doc(leadId).get() : null
|
|
74
81
|
]);
|
|
75
|
-
const
|
|
82
|
+
const leadData = (lead == null ? void 0 : lead.exists) ? lead.data() : null;
|
|
83
|
+
if (leadId && !leadData) {
|
|
84
|
+
return outreachRefusal(404, 'contact-not-found', "That lead isn't in this sequence's site's CRM.");
|
|
85
|
+
}
|
|
86
|
+
const contactGroupId = consentGroupForHost(caller.org, sequence.hostId).groupId;
|
|
87
|
+
const contactData = (contact == null ? void 0 : contact.exists) ? contact.data() : leadData ? leadAsContact(leadData, contactGroupId) : null;
|
|
76
88
|
if (contactId && (!contactData || !visibleToHost(contactData['visibleTo'], sequence.hostId))) {
|
|
77
89
|
return outreachRefusal(404, 'contact-not-found', "That contact isn't in this sequence's site's CRM.");
|
|
78
90
|
}
|
|
@@ -84,7 +96,8 @@ import { outreachMethodNotAllowed, outreachOk, outreachRefusal, readOutreachDocu
|
|
|
84
96
|
const siteName = (host == null ? void 0 : host.exists) ? String((_host_get = host.get('name')) != null ? _host_get : '') : '';
|
|
85
97
|
const merge = contactData ? {
|
|
86
98
|
contact: contactData,
|
|
87
|
-
contactGroupId
|
|
99
|
+
contactGroupId,
|
|
100
|
+
lead: leadData,
|
|
88
101
|
sender,
|
|
89
102
|
site: {
|
|
90
103
|
name: siteName
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/preview-routes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { consentGroupForHost } from '@aglyn/aglyn/app-utils/consent-groups'\nimport { normalizeContactEmail } from '@aglyn/aglyn/app-utils/contacts'\nimport { CRM_COLLECTIONS } from '@aglyn/aglyn/app-utils/crm'\nimport { normalizeCrmEmailTemplate } from '@aglyn/aglyn/app-utils/crm-email-templates'\nimport { visibleToHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport type { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport { firstEmailStepIndex } from '../engine/sequence-validation'\nimport type { OutreachPreviewResponse } from '../model/outreach-api'\nimport {\n OUTREACH_SAMPLE_PERSON,\n outreachSampleMergeContext,\n previewOutreachStep,\n} from '../model/step-preview'\nimport { readOutreachComplianceSettingsDoc } from '../storage/compliance-settings-store'\nimport {\n outreachOrgCollection,\n readStoredOutreachMailbox,\n readStoredOutreachSequence,\n} from '../storage/outreach-records'\nimport { OUTREACH_READS_PEOPLE } from './enroll-routes'\nimport type { OutreachRouteDeps } from './route-deps'\nimport { outreachRouteGate } from './route-gate'\nimport {\n outreachMethodNotAllowed,\n outreachOk,\n outreachRefusal,\n readOutreachDocumentId,\n readOutreachJsonBody,\n} from './route-http'\n\n/**\n * ONE EMAIL OF A SAVED SEQUENCE, AS A PERSON WOULD GET IT (AGL-2980):\n * `outreach/preview`.\n *\n * Written from what is stored — the sequence, the CRM template a step names,\n * the mailbox it sends as, the organization's footer — by the engine's own\n * composer (`previewOutreachStep`), for a real contact with the personal\n * line the rep is writing, or for a sample person. The enroll dialog shows\n * it beside each person before Confirm, so the rep reads the sentence they\n * wrote in the email it goes into.\n *\n * A real contact is read from the CRM, so naming one asks for `data.manage`\n * as enrolling does; the sample person asks for nothing more.\n */\n\nexport function createOutreachPreviewRoute(deps: OutreachRouteDeps): PluginWebApiHandler {\n return async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const contactId = body['contactId'] === undefined ? null : readOutreachDocumentId(body['contactId'])\n if (body['contactId'] !== undefined && !contactId) {\n return outreachRefusal(400, 'invalid-request', 'Name the contact to preview for.')\n }\n const caller = await outreachRouteGate(\n request,\n body['orgId'],\n deps.gate,\n contactId ? [OUTREACH_READS_PEOPLE] : [],\n )\n if (caller instanceof Response) return caller\n const sequenceId = readOutreachDocumentId(body['sequenceId'])\n if (!sequenceId) return outreachRefusal(400, 'invalid-request', 'Name the sequence to preview.')\n const firestore = deps.firestore()\n const org = firestore.collection('orgs').doc(caller.orgId)\n const snapshot = await outreachOrgCollection(firestore, caller.orgId, 'sequences').doc(sequenceId).get()\n const sequence = readStoredOutreachSequence(sequenceId, snapshot.exists ? snapshot.data() : undefined)\n if (!sequence) return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n\n const requested = body['stepIndex']\n const stepIndex = typeof requested === 'number' ? requested : firstEmailStepIndex(sequence.steps)\n const step = sequence.steps[stepIndex]\n if (step?.kind !== 'email') {\n return outreachRefusal(400, 'invalid-request', 'That step does not send an email.')\n }\n\n const [orgSettings, mailboxSnapshot, host, template, contact] = await Promise.all([\n readOutreachComplianceSettingsDoc(firestore, caller.orgId),\n sequence.mailboxId ? outreachOrgCollection(firestore, caller.orgId, 'mailboxes').doc(sequence.mailboxId).get() : null,\n sequence.hostId ? firestore.collection('hosts').doc(sequence.hostId).get() : null,\n step.templateId ? org.collection(CRM_COLLECTIONS.emailTemplates).doc(step.templateId).get() : null,\n contactId ? org.collection('contacts').doc(contactId).get() : null,\n ])\n const contactData = contact?.exists ? (contact.data() as Record<string, unknown>) : null\n if (\n contactId &&\n (!contactData || !visibleToHost(contactData['visibleTo'] as string[] | undefined, sequence.hostId))\n ) {\n return outreachRefusal(404, 'contact-not-found', \"That contact isn't in this sequence's site's CRM.\")\n }\n const mailbox = readStoredOutreachMailbox(\n sequence.mailboxId,\n mailboxSnapshot?.exists ? mailboxSnapshot.data() : undefined,\n )\n const sender = mailbox ? { name: mailbox.displayName, email: mailbox.sendAs || mailbox.email } : null\n const siteName = host?.exists ? String(host.get('name') ?? '') : ''\n const merge = contactData\n ? {\n contact: contactData,\n contactGroupId: consentGroupForHost(caller.org, sequence.hostId).groupId,\n sender,\n site: { name: siteName },\n }\n : outreachSampleMergeContext({ sender, siteName })\n const personalLine =\n typeof body['personalLine'] === 'string'\n ? body['personalLine']\n : contactData\n ? ''\n : OUTREACH_SAMPLE_PERSON.personalLine\n const result = previewOutreachStep({\n steps: sequence.steps,\n stepIndex,\n orgSettings,\n merge,\n email: contactData ? (normalizeContactEmail(contactData['email']) ?? '') : OUTREACH_SAMPLE_PERSON.email,\n personalLine,\n templateBody: template?.exists\n ? normalizeCrmEmailTemplate(template.data() as Record<string, unknown>).body\n : null,\n })\n return outreachOk({\n ok: true,\n stepIndex,\n subject: result.email?.subject ?? '',\n text: result.email?.text ?? '',\n unresolvedFields: result.unresolvedFields,\n error: result.error,\n } satisfies OutreachPreviewResponse)\n }\n}\n"],"names":["consentGroupForHost","normalizeContactEmail","CRM_COLLECTIONS","normalizeCrmEmailTemplate","visibleToHost","firstEmailStepIndex","OUTREACH_SAMPLE_PERSON","outreachSampleMergeContext","previewOutreachStep","readOutreachComplianceSettingsDoc","outreachOrgCollection","readStoredOutreachMailbox","readStoredOutreachSequence","OUTREACH_READS_PEOPLE","outreachRouteGate","outreachMethodNotAllowed","outreachOk","outreachRefusal","readOutreachDocumentId","readOutreachJsonBody","createOutreachPreviewRoute","deps","request","host","result","method","body","contactId","undefined","caller","gate","Response","sequenceId","firestore","org","collection","doc","orgId","snapshot","get","sequence","exists","data","requested","stepIndex","steps","step","kind","orgSettings","mailboxSnapshot","template","contact","Promise","all","mailboxId","hostId","templateId","emailTemplates","contactData","mailbox","sender","name","displayName","email","sendAs","siteName","String","merge","contactGroupId","groupId","site","personalLine","templateBody","ok","subject","text","unresolvedFields","error"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/preview-routes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { consentGroupForHost } from '@aglyn/aglyn/app-utils/consent-groups'\nimport { normalizeContactEmail } from '@aglyn/aglyn/app-utils/contacts'\nimport { CRM_COLLECTIONS } from '@aglyn/aglyn/app-utils/crm'\nimport { normalizeCrmEmailTemplate } from '@aglyn/aglyn/app-utils/crm-email-templates'\nimport { visibleToHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport type { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport { firstEmailStepIndex } from '../engine/sequence-validation'\nimport { leadAsContact } from '../enrollment/enroll-people'\nimport type { OutreachPreviewResponse } from '../model/outreach-api'\nimport {\n OUTREACH_SAMPLE_PERSON,\n outreachSampleMergeContext,\n previewOutreachStep,\n} from '../model/step-preview'\nimport { readOutreachComplianceSettingsDoc } from '../storage/compliance-settings-store'\nimport {\n outreachOrgCollection,\n readStoredOutreachMailbox,\n readStoredOutreachSequence,\n} from '../storage/outreach-records'\nimport { OUTREACH_READS_PEOPLE } from './enroll-routes'\nimport type { OutreachRouteDeps } from './route-deps'\nimport { outreachRouteGate } from './route-gate'\nimport {\n outreachMethodNotAllowed,\n outreachOk,\n outreachRefusal,\n readOutreachDocumentId,\n readOutreachJsonBody,\n} from './route-http'\n\n/**\n * ONE EMAIL OF A SAVED SEQUENCE, AS A PERSON WOULD GET IT (AGL-2980):\n * `outreach/preview`.\n *\n * Written from what is stored — the sequence, the CRM template a step names,\n * the mailbox it sends as, the organization's footer — by the engine's own\n * composer (`previewOutreachStep`), for a real contact with the personal\n * line the rep is writing, or for a sample person. The enroll dialog shows\n * it beside each person before Confirm, so the rep reads the sentence they\n * wrote in the email it goes into.\n *\n * A real contact is read from the CRM, so naming one asks for `data.manage`\n * as enrolling does; the sample person asks for nothing more.\n */\n\nexport function createOutreachPreviewRoute(deps: OutreachRouteDeps): PluginWebApiHandler {\n return async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const contactId = body['contactId'] === undefined ? null : readOutreachDocumentId(body['contactId'])\n if (body['contactId'] !== undefined && !contactId) {\n return outreachRefusal(400, 'invalid-request', 'Name the contact to preview for.')\n }\n // A lead the sequence's site holds (AGL-3234), previewed as the contact it would be.\n const leadId = body['leadId'] === undefined ? null : readOutreachDocumentId(body['leadId'])\n if (body['leadId'] !== undefined && !leadId) {\n return outreachRefusal(400, 'invalid-request', 'Name the lead to preview for.')\n }\n const caller = await outreachRouteGate(\n request,\n body['orgId'],\n deps.gate,\n contactId || leadId ? [OUTREACH_READS_PEOPLE] : [],\n )\n if (caller instanceof Response) return caller\n const sequenceId = readOutreachDocumentId(body['sequenceId'])\n if (!sequenceId) return outreachRefusal(400, 'invalid-request', 'Name the sequence to preview.')\n const firestore = deps.firestore()\n const org = firestore.collection('orgs').doc(caller.orgId)\n const snapshot = await outreachOrgCollection(firestore, caller.orgId, 'sequences').doc(sequenceId).get()\n const sequence = readStoredOutreachSequence(sequenceId, snapshot.exists ? snapshot.data() : undefined)\n if (!sequence) return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n\n const requested = body['stepIndex']\n const stepIndex = typeof requested === 'number' ? requested : firstEmailStepIndex(sequence.steps)\n const step = sequence.steps[stepIndex]\n if (step?.kind !== 'email') {\n return outreachRefusal(400, 'invalid-request', 'That step does not send an email.')\n }\n\n const [orgSettings, mailboxSnapshot, host, template, contact, lead] = await Promise.all([\n readOutreachComplianceSettingsDoc(firestore, caller.orgId),\n sequence.mailboxId ? outreachOrgCollection(firestore, caller.orgId, 'mailboxes').doc(sequence.mailboxId).get() : null,\n sequence.hostId ? firestore.collection('hosts').doc(sequence.hostId).get() : null,\n step.templateId ? org.collection(CRM_COLLECTIONS.emailTemplates).doc(step.templateId).get() : null,\n contactId ? org.collection('contacts').doc(contactId).get() : null,\n leadId && sequence.hostId\n ? firestore.collection('hosts').doc(sequence.hostId).collection('leads').doc(leadId).get()\n : null,\n ])\n const leadData = lead?.exists ? (lead.data() as Record<string, unknown>) : null\n if (leadId && !leadData) {\n return outreachRefusal(404, 'contact-not-found', \"That lead isn't in this sequence's site's CRM.\")\n }\n const contactGroupId = consentGroupForHost(caller.org, sequence.hostId).groupId\n const contactData = contact?.exists\n ? (contact.data() as Record<string, unknown>)\n : leadData\n ? leadAsContact(leadData, contactGroupId)\n : null\n if (\n contactId &&\n (!contactData || !visibleToHost(contactData['visibleTo'] as string[] | undefined, sequence.hostId))\n ) {\n return outreachRefusal(404, 'contact-not-found', \"That contact isn't in this sequence's site's CRM.\")\n }\n const mailbox = readStoredOutreachMailbox(\n sequence.mailboxId,\n mailboxSnapshot?.exists ? mailboxSnapshot.data() : undefined,\n )\n const sender = mailbox ? { name: mailbox.displayName, email: mailbox.sendAs || mailbox.email } : null\n const siteName = host?.exists ? String(host.get('name') ?? '') : ''\n const merge = contactData\n ? {\n contact: contactData,\n contactGroupId,\n lead: leadData,\n sender,\n site: { name: siteName },\n }\n : outreachSampleMergeContext({ sender, siteName })\n const personalLine =\n typeof body['personalLine'] === 'string'\n ? body['personalLine']\n : contactData\n ? ''\n : OUTREACH_SAMPLE_PERSON.personalLine\n const result = previewOutreachStep({\n steps: sequence.steps,\n stepIndex,\n orgSettings,\n merge,\n email: contactData ? (normalizeContactEmail(contactData['email']) ?? '') : OUTREACH_SAMPLE_PERSON.email,\n personalLine,\n templateBody: template?.exists\n ? normalizeCrmEmailTemplate(template.data() as Record<string, unknown>).body\n : null,\n })\n return outreachOk({\n ok: true,\n stepIndex,\n subject: result.email?.subject ?? '',\n text: result.email?.text ?? '',\n unresolvedFields: result.unresolvedFields,\n error: result.error,\n } satisfies OutreachPreviewResponse)\n }\n}\n"],"names":["consentGroupForHost","normalizeContactEmail","CRM_COLLECTIONS","normalizeCrmEmailTemplate","visibleToHost","firstEmailStepIndex","leadAsContact","OUTREACH_SAMPLE_PERSON","outreachSampleMergeContext","previewOutreachStep","readOutreachComplianceSettingsDoc","outreachOrgCollection","readStoredOutreachMailbox","readStoredOutreachSequence","OUTREACH_READS_PEOPLE","outreachRouteGate","outreachMethodNotAllowed","outreachOk","outreachRefusal","readOutreachDocumentId","readOutreachJsonBody","createOutreachPreviewRoute","deps","request","host","result","method","body","contactId","undefined","leadId","caller","gate","Response","sequenceId","firestore","org","collection","doc","orgId","snapshot","get","sequence","exists","data","requested","stepIndex","steps","step","kind","orgSettings","mailboxSnapshot","template","contact","lead","Promise","all","mailboxId","hostId","templateId","emailTemplates","leadData","contactGroupId","groupId","contactData","mailbox","sender","name","displayName","email","sendAs","siteName","String","merge","site","personalLine","templateBody","ok","subject","text","unresolvedFields","error"],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import type { PluginLeadConversionReport, PluginLeadConversionRequest } from '@aglyn/aglyn/plugin-manager/plugin-lead-conversion';
|
|
18
|
+
type Firestore = FirebaseFirestore.Firestore;
|
|
19
|
+
/** A lead nobody has touched, once somebody replied: Working. `true` when it moved. */
|
|
20
|
+
export declare function markOutreachLeadWorking(firestore: Firestore, input: {
|
|
21
|
+
hostId: string;
|
|
22
|
+
leadId: string;
|
|
23
|
+
}): Promise<boolean>;
|
|
24
|
+
/** Every enrollment made on the lead now names the contact it became. */
|
|
25
|
+
export declare function followOutreachLeadToContact(firestore: Firestore, input: Pick<PluginLeadConversionRequest, 'orgId' | 'leadId' | 'contactId'>): Promise<PluginLeadConversionReport>;
|
|
26
|
+
export {};
|