@aglyn/plugins-outreach 1.0.0-beta.199 → 1.0.0-beta.201
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/package.json +14 -14
- package/src/lib/components/do-not-contact-domains.js +60 -42
- package/src/lib/components/do-not-contact-domains.js.map +1 -1
- package/src/lib/components/enrollment-actions.d.ts +33 -0
- package/src/lib/components/enrollment-actions.js +174 -0
- package/src/lib/components/enrollment-actions.js.map +1 -0
- package/src/lib/components/enrollment-detail.d.ts +21 -0
- package/src/lib/components/enrollment-detail.js +870 -0
- package/src/lib/components/enrollment-detail.js.map +1 -0
- package/src/lib/components/enrollment-filter-params.d.ts +58 -0
- package/src/lib/components/enrollment-filter-params.js +98 -0
- package/src/lib/components/enrollment-filter-params.js.map +1 -0
- package/src/lib/components/enrollments-table.d.ts +25 -2
- package/src/lib/components/enrollments-table.js +303 -283
- package/src/lib/components/enrollments-table.js.map +1 -1
- package/src/lib/components/outreach-console-page.js +10 -0
- package/src/lib/components/outreach-console-page.js.map +1 -1
- package/src/lib/components/sequence-detail.d.ts +2 -0
- package/src/lib/components/sequence-detail.js +73 -11
- package/src/lib/components/sequence-detail.js.map +1 -1
- package/src/lib/components/sequence-report-card.d.ts +17 -0
- package/src/lib/components/sequence-report-card.js +63 -15
- package/src/lib/components/sequence-report-card.js.map +1 -1
- package/src/lib/components/sequences-section.d.ts +13 -1
- package/src/lib/components/sequences-section.js +108 -83
- package/src/lib/components/sequences-section.js.map +1 -1
- package/src/lib/components/use-outreach-api.d.ts +3 -1
- package/src/lib/components/use-outreach-api.js +6 -0
- package/src/lib/components/use-outreach-api.js.map +1 -1
- package/src/lib/components/use-outreach-data.d.ts +30 -25
- package/src/lib/components/use-outreach-data.js +93 -104
- package/src/lib/components/use-outreach-data.js.map +1 -1
- package/src/lib/components/use-outreach-enrollment.d.ts +18 -0
- package/src/lib/components/use-outreach-enrollment.js +133 -0
- package/src/lib/components/use-outreach-enrollment.js.map +1 -0
- package/src/lib/constants/api-routes.d.ts +2 -0
- package/src/lib/constants/api-routes.js +1 -0
- package/src/lib/constants/api-routes.js.map +1 -1
- package/src/lib/engine/gates.d.ts +4 -3
- package/src/lib/engine/gates.js.map +1 -1
- package/src/lib/engine/gateway-block.d.ts +4 -8
- package/src/lib/engine/gateway-block.js +6 -26
- package/src/lib/engine/gateway-block.js.map +1 -1
- package/src/lib/engine/mail-gateway.d.ts +18 -73
- package/src/lib/engine/mail-gateway.js +32 -206
- package/src/lib/engine/mail-gateway.js.map +1 -1
- package/src/lib/enrollment/enrollment-search.d.ts +32 -0
- package/src/lib/enrollment/enrollment-search.js +40 -0
- package/src/lib/enrollment/enrollment-search.js.map +1 -0
- package/src/lib/model/do-not-contact-domain-list-query.d.ts +32 -0
- package/src/lib/model/do-not-contact-domain-list-query.js +107 -0
- package/src/lib/model/do-not-contact-domain-list-query.js.map +1 -0
- package/src/lib/model/enrollment-engagement.d.ts +26 -2
- package/src/lib/model/enrollment-engagement.js +40 -3
- package/src/lib/model/enrollment-engagement.js.map +1 -1
- package/src/lib/model/enrollment-history.d.ts +40 -0
- package/src/lib/model/enrollment-history.js +95 -0
- package/src/lib/model/enrollment-history.js.map +1 -0
- package/src/lib/model/enrollment-list-query.d.ts +28 -0
- package/src/lib/model/enrollment-list-query.js +151 -0
- package/src/lib/model/enrollment-list-query.js.map +1 -0
- package/src/lib/model/enrollment-timeline.d.ts +78 -0
- package/src/lib/model/enrollment-timeline.js +430 -0
- package/src/lib/model/enrollment-timeline.js.map +1 -0
- package/src/lib/model/outreach-api.d.ts +24 -0
- package/src/lib/model/outreach-api.js.map +1 -1
- package/src/lib/model/outreach.types.d.ts +90 -29
- package/src/lib/model/outreach.types.js +33 -8
- package/src/lib/model/outreach.types.js.map +1 -1
- package/src/lib/model/sequence-list-query.d.ts +22 -0
- package/src/lib/model/sequence-list-query.js +99 -0
- package/src/lib/model/sequence-list-query.js.map +1 -0
- package/src/lib/routes/enroll-routes.js +34 -3
- package/src/lib/routes/enroll-routes.js.map +1 -1
- package/src/lib/routes/enrollment-routes.d.ts +10 -0
- package/src/lib/routes/enrollment-routes.js +82 -1
- package/src/lib/routes/enrollment-routes.js.map +1 -1
- package/src/lib/routes/register-routes.js +14 -4
- package/src/lib/routes/register-routes.js.map +1 -1
- package/src/lib/routes/route-deps.d.ts +3 -1
- package/src/lib/routes/route-deps.js.map +1 -1
- package/src/lib/routes/sequence-routes.js +6 -1
- package/src/lib/routes/sequence-routes.js.map +1 -1
- package/src/lib/runtime/click-events.d.ts +10 -5
- package/src/lib/runtime/click-events.js +45 -6
- package/src/lib/runtime/click-events.js.map +1 -1
- package/src/lib/runtime/gateway-ledger.d.ts +6 -0
- package/src/lib/runtime/gateway-ledger.js +9 -3
- package/src/lib/runtime/gateway-ledger.js.map +1 -1
- package/src/lib/runtime/host-suspension.d.ts +30 -0
- package/src/lib/runtime/host-suspension.js +35 -0
- package/src/lib/runtime/host-suspension.js.map +1 -0
- package/src/lib/runtime/person-erasure.d.ts +4 -1
- package/src/lib/runtime/person-erasure.js +14 -3
- package/src/lib/runtime/person-erasure.js.map +1 -1
- package/src/lib/runtime/platform-runtime-deps.js +34 -3
- package/src/lib/runtime/platform-runtime-deps.js.map +1 -1
- package/src/lib/runtime/runtime-deps.d.ts +27 -1
- package/src/lib/runtime/runtime-deps.js.map +1 -1
- package/src/lib/runtime/send-job.js +46 -6
- package/src/lib/runtime/send-job.js.map +1 -1
- package/src/lib/runtime/sync-job.js +4 -0
- package/src/lib/runtime/sync-job.js.map +1 -1
- package/src/lib/storage/do-not-contact-store.js +6 -1
- package/src/lib/storage/do-not-contact-store.js.map +1 -1
- package/src/lib/storage/domain-intel-store.d.ts +61 -43
- package/src/lib/storage/domain-intel-store.js +138 -226
- package/src/lib/storage/domain-intel-store.js.map +1 -1
|
@@ -20,15 +20,18 @@ import { scopeTokensForHost } from "@aglyn/aglyn/app-utils/scope-tokens";
|
|
|
20
20
|
import { CRM_COLLECTIONS, crmLeadStatus, crmViewIsListed, isCrmLeadOpen, normalizeCrmViewFilters } from "@aglyn/aglyn/app-utils/crm";
|
|
21
21
|
import { dynamicListDimensionsForCrmView } from "@aglyn/aglyn/app-utils/dynamic-list-rule";
|
|
22
22
|
import { findContactByEmail } from "@aglyn/tenant-data-admin/server/contact-email-index";
|
|
23
|
+
import { restampCrmListFieldsAt } from "@aglyn/tenant-data-admin/server/crm-records";
|
|
23
24
|
import { FieldValue } from "firebase-admin/firestore";
|
|
24
25
|
import { outreachCuratedEntry, outreachEnrolledEntry } from "../engine/enrollment-activity.js";
|
|
25
26
|
import { buildOutreachEnrollment, planOutreachFirstDue } from "../engine/enrollment-state.js";
|
|
27
|
+
import { outreachEnrollmentSearchTokens } from "../enrollment/enrollment-search.js";
|
|
26
28
|
import { decideOutreachEnrollment, previewOutreachPerson, readOutreachEnrollCandidates } from "../enrollment/enroll-people.js";
|
|
27
29
|
import { readOutreachGateLookups } from "../enrollment/gate-lookups.js";
|
|
28
30
|
import { outreachStepOverridesRefused, readOutreachStepOverrideRequests } from "../enrollment/step-overrides.js";
|
|
29
31
|
import { effectiveOutreachAllowedCountries } from "../model/compliance-settings.js";
|
|
30
32
|
import { OUTREACH_ENROLL_BATCH_MAX } from "../model/outreach-api.js";
|
|
31
33
|
import { readOutreachComplianceSettingsDoc } from "../storage/compliance-settings-store.js";
|
|
34
|
+
import { outreachMailboxSendingDomain } from "../storage/domain-intel-store.js";
|
|
32
35
|
import { fileOutreachNote } from "../runtime/timeline.js";
|
|
33
36
|
import { outreachEnrollmentId, outreachEnrollmentLink, outreachOrgCollection, readStoredOutreachMailbox, readStoredOutreachSequence } from "../storage/outreach-records.js";
|
|
34
37
|
import { OUTREACH_SEQUENCE_ACTIVITY_TARGET } from "./route-deps.js";
|
|
@@ -114,7 +117,22 @@ async function enrollContext(firestore, caller, sequence) {
|
|
|
114
117
|
* that carries the person's ADDRESS. The second is what keeps a contact
|
|
115
118
|
* from being enrolled beside the lead they were converted from: the lead's
|
|
116
119
|
* enrollment followed them, under the lead's key, and still names them.
|
|
117
|
-
*/
|
|
120
|
+
*/ /**
|
|
121
|
+
* The domain the sequence's mailbox sends from (AGL-3328), whose gateway
|
|
122
|
+
* ledger the Check-people chips read — or `null` when the mailbox cannot
|
|
123
|
+
* be read, which shows the gateways without a history.
|
|
124
|
+
*/ async function sequenceSendingDomain(firestore, orgId, sequence) {
|
|
125
|
+
if (!sequence.mailboxId) return null;
|
|
126
|
+
try {
|
|
127
|
+
const snapshot = await outreachOrgCollection(firestore, orgId, 'mailboxes').doc(sequence.mailboxId).get();
|
|
128
|
+
const mailbox = readStoredOutreachMailbox(sequence.mailboxId, (snapshot == null ? void 0 : snapshot.exists) ? snapshot.data() : undefined);
|
|
129
|
+
return outreachMailboxSendingDomain(mailbox);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.error('[outreach] the sequence mailbox could not be read for its sending domain', error);
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async function readPeople(firestore, caller, sequence, context, people, gateway) {
|
|
118
136
|
const candidates = await readOutreachEnrollCandidates(firestore, {
|
|
119
137
|
orgId: caller.orgId,
|
|
120
138
|
hostId: sequence.hostId,
|
|
@@ -211,6 +229,9 @@ export function createOutreachEnrollRoutes(deps) {
|
|
|
211
229
|
[field]: FieldValue.arrayUnion(...campaignIds),
|
|
212
230
|
updatedAt: FieldValue.serverTimestamp()
|
|
213
231
|
});
|
|
232
|
+
// A lead's campaigns are what the Leads list's Campaign filter reads
|
|
233
|
+
// under a site (AGL-3321): restamped from the lead as it now stands.
|
|
234
|
+
if (candidate.target === 'lead') await restampCrmListFieldsAt(ref, 'leads');
|
|
214
235
|
}
|
|
215
236
|
} catch (error) {
|
|
216
237
|
console.error('[outreach] the enrolled person could not join the sequence’s campaigns', error);
|
|
@@ -331,7 +352,9 @@ export function createOutreachEnrollRoutes(deps) {
|
|
|
331
352
|
const context = await enrollContext(firestore, caller, sequence);
|
|
332
353
|
const { candidates, enrolledHere, lookups } = await readPeople(firestore, caller, sequence, context, named.people, {
|
|
333
354
|
resolveMx: deps.resolveMx,
|
|
334
|
-
|
|
355
|
+
resolveAddress: deps.resolveAddress,
|
|
356
|
+
nowMs: deps.now(),
|
|
357
|
+
sendingDomain: await sequenceSendingDomain(firestore, caller.orgId, sequence)
|
|
335
358
|
});
|
|
336
359
|
const people = candidates.map((candidate)=>{
|
|
337
360
|
var _lookups_get;
|
|
@@ -420,7 +443,9 @@ export function createOutreachEnrollRoutes(deps) {
|
|
|
420
443
|
...requested.values()
|
|
421
444
|
].map((entry)=>entry.ref), {
|
|
422
445
|
resolveMx: deps.resolveMx,
|
|
423
|
-
|
|
446
|
+
resolveAddress: deps.resolveAddress,
|
|
447
|
+
nowMs,
|
|
448
|
+
sendingDomain: outreachMailboxSendingDomain(mailbox)
|
|
424
449
|
});
|
|
425
450
|
const enrollments = outreachOrgCollection(firestore, caller.orgId, 'enrollments');
|
|
426
451
|
const campaignNames = await sequenceCampaignNames(firestore, caller.orgId, sequence);
|
|
@@ -504,6 +529,12 @@ export function createOutreachEnrollRoutes(deps) {
|
|
|
504
529
|
// The person's own copies of steps (AGL-3324) ride with the
|
|
505
530
|
// enrollment from its first write: nothing sends between.
|
|
506
531
|
if (Object.keys(stepOverrides).length) enrollment.stepOverrides = stepOverrides;
|
|
532
|
+
// What the enrollments table searches the person by (AGL-3321),
|
|
533
|
+
// from the name and address this document captures for good.
|
|
534
|
+
enrollment.searchTokens = outreachEnrollmentSearchTokens(enrollment);
|
|
535
|
+
// Nobody has clicked yet (AGL-3332): stored, because the "Clicked: No"
|
|
536
|
+
// filter is a query, and a query cannot find a field's absence.
|
|
537
|
+
enrollment.clicked = false;
|
|
507
538
|
try {
|
|
508
539
|
await enrollments.doc(id).create(enrollment);
|
|
509
540
|
} catch (error) {
|
|
@@ -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 {\n CAMPAIGN_MEMBERSHIP_FIELD,\n contactCampaignFieldPath,\n normalizeCampaignIds,\n} from '@aglyn/aglyn/app-utils/campaign-membership'\nimport { consentGroupForHost } from '@aglyn/aglyn/app-utils/consent-groups'\nimport { scopeTokensForHost } from '@aglyn/aglyn/app-utils/scope-tokens'\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 { PluginRecordTimelineWriter } from '@aglyn/aglyn/plugin-manager/plugin-record-timeline'\nimport type { PluginTextGenerator } from '@aglyn/aglyn/plugin-manager/plugin-text-generation'\nimport type { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport { findContactByEmail } from '@aglyn/tenant-data-admin/server/contact-email-index'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { outreachCuratedEntry, outreachEnrolledEntry } from '../engine/enrollment-activity'\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 {\n outreachStepOverridesRefused,\n readOutreachStepOverrideRequests,\n type OutreachStepOverrideRead,\n} from '../enrollment/step-overrides'\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, OutreachStepOverrides } from '../model/outreach.types'\nimport { readOutreachComplianceSettingsDoc } from '../storage/compliance-settings-store'\nimport type { OutreachDomainIntelReadInput } from '../storage/domain-intel-store'\nimport { fileOutreachNote } from '../runtime/timeline'\nimport {\n outreachEnrollmentId,\n outreachEnrollmentLink,\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 * The workspace's record system on the timeline seam (AGL-3274), when a\n * plugin keeps one: the enroll files \"Enrolled in\" on the person's record\n * — the runtime's `timeline`, reached the same way.\n */\n timeline(): PluginRecordTimelineWriter | null\n /**\n * The workspace's text generator on the core's text-generation seam\n * (AGL-3324), which drafts one person's copies of the steps; `null` when\n * no plugin generates text, and the curate route says so.\n */\n textGenerator(): PluginTextGenerator | null\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 doNotContactDomain: null,\n workspaceMembers: null,\n openEnrollments: null,\n hasInboundEmail: null,\n gateway: 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 /** The sequence site's consent group, whose opt-outs all apply. */\n consentHostIds: readonly string[]\n /** Whether that group's sites wait for each other's confirmation click. */\n consentAwaitsConfirmation: boolean\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 const consentGroup = consentGroupForHost(caller.org, sequence.hostId)\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: consentGroup.groupId,\n consentHostIds: consentGroup.hostIds,\n consentAwaitsConfirmation: consentGroup.awaitsConfirmation,\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 gateway: OutreachDomainIntelReadInput,\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 consentHostIds: context.consentHostIds,\n consentAwaitsConfirmation: context.consentAwaitsConfirmation,\n gateway,\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`. The person key IS\n * the id, so there is no address to resolve — and since AGL-3275 the window is\n * narrowed by `visibleTo` rather than by the collection's parent.\n */\nasync function leadsViewPeople(\n firestore: Firestore,\n orgId: string,\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 /*\n * SCOPED (AGL-3275). The collection is org-wide, so the window is narrowed\n * to what the sequence's site may see before it is ordered — otherwise an\n * agency's sequence would offer another client's people to enroll. The\n * composite index for `visibleTo array-contains-any` + `lastSeenAtMs desc`\n * is in `cloud/firestore.indexes.json`.\n */\n const window = await firestore\n .collection('orgs')\n .doc(orgId)\n .collection('leads')\n .where('visibleTo', 'array-contains-any', scopeTokensForHost(sequence.hostId))\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 person joins the sequence's campaigns (AGL-3254), the moment they\n * are enrolled: `arrayUnion` on the lead's own `campaignIds`, or on the\n * contact's facet for the sequence's site — the same field, the same\n * shape, every other campaign member carries — and `enrolled` credited\n * to each campaign. After the enrollment is created and never before:\n * a person the create refused joins nothing. Neither write may fail the\n * enrollment, which already exists; a membership that could not be\n * stamped is one the list's bulk bar can add by hand.\n */\n async function joinSequenceCampaigns(\n firestore: Firestore,\n orgId: string,\n sequence: OutreachSequence,\n contactGroupId: string,\n candidate: OutreachEnrollCandidate,\n nowMs: number,\n ): Promise<void> {\n const campaignIds = normalizeCampaignIds(sequence.campaignIds)\n if (!campaignIds.length) return\n try {\n const ref =\n candidate.target === 'lead' && candidate.leadId\n ? firestore.collection('orgs').doc(orgId).collection('leads').doc(candidate.leadId)\n : candidate.contactId\n ? firestore.collection('orgs').doc(orgId).collection('contacts').doc(candidate.contactId)\n : null\n if (ref) {\n const field =\n candidate.target === 'lead' ? CAMPAIGN_MEMBERSHIP_FIELD : contactCampaignFieldPath(contactGroupId)\n await ref.update({ [field]: FieldValue.arrayUnion(...campaignIds), updatedAt: FieldValue.serverTimestamp() })\n }\n } catch (error) {\n console.error('[outreach] the enrolled person could not join the sequence’s campaigns', error)\n }\n try {\n await deps.creditCampaign({ hostId: sequence.hostId, orgId, campaignIds, outcome: 'enrolled', atMs: nowMs })\n } catch (error) {\n console.error('[outreach] the enrollment could not be credited to the sequence’s campaigns', error)\n }\n }\n\n /**\n * The names of the sequence's campaigns as they stand, for the entry the\n * enroll files on the person's record (AGL-3274) — the org's containers,\n * `orgs/{orgId}/emailCampaigns`. Read once per request, not per person; a\n * container that is gone answers nothing and the entry names the rest.\n */\n async function sequenceCampaignNames(\n firestore: Firestore,\n orgId: string,\n sequence: OutreachSequence,\n ): Promise<string[]> {\n const campaignIds = normalizeCampaignIds(sequence.campaignIds)\n if (!campaignIds.length || !orgId) return []\n try {\n const containers = firestore.collection('orgs').doc(orgId).collection('emailCampaigns')\n const found = await firestore.getAll(...campaignIds.map((id) => containers.doc(id)))\n return found.map((snapshot) => String(snapshot.get('name') ?? '').trim()).filter(Boolean)\n } catch (error) {\n console.error('[outreach] the sequence’s campaigns could not be named for the record', error)\n return []\n }\n }\n\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, caller.orgId, 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 { resolveMx: deps.resolveMx, nowMs: deps.now() },\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 const nowMs = deps.now()\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[]; overrides: OutreachStepOverrideRead }\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 // The steps the member curated for this person (AGL-3324), validated\n // here rather than trusted: a copy that breaks a rule refuses the\n // whole request, so nobody is enrolled with a copy the route dropped.\n const overrides = readOutreachStepOverrideRequests(person['stepOverrides'], sequence.steps, {\n uid: caller.uid,\n nowMs,\n })\n if (outreachStepOverridesRefused(overrides)) {\n return outreachRefusal(\n 400,\n 'invalid-override',\n overrides.refusal ?? 'A curated step breaks a rule every sequence email keeps.',\n { issues: overrides.issues },\n )\n }\n requested.set(ref.id, {\n ref,\n personalLine: typeof person['personalLine'] === 'string' ? person['personalLine'] : '',\n attestations: Array.isArray(person['attestations']) ? person['attestations'] : [],\n overrides,\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 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 { resolveMx: deps.resolveMx, nowMs },\n )\n const enrollments = outreachOrgCollection(firestore, caller.orgId, 'enrollments')\n const campaignNames = await sequenceCampaignNames(firestore, caller.orgId, sequence)\n // The member's name for the curated entries (AGL-3324), read once per\n // request and only when a person carries a curated step.\n let memberNameRead: Promise<string | null> | null = null\n const memberName = () => {\n memberNameRead ??= firestore\n .collection('orgs')\n .doc(caller.orgId)\n .collection('members')\n .doc(caller.uid)\n .get()\n .then(\n (member) =>\n (member.exists\n ? String(member.get('displayName') ?? '').trim() || String(member.get('email') ?? '').trim()\n : '') || caller.email,\n )\n .catch(() => caller.email)\n return memberNameRead\n }\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) ?? {\n personalLine: '',\n attestations: [],\n overrides: { overrides: {}, issues: [], refusal: null } satisfies OutreachStepOverrideRead,\n }\n const stepOverrides: OutreachStepOverrides = supplied.overrides.overrides\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 // The member ticked this person past the red gateway chip\n // (AGL-3326): that is their say-so, stamped as the hold's release,\n // so the engine does not hold the send they were just shown.\n if (decision.hold) {\n enrollment.gatewayHold = {\n gateway: decision.hold.gateway,\n heldAtMs: null,\n releasedByUid: caller.uid,\n releasedAtMs: nowMs,\n }\n }\n // The person's own copies of steps (AGL-3324) ride with the\n // enrollment from its first write: nothing sends between.\n if (Object.keys(stepOverrides).length) enrollment.stepOverrides = stepOverrides\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 await joinSequenceCampaigns(firestore, caller.orgId, sequence, context.contactGroupId, candidate, nowMs)\n // The person's record says so (AGL-3274): \"Enrolled in <sequence>\",\n // with the campaigns it carried them into, once per enrollment.\n const entry = outreachEnrolledEntry({ enrollmentId: id, sequenceName: sequence.name, campaignNames })\n await fileOutreachNote(deps, {\n orgId: caller.orgId,\n hostId: sequence.hostId,\n link: outreachEnrollmentLink(enrollment),\n dedupeKey: entry.dedupeKey,\n body: entry.body,\n atMs: nowMs,\n })\n // And one line per step the member curated for them (AGL-3324),\n // after the enrollment's own, so the record reads in order.\n for (const [key, override] of Object.entries(stepOverrides)) {\n const curated = outreachCuratedEntry({\n enrollmentId: id,\n stepIndex: Number(key),\n source: override.source,\n edited: override.edited === true,\n memberName: await memberName(),\n atMs: nowMs,\n })\n await fileOutreachNote(deps, {\n orgId: caller.orgId,\n hostId: sequence.hostId,\n link: outreachEnrollmentLink(enrollment),\n dedupeKey: curated.dedupeKey,\n body: curated.body,\n atMs: nowMs,\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":["CAMPAIGN_MEMBERSHIP_FIELD","contactCampaignFieldPath","normalizeCampaignIds","consentGroupForHost","scopeTokensForHost","CRM_COLLECTIONS","crmLeadStatus","crmViewIsListed","isCrmLeadOpen","normalizeCrmViewFilters","dynamicListDimensionsForCrmView","findContactByEmail","FieldValue","outreachCuratedEntry","outreachEnrolledEntry","buildOutreachEnrollment","planOutreachFirstDue","decideOutreachEnrollment","previewOutreachPerson","readOutreachEnrollCandidates","readOutreachGateLookups","outreachStepOverridesRefused","readOutreachStepOverrideRequests","effectiveOutreachAllowedCountries","OUTREACH_ENROLL_BATCH_MAX","readOutreachComplianceSettingsDoc","fileOutreachNote","outreachEnrollmentId","outreachEnrollmentLink","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","doNotContactDomain","workspaceMembers","openEnrollments","hasInboundEmail","gateway","ALREADY_EXISTS","loadActiveSequence","firestore","orgId","rawSequenceId","sequenceId","snapshot","doc","get","sequence","exists","data","undefined","status","enrollContext","caller","host","orgSettings","Promise","all","collection","hostId","consentGroup","org","settings","allowedCountries","contactGroupId","groupId","consentHostIds","hostIds","consentAwaitsConfirmation","awaitsConfirmation","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","where","orderBy","limit","matching","docs","slice","lead","kind","total","truncated","createOutreachEnrollRoutes","deps","joinSequenceCampaigns","nowMs","campaignIds","ref","target","update","arrayUnion","updatedAt","serverTimestamp","error","console","creditCampaign","outcome","atMs","sequenceCampaignNames","containers","found","trim","Boolean","sourcePeople","source","ids","viewId","view","views","shared","ownerUid","uid","unsupported","label","join","emails","complete","crmViewEmails","ordered","sort","contacts","preview","request","method","body","gate","Response","named","resolveMx","now","alreadyInSequence","has","ok","confirm","requested","person","overrides","steps","issues","set","personalLine","attestations","size","mailboxSnapshot","mailboxId","mailbox","enrolledAtMs","random","campaignNames","memberNameRead","memberName","then","member","catch","results","supplied","stepOverrides","decision","allowed","blocks","enrollment","contactName","name","cold","enrolledByUid","hold","gatewayHold","heldAtMs","releasedByUid","releasedAtMs","Object","keys","create","code","reason","enrollmentId","sequenceName","link","dedupeKey","override","entries","curated","stepIndex","Number","edited","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 {\n CAMPAIGN_MEMBERSHIP_FIELD,\n contactCampaignFieldPath,\n normalizeCampaignIds,\n} from '@aglyn/aglyn/app-utils/campaign-membership'\nimport { consentGroupForHost } from '@aglyn/aglyn/app-utils/consent-groups'\nimport { scopeTokensForHost } from '@aglyn/aglyn/app-utils/scope-tokens'\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 { PluginRecordTimelineWriter } from '@aglyn/aglyn/plugin-manager/plugin-record-timeline'\nimport type { PluginTextGenerator } from '@aglyn/aglyn/plugin-manager/plugin-text-generation'\nimport type { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport { findContactByEmail } from '@aglyn/tenant-data-admin/server/contact-email-index'\nimport { restampCrmListFieldsAt } from '@aglyn/tenant-data-admin/server/crm-records'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { outreachCuratedEntry, outreachEnrolledEntry } from '../engine/enrollment-activity'\nimport { buildOutreachEnrollment, planOutreachFirstDue } from '../engine/enrollment-state'\nimport { outreachEnrollmentSearchTokens } from '../enrollment/enrollment-search'\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 {\n outreachStepOverridesRefused,\n readOutreachStepOverrideRequests,\n type OutreachStepOverrideRead,\n} from '../enrollment/step-overrides'\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, OutreachStepOverrides } from '../model/outreach.types'\nimport { readOutreachComplianceSettingsDoc } from '../storage/compliance-settings-store'\nimport { type OutreachDomainIntelReadInput, outreachMailboxSendingDomain } from '../storage/domain-intel-store'\nimport { fileOutreachNote } from '../runtime/timeline'\nimport {\n outreachEnrollmentId,\n outreachEnrollmentLink,\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 * The workspace's record system on the timeline seam (AGL-3274), when a\n * plugin keeps one: the enroll files \"Enrolled in\" on the person's record\n * — the runtime's `timeline`, reached the same way.\n */\n timeline(): PluginRecordTimelineWriter | null\n /**\n * The workspace's text generator on the core's text-generation seam\n * (AGL-3324), which drafts one person's copies of the steps; `null` when\n * no plugin generates text, and the curate route says so.\n */\n textGenerator(): PluginTextGenerator | null\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 doNotContactDomain: null,\n workspaceMembers: null,\n openEnrollments: null,\n hasInboundEmail: null,\n gateway: 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 /** The sequence site's consent group, whose opt-outs all apply. */\n consentHostIds: readonly string[]\n /** Whether that group's sites wait for each other's confirmation click. */\n consentAwaitsConfirmation: boolean\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 const consentGroup = consentGroupForHost(caller.org, sequence.hostId)\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: consentGroup.groupId,\n consentHostIds: consentGroup.hostIds,\n consentAwaitsConfirmation: consentGroup.awaitsConfirmation,\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 */\n/**\n * The domain the sequence's mailbox sends from (AGL-3328), whose gateway\n * ledger the Check-people chips read — or `null` when the mailbox cannot\n * be read, which shows the gateways without a history.\n */\nasync function sequenceSendingDomain(\n firestore: Firestore,\n orgId: string,\n sequence: OutreachSequence,\n): Promise<string | null> {\n if (!sequence.mailboxId) return null\n try {\n const snapshot = await outreachOrgCollection(firestore, orgId, 'mailboxes').doc(sequence.mailboxId).get()\n const mailbox = readStoredOutreachMailbox(sequence.mailboxId, snapshot?.exists ? snapshot.data() : undefined)\n return outreachMailboxSendingDomain(mailbox)\n } catch (error) {\n console.error('[outreach] the sequence mailbox could not be read for its sending domain', error)\n return null\n }\n}\n\nasync function readPeople(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n sequence: OutreachSequence,\n context: EnrollContext,\n people: readonly OutreachPersonRef[],\n gateway: OutreachDomainIntelReadInput,\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 consentHostIds: context.consentHostIds,\n consentAwaitsConfirmation: context.consentAwaitsConfirmation,\n gateway,\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`. The person key IS\n * the id, so there is no address to resolve — and since AGL-3275 the window is\n * narrowed by `visibleTo` rather than by the collection's parent.\n */\nasync function leadsViewPeople(\n firestore: Firestore,\n orgId: string,\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 /*\n * SCOPED (AGL-3275). The collection is org-wide, so the window is narrowed\n * to what the sequence's site may see before it is ordered — otherwise an\n * agency's sequence would offer another client's people to enroll. The\n * composite index for `visibleTo array-contains-any` + `lastSeenAtMs desc`\n * is in `cloud/firestore.indexes.json`.\n */\n const window = await firestore\n .collection('orgs')\n .doc(orgId)\n .collection('leads')\n .where('visibleTo', 'array-contains-any', scopeTokensForHost(sequence.hostId))\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 person joins the sequence's campaigns (AGL-3254), the moment they\n * are enrolled: `arrayUnion` on the lead's own `campaignIds`, or on the\n * contact's facet for the sequence's site — the same field, the same\n * shape, every other campaign member carries — and `enrolled` credited\n * to each campaign. After the enrollment is created and never before:\n * a person the create refused joins nothing. Neither write may fail the\n * enrollment, which already exists; a membership that could not be\n * stamped is one the list's bulk bar can add by hand.\n */\n async function joinSequenceCampaigns(\n firestore: Firestore,\n orgId: string,\n sequence: OutreachSequence,\n contactGroupId: string,\n candidate: OutreachEnrollCandidate,\n nowMs: number,\n ): Promise<void> {\n const campaignIds = normalizeCampaignIds(sequence.campaignIds)\n if (!campaignIds.length) return\n try {\n const ref =\n candidate.target === 'lead' && candidate.leadId\n ? firestore.collection('orgs').doc(orgId).collection('leads').doc(candidate.leadId)\n : candidate.contactId\n ? firestore.collection('orgs').doc(orgId).collection('contacts').doc(candidate.contactId)\n : null\n if (ref) {\n const field =\n candidate.target === 'lead' ? CAMPAIGN_MEMBERSHIP_FIELD : contactCampaignFieldPath(contactGroupId)\n await ref.update({ [field]: FieldValue.arrayUnion(...campaignIds), updatedAt: FieldValue.serverTimestamp() })\n // A lead's campaigns are what the Leads list's Campaign filter reads\n // under a site (AGL-3321): restamped from the lead as it now stands.\n if (candidate.target === 'lead') await restampCrmListFieldsAt(ref, 'leads')\n }\n } catch (error) {\n console.error('[outreach] the enrolled person could not join the sequence’s campaigns', error)\n }\n try {\n await deps.creditCampaign({ hostId: sequence.hostId, orgId, campaignIds, outcome: 'enrolled', atMs: nowMs })\n } catch (error) {\n console.error('[outreach] the enrollment could not be credited to the sequence’s campaigns', error)\n }\n }\n\n /**\n * The names of the sequence's campaigns as they stand, for the entry the\n * enroll files on the person's record (AGL-3274) — the org's containers,\n * `orgs/{orgId}/emailCampaigns`. Read once per request, not per person; a\n * container that is gone answers nothing and the entry names the rest.\n */\n async function sequenceCampaignNames(\n firestore: Firestore,\n orgId: string,\n sequence: OutreachSequence,\n ): Promise<string[]> {\n const campaignIds = normalizeCampaignIds(sequence.campaignIds)\n if (!campaignIds.length || !orgId) return []\n try {\n const containers = firestore.collection('orgs').doc(orgId).collection('emailCampaigns')\n const found = await firestore.getAll(...campaignIds.map((id) => containers.doc(id)))\n return found.map((snapshot) => String(snapshot.get('name') ?? '').trim()).filter(Boolean)\n } catch (error) {\n console.error('[outreach] the sequence’s campaigns could not be named for the record', error)\n return []\n }\n }\n\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, caller.orgId, 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 resolveMx: deps.resolveMx,\n resolveAddress: deps.resolveAddress,\n nowMs: deps.now(),\n sendingDomain: await sequenceSendingDomain(firestore, caller.orgId, sequence),\n },\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 const nowMs = deps.now()\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[]; overrides: OutreachStepOverrideRead }\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 // The steps the member curated for this person (AGL-3324), validated\n // here rather than trusted: a copy that breaks a rule refuses the\n // whole request, so nobody is enrolled with a copy the route dropped.\n const overrides = readOutreachStepOverrideRequests(person['stepOverrides'], sequence.steps, {\n uid: caller.uid,\n nowMs,\n })\n if (outreachStepOverridesRefused(overrides)) {\n return outreachRefusal(\n 400,\n 'invalid-override',\n overrides.refusal ?? 'A curated step breaks a rule every sequence email keeps.',\n { issues: overrides.issues },\n )\n }\n requested.set(ref.id, {\n ref,\n personalLine: typeof person['personalLine'] === 'string' ? person['personalLine'] : '',\n attestations: Array.isArray(person['attestations']) ? person['attestations'] : [],\n overrides,\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 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 resolveMx: deps.resolveMx,\n resolveAddress: deps.resolveAddress,\n nowMs,\n sendingDomain: outreachMailboxSendingDomain(mailbox),\n },\n )\n const enrollments = outreachOrgCollection(firestore, caller.orgId, 'enrollments')\n const campaignNames = await sequenceCampaignNames(firestore, caller.orgId, sequence)\n // The member's name for the curated entries (AGL-3324), read once per\n // request and only when a person carries a curated step.\n let memberNameRead: Promise<string | null> | null = null\n const memberName = () => {\n memberNameRead ??= firestore\n .collection('orgs')\n .doc(caller.orgId)\n .collection('members')\n .doc(caller.uid)\n .get()\n .then(\n (member) =>\n (member.exists\n ? String(member.get('displayName') ?? '').trim() || String(member.get('email') ?? '').trim()\n : '') || caller.email,\n )\n .catch(() => caller.email)\n return memberNameRead\n }\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) ?? {\n personalLine: '',\n attestations: [],\n overrides: { overrides: {}, issues: [], refusal: null } satisfies OutreachStepOverrideRead,\n }\n const stepOverrides: OutreachStepOverrides = supplied.overrides.overrides\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 // The member ticked this person past the red gateway chip\n // (AGL-3326): that is their say-so, stamped as the hold's release,\n // so the engine does not hold the send they were just shown.\n if (decision.hold) {\n enrollment.gatewayHold = {\n gateway: decision.hold.gateway,\n heldAtMs: null,\n releasedByUid: caller.uid,\n releasedAtMs: nowMs,\n }\n }\n // The person's own copies of steps (AGL-3324) ride with the\n // enrollment from its first write: nothing sends between.\n if (Object.keys(stepOverrides).length) enrollment.stepOverrides = stepOverrides\n // What the enrollments table searches the person by (AGL-3321),\n // from the name and address this document captures for good.\n enrollment.searchTokens = outreachEnrollmentSearchTokens(enrollment)\n // Nobody has clicked yet (AGL-3332): stored, because the \"Clicked: No\"\n // filter is a query, and a query cannot find a field's absence.\n enrollment.clicked = false\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 await joinSequenceCampaigns(firestore, caller.orgId, sequence, context.contactGroupId, candidate, nowMs)\n // The person's record says so (AGL-3274): \"Enrolled in <sequence>\",\n // with the campaigns it carried them into, once per enrollment.\n const entry = outreachEnrolledEntry({ enrollmentId: id, sequenceName: sequence.name, campaignNames })\n await fileOutreachNote(deps, {\n orgId: caller.orgId,\n hostId: sequence.hostId,\n link: outreachEnrollmentLink(enrollment),\n dedupeKey: entry.dedupeKey,\n body: entry.body,\n atMs: nowMs,\n })\n // And one line per step the member curated for them (AGL-3324),\n // after the enrollment's own, so the record reads in order.\n for (const [key, override] of Object.entries(stepOverrides)) {\n const curated = outreachCuratedEntry({\n enrollmentId: id,\n stepIndex: Number(key),\n source: override.source,\n edited: override.edited === true,\n memberName: await memberName(),\n atMs: nowMs,\n })\n await fileOutreachNote(deps, {\n orgId: caller.orgId,\n hostId: sequence.hostId,\n link: outreachEnrollmentLink(enrollment),\n dedupeKey: curated.dedupeKey,\n body: curated.body,\n atMs: nowMs,\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":["CAMPAIGN_MEMBERSHIP_FIELD","contactCampaignFieldPath","normalizeCampaignIds","consentGroupForHost","scopeTokensForHost","CRM_COLLECTIONS","crmLeadStatus","crmViewIsListed","isCrmLeadOpen","normalizeCrmViewFilters","dynamicListDimensionsForCrmView","findContactByEmail","restampCrmListFieldsAt","FieldValue","outreachCuratedEntry","outreachEnrolledEntry","buildOutreachEnrollment","planOutreachFirstDue","outreachEnrollmentSearchTokens","decideOutreachEnrollment","previewOutreachPerson","readOutreachEnrollCandidates","readOutreachGateLookups","outreachStepOverridesRefused","readOutreachStepOverrideRequests","effectiveOutreachAllowedCountries","OUTREACH_ENROLL_BATCH_MAX","readOutreachComplianceSettingsDoc","outreachMailboxSendingDomain","fileOutreachNote","outreachEnrollmentId","outreachEnrollmentLink","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","doNotContactDomain","workspaceMembers","openEnrollments","hasInboundEmail","gateway","ALREADY_EXISTS","loadActiveSequence","firestore","orgId","rawSequenceId","sequenceId","snapshot","doc","get","sequence","exists","data","undefined","status","enrollContext","caller","host","orgSettings","Promise","all","collection","hostId","consentGroup","org","settings","allowedCountries","contactGroupId","groupId","consentHostIds","hostIds","consentAwaitsConfirmation","awaitsConfirmation","siteName","String","sequenceSendingDomain","mailboxId","mailbox","error","console","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","where","orderBy","limit","matching","docs","slice","lead","kind","total","truncated","createOutreachEnrollRoutes","deps","joinSequenceCampaigns","nowMs","campaignIds","ref","target","update","arrayUnion","updatedAt","serverTimestamp","creditCampaign","outcome","atMs","sequenceCampaignNames","containers","found","trim","Boolean","sourcePeople","source","ids","viewId","view","views","shared","ownerUid","uid","unsupported","label","join","emails","complete","crmViewEmails","ordered","sort","contacts","preview","request","method","body","gate","Response","named","resolveMx","resolveAddress","now","sendingDomain","alreadyInSequence","has","ok","confirm","requested","person","overrides","steps","issues","set","personalLine","attestations","size","mailboxSnapshot","enrolledAtMs","random","campaignNames","memberNameRead","memberName","then","member","catch","results","supplied","stepOverrides","decision","allowed","blocks","enrollment","contactName","name","cold","enrolledByUid","hold","gatewayHold","heldAtMs","releasedByUid","releasedAtMs","Object","keys","searchTokens","clicked","create","code","reason","enrollmentId","sequenceName","link","dedupeKey","override","entries","curated","stepIndex","Number","edited","enrolled","result","logOrgActivity","type"],"mappings":"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"}
|
|
@@ -17,3 +17,13 @@
|
|
|
17
17
|
import type { PluginWebApiHandler } from '@aglyn/aglyn/server';
|
|
18
18
|
import type { OutreachRouteDeps } from './route-deps';
|
|
19
19
|
export declare function createOutreachEnrollmentActionRoute(deps: OutreachRouteDeps): PluginWebApiHandler;
|
|
20
|
+
/**
|
|
21
|
+
* THE MAIL GATEWAY IN FRONT OF ONE ENROLLED PERSON (AGL-3332):
|
|
22
|
+
* `outreach/enrollments/gateway`, for the Details of their page.
|
|
23
|
+
*
|
|
24
|
+
* The same two reads the Check-people step makes (AGL-3326, AGL-3328): the
|
|
25
|
+
* domain's MX through the platform cache, which no member may read and a
|
|
26
|
+
* lookup refreshes, and that gateway's standing with the domain the
|
|
27
|
+
* enrollment's mailbox sends from. Nothing is written but the cache.
|
|
28
|
+
*/
|
|
29
|
+
export declare function createOutreachEnrollmentGatewayRoute(deps: OutreachRouteDeps): PluginWebApiHandler;
|
|
@@ -16,8 +16,12 @@
|
|
|
16
16
|
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
17
17
|
import { applyOutreachEnrollmentEvent } from "../engine/enrollment-state.js";
|
|
18
18
|
import { OUTREACH_OPEN_ENROLLMENT_STATUSES } from "../engine/gates.js";
|
|
19
|
+
import { outreachEmailDomain } from "../engine/do-not-contact-domain.js";
|
|
20
|
+
import { outreachActionHistoryRow, outreachHistoryEntryId } from "../model/enrollment-history.js";
|
|
21
|
+
import { OUTREACH_ENROLLMENT_HISTORY } from "../model/outreach.types.js";
|
|
19
22
|
import { addOutreachDoNotContact } from "../storage/do-not-contact-store.js";
|
|
20
|
-
import {
|
|
23
|
+
import { outreachMailboxSendingDomain, readOutreachDomainIntel, readOutreachGatewayStandings } from "../storage/domain-intel-store.js";
|
|
24
|
+
import { outreachOrgCollection, readStoredOutreachEnrollment, readStoredOutreachMailbox } from "../storage/outreach-records.js";
|
|
21
25
|
import { outreachRouteGate } from "./route-gate.js";
|
|
22
26
|
import { outreachMethodNotAllowed, outreachOk, outreachRefusal, readOutreachDocumentId, readOutreachJsonBody } from "./route-http.js";
|
|
23
27
|
/**
|
|
@@ -40,6 +44,11 @@ import { outreachMethodNotAllowed, outreachOk, outreachRefusal, readOutreachDocu
|
|
|
40
44
|
* RESUME on an enrollment the engine held for its gateway (AGL-3326) is the
|
|
41
45
|
* member saying "send it anyway": the hold is stamped released with who and
|
|
42
46
|
* when, and the gates do not hold that enrollment on the gateway again.
|
|
47
|
+
*
|
|
48
|
+
* Every act that moves the enrollment is also a row of its history
|
|
49
|
+
* (AGL-3332), with who and why, in the same transaction: the enrollment
|
|
50
|
+
* holds only its CURRENT stop, and a resume clears it, so without the row
|
|
51
|
+
* the person's page could not say they were ever paused.
|
|
43
52
|
*/ const ACTIONS = [
|
|
44
53
|
'pause',
|
|
45
54
|
'resume',
|
|
@@ -121,6 +130,12 @@ export function createOutreachEnrollmentActionRoute(deps) {
|
|
|
121
130
|
updatedAtMs: nowMs
|
|
122
131
|
});
|
|
123
132
|
transaction.update(ref, written);
|
|
133
|
+
transaction.set(ref.collection(OUTREACH_ENROLLMENT_HISTORY).doc(outreachHistoryEntryId(nowMs)), outreachActionHistoryRow({
|
|
134
|
+
atMs: nowMs,
|
|
135
|
+
action,
|
|
136
|
+
byUid: caller.uid,
|
|
137
|
+
detail
|
|
138
|
+
}));
|
|
124
139
|
return {
|
|
125
140
|
enrollment: _extends({}, enrollment, written),
|
|
126
141
|
error: null,
|
|
@@ -194,5 +209,71 @@ export function createOutreachEnrollmentActionRoute(deps) {
|
|
|
194
209
|
if (writes) await batch.commit();
|
|
195
210
|
return writes;
|
|
196
211
|
}
|
|
212
|
+
/**
|
|
213
|
+
* THE MAIL GATEWAY IN FRONT OF ONE ENROLLED PERSON (AGL-3332):
|
|
214
|
+
* `outreach/enrollments/gateway`, for the Details of their page.
|
|
215
|
+
*
|
|
216
|
+
* The same two reads the Check-people step makes (AGL-3326, AGL-3328): the
|
|
217
|
+
* domain's MX through the platform cache, which no member may read and a
|
|
218
|
+
* lookup refreshes, and that gateway's standing with the domain the
|
|
219
|
+
* enrollment's mailbox sends from. Nothing is written but the cache.
|
|
220
|
+
*/ export function createOutreachEnrollmentGatewayRoute(deps) {
|
|
221
|
+
return async (request)=>{
|
|
222
|
+
var _intel_get, _standings_get;
|
|
223
|
+
if (request.method !== 'POST') return outreachMethodNotAllowed('POST');
|
|
224
|
+
const body = await readOutreachJsonBody(request);
|
|
225
|
+
const caller = await outreachRouteGate(request, body['orgId'], deps.gate);
|
|
226
|
+
if (caller instanceof Response) return caller;
|
|
227
|
+
const enrollmentId = readOutreachDocumentId(body['enrollmentId']);
|
|
228
|
+
if (!enrollmentId) return outreachRefusal(400, 'invalid-request', 'Name the enrollment.');
|
|
229
|
+
const firestore = deps.firestore();
|
|
230
|
+
const snapshot = await outreachOrgCollection(firestore, caller.orgId, 'enrollments').doc(enrollmentId).get();
|
|
231
|
+
const enrollment = readStoredOutreachEnrollment(enrollmentId, snapshot.exists ? snapshot.data() : undefined);
|
|
232
|
+
if (!enrollment) return outreachRefusal(404, 'enrollment-not-found', 'That enrollment no longer exists.');
|
|
233
|
+
const domain = outreachEmailDomain(enrollment.email);
|
|
234
|
+
let sendingDomain = null;
|
|
235
|
+
if (enrollment.mailboxId) {
|
|
236
|
+
try {
|
|
237
|
+
const mailbox = await outreachOrgCollection(firestore, caller.orgId, 'mailboxes').doc(enrollment.mailboxId).get();
|
|
238
|
+
sendingDomain = outreachMailboxSendingDomain(readStoredOutreachMailbox(enrollment.mailboxId, mailbox.exists ? mailbox.data() : undefined));
|
|
239
|
+
} catch (error) {
|
|
240
|
+
console.error('[outreach] the enrollment mailbox could not be read for its sending domain', error);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const input = {
|
|
244
|
+
resolveMx: deps.resolveMx,
|
|
245
|
+
resolveAddress: deps.resolveAddress,
|
|
246
|
+
nowMs: deps.now(),
|
|
247
|
+
sendingDomain
|
|
248
|
+
};
|
|
249
|
+
const [intel, standings] = domain ? await Promise.all([
|
|
250
|
+
readOutreachDomainIntel(firestore, caller.orgId, [
|
|
251
|
+
enrollment.email
|
|
252
|
+
], input),
|
|
253
|
+
readOutreachGatewayStandings(firestore, caller.orgId, [
|
|
254
|
+
enrollment.email
|
|
255
|
+
], input).catch((error)=>{
|
|
256
|
+
console.error('[outreach] the gateway standing could not be read', error);
|
|
257
|
+
return new Map();
|
|
258
|
+
})
|
|
259
|
+
]) : [
|
|
260
|
+
new Map(),
|
|
261
|
+
new Map()
|
|
262
|
+
];
|
|
263
|
+
const found = (_intel_get = intel.get(enrollment.email)) != null ? _intel_get : null;
|
|
264
|
+
return outreachOk({
|
|
265
|
+
ok: true,
|
|
266
|
+
domain,
|
|
267
|
+
sendingDomain,
|
|
268
|
+
intel: found ? {
|
|
269
|
+
status: found.status,
|
|
270
|
+
mx: found.mx,
|
|
271
|
+
gateway: found.gateway,
|
|
272
|
+
resolvedAtMs: found.resolvedAtMs
|
|
273
|
+
} : null,
|
|
274
|
+
standing: (_standings_get = standings.get(enrollment.email)) != null ? _standings_get : null
|
|
275
|
+
});
|
|
276
|
+
};
|
|
277
|
+
}
|
|
197
278
|
|
|
198
279
|
//# sourceMappingURL=enrollment-routes.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/enrollment-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 type { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport {\n applyOutreachEnrollmentEvent,\n type OutreachEnrollmentEvent,\n} from '../engine/enrollment-state'\nimport { OUTREACH_OPEN_ENROLLMENT_STATUSES } from '../engine/gates'\nimport type {\n OutreachEnrollmentAction,\n OutreachEnrollmentActionResponse,\n} from '../model/outreach-api'\nimport type { OutreachEnrollment } from '../model/outreach.types'\nimport { addOutreachDoNotContact } from '../storage/do-not-contact-store'\nimport { outreachOrgCollection, readStoredOutreachEnrollment } from '../storage/outreach-records'\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 * WHAT A MEMBER DOES TO ONE ENROLLMENT (AGL-2980):\n * `outreach/enrollments/action` — pause, resume, stop, or mark the person\n * do-not-contact.\n *\n * Every transition is the engine's (`applyOutreachEnrollmentEvent`): the\n * route decides nothing about which status may follow which. Each write is\n * a transaction over the one document, so a member's pause and the sending\n * runtime's step completion cannot interleave and leave a paused enrollment\n * with a step it should not have run.\n *\n * MARK DO-NOT-CONTACT puts the address on the organization's list, with\n * the member as its author, and then opts out every enrollment that address\n * has in the organization — this one wherever it stood, and any other that\n * is still active or paused — so no sequence emails it again, whichever one\n * would have been next.\n *\n * RESUME on an enrollment the engine held for its gateway (AGL-3326) is the\n * member saying \"send it anyway\": the hold is stamped released with who and\n * when, and the gates do not hold that enrollment on the gateway again.\n */\n\nconst ACTIONS: readonly OutreachEnrollmentAction[] = ['pause', 'resume', 'stop', 'do_not_contact']\n\nfunction eventFor(\n action: OutreachEnrollmentAction,\n atMs: number,\n byUid: string,\n detail: string | null,\n): OutreachEnrollmentEvent {\n switch (action) {\n case 'pause':\n return { type: 'pause', atMs, byUid, detail }\n case 'resume':\n return { type: 'resume', atMs, byUid }\n case 'stop':\n return { type: 'stop', atMs, byUid, reason: 'manual', detail }\n case 'do_not_contact':\n return { type: 'opt_out', atMs, reason: 'do_not_contact', detail }\n }\n}\n\nexport function createOutreachEnrollmentActionRoute(deps: OutreachRouteDeps): PluginWebApiHandler {\n return 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)\n if (caller instanceof Response) return caller\n const enrollmentId = readOutreachDocumentId(body['enrollmentId'])\n const action = body['action'] as OutreachEnrollmentAction\n if (!enrollmentId || !ACTIONS.includes(action)) {\n return outreachRefusal(400, 'invalid-request', 'Name the enrollment, and pause, resume, stop or mark it do-not-contact.')\n }\n const detail = typeof body['detail'] === 'string' ? body['detail'] : null\n const firestore = deps.firestore()\n const enrollments = outreachOrgCollection(firestore, caller.orgId, 'enrollments')\n const ref = enrollments.doc(enrollmentId)\n const nowMs = deps.now()\n\n const applied = await firestore.runTransaction(async (transaction) => {\n const snapshot = await transaction.get(ref)\n const enrollment = readStoredOutreachEnrollment(enrollmentId, snapshot.exists ? snapshot.data() : undefined)\n if (!enrollment) return null\n const { patch, error } = applyOutreachEnrollmentEvent(\n enrollment,\n eventFor(action, nowMs, caller.uid, detail),\n )\n if (!patch) return { enrollment, error, changed: false }\n // The opt-out event names no member; the person who marked it is\n // recorded here, beside the reason, and on the list entry.\n const released =\n action === 'resume' && enrollment.stopReason === 'gateway_blocked_here'\n ? {\n gatewayHold: {\n gateway: enrollment.gatewayHold?.gateway ?? 'other',\n heldAtMs: enrollment.gatewayHold?.heldAtMs ?? enrollment.stoppedAtMs,\n releasedByUid: caller.uid,\n releasedAtMs: nowMs,\n },\n }\n : {}\n const written = {\n ...patch,\n ...released,\n stoppedByUid: action === 'do_not_contact' ? caller.uid : patch.stoppedByUid,\n updatedAtMs: nowMs,\n }\n transaction.update(ref, written)\n return { enrollment: { ...enrollment, ...written } as OutreachEnrollment, error: null, changed: true }\n })\n if (!applied) return outreachRefusal(404, 'enrollment-not-found', 'That enrollment no longer exists.')\n // An address that bounced still goes on the list when a member asks:\n // the enrollment's own status stays final, the list entry is what they\n // asked for.\n if (applied.error && action !== 'do_not_contact') {\n return outreachRefusal(409, 'transition-refused', applied.error)\n }\n\n let stoppedOthers = 0\n if (action === 'do_not_contact') {\n await addOutreachDoNotContact(firestore, {\n orgId: caller.orgId,\n email: applied.enrollment.email,\n reason: 'manual',\n source: 'member',\n addedByUid: caller.uid,\n nowMs,\n enrollmentId,\n sequenceId: applied.enrollment.sequenceId,\n detail,\n })\n stoppedOthers = await optOutOtherEnrollments(firestore, enrollments, applied.enrollment, caller.uid, nowMs)\n // The mark on the record the person is (AGL-3245): the member's verdict\n // stands over every automatic one.\n await deps.stampRecordEmailState({\n orgId: caller.orgId,\n email: applied.enrollment.email,\n state: { status: 'do_not_contact', atMs: nowMs, source: 'member', detail, enrollmentId },\n })\n }\n return outreachOk({\n ok: true,\n changed: applied.changed,\n enrollment: applied.enrollment,\n stoppedOthers,\n } satisfies OutreachEnrollmentActionResponse)\n }\n}\n\n/** Opts out every OTHER open enrollment of the same address; returns how many. */\nasync function optOutOtherEnrollments(\n firestore: FirebaseFirestore.Firestore,\n enrollments: FirebaseFirestore.CollectionReference,\n marked: OutreachEnrollment,\n byUid: string,\n nowMs: number,\n): Promise<number> {\n const found = await enrollments.where('email', '==', marked.email).get()\n const batch = firestore.batch()\n let writes = 0\n for (const doc of found.docs) {\n if (doc.id === marked.id) continue\n const other = readStoredOutreachEnrollment(doc.id, doc.data())\n if (!other || !OUTREACH_OPEN_ENROLLMENT_STATUSES.includes(other.status)) continue\n const { patch } = applyOutreachEnrollmentEvent(other, {\n type: 'opt_out',\n atMs: nowMs,\n reason: 'do_not_contact',\n detail: 'Marked do-not-contact from another sequence.',\n })\n if (!patch) continue\n batch.update(doc.ref, { ...patch, stoppedByUid: byUid, updatedAtMs: nowMs })\n writes += 1\n }\n if (writes) await batch.commit()\n return writes\n}\n"],"names":["applyOutreachEnrollmentEvent","OUTREACH_OPEN_ENROLLMENT_STATUSES","addOutreachDoNotContact","outreachOrgCollection","readStoredOutreachEnrollment","outreachRouteGate","outreachMethodNotAllowed","outreachOk","outreachRefusal","readOutreachDocumentId","readOutreachJsonBody","ACTIONS","eventFor","action","atMs","byUid","detail","type","reason","createOutreachEnrollmentActionRoute","deps","request","method","body","caller","gate","Response","enrollmentId","includes","firestore","enrollments","orgId","ref","doc","nowMs","now","applied","runTransaction","transaction","enrollment","snapshot","get","exists","data","undefined","patch","error","uid","changed","released","stopReason","gatewayHold","gateway","heldAtMs","stoppedAtMs","releasedByUid","releasedAtMs","written","stoppedByUid","updatedAtMs","update","stoppedOthers","email","source","addedByUid","sequenceId","optOutOtherEnrollments","stampRecordEmailState","state","status","ok","marked","found","where","batch","writes","docs","id","other","commit"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/enrollment-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 type { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport {\n applyOutreachEnrollmentEvent,\n type OutreachEnrollmentEvent,\n} from '../engine/enrollment-state'\nimport { OUTREACH_OPEN_ENROLLMENT_STATUSES } from '../engine/gates'\nimport { outreachEmailDomain } from '../engine/do-not-contact-domain'\nimport type {\n OutreachEnrollmentAction,\n OutreachEnrollmentActionResponse,\n OutreachEnrollmentGatewayResponse,\n} from '../model/outreach-api'\nimport { outreachActionHistoryRow, outreachHistoryEntryId } from '../model/enrollment-history'\nimport { OUTREACH_ENROLLMENT_HISTORY, type OutreachEnrollment } from '../model/outreach.types'\nimport { addOutreachDoNotContact } from '../storage/do-not-contact-store'\nimport {\n outreachMailboxSendingDomain,\n readOutreachDomainIntel,\n readOutreachGatewayStandings,\n} from '../storage/domain-intel-store'\nimport {\n outreachOrgCollection,\n readStoredOutreachEnrollment,\n readStoredOutreachMailbox,\n} from '../storage/outreach-records'\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 * WHAT A MEMBER DOES TO ONE ENROLLMENT (AGL-2980):\n * `outreach/enrollments/action` — pause, resume, stop, or mark the person\n * do-not-contact.\n *\n * Every transition is the engine's (`applyOutreachEnrollmentEvent`): the\n * route decides nothing about which status may follow which. Each write is\n * a transaction over the one document, so a member's pause and the sending\n * runtime's step completion cannot interleave and leave a paused enrollment\n * with a step it should not have run.\n *\n * MARK DO-NOT-CONTACT puts the address on the organization's list, with\n * the member as its author, and then opts out every enrollment that address\n * has in the organization — this one wherever it stood, and any other that\n * is still active or paused — so no sequence emails it again, whichever one\n * would have been next.\n *\n * RESUME on an enrollment the engine held for its gateway (AGL-3326) is the\n * member saying \"send it anyway\": the hold is stamped released with who and\n * when, and the gates do not hold that enrollment on the gateway again.\n *\n * Every act that moves the enrollment is also a row of its history\n * (AGL-3332), with who and why, in the same transaction: the enrollment\n * holds only its CURRENT stop, and a resume clears it, so without the row\n * the person's page could not say they were ever paused.\n */\n\nconst ACTIONS: readonly OutreachEnrollmentAction[] = ['pause', 'resume', 'stop', 'do_not_contact']\n\nfunction eventFor(\n action: OutreachEnrollmentAction,\n atMs: number,\n byUid: string,\n detail: string | null,\n): OutreachEnrollmentEvent {\n switch (action) {\n case 'pause':\n return { type: 'pause', atMs, byUid, detail }\n case 'resume':\n return { type: 'resume', atMs, byUid }\n case 'stop':\n return { type: 'stop', atMs, byUid, reason: 'manual', detail }\n case 'do_not_contact':\n return { type: 'opt_out', atMs, reason: 'do_not_contact', detail }\n }\n}\n\nexport function createOutreachEnrollmentActionRoute(deps: OutreachRouteDeps): PluginWebApiHandler {\n return 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)\n if (caller instanceof Response) return caller\n const enrollmentId = readOutreachDocumentId(body['enrollmentId'])\n const action = body['action'] as OutreachEnrollmentAction\n if (!enrollmentId || !ACTIONS.includes(action)) {\n return outreachRefusal(400, 'invalid-request', 'Name the enrollment, and pause, resume, stop or mark it do-not-contact.')\n }\n const detail = typeof body['detail'] === 'string' ? body['detail'] : null\n const firestore = deps.firestore()\n const enrollments = outreachOrgCollection(firestore, caller.orgId, 'enrollments')\n const ref = enrollments.doc(enrollmentId)\n const nowMs = deps.now()\n\n const applied = await firestore.runTransaction(async (transaction) => {\n const snapshot = await transaction.get(ref)\n const enrollment = readStoredOutreachEnrollment(enrollmentId, snapshot.exists ? snapshot.data() : undefined)\n if (!enrollment) return null\n const { patch, error } = applyOutreachEnrollmentEvent(\n enrollment,\n eventFor(action, nowMs, caller.uid, detail),\n )\n if (!patch) return { enrollment, error, changed: false }\n // The opt-out event names no member; the person who marked it is\n // recorded here, beside the reason, and on the list entry.\n const released =\n action === 'resume' && enrollment.stopReason === 'gateway_blocked_here'\n ? {\n gatewayHold: {\n gateway: enrollment.gatewayHold?.gateway ?? 'other',\n heldAtMs: enrollment.gatewayHold?.heldAtMs ?? enrollment.stoppedAtMs,\n releasedByUid: caller.uid,\n releasedAtMs: nowMs,\n },\n }\n : {}\n const written = {\n ...patch,\n ...released,\n stoppedByUid: action === 'do_not_contact' ? caller.uid : patch.stoppedByUid,\n updatedAtMs: nowMs,\n }\n transaction.update(ref, written)\n transaction.set(\n ref.collection(OUTREACH_ENROLLMENT_HISTORY).doc(outreachHistoryEntryId(nowMs)),\n outreachActionHistoryRow({ atMs: nowMs, action, byUid: caller.uid, detail }),\n )\n return { enrollment: { ...enrollment, ...written } as OutreachEnrollment, error: null, changed: true }\n })\n if (!applied) return outreachRefusal(404, 'enrollment-not-found', 'That enrollment no longer exists.')\n // An address that bounced still goes on the list when a member asks:\n // the enrollment's own status stays final, the list entry is what they\n // asked for.\n if (applied.error && action !== 'do_not_contact') {\n return outreachRefusal(409, 'transition-refused', applied.error)\n }\n\n let stoppedOthers = 0\n if (action === 'do_not_contact') {\n await addOutreachDoNotContact(firestore, {\n orgId: caller.orgId,\n email: applied.enrollment.email,\n reason: 'manual',\n source: 'member',\n addedByUid: caller.uid,\n nowMs,\n enrollmentId,\n sequenceId: applied.enrollment.sequenceId,\n detail,\n })\n stoppedOthers = await optOutOtherEnrollments(firestore, enrollments, applied.enrollment, caller.uid, nowMs)\n // The mark on the record the person is (AGL-3245): the member's verdict\n // stands over every automatic one.\n await deps.stampRecordEmailState({\n orgId: caller.orgId,\n email: applied.enrollment.email,\n state: { status: 'do_not_contact', atMs: nowMs, source: 'member', detail, enrollmentId },\n })\n }\n return outreachOk({\n ok: true,\n changed: applied.changed,\n enrollment: applied.enrollment,\n stoppedOthers,\n } satisfies OutreachEnrollmentActionResponse)\n }\n}\n\n/** Opts out every OTHER open enrollment of the same address; returns how many. */\nasync function optOutOtherEnrollments(\n firestore: FirebaseFirestore.Firestore,\n enrollments: FirebaseFirestore.CollectionReference,\n marked: OutreachEnrollment,\n byUid: string,\n nowMs: number,\n): Promise<number> {\n const found = await enrollments.where('email', '==', marked.email).get()\n const batch = firestore.batch()\n let writes = 0\n for (const doc of found.docs) {\n if (doc.id === marked.id) continue\n const other = readStoredOutreachEnrollment(doc.id, doc.data())\n if (!other || !OUTREACH_OPEN_ENROLLMENT_STATUSES.includes(other.status)) continue\n const { patch } = applyOutreachEnrollmentEvent(other, {\n type: 'opt_out',\n atMs: nowMs,\n reason: 'do_not_contact',\n detail: 'Marked do-not-contact from another sequence.',\n })\n if (!patch) continue\n batch.update(doc.ref, { ...patch, stoppedByUid: byUid, updatedAtMs: nowMs })\n writes += 1\n }\n if (writes) await batch.commit()\n return writes\n}\n\n/**\n * THE MAIL GATEWAY IN FRONT OF ONE ENROLLED PERSON (AGL-3332):\n * `outreach/enrollments/gateway`, for the Details of their page.\n *\n * The same two reads the Check-people step makes (AGL-3326, AGL-3328): the\n * domain's MX through the platform cache, which no member may read and a\n * lookup refreshes, and that gateway's standing with the domain the\n * enrollment's mailbox sends from. Nothing is written but the cache.\n */\nexport function createOutreachEnrollmentGatewayRoute(deps: OutreachRouteDeps): PluginWebApiHandler {\n return 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)\n if (caller instanceof Response) return caller\n const enrollmentId = readOutreachDocumentId(body['enrollmentId'])\n if (!enrollmentId) return outreachRefusal(400, 'invalid-request', 'Name the enrollment.')\n const firestore = deps.firestore()\n const snapshot = await outreachOrgCollection(firestore, caller.orgId, 'enrollments').doc(enrollmentId).get()\n const enrollment = readStoredOutreachEnrollment(enrollmentId, snapshot.exists ? snapshot.data() : undefined)\n if (!enrollment) return outreachRefusal(404, 'enrollment-not-found', 'That enrollment no longer exists.')\n const domain = outreachEmailDomain(enrollment.email)\n let sendingDomain: string | null = null\n if (enrollment.mailboxId) {\n try {\n const mailbox = await outreachOrgCollection(firestore, caller.orgId, 'mailboxes').doc(enrollment.mailboxId).get()\n sendingDomain = outreachMailboxSendingDomain(\n readStoredOutreachMailbox(enrollment.mailboxId, mailbox.exists ? mailbox.data() : undefined),\n )\n } catch (error) {\n console.error('[outreach] the enrollment mailbox could not be read for its sending domain', error)\n }\n }\n const input = {\n resolveMx: deps.resolveMx,\n resolveAddress: deps.resolveAddress,\n nowMs: deps.now(),\n sendingDomain,\n }\n const [intel, standings] = domain\n ? await Promise.all([\n readOutreachDomainIntel(firestore, caller.orgId, [enrollment.email], input),\n readOutreachGatewayStandings(firestore, caller.orgId, [enrollment.email], input).catch(\n (error: unknown) => {\n console.error('[outreach] the gateway standing could not be read', error)\n return new Map()\n },\n ),\n ])\n : [new Map(), new Map()]\n const found = intel.get(enrollment.email) ?? null\n return outreachOk({\n ok: true,\n domain,\n sendingDomain,\n intel: found\n ? { status: found.status, mx: found.mx, gateway: found.gateway, resolvedAtMs: found.resolvedAtMs }\n : null,\n standing: standings.get(enrollment.email) ?? null,\n } satisfies OutreachEnrollmentGatewayResponse)\n }\n}\n"],"names":["applyOutreachEnrollmentEvent","OUTREACH_OPEN_ENROLLMENT_STATUSES","outreachEmailDomain","outreachActionHistoryRow","outreachHistoryEntryId","OUTREACH_ENROLLMENT_HISTORY","addOutreachDoNotContact","outreachMailboxSendingDomain","readOutreachDomainIntel","readOutreachGatewayStandings","outreachOrgCollection","readStoredOutreachEnrollment","readStoredOutreachMailbox","outreachRouteGate","outreachMethodNotAllowed","outreachOk","outreachRefusal","readOutreachDocumentId","readOutreachJsonBody","ACTIONS","eventFor","action","atMs","byUid","detail","type","reason","createOutreachEnrollmentActionRoute","deps","request","method","body","caller","gate","Response","enrollmentId","includes","firestore","enrollments","orgId","ref","doc","nowMs","now","applied","runTransaction","transaction","enrollment","snapshot","get","exists","data","undefined","patch","error","uid","changed","released","stopReason","gatewayHold","gateway","heldAtMs","stoppedAtMs","releasedByUid","releasedAtMs","written","stoppedByUid","updatedAtMs","update","set","collection","stoppedOthers","email","source","addedByUid","sequenceId","optOutOtherEnrollments","stampRecordEmailState","state","status","ok","marked","found","where","batch","writes","docs","id","other","commit","createOutreachEnrollmentGatewayRoute","intel","standings","domain","sendingDomain","mailboxId","mailbox","console","input","resolveMx","resolveAddress","Promise","all","catch","Map","mx","resolvedAtMs","standing"],"mappings":"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"}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import { pluginTextGenerator } from "@aglyn/aglyn/plugin-manager/plugin-text-generation";
|
|
18
18
|
import { registerPluginApiRoute } from "@aglyn/aglyn/server";
|
|
19
19
|
import { firebaseAdmin } from "@aglyn/tenant-data-admin/server/firebase-admin";
|
|
20
|
-
import {
|
|
20
|
+
import { platformMailDnsResolver } from "@aglyn/tenant-data-admin/server/email-deliverability";
|
|
21
21
|
import { OUTREACH_API_ROUTES } from "../constants/api-routes.js";
|
|
22
22
|
import { openOutreachMailboxClient } from "../mailboxes/mailbox-transport.js";
|
|
23
23
|
import { outreachOrgSubject } from "../mailboxes/register-mailbox-routes.js";
|
|
@@ -25,7 +25,7 @@ import { outreachShortLinkUrl } from "../runtime/click-link.js";
|
|
|
25
25
|
import { createOutreachCurateRoutes } from "./curate-routes.js";
|
|
26
26
|
import { createOutreachDoNotContactDomainsRoute } from "./do-not-contact-routes.js";
|
|
27
27
|
import { createOutreachEnrollRoutes } from "./enroll-routes.js";
|
|
28
|
-
import { createOutreachEnrollmentActionRoute } from "./enrollment-routes.js";
|
|
28
|
+
import { createOutreachEnrollmentActionRoute, createOutreachEnrollmentGatewayRoute } from "./enrollment-routes.js";
|
|
29
29
|
import { canonicalConsoleOrigin } from "../mailboxes/oauth-redirect.js";
|
|
30
30
|
import { createOutreachLinkDomainsRoute } from "./link-domain-routes.js";
|
|
31
31
|
import { createOutreachPreviewRoute } from "./preview-routes.js";
|
|
@@ -85,8 +85,15 @@ export function defaultOutreachRouteDeps() {
|
|
|
85
85
|
var _pluginRecordTimelineWriter;
|
|
86
86
|
return (_ref = (_pluginRecordTimelineWriter = pluginRecordTimelineWriter()) == null ? void 0 : _pluginRecordTimelineWriter.writer) != null ? _ref : null;
|
|
87
87
|
},
|
|
88
|
-
// The domain's MX (AGL-3326),
|
|
89
|
-
|
|
88
|
+
// The domain's MX (AGL-3326) and, for a domain with none, its address
|
|
89
|
+
// record — through the platform's pinned resolver (AGL-3328), which
|
|
90
|
+
// asks twice before it says a domain takes no mail.
|
|
91
|
+
resolveMx: (domain)=>platformMailDnsResolver().resolveMx(domain),
|
|
92
|
+
resolveAddress: (domain)=>{
|
|
93
|
+
var _ref;
|
|
94
|
+
var _platformMailDnsResolver_resolveAddress, _platformMailDnsResolver;
|
|
95
|
+
return (_ref = (_platformMailDnsResolver_resolveAddress = (_platformMailDnsResolver = platformMailDnsResolver()).resolveAddress) == null ? void 0 : _platformMailDnsResolver_resolveAddress.call(_platformMailDnsResolver, domain)) != null ? _ref : Promise.resolve(false);
|
|
96
|
+
},
|
|
90
97
|
// The workspace's text generator (AGL-3324), resolved per call the same
|
|
91
98
|
// way: the AI plugin registers at boot, and a request before it did is
|
|
92
99
|
// told drafting is unavailable rather than caching a `null`.
|
|
@@ -157,6 +164,9 @@ export function registerOutreachRoutes(deps = defaultOutreachRouteDeps()) {
|
|
|
157
164
|
registerPluginApiRoute(OUTREACH_API_ROUTES.enrollmentsAction, {
|
|
158
165
|
web: createOutreachEnrollmentActionRoute(deps)
|
|
159
166
|
}, orgSubject);
|
|
167
|
+
registerPluginApiRoute(OUTREACH_API_ROUTES.enrollmentsGateway, {
|
|
168
|
+
web: createOutreachEnrollmentGatewayRoute(deps)
|
|
169
|
+
}, orgSubject);
|
|
160
170
|
registerPluginApiRoute(OUTREACH_API_ROUTES.preview, {
|
|
161
171
|
web: createOutreachPreviewRoute(deps)
|
|
162
172
|
}, orgSubject);
|