@aglyn/plugins-outreach 1.0.0-beta.220 → 1.0.0-beta.222
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.d.ts +6 -2
- package/src/lib/components/do-not-contact-domains.js +73 -11
- package/src/lib/components/do-not-contact-domains.js.map +1 -1
- package/src/lib/components/enrollments-table.d.ts +6 -0
- package/src/lib/components/enrollments-table.js +77 -4
- package/src/lib/components/enrollments-table.js.map +1 -1
- package/src/lib/components/sequence-detail.js +3 -1
- package/src/lib/components/sequence-detail.js.map +1 -1
- package/src/lib/components/sequence-report-card.js +7 -3
- package/src/lib/components/sequence-report-card.js.map +1 -1
- package/src/lib/components/use-outreach-crm.d.ts +18 -15
- package/src/lib/components/use-outreach-crm.js +118 -87
- package/src/lib/components/use-outreach-crm.js.map +1 -1
- package/src/lib/components/use-outreach-data.d.ts +2 -2
- package/src/lib/components/use-outreach-data.js.map +1 -1
- package/src/lib/constants/transfer-resources.d.ts +33 -0
- package/src/lib/constants/transfer-resources.js +38 -0
- package/src/lib/constants/transfer-resources.js.map +1 -0
- package/src/lib/declarations.console-server.d.ts +7 -1
- package/src/lib/declarations.console-server.js +66 -1
- package/src/lib/declarations.console-server.js.map +1 -1
- package/src/lib/engine/click-tracking.js +1 -1
- package/src/lib/engine/click-tracking.js.map +1 -1
- package/src/lib/engine/ip-range.d.ts +26 -0
- package/src/lib/engine/ip-range.js +78 -0
- package/src/lib/engine/ip-range.js.map +1 -0
- package/src/lib/engine/open-source.d.ts +24 -0
- package/src/lib/engine/open-source.js +130 -0
- package/src/lib/engine/open-source.js.map +1 -0
- package/src/lib/engine/open-tracking.d.ts +15 -2
- package/src/lib/engine/open-tracking.js +59 -16
- package/src/lib/engine/open-tracking.js.map +1 -1
- package/src/lib/engine/outreach-access.d.ts +47 -0
- package/src/lib/engine/outreach-access.js +56 -0
- package/src/lib/engine/outreach-access.js.map +1 -0
- package/src/lib/enrollment/enroll-people.d.ts +10 -10
- package/src/lib/enrollment/enroll-people.js +30 -25
- package/src/lib/enrollment/enroll-people.js.map +1 -1
- package/src/lib/enrollment/gate-lookups.js +30 -15
- package/src/lib/enrollment/gate-lookups.js.map +1 -1
- package/src/lib/enrollment/message-templates.d.ts +24 -0
- package/src/lib/enrollment/message-templates.js +35 -0
- package/src/lib/enrollment/message-templates.js.map +1 -0
- package/src/lib/model/enrollment-engagement.d.ts +16 -0
- package/src/lib/model/enrollment-engagement.js +16 -0
- package/src/lib/model/enrollment-engagement.js.map +1 -1
- package/src/lib/model/enrollment-history.d.ts +8 -1
- package/src/lib/model/enrollment-history.js +17 -5
- package/src/lib/model/enrollment-history.js.map +1 -1
- package/src/lib/model/enrollment-timeline.d.ts +3 -0
- package/src/lib/model/enrollment-timeline.js +15 -4
- package/src/lib/model/enrollment-timeline.js.map +1 -1
- package/src/lib/model/outreach.types.d.ts +12 -3
- package/src/lib/model/outreach.types.js +12 -1
- package/src/lib/model/outreach.types.js.map +1 -1
- package/src/lib/model/sequence-report.d.ts +14 -7
- package/src/lib/model/sequence-report.js +12 -8
- package/src/lib/model/sequence-report.js.map +1 -1
- package/src/lib/plugin.js +18 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/routes/curate-routes.js +4 -6
- package/src/lib/routes/curate-routes.js.map +1 -1
- package/src/lib/routes/enroll-routes.d.ts +0 -12
- package/src/lib/routes/enroll-routes.js +52 -92
- package/src/lib/routes/enroll-routes.js.map +1 -1
- package/src/lib/routes/platform-deps.d.ts +4 -16
- package/src/lib/routes/platform-deps.js +5 -25
- package/src/lib/routes/platform-deps.js.map +1 -1
- package/src/lib/routes/preview-routes.js +25 -13
- package/src/lib/routes/preview-routes.js.map +1 -1
- package/src/lib/routes/register-routes.js +7 -5
- package/src/lib/routes/register-routes.js.map +1 -1
- package/src/lib/routes/route-gate.js +6 -17
- package/src/lib/routes/route-gate.js.map +1 -1
- package/src/lib/routes/sequence-routes.d.ts +46 -0
- package/src/lib/routes/sequence-routes.js +155 -63
- package/src/lib/routes/sequence-routes.js.map +1 -1
- package/src/lib/runtime/click-events.js +8 -8
- package/src/lib/runtime/click-events.js.map +1 -1
- package/src/lib/runtime/click-route.js +5 -2
- package/src/lib/runtime/click-route.js.map +1 -1
- package/src/lib/runtime/lead-records.d.ts +18 -10
- package/src/lib/runtime/lead-records.js +14 -60
- package/src/lib/runtime/lead-records.js.map +1 -1
- package/src/lib/runtime/open-events.d.ts +8 -0
- package/src/lib/runtime/open-events.js +10 -3
- package/src/lib/runtime/open-events.js.map +1 -1
- package/src/lib/runtime/platform-runtime-deps.d.ts +9 -5
- package/src/lib/runtime/platform-runtime-deps.js +31 -24
- package/src/lib/runtime/platform-runtime-deps.js.map +1 -1
- package/src/lib/runtime/send-job.js +20 -17
- package/src/lib/runtime/send-job.js.map +1 -1
- package/src/lib/runtime/sync-job.js +10 -5
- package/src/lib/runtime/sync-job.js.map +1 -1
- package/src/lib/storage/do-not-contact-store.d.ts +8 -0
- package/src/lib/storage/do-not-contact-store.js +13 -0
- package/src/lib/storage/do-not-contact-store.js.map +1 -1
- package/src/lib/testing/stand-in-record-system.d.ts +54 -0
- package/src/lib/testing/stand-in-record-system.js +243 -0
- package/src/lib/testing/stand-in-record-system.js.map +1 -0
- package/src/lib/transfer/do-not-contact-transfer.d.ts +129 -0
- package/src/lib/transfer/do-not-contact-transfer.js +801 -0
- package/src/lib/transfer/do-not-contact-transfer.js.map +1 -0
- package/src/lib/transfer/plan-gate.d.ts +25 -0
- package/src/lib/transfer/plan-gate.js +35 -0
- package/src/lib/transfer/plan-gate.js.map +1 -0
- package/src/lib/transfer/platform-transfer-deps.d.ts +24 -0
- package/src/lib/transfer/platform-transfer-deps.js +39 -0
- package/src/lib/transfer/platform-transfer-deps.js.map +1 -0
- package/src/lib/transfer/sequences-package.d.ts +48 -0
- package/src/lib/transfer/sequences-package.js +93 -0
- package/src/lib/transfer/sequences-package.js.map +1 -0
- package/src/lib/transfer/sequences-package.server.d.ts +31 -0
- package/src/lib/transfer/sequences-package.server.js +207 -0
- package/src/lib/transfer/sequences-package.server.js.map +1 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/runtime/lead-records.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\n/**\n * WHAT A SEQUENCE DOES
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/runtime/lead-records.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\n/**\n * WHAT A SEQUENCE DOES WHEN A LEAD BECOMES A CONTACT (AGL-3234).\n *\n * A CONVERSION re-points every enrollment made on the lead at the contact it\n * became: the record system tells every plugin through its lead-conversion\n * seam, and this is Sequences' share of it. The enrollment keeps its id, its\n * thread and its place in the steps, and keeps naming the lead beside the\n * contact, so both pages list it.\n *\n * What a sequence does to the lead's OWN record — a sent step moving a New\n * lead to Nurturing, a reply moving it to Working — is the record system's\n * to write, and Sequences tells it through the record email-state seam\n * (`stampRecordEmailReach`, `stampRecordEmailReply`, AGL-3080) rather than\n * writing the lead here.\n *\n * Never throws for a missing enrollment: one that could not follow its lead\n * is one the next send stops as `contact_missing` and a rep re-enrolls.\n */\n\nimport type {\n PluginLeadConversionReport,\n PluginLeadConversionRequest,\n} from '@aglyn/aglyn/plugin-manager/plugin-lead-conversion'\nimport { outreachOrgCollection } from '../storage/outreach-records'\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** Firestore refuses a batch of more than 500 writes. */\nconst BATCH_LIMIT = 450\n\n/** Every enrollment made on the lead now names the contact it became. */\nexport async function followOutreachLeadToContact(\n firestore: Firestore,\n input: Pick<PluginLeadConversionRequest, 'orgId' | 'leadId' | 'contactId'>,\n): Promise<PluginLeadConversionReport> {\n const enrollments = outreachOrgCollection(firestore, input.orgId, 'enrollments')\n const rows = await enrollments.where('leadId', '==', input.leadId).get()\n const refs = rows.docs\n .filter((row) => row.get('target') !== 'contact' || String(row.get('contactId') ?? '') !== input.contactId)\n .map((row) => row.ref)\n for (let start = 0; start < refs.length; start += BATCH_LIMIT) {\n const batch = firestore.batch()\n for (const ref of refs.slice(start, start + BATCH_LIMIT)) {\n batch.update(ref, { target: 'contact', contactId: input.contactId, updatedAtMs: Date.now() })\n }\n await batch.commit()\n }\n return { enrollments: refs.length }\n}\n"],"names":["outreachOrgCollection","BATCH_LIMIT","followOutreachLeadToContact","firestore","input","enrollments","orgId","rows","where","leadId","get","refs","docs","filter","row","String","contactId","map","ref","start","length","batch","slice","update","target","updatedAtMs","Date","now","commit"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;CAiBC,GAMD,SAASA,qBAAqB,QAAQ,iCAA6B;AAInE,uDAAuD,GACvD,MAAMC,cAAc;AAEpB,uEAAuE,GACvE,OAAO,eAAeC,4BACpBC,SAAoB,EACpBC,KAA0E;IAE1E,MAAMC,cAAcL,sBAAsBG,WAAWC,MAAME,KAAK,EAAE;IAClE,MAAMC,OAAO,MAAMF,YAAYG,KAAK,CAAC,UAAU,MAAMJ,MAAMK,MAAM,EAAEC,GAAG;IACtE,MAAMC,OAAOJ,KAAKK,IAAI,CACnBC,MAAM,CAAC,CAACC;YAAkDA;eAA1CA,IAAIJ,GAAG,CAAC,cAAc,aAAaK,QAAOD,WAAAA,IAAIJ,GAAG,CAAC,wBAARI,WAAwB,QAAQV,MAAMY,SAAS;OACzGC,GAAG,CAAC,CAACH,MAAQA,IAAII,GAAG;IACvB,IAAK,IAAIC,QAAQ,GAAGA,QAAQR,KAAKS,MAAM,EAAED,SAASlB,YAAa;QAC7D,MAAMoB,QAAQlB,UAAUkB,KAAK;QAC7B,KAAK,MAAMH,OAAOP,KAAKW,KAAK,CAACH,OAAOA,QAAQlB,aAAc;YACxDoB,MAAME,MAAM,CAACL,KAAK;gBAAEM,QAAQ;gBAAWR,WAAWZ,MAAMY,SAAS;gBAAES,aAAaC,KAAKC,GAAG;YAAG;QAC7F;QACA,MAAMN,MAAMO,MAAM;IACpB;IACA,OAAO;QAAEvB,aAAaM,KAAKS,MAAM;IAAC;AACpC"}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
+
import { type OutreachOpenSource } from '../engine/open-source';
|
|
17
18
|
import { type OutreachOpenJudgement } from '../engine/open-tracking';
|
|
18
19
|
import { type OutreachEnrollment } from '../model/outreach.types';
|
|
19
20
|
import type { OutreachOpenTarget } from './click-link';
|
|
@@ -34,6 +35,8 @@ import type { OutreachRuntimeDeps } from './runtime-deps';
|
|
|
34
35
|
*/
|
|
35
36
|
/** What one recorded open turned out to be. */
|
|
36
37
|
export interface OutreachOpenOutcome extends OutreachOpenJudgement {
|
|
38
|
+
/** The network the fetch came from (AGL-3488); `null` when no address was read. */
|
|
39
|
+
source: OutreachOpenSource | null;
|
|
37
40
|
/** Whether it was this person's first human open — the sequence's `uniqueOpens`. */
|
|
38
41
|
first: boolean;
|
|
39
42
|
/** The enrollment it was recorded against, or `null` when there is none. */
|
|
@@ -52,4 +55,9 @@ export declare function recordOutreachOpen(deps: Pick<OutreachRuntimeDeps, 'fire
|
|
|
52
55
|
target: OutreachOpenTarget;
|
|
53
56
|
method: string;
|
|
54
57
|
userAgent: string | null | undefined;
|
|
58
|
+
/**
|
|
59
|
+
* The fetch's address (AGL-3488), read for the network it belongs to and
|
|
60
|
+
* dropped: the row keeps the network, never the address.
|
|
61
|
+
*/
|
|
62
|
+
address?: string | null;
|
|
55
63
|
}): Promise<OutreachOpenOutcome>;
|
|
@@ -15,7 +15,8 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
17
|
*/ import { FieldValue } from "firebase-admin/firestore";
|
|
18
|
-
import {
|
|
18
|
+
import { outreachOpenSourceOf } from "../engine/open-source.js";
|
|
19
|
+
import { judgeOutreachOpen, outreachOpenAgentEvidence, OUTREACH_PROXY_OPEN_REASONS } from "../engine/open-tracking.js";
|
|
19
20
|
import { outreachLoggedEngagementRows, readOutreachEngagement } from "../model/enrollment-engagement.js";
|
|
20
21
|
import { outreachHistoryEntryId, outreachOpenHistoryRow } from "../model/enrollment-history.js";
|
|
21
22
|
import { OUTREACH_ENROLLMENT_HISTORY, OUTREACH_ENROLLMENT_HISTORY_MAX } from "../model/outreach.types.js";
|
|
@@ -24,6 +25,7 @@ import { outreachStepSentAtMs } from "./click-events.js";
|
|
|
24
25
|
const nothing = ()=>({
|
|
25
26
|
human: false,
|
|
26
27
|
machineReason: null,
|
|
28
|
+
source: null,
|
|
27
29
|
first: false,
|
|
28
30
|
enrollment: null
|
|
29
31
|
});
|
|
@@ -42,6 +44,7 @@ const nothing = ()=>({
|
|
|
42
44
|
const firestore = deps.firestore();
|
|
43
45
|
const { orgId, enrollmentId, stepIndex } = input.target;
|
|
44
46
|
const nowMs = deps.now();
|
|
47
|
+
const source = outreachOpenSourceOf(input.address);
|
|
45
48
|
const ref = outreachOrgCollection(firestore, orgId, 'enrollments').doc(enrollmentId);
|
|
46
49
|
const outcome = await firestore.runTransaction(async (transaction)=>{
|
|
47
50
|
var _held_firstOpenAtMs;
|
|
@@ -52,7 +55,8 @@ const nothing = ()=>({
|
|
|
52
55
|
const judgement = judgeOutreachOpen({
|
|
53
56
|
method: input.method,
|
|
54
57
|
userAgent: input.userAgent,
|
|
55
|
-
sinceSentMs: sent === null ? null : nowMs - sent
|
|
58
|
+
sinceSentMs: sent === null ? null : nowMs - sent,
|
|
59
|
+
source
|
|
56
60
|
});
|
|
57
61
|
const held = readOutreachEngagement(enrollment.engagement);
|
|
58
62
|
const first = judgement.human && held.firstOpenAtMs === null;
|
|
@@ -76,10 +80,13 @@ const nothing = ()=>({
|
|
|
76
80
|
atMs: nowMs,
|
|
77
81
|
stepIndex,
|
|
78
82
|
human: judgement.human,
|
|
79
|
-
machineReason: judgement.machineReason
|
|
83
|
+
machineReason: judgement.machineReason,
|
|
84
|
+
userAgent: outreachOpenAgentEvidence(input.userAgent),
|
|
85
|
+
source
|
|
80
86
|
}));
|
|
81
87
|
}
|
|
82
88
|
return _extends({}, judgement, {
|
|
89
|
+
source,
|
|
83
90
|
first,
|
|
84
91
|
enrollment: _extends({}, enrollment, {
|
|
85
92
|
engagement
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/runtime/open-events.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 { FieldValue } from 'firebase-admin/firestore'\nimport {\n judgeOutreachOpen,\n OUTREACH_PROXY_OPEN_REASONS,\n type OutreachOpenJudgement,\n} from '../engine/open-tracking'\nimport { outreachLoggedEngagementRows, readOutreachEngagement } from '../model/enrollment-engagement'\nimport { outreachHistoryEntryId, outreachOpenHistoryRow } from '../model/enrollment-history'\nimport {\n OUTREACH_ENROLLMENT_HISTORY,\n OUTREACH_ENROLLMENT_HISTORY_MAX,\n type OutreachEnrollment,\n type OutreachEnrollmentEngagement,\n} from '../model/outreach.types'\nimport { outreachOrgCollection, readStoredOutreachEnrollment } from '../storage/outreach-records'\nimport { outreachStepSentAtMs } from './click-events'\nimport type { OutreachOpenTarget } from './click-link'\nimport type { OutreachRuntimeDeps } from './runtime-deps'\n\n/**\n * RECORDING AN OPEN (AGL-3395).\n *\n * A fetch of a tracking image, written in the places a click is\n * (`./click-events.ts`) and for the same questions: the enrollment (\"did\n * this person open it?\"), its history (\"when, and was it them?\") and the\n * sequence (\"what is its open rate?\"). There is no per-destination rollup —\n * an image has no destination — and nothing is filed on the person's CRM\n * record: an open is too weak a signal to put in front of a rep as an act,\n * and a proxy's fetch would put one there for nobody.\n *\n * Every write happens after the image has been answered, and a failure costs\n * a number rather than a broken image.\n */\n\n/** What one recorded open turned out to be. */\nexport interface OutreachOpenOutcome extends OutreachOpenJudgement {\n /** Whether it was this person's first human open — the sequence's `uniqueOpens`. */\n first: boolean\n /** The enrollment it was recorded against, or `null` when there is none. */\n enrollment: OutreachEnrollment | null\n}\n\nconst nothing = (): OutreachOpenOutcome => ({ human: false, machineReason: null, first: false, enrollment: null })\n\n/**\n * Records one fetch of a tracking image.\n *\n * In a transaction over the enrollment, as a click is: two fetches at once\n * must not both read `opens` of nought, and whether this is the person's\n * FIRST open is decided from the read that writes it. An enrollment that no\n * longer exists belongs to a person or a workspace already erased, and\n * nothing is recorded for them anywhere.\n */\nexport async function recordOutreachOpen(\n deps: Pick<OutreachRuntimeDeps, 'firestore' | 'now'>,\n input: {\n target: OutreachOpenTarget\n method: string\n userAgent: string | null | undefined\n },\n): Promise<OutreachOpenOutcome> {\n // A test's image (AGL-3325) names no enrollment and counts on nothing.\n if (input.target.test) return nothing()\n const firestore = deps.firestore()\n const { orgId, enrollmentId, stepIndex } = input.target\n const nowMs = deps.now()\n const ref = outreachOrgCollection(firestore, orgId, 'enrollments').doc(enrollmentId)\n\n const outcome = 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 nothing()\n const sent = outreachStepSentAtMs(enrollment, stepIndex)\n const judgement = judgeOutreachOpen({\n method: input.method,\n userAgent: input.userAgent,\n sinceSentMs: sent === null ? null : nowMs - sent,\n })\n const held = readOutreachEngagement(enrollment.engagement)\n const first = judgement.human && held.firstOpenAtMs === null\n const logged = outreachLoggedEngagementRows(held) < OUTREACH_ENROLLMENT_HISTORY_MAX\n const engagement: OutreachEnrollmentEngagement = judgement.human\n ? {\n ...held,\n opens: held.opens + 1,\n firstOpenAtMs: held.firstOpenAtMs ?? nowMs,\n lastOpenAtMs: nowMs,\n loggedOpens: held.loggedOpens + (logged ? 1 : 0),\n }\n : {\n ...held,\n machineOpens: held.machineOpens + 1,\n loggedOpens: held.loggedOpens + (logged ? 1 : 0),\n }\n // `updatedAtMs` is left alone, as a click leaves it: a proxy fetching an\n // image is not something that happened to the enrollment.\n transaction.update(ref, { engagement })\n if (logged) {\n transaction.set(\n ref.collection(OUTREACH_ENROLLMENT_HISTORY).doc(outreachHistoryEntryId(nowMs)),\n outreachOpenHistoryRow({\n atMs: nowMs,\n stepIndex,\n human: judgement.human,\n machineReason: judgement.machineReason,\n }),\n )\n }\n return { ...judgement, first, enrollment: { ...enrollment, engagement } } as OutreachOpenOutcome\n })\n\n if (outcome.enrollment?.sequenceId) {\n await bumpSequenceOpenStats(firestore, outcome, { orgId, sequenceId: outcome.enrollment.sequenceId, nowMs })\n }\n return outcome\n}\n\n/**\n * The sequence's open counters: blind increments outside the enrollment's\n * transaction, for the reason the click counters are — one sequence holds\n * every recipient, and a transaction over it would serialise them all.\n */\nasync function bumpSequenceOpenStats(\n firestore: FirebaseFirestore.Firestore,\n outcome: OutreachOpenOutcome,\n input: { orgId: string; sequenceId: string; nowMs: number },\n): Promise<void> {\n const proxy = outcome.machineReason !== null && OUTREACH_PROXY_OPEN_REASONS.includes(outcome.machineReason)\n const patch: Record<string, unknown> = outcome.human\n ? {\n 'stats.opens': FieldValue.increment(1),\n 'stats.lastOpenAtMs': input.nowMs,\n ...(outcome.first ? { 'stats.uniqueOpens': FieldValue.increment(1) } : {}),\n }\n : {\n 'stats.machineOpens': FieldValue.increment(1),\n ...(proxy ? { 'stats.proxyOpens': FieldValue.increment(1) } : {}),\n }\n await outreachOrgCollection(firestore, input.orgId, 'sequences')\n .doc(input.sequenceId)\n .update(patch)\n .catch((error: unknown) => {\n // A sequence deleted while its mail is still out there: the open is\n // on the enrollment, and the aggregate has nowhere to go.\n console.warn('[outreach] an open could not be counted on its sequence', error)\n })\n}\n"],"names":["FieldValue","judgeOutreachOpen","OUTREACH_PROXY_OPEN_REASONS","outreachLoggedEngagementRows","readOutreachEngagement","outreachHistoryEntryId","outreachOpenHistoryRow","OUTREACH_ENROLLMENT_HISTORY","OUTREACH_ENROLLMENT_HISTORY_MAX","outreachOrgCollection","readStoredOutreachEnrollment","outreachStepSentAtMs","nothing","human","machineReason","first","enrollment","recordOutreachOpen","deps","input","outcome","target","test","firestore","orgId","enrollmentId","stepIndex","nowMs","now","ref","doc","runTransaction","transaction","held","snapshot","get","exists","data","undefined","sent","judgement","method","userAgent","sinceSentMs","engagement","firstOpenAtMs","logged","opens","lastOpenAtMs","loggedOpens","machineOpens","update","set","collection","atMs","sequenceId","bumpSequenceOpenStats","proxy","includes","patch","increment","catch","error","console","warn"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/runtime/open-events.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 { FieldValue } from 'firebase-admin/firestore'\nimport { outreachOpenSourceOf, type OutreachOpenSource } from '../engine/open-source'\nimport {\n judgeOutreachOpen,\n outreachOpenAgentEvidence,\n OUTREACH_PROXY_OPEN_REASONS,\n type OutreachOpenJudgement,\n} from '../engine/open-tracking'\nimport { outreachLoggedEngagementRows, readOutreachEngagement } from '../model/enrollment-engagement'\nimport { outreachHistoryEntryId, outreachOpenHistoryRow } from '../model/enrollment-history'\nimport {\n OUTREACH_ENROLLMENT_HISTORY,\n OUTREACH_ENROLLMENT_HISTORY_MAX,\n type OutreachEnrollment,\n type OutreachEnrollmentEngagement,\n} from '../model/outreach.types'\nimport { outreachOrgCollection, readStoredOutreachEnrollment } from '../storage/outreach-records'\nimport { outreachStepSentAtMs } from './click-events'\nimport type { OutreachOpenTarget } from './click-link'\nimport type { OutreachRuntimeDeps } from './runtime-deps'\n\n/**\n * RECORDING AN OPEN (AGL-3395).\n *\n * A fetch of a tracking image, written in the places a click is\n * (`./click-events.ts`) and for the same questions: the enrollment (\"did\n * this person open it?\"), its history (\"when, and was it them?\") and the\n * sequence (\"what is its open rate?\"). There is no per-destination rollup —\n * an image has no destination — and nothing is filed on the person's CRM\n * record: an open is too weak a signal to put in front of a rep as an act,\n * and a proxy's fetch would put one there for nobody.\n *\n * Every write happens after the image has been answered, and a failure costs\n * a number rather than a broken image.\n */\n\n/** What one recorded open turned out to be. */\nexport interface OutreachOpenOutcome extends OutreachOpenJudgement {\n /** The network the fetch came from (AGL-3488); `null` when no address was read. */\n source: OutreachOpenSource | null\n /** Whether it was this person's first human open — the sequence's `uniqueOpens`. */\n first: boolean\n /** The enrollment it was recorded against, or `null` when there is none. */\n enrollment: OutreachEnrollment | null\n}\n\nconst nothing = (): OutreachOpenOutcome => ({\n human: false,\n machineReason: null,\n source: null,\n first: false,\n enrollment: null,\n})\n\n/**\n * Records one fetch of a tracking image.\n *\n * In a transaction over the enrollment, as a click is: two fetches at once\n * must not both read `opens` of nought, and whether this is the person's\n * FIRST open is decided from the read that writes it. An enrollment that no\n * longer exists belongs to a person or a workspace already erased, and\n * nothing is recorded for them anywhere.\n */\nexport async function recordOutreachOpen(\n deps: Pick<OutreachRuntimeDeps, 'firestore' | 'now'>,\n input: {\n target: OutreachOpenTarget\n method: string\n userAgent: string | null | undefined\n /**\n * The fetch's address (AGL-3488), read for the network it belongs to and\n * dropped: the row keeps the network, never the address.\n */\n address?: string | null\n },\n): Promise<OutreachOpenOutcome> {\n // A test's image (AGL-3325) names no enrollment and counts on nothing.\n if (input.target.test) return nothing()\n const firestore = deps.firestore()\n const { orgId, enrollmentId, stepIndex } = input.target\n const nowMs = deps.now()\n const source = outreachOpenSourceOf(input.address)\n const ref = outreachOrgCollection(firestore, orgId, 'enrollments').doc(enrollmentId)\n\n const outcome = 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 nothing()\n const sent = outreachStepSentAtMs(enrollment, stepIndex)\n const judgement = judgeOutreachOpen({\n method: input.method,\n userAgent: input.userAgent,\n sinceSentMs: sent === null ? null : nowMs - sent,\n source,\n })\n const held = readOutreachEngagement(enrollment.engagement)\n const first = judgement.human && held.firstOpenAtMs === null\n const logged = outreachLoggedEngagementRows(held) < OUTREACH_ENROLLMENT_HISTORY_MAX\n const engagement: OutreachEnrollmentEngagement = judgement.human\n ? {\n ...held,\n opens: held.opens + 1,\n firstOpenAtMs: held.firstOpenAtMs ?? nowMs,\n lastOpenAtMs: nowMs,\n loggedOpens: held.loggedOpens + (logged ? 1 : 0),\n }\n : {\n ...held,\n machineOpens: held.machineOpens + 1,\n loggedOpens: held.loggedOpens + (logged ? 1 : 0),\n }\n // `updatedAtMs` is left alone, as a click leaves it: a proxy fetching an\n // image is not something that happened to the enrollment.\n transaction.update(ref, { engagement })\n if (logged) {\n transaction.set(\n ref.collection(OUTREACH_ENROLLMENT_HISTORY).doc(outreachHistoryEntryId(nowMs)),\n outreachOpenHistoryRow({\n atMs: nowMs,\n stepIndex,\n human: judgement.human,\n machineReason: judgement.machineReason,\n userAgent: outreachOpenAgentEvidence(input.userAgent),\n source,\n }),\n )\n }\n return { ...judgement, source, first, enrollment: { ...enrollment, engagement } } as OutreachOpenOutcome\n })\n\n if (outcome.enrollment?.sequenceId) {\n await bumpSequenceOpenStats(firestore, outcome, { orgId, sequenceId: outcome.enrollment.sequenceId, nowMs })\n }\n return outcome\n}\n\n/**\n * The sequence's open counters: blind increments outside the enrollment's\n * transaction, for the reason the click counters are — one sequence holds\n * every recipient, and a transaction over it would serialise them all.\n */\nasync function bumpSequenceOpenStats(\n firestore: FirebaseFirestore.Firestore,\n outcome: OutreachOpenOutcome,\n input: { orgId: string; sequenceId: string; nowMs: number },\n): Promise<void> {\n const proxy = outcome.machineReason !== null && OUTREACH_PROXY_OPEN_REASONS.includes(outcome.machineReason)\n const patch: Record<string, unknown> = outcome.human\n ? {\n 'stats.opens': FieldValue.increment(1),\n 'stats.lastOpenAtMs': input.nowMs,\n ...(outcome.first ? { 'stats.uniqueOpens': FieldValue.increment(1) } : {}),\n }\n : {\n 'stats.machineOpens': FieldValue.increment(1),\n ...(proxy ? { 'stats.proxyOpens': FieldValue.increment(1) } : {}),\n }\n await outreachOrgCollection(firestore, input.orgId, 'sequences')\n .doc(input.sequenceId)\n .update(patch)\n .catch((error: unknown) => {\n // A sequence deleted while its mail is still out there: the open is\n // on the enrollment, and the aggregate has nowhere to go.\n console.warn('[outreach] an open could not be counted on its sequence', error)\n })\n}\n"],"names":["FieldValue","outreachOpenSourceOf","judgeOutreachOpen","outreachOpenAgentEvidence","OUTREACH_PROXY_OPEN_REASONS","outreachLoggedEngagementRows","readOutreachEngagement","outreachHistoryEntryId","outreachOpenHistoryRow","OUTREACH_ENROLLMENT_HISTORY","OUTREACH_ENROLLMENT_HISTORY_MAX","outreachOrgCollection","readStoredOutreachEnrollment","outreachStepSentAtMs","nothing","human","machineReason","source","first","enrollment","recordOutreachOpen","deps","input","outcome","target","test","firestore","orgId","enrollmentId","stepIndex","nowMs","now","address","ref","doc","runTransaction","transaction","held","snapshot","get","exists","data","undefined","sent","judgement","method","userAgent","sinceSentMs","engagement","firstOpenAtMs","logged","opens","lastOpenAtMs","loggedOpens","machineOpens","update","set","collection","atMs","sequenceId","bumpSequenceOpenStats","proxy","includes","patch","increment","catch","error","console","warn"],"mappings":"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"}
|
|
@@ -18,12 +18,16 @@ import type { PluginWebApiHandler } from '@aglyn/aglyn/server';
|
|
|
18
18
|
import type { PluginPersonEraser } from '@aglyn/aglyn/plugin-manager/plugin-person-erasure';
|
|
19
19
|
import type { OutreachCampaignCredit, OutreachRuntimeDeps } from './runtime-deps';
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
22
|
-
* (
|
|
23
|
-
* credited through, so a sequence's numbers
|
|
24
|
-
* readings of one rule.
|
|
21
|
+
* What a sequence produced (AGL-3254), handed to whichever plugin credits
|
|
22
|
+
* outcomes (`plugin-conversion-credit`): the same contract every form,
|
|
23
|
+
* booking and campaign email is credited through, so a sequence's numbers
|
|
24
|
+
* and a campaign send's are two readings of one rule. A workspace whose
|
|
25
|
+
* plugins credit nothing credits nothing here either; the seam never throws.
|
|
26
|
+
*
|
|
27
|
+
* The campaign a sequence is filed under is what its mail is credited to,
|
|
28
|
+
* and the sequence and the enrollment ride with it as the mail's own facts.
|
|
25
29
|
*/
|
|
26
|
-
export declare function platformOutreachCampaignCredit(
|
|
30
|
+
export declare function platformOutreachCampaignCredit(): OutreachCampaignCredit;
|
|
27
31
|
/**
|
|
28
32
|
* The platform's own reach for the sending runtime (AGL-2981). Loaded only
|
|
29
33
|
* when a job runs or the unsubscribe route answers — never at boot — so the
|
|
@@ -18,8 +18,7 @@ import { platformMailDnsResolver } from "@aglyn/tenant-data-admin/server/email-d
|
|
|
18
18
|
import { checkEntitlement } from "@aglyn/aglyn/app-utils/plan-entitlements";
|
|
19
19
|
import { isPluginEnabled } from "@aglyn/aglyn/plugin-manager/enabled-plugins";
|
|
20
20
|
import { pluginRecordTimelineWriter } from "@aglyn/aglyn/plugin-manager/plugin-record-timeline";
|
|
21
|
-
import {
|
|
22
|
-
import { recordEmailCampaignTouch } from "@aglyn/tenant-data-admin/server/email-delivery-log";
|
|
21
|
+
import { creditConversion, creditConversionOutcome, recordConversionClick } from "@aglyn/aglyn/plugin-manager/plugin-conversion-credit";
|
|
23
22
|
import { suppressEmail } from "@aglyn/tenant-data-admin/server/email-suppression";
|
|
24
23
|
import { recordTopicOptOut } from "@aglyn/tenant-data-admin/server/topic-subscriptions";
|
|
25
24
|
import { firebaseAdmin } from "@aglyn/tenant-data-admin/server/firebase-admin";
|
|
@@ -41,45 +40,53 @@ import { createOutreachPersonEraser } from "./person-erasure.js";
|
|
|
41
40
|
import { outreachUnsubscribeUrl } from "./unsubscribe-link.js";
|
|
42
41
|
import { createOutreachUnsubscribeRoute } from "./unsubscribe-route.js";
|
|
43
42
|
/**
|
|
44
|
-
*
|
|
45
|
-
* (
|
|
46
|
-
* credited through, so a sequence's numbers
|
|
47
|
-
* readings of one rule.
|
|
48
|
-
|
|
43
|
+
* What a sequence produced (AGL-3254), handed to whichever plugin credits
|
|
44
|
+
* outcomes (`plugin-conversion-credit`): the same contract every form,
|
|
45
|
+
* booking and campaign email is credited through, so a sequence's numbers
|
|
46
|
+
* and a campaign send's are two readings of one rule. A workspace whose
|
|
47
|
+
* plugins credit nothing credits nothing here either; the seam never throws.
|
|
48
|
+
*
|
|
49
|
+
* The campaign a sequence is filed under is what its mail is credited to,
|
|
50
|
+
* and the sequence and the enrollment ride with it as the mail's own facts.
|
|
51
|
+
*/ export function platformOutreachCampaignCredit() {
|
|
49
52
|
return {
|
|
50
53
|
async credit ({ hostId, orgId, campaignIds, outcome, atMs }) {
|
|
51
|
-
await
|
|
54
|
+
await creditConversionOutcome({
|
|
52
55
|
hostId,
|
|
53
56
|
orgId,
|
|
54
|
-
campaignIds,
|
|
57
|
+
containerIds: campaignIds,
|
|
55
58
|
outcome,
|
|
56
59
|
atMs
|
|
57
|
-
}
|
|
60
|
+
});
|
|
58
61
|
},
|
|
59
62
|
async attributeRecord ({ hostId, kind, refId, campaignId, sequenceId, enrollmentId, atMs }) {
|
|
60
|
-
await
|
|
63
|
+
await creditConversion({
|
|
61
64
|
hostId,
|
|
62
65
|
kind,
|
|
63
66
|
refId,
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
campaignId,
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
click: {
|
|
68
|
+
hostId,
|
|
69
|
+
creditTo: campaignId,
|
|
70
|
+
atMs,
|
|
71
|
+
via: {
|
|
72
|
+
sequenceId,
|
|
73
|
+
enrollmentId
|
|
74
|
+
}
|
|
70
75
|
},
|
|
71
76
|
convertedAtMs: atMs
|
|
72
|
-
}
|
|
77
|
+
});
|
|
73
78
|
},
|
|
74
79
|
async recordTouch ({ hostId, email, campaignId, sequenceId, enrollmentId, atMs }) {
|
|
75
|
-
await
|
|
80
|
+
await recordConversionClick({
|
|
76
81
|
email,
|
|
77
82
|
hostId,
|
|
78
|
-
campaignId,
|
|
83
|
+
creditTo: campaignId,
|
|
79
84
|
atMs,
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
85
|
+
via: {
|
|
86
|
+
sequenceId,
|
|
87
|
+
enrollmentId
|
|
88
|
+
}
|
|
89
|
+
});
|
|
83
90
|
}
|
|
84
91
|
};
|
|
85
92
|
}
|
|
@@ -93,7 +100,7 @@ import { createOutreachUnsubscribeRoute } from "./unsubscribe-route.js";
|
|
|
93
100
|
firestore,
|
|
94
101
|
now: Date.now,
|
|
95
102
|
random: Math.random,
|
|
96
|
-
campaignCredit: platformOutreachCampaignCredit(
|
|
103
|
+
campaignCredit: platformOutreachCampaignCredit(),
|
|
97
104
|
openMailbox: (mailboxId)=>openOutreachMailboxClient(firestore(), {
|
|
98
105
|
mailboxId
|
|
99
106
|
}),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/runtime/platform-runtime-deps.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 { SALES_TOPIC_ID } from '../model/sales-topic'\nimport { platformMailDnsResolver } from '@aglyn/tenant-data-admin/server/email-deliverability'\nimport { checkEntitlement } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport { isPluginEnabled } from '@aglyn/aglyn/plugin-manager/enabled-plugins'\nimport { pluginRecordTimelineWriter } from '@aglyn/aglyn/plugin-manager/plugin-record-timeline'\nimport {\n attributeCampaignConversion,\n creditCampaignSequenceOutcome,\n} from '@aglyn/tenant-data-admin/server/campaign-conversion-attribution'\nimport { recordEmailCampaignTouch } from '@aglyn/tenant-data-admin/server/email-delivery-log'\nimport { suppressEmail } from '@aglyn/tenant-data-admin/server/email-suppression'\nimport { recordTopicOptOut } from '@aglyn/tenant-data-admin/server/topic-subscriptions'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { getLockdownVerdict } from '@aglyn/tenant-data-admin/server/lockdown'\nimport { sendOrgMemberNotice } from '@aglyn/tenant-data-admin/server/org-member-notice'\nimport { logOrgActivity } from '@aglyn/tenant-data-admin/server/organizations'\nimport { filterEnabledPluginsByReleaseFlags } from '@aglyn/tenant-data-admin/server/release-flags'\nimport { resolveTrackingLinkOrigin } from '@aglyn/tenant-data-admin/server/tracking-hosts'\nimport { sendingWorkspaceFor } from '@aglyn/tenant-data-admin/server/outbound-send-review'\nimport { screenTenantMessage } from '@aglyn/shared-util-email/outbound-screen-gate'\nimport { stampRecordEmailState } from '@aglyn/aglyn/plugin-manager/plugin-record-email-state'\nimport { OUTREACH_PLUGIN_ID } from '../constants/bundle-common'\nimport { composeOutreachMailboxNotice } from '../engine/mailbox-notice'\nimport { openOutreachMailboxClient } from '../mailboxes/mailbox-transport'\nimport { canonicalConsoleOrigin } from '../mailboxes/oauth-redirect'\nimport type { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport type { PluginPersonEraser } from '@aglyn/aglyn/plugin-manager/plugin-person-erasure'\nimport { outreachShortLinkUrl } from './click-link'\nimport { createOutreachClickRoute, createOutreachShortLinkRoute } from './click-route'\nimport { createOutreachPersonEraser } from './person-erasure'\nimport type { OutreachCampaignCredit, OutreachRuntimeDeps } from './runtime-deps'\nimport { outreachUnsubscribeUrl } from './unsubscribe-link'\nimport { createOutreachUnsubscribeRoute } from './unsubscribe-route'\n\n/**\n * The platform's campaign attribution, for what a sequence produced\n * (AGL-3254): the same join every form, booking and campaign email is\n * credited through, so a sequence's numbers and a campaign send's are two\n * readings of one rule. Each writer already never throws.\n */\nexport function platformOutreachCampaignCredit(\n firestore: () => FirebaseFirestore.Firestore,\n): OutreachCampaignCredit {\n return {\n async credit({ hostId, orgId, campaignIds, outcome, atMs }) {\n await creditCampaignSequenceOutcome({ hostId, orgId, campaignIds, outcome, atMs }, firestore())\n },\n async attributeRecord({ hostId, kind, refId, campaignId, sequenceId, enrollmentId, atMs }) {\n await attributeCampaignConversion(\n {\n hostId,\n kind,\n refId,\n touch: { channel: 'sequence', campaignId, sequenceId, enrollmentId, touchedAtMs: atMs },\n convertedAtMs: atMs,\n },\n firestore(),\n )\n },\n async recordTouch({ hostId, email, campaignId, sequenceId, enrollmentId, atMs }) {\n await recordEmailCampaignTouch({ email, hostId, campaignId, atMs, sequenceId, enrollmentId }, firestore())\n },\n }\n}\n\n/**\n * The platform's own reach for the sending runtime (AGL-2981). Loaded only\n * when a job runs or the unsubscribe route answers — never at boot — so the\n * console pays for these modules when Outreach does work.\n */\nexport function platformOutreachRuntimeDeps(): OutreachRuntimeDeps {\n const firestore = () => firebaseAdmin.app().firestore()\n return {\n firestore,\n now: Date.now,\n random: Math.random,\n campaignCredit: platformOutreachCampaignCredit(firestore),\n openMailbox: (mailboxId) => openOutreachMailboxClient(firestore(), { mailboxId }),\n // The domain's MX (AGL-3326) and, for a domain with none, its address\n // record — through the platform's pinned resolver (AGL-3328), which\n // asks twice before it says a domain takes no mail.\n resolveMx: (domain) => platformMailDnsResolver().resolveMx(domain),\n resolveAddress: (domain) => platformMailDnsResolver().resolveAddress?.(domain) ?? Promise.resolve(false),\n async orgRefusal(orgId, org) {\n if (!isPluginEnabled(org as { enabledPlugins?: string[] }, OUTREACH_PLUGIN_ID)) return 'plugin-disabled'\n if (!checkEntitlement(org, 'outreach')) return 'not-entitled'\n const released = await filterEnabledPluginsByReleaseFlags([OUTREACH_PLUGIN_ID], { orgId })\n return released.length ? null : 'not-released'\n },\n async sendRefusal({ org, uid }) {\n const locked = await getLockdownVerdict({ org, uid, intent: 'write' })\n return locked ? `locked:${locked.scope}` : null\n },\n timeline: () => pluginRecordTimelineWriter()?.writer ?? null,\n logOrgActivity: (orgId, actor, action, target) => logOrgActivity(orgId, actor, action, target),\n async optOutOfSalesTopic({ hostId, email }) {\n await recordTopicOptOut(hostId, email, SALES_TOPIC_ID, { firestore: firestore() })\n },\n async suppressBouncedEmail({ email, hostId }) {\n // The runtime stamps the record itself, with the richer verdict — the\n // domain block, the enrollment — so the list's own stamp stands down.\n await suppressEmail({ email, reason: 'bounce', context: 'outreach', hostId, stampRecord: false, firestore: firestore() })\n },\n async stampRecordEmailState(stamp) {\n await stampRecordEmailState(stamp)\n },\n async notifyMailboxOwner(notice) {\n // The Mailboxes page, by the organization's slug — the page Resume and\n // Reconnect live on (AGL-3244). Without a slug or a console origin the\n // notice still goes, and says where the page is instead.\n const org = await firestore().collection('orgs').doc(notice.orgId).get()\n const slug = typeof org.get('slug') === 'string' ? String(org.get('slug')) : ''\n const origin = canonicalConsoleOrigin()\n const mailboxesUrl = slug && origin ? `${origin}/${encodeURIComponent(slug)}/outreach/mailboxes` : null\n const email = composeOutreachMailboxNotice(notice, { mailboxesUrl })\n const result = await sendOrgMemberNotice({\n orgId: notice.orgId,\n uids: [notice.connectedByUid],\n includeAdmins: true,\n subject: email.subject,\n text: email.text,\n context: 'outreach-mailbox-notice',\n })\n if (!result.sent) console.warn(`[outreach] the mailbox owner was not emailed: ${result.reason}`)\n },\n unsubscribeUrl: (target) => outreachUnsubscribeUrl({ origin: canonicalConsoleOrigin(), target }),\n clickLinkUrl: (linkId, trackingOrigin) =>\n outreachShortLinkUrl({ origin: canonicalConsoleOrigin(), linkId, trackingOrigin }),\n // The sending domain's own `links.` host once the organization has\n // verified it (AGL-3306). A static import like every other\n // `tenant-data-admin` module here: a dynamic one marks the whole library\n // lazy-loaded, and `@nx/enforce-module-boundaries` then refuses every\n // static import of it across the plugin and the console.\n clickLinkOrigin({ orgId, senderAddress }) {\n return resolveTrackingLinkOrigin(firestore(), orgId, senderAddress)\n },\n // The one phishing screen every tenant message passes (AGL-3356). The\n // review module's import installs its store, so a hold files the same\n // abuse-queue row a held newsletter or campaign does.\n async screenMessage({ orgId, org, hostId, host, subject, text, fromName, fromAddress }) {\n const refusal = await screenTenantMessage(\n {\n workspace: sendingWorkspaceFor({ hostId, orgId, org, host }),\n subject,\n fromName,\n fromAddress: fromAddress ?? null,\n bodies: [text],\n context: 'outreach sequence',\n },\n 'outreach email',\n )\n return refusal?.detail ?? null\n },\n }\n}\n\n/** The unsubscribe route on the platform's own reach. */\nexport function platformOutreachUnsubscribeRoute(): PluginWebApiHandler {\n return createOutreachUnsubscribeRoute(platformOutreachRuntimeDeps())\n}\n\n/** The click-tracking redirect on the platform's own reach (AGL-3239). */\nexport function platformOutreachClickRoute(): PluginWebApiHandler {\n return createOutreachClickRoute(platformOutreachRuntimeDeps())\n}\n\n/** The short tracking link's redirect on the platform's own reach (AGL-3297). */\nexport function platformOutreachShortLinkRoute(): PluginWebApiHandler {\n return createOutreachShortLinkRoute(platformOutreachRuntimeDeps())\n}\n\n/** The person eraser on the platform's own Firestore. */\nexport function platformOutreachPersonEraser(): PluginPersonEraser {\n return createOutreachPersonEraser({ firestore: () => firebaseAdmin.app().firestore() })\n}\n"],"names":["SALES_TOPIC_ID","platformMailDnsResolver","checkEntitlement","isPluginEnabled","pluginRecordTimelineWriter","attributeCampaignConversion","creditCampaignSequenceOutcome","recordEmailCampaignTouch","suppressEmail","recordTopicOptOut","firebaseAdmin","getLockdownVerdict","sendOrgMemberNotice","logOrgActivity","filterEnabledPluginsByReleaseFlags","resolveTrackingLinkOrigin","sendingWorkspaceFor","screenTenantMessage","stampRecordEmailState","OUTREACH_PLUGIN_ID","composeOutreachMailboxNotice","openOutreachMailboxClient","canonicalConsoleOrigin","outreachShortLinkUrl","createOutreachClickRoute","createOutreachShortLinkRoute","createOutreachPersonEraser","outreachUnsubscribeUrl","createOutreachUnsubscribeRoute","platformOutreachCampaignCredit","firestore","credit","hostId","orgId","campaignIds","outcome","atMs","attributeRecord","kind","refId","campaignId","sequenceId","enrollmentId","touch","channel","touchedAtMs","convertedAtMs","recordTouch","email","platformOutreachRuntimeDeps","app","now","Date","random","Math","campaignCredit","openMailbox","mailboxId","resolveMx","domain","resolveAddress","Promise","resolve","orgRefusal","org","released","length","sendRefusal","uid","locked","intent","scope","timeline","writer","actor","action","target","optOutOfSalesTopic","suppressBouncedEmail","reason","context","stampRecord","stamp","notifyMailboxOwner","notice","collection","doc","get","slug","String","origin","mailboxesUrl","encodeURIComponent","result","uids","connectedByUid","includeAdmins","subject","text","sent","console","warn","unsubscribeUrl","clickLinkUrl","linkId","trackingOrigin","clickLinkOrigin","senderAddress","screenMessage","host","fromName","fromAddress","refusal","workspace","bodies","detail","platformOutreachUnsubscribeRoute","platformOutreachClickRoute","platformOutreachShortLinkRoute","platformOutreachPersonEraser"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/runtime/platform-runtime-deps.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 { SALES_TOPIC_ID } from '../model/sales-topic'\nimport { platformMailDnsResolver } from '@aglyn/tenant-data-admin/server/email-deliverability'\nimport { checkEntitlement } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport { isPluginEnabled } from '@aglyn/aglyn/plugin-manager/enabled-plugins'\nimport { pluginRecordTimelineWriter } from '@aglyn/aglyn/plugin-manager/plugin-record-timeline'\nimport {\n creditConversion,\n creditConversionOutcome,\n recordConversionClick,\n} from '@aglyn/aglyn/plugin-manager/plugin-conversion-credit'\nimport { suppressEmail } from '@aglyn/tenant-data-admin/server/email-suppression'\nimport { recordTopicOptOut } from '@aglyn/tenant-data-admin/server/topic-subscriptions'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { getLockdownVerdict } from '@aglyn/tenant-data-admin/server/lockdown'\nimport { sendOrgMemberNotice } from '@aglyn/tenant-data-admin/server/org-member-notice'\nimport { logOrgActivity } from '@aglyn/tenant-data-admin/server/organizations'\nimport { filterEnabledPluginsByReleaseFlags } from '@aglyn/tenant-data-admin/server/release-flags'\nimport { resolveTrackingLinkOrigin } from '@aglyn/tenant-data-admin/server/tracking-hosts'\nimport { sendingWorkspaceFor } from '@aglyn/tenant-data-admin/server/outbound-send-review'\nimport { screenTenantMessage } from '@aglyn/shared-util-email/outbound-screen-gate'\nimport { stampRecordEmailState } from '@aglyn/aglyn/plugin-manager/plugin-record-email-state'\nimport { OUTREACH_PLUGIN_ID } from '../constants/bundle-common'\nimport { composeOutreachMailboxNotice } from '../engine/mailbox-notice'\nimport { openOutreachMailboxClient } from '../mailboxes/mailbox-transport'\nimport { canonicalConsoleOrigin } from '../mailboxes/oauth-redirect'\nimport type { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport type { PluginPersonEraser } from '@aglyn/aglyn/plugin-manager/plugin-person-erasure'\nimport { outreachShortLinkUrl } from './click-link'\nimport { createOutreachClickRoute, createOutreachShortLinkRoute } from './click-route'\nimport { createOutreachPersonEraser } from './person-erasure'\nimport type { OutreachCampaignCredit, OutreachRuntimeDeps } from './runtime-deps'\nimport { outreachUnsubscribeUrl } from './unsubscribe-link'\nimport { createOutreachUnsubscribeRoute } from './unsubscribe-route'\n\n/**\n * What a sequence produced (AGL-3254), handed to whichever plugin credits\n * outcomes (`plugin-conversion-credit`): the same contract every form,\n * booking and campaign email is credited through, so a sequence's numbers\n * and a campaign send's are two readings of one rule. A workspace whose\n * plugins credit nothing credits nothing here either; the seam never throws.\n *\n * The campaign a sequence is filed under is what its mail is credited to,\n * and the sequence and the enrollment ride with it as the mail's own facts.\n */\nexport function platformOutreachCampaignCredit(): OutreachCampaignCredit {\n return {\n async credit({ hostId, orgId, campaignIds, outcome, atMs }) {\n await creditConversionOutcome({ hostId, orgId, containerIds: campaignIds, outcome, atMs })\n },\n async attributeRecord({ hostId, kind, refId, campaignId, sequenceId, enrollmentId, atMs }) {\n await creditConversion({\n hostId,\n kind,\n refId,\n click: { hostId, creditTo: campaignId, atMs, via: { sequenceId, enrollmentId } },\n convertedAtMs: atMs,\n })\n },\n async recordTouch({ hostId, email, campaignId, sequenceId, enrollmentId, atMs }) {\n await recordConversionClick({\n email,\n hostId,\n creditTo: campaignId,\n atMs,\n via: { sequenceId, enrollmentId },\n })\n },\n }\n}\n\n/**\n * The platform's own reach for the sending runtime (AGL-2981). Loaded only\n * when a job runs or the unsubscribe route answers — never at boot — so the\n * console pays for these modules when Outreach does work.\n */\nexport function platformOutreachRuntimeDeps(): OutreachRuntimeDeps {\n const firestore = () => firebaseAdmin.app().firestore()\n return {\n firestore,\n now: Date.now,\n random: Math.random,\n campaignCredit: platformOutreachCampaignCredit(),\n openMailbox: (mailboxId) => openOutreachMailboxClient(firestore(), { mailboxId }),\n // The domain's MX (AGL-3326) and, for a domain with none, its address\n // record — through the platform's pinned resolver (AGL-3328), which\n // asks twice before it says a domain takes no mail.\n resolveMx: (domain) => platformMailDnsResolver().resolveMx(domain),\n resolveAddress: (domain) => platformMailDnsResolver().resolveAddress?.(domain) ?? Promise.resolve(false),\n async orgRefusal(orgId, org) {\n if (!isPluginEnabled(org as { enabledPlugins?: string[] }, OUTREACH_PLUGIN_ID)) return 'plugin-disabled'\n if (!checkEntitlement(org, 'outreach')) return 'not-entitled'\n const released = await filterEnabledPluginsByReleaseFlags([OUTREACH_PLUGIN_ID], { orgId })\n return released.length ? null : 'not-released'\n },\n async sendRefusal({ org, uid }) {\n const locked = await getLockdownVerdict({ org, uid, intent: 'write' })\n return locked ? `locked:${locked.scope}` : null\n },\n timeline: () => pluginRecordTimelineWriter()?.writer ?? null,\n logOrgActivity: (orgId, actor, action, target) => logOrgActivity(orgId, actor, action, target),\n async optOutOfSalesTopic({ hostId, email }) {\n await recordTopicOptOut(hostId, email, SALES_TOPIC_ID, { firestore: firestore() })\n },\n async suppressBouncedEmail({ email, hostId }) {\n // The runtime stamps the record itself, with the richer verdict — the\n // domain block, the enrollment — so the list's own stamp stands down.\n await suppressEmail({ email, reason: 'bounce', context: 'outreach', hostId, stampRecord: false, firestore: firestore() })\n },\n async stampRecordEmailState(stamp) {\n await stampRecordEmailState(stamp)\n },\n async notifyMailboxOwner(notice) {\n // The Mailboxes page, by the organization's slug — the page Resume and\n // Reconnect live on (AGL-3244). Without a slug or a console origin the\n // notice still goes, and says where the page is instead.\n const org = await firestore().collection('orgs').doc(notice.orgId).get()\n const slug = typeof org.get('slug') === 'string' ? String(org.get('slug')) : ''\n const origin = canonicalConsoleOrigin()\n const mailboxesUrl = slug && origin ? `${origin}/${encodeURIComponent(slug)}/outreach/mailboxes` : null\n const email = composeOutreachMailboxNotice(notice, { mailboxesUrl })\n const result = await sendOrgMemberNotice({\n orgId: notice.orgId,\n uids: [notice.connectedByUid],\n includeAdmins: true,\n subject: email.subject,\n text: email.text,\n context: 'outreach-mailbox-notice',\n })\n if (!result.sent) console.warn(`[outreach] the mailbox owner was not emailed: ${result.reason}`)\n },\n unsubscribeUrl: (target) => outreachUnsubscribeUrl({ origin: canonicalConsoleOrigin(), target }),\n clickLinkUrl: (linkId, trackingOrigin) =>\n outreachShortLinkUrl({ origin: canonicalConsoleOrigin(), linkId, trackingOrigin }),\n // The sending domain's own `links.` host once the organization has\n // verified it (AGL-3306). A static import like every other\n // `tenant-data-admin` module here: a dynamic one marks the whole library\n // lazy-loaded, and `@nx/enforce-module-boundaries` then refuses every\n // static import of it across the plugin and the console.\n clickLinkOrigin({ orgId, senderAddress }) {\n return resolveTrackingLinkOrigin(firestore(), orgId, senderAddress)\n },\n // The one phishing screen every tenant message passes (AGL-3356). The\n // review module's import installs its store, so a hold files the same\n // abuse-queue row a held newsletter or campaign does.\n async screenMessage({ orgId, org, hostId, host, subject, text, fromName, fromAddress }) {\n const refusal = await screenTenantMessage(\n {\n workspace: sendingWorkspaceFor({ hostId, orgId, org, host }),\n subject,\n fromName,\n fromAddress: fromAddress ?? null,\n bodies: [text],\n context: 'outreach sequence',\n },\n 'outreach email',\n )\n return refusal?.detail ?? null\n },\n }\n}\n\n/** The unsubscribe route on the platform's own reach. */\nexport function platformOutreachUnsubscribeRoute(): PluginWebApiHandler {\n return createOutreachUnsubscribeRoute(platformOutreachRuntimeDeps())\n}\n\n/** The click-tracking redirect on the platform's own reach (AGL-3239). */\nexport function platformOutreachClickRoute(): PluginWebApiHandler {\n return createOutreachClickRoute(platformOutreachRuntimeDeps())\n}\n\n/** The short tracking link's redirect on the platform's own reach (AGL-3297). */\nexport function platformOutreachShortLinkRoute(): PluginWebApiHandler {\n return createOutreachShortLinkRoute(platformOutreachRuntimeDeps())\n}\n\n/** The person eraser on the platform's own Firestore. */\nexport function platformOutreachPersonEraser(): PluginPersonEraser {\n return createOutreachPersonEraser({ firestore: () => firebaseAdmin.app().firestore() })\n}\n"],"names":["SALES_TOPIC_ID","platformMailDnsResolver","checkEntitlement","isPluginEnabled","pluginRecordTimelineWriter","creditConversion","creditConversionOutcome","recordConversionClick","suppressEmail","recordTopicOptOut","firebaseAdmin","getLockdownVerdict","sendOrgMemberNotice","logOrgActivity","filterEnabledPluginsByReleaseFlags","resolveTrackingLinkOrigin","sendingWorkspaceFor","screenTenantMessage","stampRecordEmailState","OUTREACH_PLUGIN_ID","composeOutreachMailboxNotice","openOutreachMailboxClient","canonicalConsoleOrigin","outreachShortLinkUrl","createOutreachClickRoute","createOutreachShortLinkRoute","createOutreachPersonEraser","outreachUnsubscribeUrl","createOutreachUnsubscribeRoute","platformOutreachCampaignCredit","credit","hostId","orgId","campaignIds","outcome","atMs","containerIds","attributeRecord","kind","refId","campaignId","sequenceId","enrollmentId","click","creditTo","via","convertedAtMs","recordTouch","email","platformOutreachRuntimeDeps","firestore","app","now","Date","random","Math","campaignCredit","openMailbox","mailboxId","resolveMx","domain","resolveAddress","Promise","resolve","orgRefusal","org","released","length","sendRefusal","uid","locked","intent","scope","timeline","writer","actor","action","target","optOutOfSalesTopic","suppressBouncedEmail","reason","context","stampRecord","stamp","notifyMailboxOwner","notice","collection","doc","get","slug","String","origin","mailboxesUrl","encodeURIComponent","result","uids","connectedByUid","includeAdmins","subject","text","sent","console","warn","unsubscribeUrl","clickLinkUrl","linkId","trackingOrigin","clickLinkOrigin","senderAddress","screenMessage","host","fromName","fromAddress","refusal","workspace","bodies","detail","platformOutreachUnsubscribeRoute","platformOutreachClickRoute","platformOutreachShortLinkRoute","platformOutreachPersonEraser"],"mappings":"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"}
|
|
@@ -15,8 +15,6 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
17
|
*/ import { consentGroupForHost } from "@aglyn/aglyn/app-utils/consent-groups";
|
|
18
|
-
import { CRM_COLLECTIONS } from "@aglyn/aglyn/app-utils/crm";
|
|
19
|
-
import { normalizeCrmEmailTemplate } from "@aglyn/aglyn/app-utils/crm-email-templates";
|
|
20
18
|
import { FieldValue } from "firebase-admin/firestore";
|
|
21
19
|
import { randomUUID } from "node:crypto";
|
|
22
20
|
import { composeOutreachEmail } from "../engine/compose.js";
|
|
@@ -25,6 +23,7 @@ import { planOutreachStepCompletion } from "../engine/enrollment-state.js";
|
|
|
25
23
|
import { evaluateOutreachGates } from "../engine/gates.js";
|
|
26
24
|
import { outreachSentToday, outreachTickAllowance, selectDueOutreachEnrollments } from "../engine/sending-capacity.js";
|
|
27
25
|
import { readOutreachEnrollCandidates } from "../enrollment/enroll-people.js";
|
|
26
|
+
import { readOutreachTemplateBody } from "../enrollment/message-templates.js";
|
|
28
27
|
import { readOutreachGateLookups } from "../enrollment/gate-lookups.js";
|
|
29
28
|
import { mailboxRef } from "../mailboxes/mailbox-credentials.js";
|
|
30
29
|
import { outreachEffectiveDailyCap, outreachLocalDay } from "../mailboxes/mailbox-settings.js";
|
|
@@ -40,7 +39,7 @@ import { creditOutreachFirstSend } from "./campaign-credit.js";
|
|
|
40
39
|
import { newOutreachLinkId, outreachStoredLink, outreachStoredOpen } from "./click-link.js";
|
|
41
40
|
import { applyOutreachEvent } from "./enrollment-events.js";
|
|
42
41
|
import { outreachSiteHeld } from "./host-suspension.js";
|
|
43
|
-
import {
|
|
42
|
+
import { stampRecordEmailReach } from "@aglyn/aglyn/plugin-manager/plugin-record-email-state";
|
|
44
43
|
import { outreachSendDigest, withRecentSend } from "./mailbox-health-store.js";
|
|
45
44
|
import { noteOutreachMailboxReconnectRequired } from "./mailbox-notices.js";
|
|
46
45
|
import { fileOutreachEmail, fileOutreachTask, OUTREACH_TASK_KIND_TO_RECORD } from "./timeline.js";
|
|
@@ -261,7 +260,7 @@ async function runMailbox(deps, firestore, input) {
|
|
|
261
260
|
// Each by the record it names — the contact, or the lead while the
|
|
262
261
|
// person is one (AGL-3234) — keyed by that record's id.
|
|
263
262
|
const [candidatesRead, lookupsRead, host] = await Promise.all([
|
|
264
|
-
readOutreachEnrollCandidates(
|
|
263
|
+
readOutreachEnrollCandidates({
|
|
265
264
|
orgId,
|
|
266
265
|
hostId,
|
|
267
266
|
contactGroupId,
|
|
@@ -608,11 +607,10 @@ async function openClient(deps, run) {
|
|
|
608
607
|
claimMessageId: null
|
|
609
608
|
});
|
|
610
609
|
}
|
|
611
|
-
/** A
|
|
610
|
+
/** A template's body, read through the record system once per run. */ async function templateBody(run, templateId) {
|
|
612
611
|
var _run_templates_get;
|
|
613
612
|
if (!run.templates.has(templateId)) {
|
|
614
|
-
|
|
615
|
-
run.templates.set(templateId, snapshot.exists ? normalizeCrmEmailTemplate(snapshot.data()).body : null);
|
|
613
|
+
run.templates.set(templateId, await readOutreachTemplateBody(run.orgId, templateId));
|
|
616
614
|
}
|
|
617
615
|
return (_run_templates_get = run.templates.get(templateId)) != null ? _run_templates_get : null;
|
|
618
616
|
}
|
|
@@ -796,7 +794,7 @@ async function runEmailStep(deps, firestore, run, input) {
|
|
|
796
794
|
name: input.siteName
|
|
797
795
|
}
|
|
798
796
|
},
|
|
799
|
-
templateBody: (step == null ? void 0 : step.kind) === 'email' && step.templateId ? await templateBody(
|
|
797
|
+
templateBody: (step == null ? void 0 : step.kind) === 'email' && step.templateId ? await templateBody(run, step.templateId) : null,
|
|
800
798
|
listUnsubscribeUrl: unsubscribe.status === 'ready' ? unsubscribe.url : null,
|
|
801
799
|
listUnsubscribeMailto: unsubscribe.status === 'ready' ? unsubscribe.mailto : null
|
|
802
800
|
});
|
|
@@ -993,17 +991,22 @@ async function runEmailStep(deps, firestore, run, input) {
|
|
|
993
991
|
enrollment,
|
|
994
992
|
atMs: record.atMs
|
|
995
993
|
});
|
|
996
|
-
await nurtureEnrolledLead(
|
|
994
|
+
await nurtureEnrolledLead(run, enrollment);
|
|
997
995
|
}
|
|
998
996
|
/**
|
|
999
|
-
* A lead an email step reached is no longer untouched (AGL-3446):
|
|
1000
|
-
*
|
|
1001
|
-
*
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
997
|
+
* A lead an email step reached is no longer untouched (AGL-3446): the record
|
|
998
|
+
* system hears the send was delivered, and moves a New lead to Nurturing
|
|
999
|
+
* through the record email-state seam's `reached` (AGL-3080). Called only
|
|
1000
|
+
* once a send is recorded, so an enrollment whose steps never go out leaves
|
|
1001
|
+
* its lead where it stood. Never throws.
|
|
1002
|
+
*/ async function nurtureEnrolledLead(run, enrollment) {
|
|
1003
|
+
if (enrollment.target !== 'lead' || !enrollment.leadId || !enrollment.email) return;
|
|
1004
|
+
await stampRecordEmailReach({
|
|
1005
1005
|
orgId: run.orgId,
|
|
1006
|
-
|
|
1006
|
+
hostId: enrollment.hostId,
|
|
1007
|
+
emails: [
|
|
1008
|
+
enrollment.email
|
|
1009
|
+
]
|
|
1007
1010
|
});
|
|
1008
1011
|
}
|
|
1009
1012
|
/**
|
|
@@ -1068,7 +1071,7 @@ async function runEmailStep(deps, firestore, run, input) {
|
|
|
1068
1071
|
enrollment,
|
|
1069
1072
|
atMs
|
|
1070
1073
|
});
|
|
1071
|
-
await nurtureEnrolledLead(
|
|
1074
|
+
await nurtureEnrolledLead(run, enrollment);
|
|
1072
1075
|
}
|
|
1073
1076
|
return completed;
|
|
1074
1077
|
}
|