@aglyn/plugins-outreach 1.0.0-beta.221 → 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
|
@@ -17,8 +17,6 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
17
17
|
*/ import { readContainerIds, readContactContainerIds } from "@aglyn/aglyn/app-utils/container-membership";
|
|
18
18
|
import { consentGroupForHost } from "@aglyn/aglyn/app-utils/consent-groups";
|
|
19
19
|
import { readContactFacet } from "@aglyn/aglyn/app-utils/contacts";
|
|
20
|
-
import { CRM_COLLECTIONS } from "@aglyn/aglyn/app-utils/crm";
|
|
21
|
-
import { normalizeCrmEmailTemplate } from "@aglyn/aglyn/app-utils/crm-email-templates";
|
|
22
20
|
import { readOrgContainers } from "@aglyn/tenant-data-admin/server/org-containers";
|
|
23
21
|
import { FieldValue } from "firebase-admin/firestore";
|
|
24
22
|
import { OUTREACH_CURATION_SYSTEM, outreachCurationPrompt, parseOutreachCurationAnswer } from "../engine/curation-prompt.js";
|
|
@@ -26,6 +24,7 @@ import { outreachCuratedEntry } from "../engine/enrollment-activity.js";
|
|
|
26
24
|
import { OUTREACH_OPEN_ENROLLMENT_STATUSES } from "../engine/gates.js";
|
|
27
25
|
import { hasOutreachValidationErrors, isInThreadEmailStep, validateOutreachCuratedStep } from "../engine/sequence-validation.js";
|
|
28
26
|
import { readOutreachEnrollCandidates } from "../enrollment/enroll-people.js";
|
|
27
|
+
import { readOutreachTemplateBody } from "../enrollment/message-templates.js";
|
|
29
28
|
import { readOutreachStepOverrideRequest } from "../enrollment/step-overrides.js";
|
|
30
29
|
import { OUTREACH_MAX_EMAIL_STEPS } from "../model/outreach.types.js";
|
|
31
30
|
import { fileOutreachNote } from "../runtime/timeline.js";
|
|
@@ -206,14 +205,13 @@ const texts = (value)=>Array.isArray(value) ? value.map(text).filter(Boolean) :
|
|
|
206
205
|
}
|
|
207
206
|
/** The email steps to draft, as the prompt states them: the step's words, or its template's. */ async function curationSteps(firestore, orgId, sequence, stepIndexes) {
|
|
208
207
|
const emails = emailStepIndexes(sequence);
|
|
209
|
-
const templates = firestore.collection('orgs').doc(orgId).collection(CRM_COLLECTIONS.emailTemplates);
|
|
210
208
|
return Promise.all(stepIndexes.map(async (stepIndex)=>{
|
|
211
209
|
const step = sequence.steps[stepIndex];
|
|
212
210
|
if ((step == null ? void 0 : step.kind) !== 'email') throw new Error(`step ${stepIndex} is not an email`);
|
|
213
211
|
let body = step.body;
|
|
214
212
|
if (step.templateId) {
|
|
215
|
-
|
|
216
|
-
body =
|
|
213
|
+
var _ref;
|
|
214
|
+
body = (_ref = await readOutreachTemplateBody(orgId, step.templateId)) != null ? _ref : '';
|
|
217
215
|
}
|
|
218
216
|
return {
|
|
219
217
|
stepIndex,
|
|
@@ -249,7 +247,7 @@ export function createOutreachCurateRoutes(deps) {
|
|
|
249
247
|
const subject = await draftSubject(firestore, caller, body);
|
|
250
248
|
if (subject instanceof Response) return subject;
|
|
251
249
|
const contactGroupId = consentGroupForHost(caller.org, subject.sequence.hostId).groupId;
|
|
252
|
-
const [candidate] = await readOutreachEnrollCandidates(
|
|
250
|
+
const [candidate] = await readOutreachEnrollCandidates({
|
|
253
251
|
orgId: caller.orgId,
|
|
254
252
|
hostId: subject.sequence.hostId,
|
|
255
253
|
contactGroupId,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/curate-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 { readContainerIds, readContactContainerIds } from '@aglyn/aglyn/app-utils/container-membership'\nimport { consentGroupForHost } from '@aglyn/aglyn/app-utils/consent-groups'\nimport { readContactFacet } from '@aglyn/aglyn/app-utils/contacts'\nimport { CRM_COLLECTIONS } from '@aglyn/aglyn/app-utils/crm'\nimport { normalizeCrmEmailTemplate } from '@aglyn/aglyn/app-utils/crm-email-templates'\nimport type { PluginTextGenerator } from '@aglyn/aglyn/plugin-manager/plugin-text-generation'\nimport type { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport { readOrgContainers } from '@aglyn/tenant-data-admin/server/org-containers'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport {\n OUTREACH_CURATION_SYSTEM,\n outreachCurationPrompt,\n parseOutreachCurationAnswer,\n type OutreachCurationFacts,\n type OutreachCurationStep,\n} from '../engine/curation-prompt'\nimport { outreachCuratedEntry } from '../engine/enrollment-activity'\nimport { OUTREACH_OPEN_ENROLLMENT_STATUSES } from '../engine/gates'\nimport {\n hasOutreachValidationErrors,\n isInThreadEmailStep,\n validateOutreachCuratedStep,\n} from '../engine/sequence-validation'\nimport { readOutreachEnrollCandidates, type OutreachEnrollCandidate } from '../enrollment/enroll-people'\nimport { readOutreachStepOverrideRequest } from '../enrollment/step-overrides'\nimport type {\n OutreachCurateDraft,\n OutreachCurateDraftResponse,\n OutreachCurateSaveResponse,\n OutreachPersonRef,\n} from '../model/outreach-api'\nimport {\n OUTREACH_MAX_EMAIL_STEPS,\n type OutreachEnrollment,\n type OutreachSequence,\n type OutreachStepOverride,\n} from '../model/outreach.types'\nimport { fileOutreachNote } from '../runtime/timeline'\nimport {\n outreachEnrollmentLink,\n outreachEnrollmentPerson,\n outreachOrgCollection,\n readStoredOutreachEnrollment,\n readStoredOutreachMailbox,\n readStoredOutreachSequence,\n} from '../storage/outreach-records'\nimport { OUTREACH_READS_PEOPLE, type OutreachEnrollRouteDeps } from './enroll-routes'\nimport { OUTREACH_SEQUENCE_ACTIVITY_TARGET } from './route-deps'\nimport { outreachRouteGate, type OutreachRouteCaller } from './route-gate'\nimport {\n outreachMethodNotAllowed,\n outreachOk,\n outreachRefusal,\n readOutreachDocumentId,\n readOutreachJsonBody,\n} from './route-http'\n\n/**\n * CURATING ONE PERSON'S EMAILS (AGL-3324): `outreach/curate/draft` and\n * `outreach/curate/save`.\n *\n * A sequence's step is one template for everyone plus the personal line;\n * what an agent-run outbound tool does instead is write each send for each\n * prospect from what it knows about them, and let the seller approve or\n * edit. These two routes are that, with the member's confirmation kept as\n * the one thing that stores anything.\n *\n * DRAFT reads the person the way enrolling reads them — the contact, or the\n * lead as the contact it would be, with the company filed beside them — and\n * asks the workspace's text generator, on the core's seam, for this\n * person's copy of the email steps named: every email step for a person\n * about to be enrolled, the next one for an enrollment. The answer is\n * validated against the playbook's rules and returned with each draft's\n * issues, the prompt and the model. NOTHING IS STORED: the member reads it\n * in the dialog, edits it, ticks the confirmation, and then either enrolls\n * (the enroll route stores the copies with the enrollment) or saves.\n *\n * SAVE stores one confirmed copy on an open enrollment, for a step that\n * has not gone out, validated again — the request is not trusted to have\n * shown the member what it sends — and files \"Curated step N\" on the\n * person's record with who wrote the words. `override: null` clears a copy,\n * so the step goes back to the sequence's own words.\n *\n * Both read the CRM, so both ask for `data.manage` beside `outreach.use`;\n * the generator applies its own rules — the generation permission, the\n * plan, the switches, the allotments — to the member named, and meters the\n * draft on the member's month and the org's. Outreach never reaches the AI\n * plugin: the seam is the platform's, and a workspace where no plugin\n * generates text is told so in the route's words.\n */\n\n/** The purpose the generator's ledger files a draft under. */\nexport const OUTREACH_CURATION_PURPOSE = 'outreach-curate'\n\n/** The most tokens one drafting answer may run to: four short emails, with room. */\nconst CURATION_MAX_TOKENS = 2_400\n\n/** The org activity line a saved copy writes. */\nexport function outreachCuratedActivity(stepIndex: number, cleared: boolean): string {\n return cleared\n ? `Cleared the curated copy of step ${stepIndex + 1} of a sequence`\n : `Curated step ${stepIndex + 1} of a sequence for one person`\n}\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** The person's copies of steps a draft covers, and the enrollment they belong to when one exists. */\ninterface DraftSubject {\n sequence: OutreachSequence\n person: OutreachPersonRef\n personalLine: string\n enrollment: OutreachEnrollment | null\n /** The steps to draft, email steps only, unique, in order. */\n stepIndexes: number[]\n}\n\n/** Every email step's index, in order. */\nfunction emailStepIndexes(sequence: Pick<OutreachSequence, 'steps'>): number[] {\n return sequence.steps.map((step, index) => (step.kind === 'email' ? index : -1)).filter((index) => index >= 0)\n}\n\n/** The step indexes a request names, or the default for its subject; a refusal for anything else. */\nfunction readStepIndexes(\n raw: unknown,\n sequence: OutreachSequence,\n fallback: number[],\n): number[] | Response {\n if (raw === undefined) return fallback\n if (!Array.isArray(raw) || !raw.length) {\n return outreachRefusal(400, 'invalid-request', 'Name the email steps to draft.')\n }\n const wanted = [...new Set(raw.map((value) => Number(value)))]\n const emails = emailStepIndexes(sequence)\n if (wanted.some((index) => !emails.includes(index))) {\n return outreachRefusal(400, 'invalid-request', 'A curated step is one of the sequence’s email steps.')\n }\n if (wanted.length > OUTREACH_MAX_EMAIL_STEPS) {\n return outreachRefusal(400, 'invalid-request', `Draft at most ${OUTREACH_MAX_EMAIL_STEPS} emails at a time.`)\n }\n return wanted.sort((a, b) => a - b)\n}\n\n/** What the request is about: a person to enroll, or an enrollment's next step. */\nasync function draftSubject(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n body: Record<string, unknown>,\n): Promise<DraftSubject | Response> {\n const enrollmentId = body['enrollmentId'] === undefined ? null : readOutreachDocumentId(body['enrollmentId'])\n if (body['enrollmentId'] !== undefined && !enrollmentId) {\n return outreachRefusal(400, 'invalid-request', 'Name the enrollment to curate.')\n }\n if (enrollmentId) {\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 if (!OUTREACH_OPEN_ENROLLMENT_STATUSES.includes(enrollment.status)) {\n return outreachRefusal(409, 'transition-refused', 'This enrollment has ended, so nothing more is sent to them.')\n }\n const sequenceSnapshot = await outreachOrgCollection(firestore, caller.orgId, 'sequences')\n .doc(enrollment.sequenceId)\n .get()\n const sequence = readStoredOutreachSequence(\n enrollment.sequenceId,\n sequenceSnapshot.exists ? sequenceSnapshot.data() : undefined,\n )\n if (!sequence) return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n // The next email step that has not gone out: the one the enrollment is\n // on when it is an email, else the first email after it.\n const next = emailStepIndexes(sequence).find((index) => index >= enrollment.stepIndex)\n const stepIndexes = readStepIndexes(body['stepIndexes'], sequence, next === undefined ? [] : [next])\n if (stepIndexes instanceof Response) return stepIndexes\n if (stepIndexes.some((index) => index < enrollment.stepIndex)) {\n return outreachRefusal(409, 'transition-refused', 'That step already went out to this person.')\n }\n if (!stepIndexes.length) {\n return outreachRefusal(409, 'transition-refused', 'Every email in this sequence has gone out to this person.')\n }\n return {\n sequence,\n person: outreachEnrollmentPerson(enrollment),\n personalLine: enrollment.personalLine,\n enrollment,\n stepIndexes,\n }\n }\n const sequenceId = readOutreachDocumentId(body['sequenceId'])\n if (!sequenceId) return outreachRefusal(400, 'invalid-request', 'Name the sequence to draft for.')\n const contactId = body['contactId'] === undefined ? null : readOutreachDocumentId(body['contactId'])\n const leadId = body['leadId'] === undefined ? null : readOutreachDocumentId(body['leadId'])\n const person: OutreachPersonRef | null = contactId\n ? { kind: 'contact', id: contactId }\n : leadId\n ? { kind: 'lead', id: leadId }\n : null\n if (!person) return outreachRefusal(400, 'invalid-request', 'Name the contact or the lead to draft for.')\n const snapshot = await outreachOrgCollection(firestore, caller.orgId, 'sequences').doc(sequenceId).get()\n const sequence = readStoredOutreachSequence(sequenceId, snapshot.exists ? snapshot.data() : undefined)\n if (!sequence) return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n const stepIndexes = readStepIndexes(body['stepIndexes'], sequence, emailStepIndexes(sequence))\n if (stepIndexes instanceof Response) return stepIndexes\n if (!stepIndexes.length) {\n return outreachRefusal(400, 'invalid-request', 'This sequence sends no email, so there is nothing to draft.')\n }\n return {\n sequence,\n person,\n personalLine: typeof body['personalLine'] === 'string' ? body['personalLine'] : '',\n enrollment: null,\n stepIndexes,\n }\n}\n\nconst text = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\nconst texts = (value: unknown): string[] =>\n Array.isArray(value) ? value.map(text).filter(Boolean) : []\n\n/**\n * The names of the campaigns the person is filed under — the org's\n * containers of the `campaign` kind. A container that is gone names nothing;\n * a read that fails names none, and the draft goes without.\n */\nasync function campaignNames(firestore: Firestore, orgId: string, campaignIds: readonly string[]): Promise<string[]> {\n if (!campaignIds.length) return []\n try {\n const found = await readOrgContainers(firestore, 'campaign', orgId, campaignIds)\n return found.map((container) => container.name).filter(Boolean)\n } catch (error) {\n console.error('[outreach] the person’s campaigns could not be named for the draft', error)\n return []\n }\n}\n\n/** What the CRM holds about the person, for the prompt. */\nasync function curationFacts(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n subject: DraftSubject,\n candidate: OutreachEnrollCandidate,\n contactGroupId: string,\n): Promise<OutreachCurationFacts> {\n const contact = candidate.contact ?? {}\n const facet = readContactFacet(contact, contactGroupId) as unknown as Record<string, unknown>\n const lead = candidate.lead ?? {}\n const company = candidate.company ?? {}\n const campaignIds =\n candidate.target === 'lead' ? readContainerIds(lead, 'campaign') : readContactContainerIds(contact, contactGroupId, 'campaign')\n const [mailboxSnapshot, host, campaigns] = await Promise.all([\n subject.sequence.mailboxId\n ? outreachOrgCollection(firestore, caller.orgId, 'mailboxes').doc(subject.sequence.mailboxId).get()\n : null,\n subject.sequence.hostId ? firestore.collection('hosts').doc(subject.sequence.hostId).get() : null,\n campaignNames(firestore, caller.orgId, campaignIds),\n ])\n const mailbox = readStoredOutreachMailbox(\n subject.sequence.mailboxId,\n mailboxSnapshot?.exists ? mailboxSnapshot.data() : undefined,\n )\n return {\n name: candidate.name || text(contact['name']),\n company: text(facet['companyName']) || text(lead['company']) || text(company['name']),\n title: text(facet['jobTitle']) || text(lead['jobTitle']),\n website: text(company['website']) || text(company['domain']) || text(lead['website']) || text(lead['domain']),\n tags: [...new Set([...texts(facet['tags']), ...texts(company['tags'])])],\n sources: Object.entries((facet['sources'] ?? {}) as Record<string, unknown>)\n .filter(([, on]) => on === true)\n .map(([source]) => source)\n .sort(),\n campaigns,\n notes: candidate.target === 'lead' ? text(lead['notes']) : '',\n record: candidate.target,\n siteName: host?.exists ? text(host.get('name')) : '',\n senderName: mailbox?.displayName || '',\n personalLine: subject.personalLine,\n }\n}\n\n/** The email steps to draft, as the prompt states them: the step's words, or its template's. */\nasync function curationSteps(\n firestore: Firestore,\n orgId: string,\n sequence: OutreachSequence,\n stepIndexes: readonly number[],\n): Promise<OutreachCurationStep[]> {\n const emails = emailStepIndexes(sequence)\n const templates = firestore.collection('orgs').doc(orgId).collection(CRM_COLLECTIONS.emailTemplates)\n return Promise.all(\n stepIndexes.map(async (stepIndex) => {\n const step = sequence.steps[stepIndex]\n if (step?.kind !== 'email') throw new Error(`step ${stepIndex} is not an email`)\n let body = step.body\n if (step.templateId) {\n const template = await templates.doc(step.templateId).get()\n body = template.exists ? normalizeCrmEmailTemplate(template.data() as Record<string, unknown>).body : ''\n }\n return {\n stepIndex,\n position: emails.indexOf(stepIndex) + 1,\n count: emails.length,\n subject: step.subject,\n body,\n startsThread: !isInThreadEmailStep(sequence.steps, stepIndex),\n }\n }),\n )\n}\n\n/** The member's name for the record's entry: as the roster lists them, else their address. */\nasync function memberName(firestore: Firestore, caller: OutreachRouteCaller): Promise<string | null> {\n try {\n const member = await firestore.collection('orgs').doc(caller.orgId).collection('members').doc(caller.uid).get()\n return (member.exists ? text(member.get('displayName')) || text(member.get('email')) : '') || caller.email\n } catch {\n return caller.email\n }\n}\n\nexport interface OutreachCurateRoutes {\n draft: PluginWebApiHandler\n save: PluginWebApiHandler\n}\n\nexport function createOutreachCurateRoutes(deps: OutreachEnrollRouteDeps): OutreachCurateRoutes {\n const draft: 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 generator = deps.textGenerator()\n if (!generator) {\n return outreachRefusal(503, 'curation-unavailable', 'AI drafting is not available in this workspace.')\n }\n const firestore = deps.firestore()\n const subject = await draftSubject(firestore, caller, body)\n if (subject instanceof Response) return subject\n const contactGroupId = consentGroupForHost(caller.org, subject.sequence.hostId).groupId\n const [candidate] = await readOutreachEnrollCandidates(firestore, {\n orgId: caller.orgId,\n hostId: subject.sequence.hostId,\n contactGroupId,\n people: [subject.person],\n })\n if (!candidate?.contact || !candidate.visible) {\n return outreachRefusal(404, 'contact-not-found', \"That person isn't in this sequence's site's CRM.\")\n }\n const [facts, steps] = await Promise.all([\n curationFacts(firestore, caller, subject, candidate, contactGroupId),\n curationSteps(firestore, caller.orgId, subject.sequence, subject.stepIndexes),\n ])\n const prompt = outreachCurationPrompt(facts, steps)\n const answer = await generator.generate({\n orgId: caller.orgId,\n hostId: subject.sequence.hostId || null,\n uid: caller.uid,\n staff: caller.staff,\n org: caller.org,\n purpose: OUTREACH_CURATION_PURPOSE,\n system: OUTREACH_CURATION_SYSTEM,\n prompt,\n maxTokens: CURATION_MAX_TOKENS,\n now: new Date(deps.now()),\n })\n // Compared to `false` rather than negated: the union narrows on the\n // literal, and this tree compiles without strict null checks.\n if (answer.ok === false) {\n return outreachRefusal(\n answer.status,\n answer.reason === 'unavailable' ? 'curation-unavailable' : 'curation-refused',\n answer.error,\n )\n }\n const parsed = parseOutreachCurationAnswer(answer.text, steps)\n if (!parsed) {\n return outreachRefusal(\n 502,\n 'curation-refused',\n 'The draft came back in a shape that can’t be read. Try again.',\n )\n }\n const drafts: OutreachCurateDraft[] = parsed.map((entry) => {\n const step = steps.find((candidateStep) => candidateStep.stepIndex === entry.stepIndex)\n return {\n ...entry,\n issues: validateOutreachCuratedStep(\n { subject: entry.subject, body: entry.body, startsThread: step?.startsThread === true },\n `stepOverrides.${entry.stepIndex}`,\n ),\n }\n })\n return outreachOk({ ok: true, drafts, prompt, model: answer.model } satisfies OutreachCurateDraftResponse)\n }\n\n const save: 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)\n if (caller instanceof Response) return caller\n const enrollmentId = readOutreachDocumentId(body['enrollmentId'])\n const stepIndex = Number(body['stepIndex'])\n if (!enrollmentId || !Number.isInteger(stepIndex) || stepIndex < 0) {\n return outreachRefusal(400, 'invalid-request', 'Name the enrollment and the step to curate.')\n }\n const firestore = deps.firestore()\n const enrollments = outreachOrgCollection(firestore, caller.orgId, 'enrollments')\n const current = await enrollments.doc(enrollmentId).get()\n const stored = readStoredOutreachEnrollment(enrollmentId, current.exists ? current.data() : undefined)\n if (!stored) return outreachRefusal(404, 'enrollment-not-found', 'That enrollment no longer exists.')\n const sequenceSnapshot = await outreachOrgCollection(firestore, caller.orgId, 'sequences').doc(stored.sequenceId).get()\n const sequence = readStoredOutreachSequence(stored.sequenceId, sequenceSnapshot.exists ? sequenceSnapshot.data() : undefined)\n if (!sequence) return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n if (sequence.steps[stepIndex]?.kind !== 'email') {\n return outreachRefusal(400, 'invalid-request', 'A curated step is one of the sequence’s email steps.')\n }\n\n const cleared = body['override'] === null\n let override: OutreachStepOverride | null = null\n if (!cleared) {\n const read = readOutreachStepOverrideRequest({ ...(body['override'] as object), stepIndex }, sequence.steps)\n if ('refusal' in read) return outreachRefusal(400, 'invalid-override', read.refusal)\n if (hasOutreachValidationErrors(read.issues)) {\n return outreachRefusal(400, 'invalid-override', 'This draft breaks a rule every sequence email keeps.', {\n issues: read.issues,\n })\n }\n override = { ...read.override, draftedAtMs: deps.now(), draftedByUid: caller.uid }\n }\n\n const nowMs = deps.now()\n const written = await firestore.runTransaction(async (transaction) => {\n const snapshot = await transaction.get(enrollments.doc(enrollmentId))\n const enrollment = readStoredOutreachEnrollment(enrollmentId, snapshot.exists ? snapshot.data() : undefined)\n if (!enrollment) return { refusal: outreachRefusal(404, 'enrollment-not-found', 'That enrollment no longer exists.') }\n if (!OUTREACH_OPEN_ENROLLMENT_STATUSES.includes(enrollment.status)) {\n return {\n refusal: outreachRefusal(409, 'transition-refused', 'This enrollment has ended, so nothing more is sent to them.'),\n }\n }\n // A step that already went out, or is going out now, is not rewritten\n // under the run: what was sent is what was sent.\n if (stepIndex < enrollment.stepIndex || (enrollment.sendClaim && enrollment.sendClaim.stepIndex === stepIndex)) {\n return { refusal: outreachRefusal(409, 'transition-refused', 'That step already went out to this person.') }\n }\n transaction.update(enrollments.doc(enrollmentId), {\n [`stepOverrides.${stepIndex}`]: override ?? FieldValue.delete(),\n updatedAtMs: nowMs,\n })\n const stepOverrides = { ...(enrollment.stepOverrides ?? {}) }\n if (override) stepOverrides[String(stepIndex)] = override\n else delete stepOverrides[String(stepIndex)]\n // Absent rather than empty once the last copy is cleared, as the\n // stored reader answers it.\n const next: OutreachEnrollment = { ...enrollment, updatedAtMs: nowMs }\n delete next.stepOverrides\n if (Object.keys(stepOverrides).length) next.stepOverrides = stepOverrides\n return { enrollment: next }\n })\n if ('refusal' in written) return written.refusal\n const { enrollment } = written\n\n if (override) {\n // The person's record says so (AGL-3274, AGL-3324): who wrote this\n // person's copy of the step, under the send it shapes.\n const entry = outreachCuratedEntry({\n enrollmentId,\n stepIndex,\n source: override.source,\n edited: override.edited === true,\n memberName: await memberName(firestore, caller),\n atMs: nowMs,\n })\n await fileOutreachNote(deps, {\n orgId: caller.orgId,\n hostId: enrollment.hostId,\n link: outreachEnrollmentLink(enrollment),\n dedupeKey: entry.dedupeKey,\n body: entry.body,\n atMs: nowMs,\n })\n }\n await deps.logOrgActivity(\n caller.orgId,\n { uid: caller.uid, email: caller.email },\n outreachCuratedActivity(stepIndex, cleared),\n { type: OUTREACH_SEQUENCE_ACTIVITY_TARGET, id: sequence.id, name: sequence.name },\n )\n return outreachOk({ ok: true, enrollment } satisfies OutreachCurateSaveResponse)\n }\n\n return { draft, save }\n}\n\n/** The text generator on the core's seam, for the default route deps; `null` when no plugin generates. */\nexport type OutreachTextGeneratorReader = () => PluginTextGenerator | null\n"],"names":["readContainerIds","readContactContainerIds","consentGroupForHost","readContactFacet","CRM_COLLECTIONS","normalizeCrmEmailTemplate","readOrgContainers","FieldValue","OUTREACH_CURATION_SYSTEM","outreachCurationPrompt","parseOutreachCurationAnswer","outreachCuratedEntry","OUTREACH_OPEN_ENROLLMENT_STATUSES","hasOutreachValidationErrors","isInThreadEmailStep","validateOutreachCuratedStep","readOutreachEnrollCandidates","readOutreachStepOverrideRequest","OUTREACH_MAX_EMAIL_STEPS","fileOutreachNote","outreachEnrollmentLink","outreachEnrollmentPerson","outreachOrgCollection","readStoredOutreachEnrollment","readStoredOutreachMailbox","readStoredOutreachSequence","OUTREACH_READS_PEOPLE","OUTREACH_SEQUENCE_ACTIVITY_TARGET","outreachRouteGate","outreachMethodNotAllowed","outreachOk","outreachRefusal","readOutreachDocumentId","readOutreachJsonBody","OUTREACH_CURATION_PURPOSE","CURATION_MAX_TOKENS","outreachCuratedActivity","stepIndex","cleared","emailStepIndexes","sequence","steps","map","step","index","kind","filter","readStepIndexes","raw","fallback","undefined","Array","isArray","length","wanted","Set","value","Number","emails","some","includes","sort","a","b","draftSubject","firestore","caller","body","enrollmentId","snapshot","orgId","doc","get","enrollment","exists","data","status","sequenceSnapshot","sequenceId","next","find","stepIndexes","Response","person","personalLine","contactId","leadId","id","text","trim","texts","Boolean","campaignNames","campaignIds","found","container","name","error","console","curationFacts","subject","candidate","contactGroupId","facet","contact","lead","company","target","mailboxSnapshot","host","campaigns","Promise","all","mailboxId","hostId","collection","mailbox","title","website","tags","sources","Object","entries","on","source","notes","record","siteName","senderName","displayName","curationSteps","templates","emailTemplates","Error","templateId","template","position","indexOf","count","startsThread","memberName","member","uid","email","createOutreachCurateRoutes","deps","draft","request","method","gate","generator","textGenerator","org","groupId","people","visible","facts","prompt","answer","generate","staff","purpose","system","maxTokens","now","Date","ok","reason","parsed","drafts","entry","candidateStep","issues","model","save","isInteger","enrollments","current","stored","override","read","refusal","draftedAtMs","draftedByUid","nowMs","written","runTransaction","transaction","sendClaim","update","delete","updatedAtMs","stepOverrides","String","keys","edited","atMs","link","dedupeKey","logOrgActivity","type"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/curate-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 { readContainerIds, readContactContainerIds } from '@aglyn/aglyn/app-utils/container-membership'\nimport { consentGroupForHost } from '@aglyn/aglyn/app-utils/consent-groups'\nimport { readContactFacet } from '@aglyn/aglyn/app-utils/contacts'\nimport type { PluginTextGenerator } from '@aglyn/aglyn/plugin-manager/plugin-text-generation'\nimport type { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport { readOrgContainers } from '@aglyn/tenant-data-admin/server/org-containers'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport {\n OUTREACH_CURATION_SYSTEM,\n outreachCurationPrompt,\n parseOutreachCurationAnswer,\n type OutreachCurationFacts,\n type OutreachCurationStep,\n} from '../engine/curation-prompt'\nimport { outreachCuratedEntry } from '../engine/enrollment-activity'\nimport { OUTREACH_OPEN_ENROLLMENT_STATUSES } from '../engine/gates'\nimport {\n hasOutreachValidationErrors,\n isInThreadEmailStep,\n validateOutreachCuratedStep,\n} from '../engine/sequence-validation'\nimport { readOutreachEnrollCandidates, type OutreachEnrollCandidate } from '../enrollment/enroll-people'\nimport { readOutreachTemplateBody } from '../enrollment/message-templates'\nimport { readOutreachStepOverrideRequest } from '../enrollment/step-overrides'\nimport type {\n OutreachCurateDraft,\n OutreachCurateDraftResponse,\n OutreachCurateSaveResponse,\n OutreachPersonRef,\n} from '../model/outreach-api'\nimport {\n OUTREACH_MAX_EMAIL_STEPS,\n type OutreachEnrollment,\n type OutreachSequence,\n type OutreachStepOverride,\n} from '../model/outreach.types'\nimport { fileOutreachNote } from '../runtime/timeline'\nimport {\n outreachEnrollmentLink,\n outreachEnrollmentPerson,\n outreachOrgCollection,\n readStoredOutreachEnrollment,\n readStoredOutreachMailbox,\n readStoredOutreachSequence,\n} from '../storage/outreach-records'\nimport { OUTREACH_READS_PEOPLE, type OutreachEnrollRouteDeps } from './enroll-routes'\nimport { OUTREACH_SEQUENCE_ACTIVITY_TARGET } from './route-deps'\nimport { outreachRouteGate, type OutreachRouteCaller } from './route-gate'\nimport {\n outreachMethodNotAllowed,\n outreachOk,\n outreachRefusal,\n readOutreachDocumentId,\n readOutreachJsonBody,\n} from './route-http'\n\n/**\n * CURATING ONE PERSON'S EMAILS (AGL-3324): `outreach/curate/draft` and\n * `outreach/curate/save`.\n *\n * A sequence's step is one template for everyone plus the personal line;\n * what an agent-run outbound tool does instead is write each send for each\n * prospect from what it knows about them, and let the seller approve or\n * edit. These two routes are that, with the member's confirmation kept as\n * the one thing that stores anything.\n *\n * DRAFT reads the person the way enrolling reads them — the contact, or the\n * lead as the contact it would be, with the company filed beside them — and\n * asks the workspace's text generator, on the core's seam, for this\n * person's copy of the email steps named: every email step for a person\n * about to be enrolled, the next one for an enrollment. The answer is\n * validated against the playbook's rules and returned with each draft's\n * issues, the prompt and the model. NOTHING IS STORED: the member reads it\n * in the dialog, edits it, ticks the confirmation, and then either enrolls\n * (the enroll route stores the copies with the enrollment) or saves.\n *\n * SAVE stores one confirmed copy on an open enrollment, for a step that\n * has not gone out, validated again — the request is not trusted to have\n * shown the member what it sends — and files \"Curated step N\" on the\n * person's record with who wrote the words. `override: null` clears a copy,\n * so the step goes back to the sequence's own words.\n *\n * Both read the CRM, so both ask for `data.manage` beside `outreach.use`;\n * the generator applies its own rules — the generation permission, the\n * plan, the switches, the allotments — to the member named, and meters the\n * draft on the member's month and the org's. Outreach never reaches the AI\n * plugin: the seam is the platform's, and a workspace where no plugin\n * generates text is told so in the route's words.\n */\n\n/** The purpose the generator's ledger files a draft under. */\nexport const OUTREACH_CURATION_PURPOSE = 'outreach-curate'\n\n/** The most tokens one drafting answer may run to: four short emails, with room. */\nconst CURATION_MAX_TOKENS = 2_400\n\n/** The org activity line a saved copy writes. */\nexport function outreachCuratedActivity(stepIndex: number, cleared: boolean): string {\n return cleared\n ? `Cleared the curated copy of step ${stepIndex + 1} of a sequence`\n : `Curated step ${stepIndex + 1} of a sequence for one person`\n}\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** The person's copies of steps a draft covers, and the enrollment they belong to when one exists. */\ninterface DraftSubject {\n sequence: OutreachSequence\n person: OutreachPersonRef\n personalLine: string\n enrollment: OutreachEnrollment | null\n /** The steps to draft, email steps only, unique, in order. */\n stepIndexes: number[]\n}\n\n/** Every email step's index, in order. */\nfunction emailStepIndexes(sequence: Pick<OutreachSequence, 'steps'>): number[] {\n return sequence.steps.map((step, index) => (step.kind === 'email' ? index : -1)).filter((index) => index >= 0)\n}\n\n/** The step indexes a request names, or the default for its subject; a refusal for anything else. */\nfunction readStepIndexes(\n raw: unknown,\n sequence: OutreachSequence,\n fallback: number[],\n): number[] | Response {\n if (raw === undefined) return fallback\n if (!Array.isArray(raw) || !raw.length) {\n return outreachRefusal(400, 'invalid-request', 'Name the email steps to draft.')\n }\n const wanted = [...new Set(raw.map((value) => Number(value)))]\n const emails = emailStepIndexes(sequence)\n if (wanted.some((index) => !emails.includes(index))) {\n return outreachRefusal(400, 'invalid-request', 'A curated step is one of the sequence’s email steps.')\n }\n if (wanted.length > OUTREACH_MAX_EMAIL_STEPS) {\n return outreachRefusal(400, 'invalid-request', `Draft at most ${OUTREACH_MAX_EMAIL_STEPS} emails at a time.`)\n }\n return wanted.sort((a, b) => a - b)\n}\n\n/** What the request is about: a person to enroll, or an enrollment's next step. */\nasync function draftSubject(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n body: Record<string, unknown>,\n): Promise<DraftSubject | Response> {\n const enrollmentId = body['enrollmentId'] === undefined ? null : readOutreachDocumentId(body['enrollmentId'])\n if (body['enrollmentId'] !== undefined && !enrollmentId) {\n return outreachRefusal(400, 'invalid-request', 'Name the enrollment to curate.')\n }\n if (enrollmentId) {\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 if (!OUTREACH_OPEN_ENROLLMENT_STATUSES.includes(enrollment.status)) {\n return outreachRefusal(409, 'transition-refused', 'This enrollment has ended, so nothing more is sent to them.')\n }\n const sequenceSnapshot = await outreachOrgCollection(firestore, caller.orgId, 'sequences')\n .doc(enrollment.sequenceId)\n .get()\n const sequence = readStoredOutreachSequence(\n enrollment.sequenceId,\n sequenceSnapshot.exists ? sequenceSnapshot.data() : undefined,\n )\n if (!sequence) return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n // The next email step that has not gone out: the one the enrollment is\n // on when it is an email, else the first email after it.\n const next = emailStepIndexes(sequence).find((index) => index >= enrollment.stepIndex)\n const stepIndexes = readStepIndexes(body['stepIndexes'], sequence, next === undefined ? [] : [next])\n if (stepIndexes instanceof Response) return stepIndexes\n if (stepIndexes.some((index) => index < enrollment.stepIndex)) {\n return outreachRefusal(409, 'transition-refused', 'That step already went out to this person.')\n }\n if (!stepIndexes.length) {\n return outreachRefusal(409, 'transition-refused', 'Every email in this sequence has gone out to this person.')\n }\n return {\n sequence,\n person: outreachEnrollmentPerson(enrollment),\n personalLine: enrollment.personalLine,\n enrollment,\n stepIndexes,\n }\n }\n const sequenceId = readOutreachDocumentId(body['sequenceId'])\n if (!sequenceId) return outreachRefusal(400, 'invalid-request', 'Name the sequence to draft for.')\n const contactId = body['contactId'] === undefined ? null : readOutreachDocumentId(body['contactId'])\n const leadId = body['leadId'] === undefined ? null : readOutreachDocumentId(body['leadId'])\n const person: OutreachPersonRef | null = contactId\n ? { kind: 'contact', id: contactId }\n : leadId\n ? { kind: 'lead', id: leadId }\n : null\n if (!person) return outreachRefusal(400, 'invalid-request', 'Name the contact or the lead to draft for.')\n const snapshot = await outreachOrgCollection(firestore, caller.orgId, 'sequences').doc(sequenceId).get()\n const sequence = readStoredOutreachSequence(sequenceId, snapshot.exists ? snapshot.data() : undefined)\n if (!sequence) return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n const stepIndexes = readStepIndexes(body['stepIndexes'], sequence, emailStepIndexes(sequence))\n if (stepIndexes instanceof Response) return stepIndexes\n if (!stepIndexes.length) {\n return outreachRefusal(400, 'invalid-request', 'This sequence sends no email, so there is nothing to draft.')\n }\n return {\n sequence,\n person,\n personalLine: typeof body['personalLine'] === 'string' ? body['personalLine'] : '',\n enrollment: null,\n stepIndexes,\n }\n}\n\nconst text = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\nconst texts = (value: unknown): string[] =>\n Array.isArray(value) ? value.map(text).filter(Boolean) : []\n\n/**\n * The names of the campaigns the person is filed under — the org's\n * containers of the `campaign` kind. A container that is gone names nothing;\n * a read that fails names none, and the draft goes without.\n */\nasync function campaignNames(firestore: Firestore, orgId: string, campaignIds: readonly string[]): Promise<string[]> {\n if (!campaignIds.length) return []\n try {\n const found = await readOrgContainers(firestore, 'campaign', orgId, campaignIds)\n return found.map((container) => container.name).filter(Boolean)\n } catch (error) {\n console.error('[outreach] the person’s campaigns could not be named for the draft', error)\n return []\n }\n}\n\n/** What the CRM holds about the person, for the prompt. */\nasync function curationFacts(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n subject: DraftSubject,\n candidate: OutreachEnrollCandidate,\n contactGroupId: string,\n): Promise<OutreachCurationFacts> {\n const contact = candidate.contact ?? {}\n const facet = readContactFacet(contact, contactGroupId) as unknown as Record<string, unknown>\n const lead = candidate.lead ?? {}\n const company = candidate.company ?? {}\n const campaignIds =\n candidate.target === 'lead' ? readContainerIds(lead, 'campaign') : readContactContainerIds(contact, contactGroupId, 'campaign')\n const [mailboxSnapshot, host, campaigns] = await Promise.all([\n subject.sequence.mailboxId\n ? outreachOrgCollection(firestore, caller.orgId, 'mailboxes').doc(subject.sequence.mailboxId).get()\n : null,\n subject.sequence.hostId ? firestore.collection('hosts').doc(subject.sequence.hostId).get() : null,\n campaignNames(firestore, caller.orgId, campaignIds),\n ])\n const mailbox = readStoredOutreachMailbox(\n subject.sequence.mailboxId,\n mailboxSnapshot?.exists ? mailboxSnapshot.data() : undefined,\n )\n return {\n name: candidate.name || text(contact['name']),\n company: text(facet['companyName']) || text(lead['company']) || text(company['name']),\n title: text(facet['jobTitle']) || text(lead['jobTitle']),\n website: text(company['website']) || text(company['domain']) || text(lead['website']) || text(lead['domain']),\n tags: [...new Set([...texts(facet['tags']), ...texts(company['tags'])])],\n sources: Object.entries((facet['sources'] ?? {}) as Record<string, unknown>)\n .filter(([, on]) => on === true)\n .map(([source]) => source)\n .sort(),\n campaigns,\n notes: candidate.target === 'lead' ? text(lead['notes']) : '',\n record: candidate.target,\n siteName: host?.exists ? text(host.get('name')) : '',\n senderName: mailbox?.displayName || '',\n personalLine: subject.personalLine,\n }\n}\n\n/** The email steps to draft, as the prompt states them: the step's words, or its template's. */\nasync function curationSteps(\n firestore: Firestore,\n orgId: string,\n sequence: OutreachSequence,\n stepIndexes: readonly number[],\n): Promise<OutreachCurationStep[]> {\n const emails = emailStepIndexes(sequence)\n return Promise.all(\n stepIndexes.map(async (stepIndex) => {\n const step = sequence.steps[stepIndex]\n if (step?.kind !== 'email') throw new Error(`step ${stepIndex} is not an email`)\n let body = step.body\n if (step.templateId) {\n body = (await readOutreachTemplateBody(orgId, step.templateId)) ?? ''\n }\n return {\n stepIndex,\n position: emails.indexOf(stepIndex) + 1,\n count: emails.length,\n subject: step.subject,\n body,\n startsThread: !isInThreadEmailStep(sequence.steps, stepIndex),\n }\n }),\n )\n}\n\n/** The member's name for the record's entry: as the roster lists them, else their address. */\nasync function memberName(firestore: Firestore, caller: OutreachRouteCaller): Promise<string | null> {\n try {\n const member = await firestore.collection('orgs').doc(caller.orgId).collection('members').doc(caller.uid).get()\n return (member.exists ? text(member.get('displayName')) || text(member.get('email')) : '') || caller.email\n } catch {\n return caller.email\n }\n}\n\nexport interface OutreachCurateRoutes {\n draft: PluginWebApiHandler\n save: PluginWebApiHandler\n}\n\nexport function createOutreachCurateRoutes(deps: OutreachEnrollRouteDeps): OutreachCurateRoutes {\n const draft: 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 generator = deps.textGenerator()\n if (!generator) {\n return outreachRefusal(503, 'curation-unavailable', 'AI drafting is not available in this workspace.')\n }\n const firestore = deps.firestore()\n const subject = await draftSubject(firestore, caller, body)\n if (subject instanceof Response) return subject\n const contactGroupId = consentGroupForHost(caller.org, subject.sequence.hostId).groupId\n const [candidate] = await readOutreachEnrollCandidates({\n orgId: caller.orgId,\n hostId: subject.sequence.hostId,\n contactGroupId,\n people: [subject.person],\n })\n if (!candidate?.contact || !candidate.visible) {\n return outreachRefusal(404, 'contact-not-found', \"That person isn't in this sequence's site's CRM.\")\n }\n const [facts, steps] = await Promise.all([\n curationFacts(firestore, caller, subject, candidate, contactGroupId),\n curationSteps(firestore, caller.orgId, subject.sequence, subject.stepIndexes),\n ])\n const prompt = outreachCurationPrompt(facts, steps)\n const answer = await generator.generate({\n orgId: caller.orgId,\n hostId: subject.sequence.hostId || null,\n uid: caller.uid,\n staff: caller.staff,\n org: caller.org,\n purpose: OUTREACH_CURATION_PURPOSE,\n system: OUTREACH_CURATION_SYSTEM,\n prompt,\n maxTokens: CURATION_MAX_TOKENS,\n now: new Date(deps.now()),\n })\n // Compared to `false` rather than negated: the union narrows on the\n // literal, and this tree compiles without strict null checks.\n if (answer.ok === false) {\n return outreachRefusal(\n answer.status,\n answer.reason === 'unavailable' ? 'curation-unavailable' : 'curation-refused',\n answer.error,\n )\n }\n const parsed = parseOutreachCurationAnswer(answer.text, steps)\n if (!parsed) {\n return outreachRefusal(\n 502,\n 'curation-refused',\n 'The draft came back in a shape that can’t be read. Try again.',\n )\n }\n const drafts: OutreachCurateDraft[] = parsed.map((entry) => {\n const step = steps.find((candidateStep) => candidateStep.stepIndex === entry.stepIndex)\n return {\n ...entry,\n issues: validateOutreachCuratedStep(\n { subject: entry.subject, body: entry.body, startsThread: step?.startsThread === true },\n `stepOverrides.${entry.stepIndex}`,\n ),\n }\n })\n return outreachOk({ ok: true, drafts, prompt, model: answer.model } satisfies OutreachCurateDraftResponse)\n }\n\n const save: 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)\n if (caller instanceof Response) return caller\n const enrollmentId = readOutreachDocumentId(body['enrollmentId'])\n const stepIndex = Number(body['stepIndex'])\n if (!enrollmentId || !Number.isInteger(stepIndex) || stepIndex < 0) {\n return outreachRefusal(400, 'invalid-request', 'Name the enrollment and the step to curate.')\n }\n const firestore = deps.firestore()\n const enrollments = outreachOrgCollection(firestore, caller.orgId, 'enrollments')\n const current = await enrollments.doc(enrollmentId).get()\n const stored = readStoredOutreachEnrollment(enrollmentId, current.exists ? current.data() : undefined)\n if (!stored) return outreachRefusal(404, 'enrollment-not-found', 'That enrollment no longer exists.')\n const sequenceSnapshot = await outreachOrgCollection(firestore, caller.orgId, 'sequences').doc(stored.sequenceId).get()\n const sequence = readStoredOutreachSequence(stored.sequenceId, sequenceSnapshot.exists ? sequenceSnapshot.data() : undefined)\n if (!sequence) return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n if (sequence.steps[stepIndex]?.kind !== 'email') {\n return outreachRefusal(400, 'invalid-request', 'A curated step is one of the sequence’s email steps.')\n }\n\n const cleared = body['override'] === null\n let override: OutreachStepOverride | null = null\n if (!cleared) {\n const read = readOutreachStepOverrideRequest({ ...(body['override'] as object), stepIndex }, sequence.steps)\n if ('refusal' in read) return outreachRefusal(400, 'invalid-override', read.refusal)\n if (hasOutreachValidationErrors(read.issues)) {\n return outreachRefusal(400, 'invalid-override', 'This draft breaks a rule every sequence email keeps.', {\n issues: read.issues,\n })\n }\n override = { ...read.override, draftedAtMs: deps.now(), draftedByUid: caller.uid }\n }\n\n const nowMs = deps.now()\n const written = await firestore.runTransaction(async (transaction) => {\n const snapshot = await transaction.get(enrollments.doc(enrollmentId))\n const enrollment = readStoredOutreachEnrollment(enrollmentId, snapshot.exists ? snapshot.data() : undefined)\n if (!enrollment) return { refusal: outreachRefusal(404, 'enrollment-not-found', 'That enrollment no longer exists.') }\n if (!OUTREACH_OPEN_ENROLLMENT_STATUSES.includes(enrollment.status)) {\n return {\n refusal: outreachRefusal(409, 'transition-refused', 'This enrollment has ended, so nothing more is sent to them.'),\n }\n }\n // A step that already went out, or is going out now, is not rewritten\n // under the run: what was sent is what was sent.\n if (stepIndex < enrollment.stepIndex || (enrollment.sendClaim && enrollment.sendClaim.stepIndex === stepIndex)) {\n return { refusal: outreachRefusal(409, 'transition-refused', 'That step already went out to this person.') }\n }\n transaction.update(enrollments.doc(enrollmentId), {\n [`stepOverrides.${stepIndex}`]: override ?? FieldValue.delete(),\n updatedAtMs: nowMs,\n })\n const stepOverrides = { ...(enrollment.stepOverrides ?? {}) }\n if (override) stepOverrides[String(stepIndex)] = override\n else delete stepOverrides[String(stepIndex)]\n // Absent rather than empty once the last copy is cleared, as the\n // stored reader answers it.\n const next: OutreachEnrollment = { ...enrollment, updatedAtMs: nowMs }\n delete next.stepOverrides\n if (Object.keys(stepOverrides).length) next.stepOverrides = stepOverrides\n return { enrollment: next }\n })\n if ('refusal' in written) return written.refusal\n const { enrollment } = written\n\n if (override) {\n // The person's record says so (AGL-3274, AGL-3324): who wrote this\n // person's copy of the step, under the send it shapes.\n const entry = outreachCuratedEntry({\n enrollmentId,\n stepIndex,\n source: override.source,\n edited: override.edited === true,\n memberName: await memberName(firestore, caller),\n atMs: nowMs,\n })\n await fileOutreachNote(deps, {\n orgId: caller.orgId,\n hostId: enrollment.hostId,\n link: outreachEnrollmentLink(enrollment),\n dedupeKey: entry.dedupeKey,\n body: entry.body,\n atMs: nowMs,\n })\n }\n await deps.logOrgActivity(\n caller.orgId,\n { uid: caller.uid, email: caller.email },\n outreachCuratedActivity(stepIndex, cleared),\n { type: OUTREACH_SEQUENCE_ACTIVITY_TARGET, id: sequence.id, name: sequence.name },\n )\n return outreachOk({ ok: true, enrollment } satisfies OutreachCurateSaveResponse)\n }\n\n return { draft, save }\n}\n\n/** The text generator on the core's seam, for the default route deps; `null` when no plugin generates. */\nexport type OutreachTextGeneratorReader = () => PluginTextGenerator | null\n"],"names":["readContainerIds","readContactContainerIds","consentGroupForHost","readContactFacet","readOrgContainers","FieldValue","OUTREACH_CURATION_SYSTEM","outreachCurationPrompt","parseOutreachCurationAnswer","outreachCuratedEntry","OUTREACH_OPEN_ENROLLMENT_STATUSES","hasOutreachValidationErrors","isInThreadEmailStep","validateOutreachCuratedStep","readOutreachEnrollCandidates","readOutreachTemplateBody","readOutreachStepOverrideRequest","OUTREACH_MAX_EMAIL_STEPS","fileOutreachNote","outreachEnrollmentLink","outreachEnrollmentPerson","outreachOrgCollection","readStoredOutreachEnrollment","readStoredOutreachMailbox","readStoredOutreachSequence","OUTREACH_READS_PEOPLE","OUTREACH_SEQUENCE_ACTIVITY_TARGET","outreachRouteGate","outreachMethodNotAllowed","outreachOk","outreachRefusal","readOutreachDocumentId","readOutreachJsonBody","OUTREACH_CURATION_PURPOSE","CURATION_MAX_TOKENS","outreachCuratedActivity","stepIndex","cleared","emailStepIndexes","sequence","steps","map","step","index","kind","filter","readStepIndexes","raw","fallback","undefined","Array","isArray","length","wanted","Set","value","Number","emails","some","includes","sort","a","b","draftSubject","firestore","caller","body","enrollmentId","snapshot","orgId","doc","get","enrollment","exists","data","status","sequenceSnapshot","sequenceId","next","find","stepIndexes","Response","person","personalLine","contactId","leadId","id","text","trim","texts","Boolean","campaignNames","campaignIds","found","container","name","error","console","curationFacts","subject","candidate","contactGroupId","facet","contact","lead","company","target","mailboxSnapshot","host","campaigns","Promise","all","mailboxId","hostId","collection","mailbox","title","website","tags","sources","Object","entries","on","source","notes","record","siteName","senderName","displayName","curationSteps","Error","templateId","position","indexOf","count","startsThread","memberName","member","uid","email","createOutreachCurateRoutes","deps","draft","request","method","gate","generator","textGenerator","org","groupId","people","visible","facts","prompt","answer","generate","staff","purpose","system","maxTokens","now","Date","ok","reason","parsed","drafts","entry","candidateStep","issues","model","save","isInteger","enrollments","current","stored","override","read","refusal","draftedAtMs","draftedByUid","nowMs","written","runTransaction","transaction","sendClaim","update","delete","updatedAtMs","stepOverrides","String","keys","edited","atMs","link","dedupeKey","logOrgActivity","type"],"mappings":"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"}
|
|
@@ -50,18 +50,6 @@ import { type OutreachRouteExtraPermission } from './route-gate';
|
|
|
50
50
|
export declare const OUTREACH_READS_PEOPLE: OutreachRouteExtraPermission;
|
|
51
51
|
/** Everything enrolling reaches beyond the routes' own dependencies. */
|
|
52
52
|
export interface OutreachEnrollRouteDeps extends OutreachRouteDeps {
|
|
53
|
-
/**
|
|
54
|
-
* The addresses a saved Contacts view selects among one site's contacts,
|
|
55
|
-
* read the way the dynamic-list sweep reads one, and whether the read
|
|
56
|
-
* reached the whole view before its budget.
|
|
57
|
-
*/
|
|
58
|
-
crmViewEmails(input: {
|
|
59
|
-
hostId: string;
|
|
60
|
-
viewId: string;
|
|
61
|
-
}): Promise<{
|
|
62
|
-
emails: string[];
|
|
63
|
-
complete: boolean;
|
|
64
|
-
}>;
|
|
65
53
|
/**
|
|
66
54
|
* The workspace's record system on the timeline seam (AGL-3274), when a
|
|
67
55
|
* plugin keeps one: the enroll files "Enrolled in" on the person's record
|
|
@@ -14,15 +14,11 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
14
14
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
|
-
*/ import {
|
|
17
|
+
*/ import { stampRecordOrigin } from "@aglyn/aglyn/plugin-manager/plugin-record-origin";
|
|
18
|
+
import { normalizeContainerIds } from "@aglyn/aglyn/app-utils/container-membership";
|
|
18
19
|
import { consentGroupForHost } from "@aglyn/aglyn/app-utils/consent-groups";
|
|
19
|
-
import {
|
|
20
|
-
import { CRM_COLLECTIONS, crmLeadStatus, crmViewIsListed, isCrmLeadOpen, normalizeCrmViewFilters } from "@aglyn/aglyn/app-utils/crm";
|
|
21
|
-
import { dynamicListDimensionsForCrmView } from "@aglyn/aglyn/app-utils/dynamic-list-rule";
|
|
22
|
-
import { findContactByEmail } from "@aglyn/tenant-data-admin/server/contact-email-index";
|
|
20
|
+
import { filePluginPersonUnder, pluginPeopleInView } from "@aglyn/aglyn/plugin-manager/plugin-person-records";
|
|
23
21
|
import { readOrgContainers } from "@aglyn/tenant-data-admin/server/org-containers";
|
|
24
|
-
import { restampCrmListFieldsAt } from "@aglyn/tenant-data-admin/server/crm-records";
|
|
25
|
-
import { FieldValue } from "firebase-admin/firestore";
|
|
26
22
|
import { outreachCuratedEntry, outreachEnrolledEntry } from "../engine/enrollment-activity.js";
|
|
27
23
|
import { buildOutreachEnrollment, planOutreachFirstDue, readOutreachStartStepIndex } from "../engine/enrollment-state.js";
|
|
28
24
|
import { outreachEnrollmentSearchTokens } from "../enrollment/enrollment-search.js";
|
|
@@ -140,7 +136,7 @@ async function enrollContext(firestore, caller, sequence) {
|
|
|
140
136
|
}
|
|
141
137
|
}
|
|
142
138
|
async function readPeople(firestore, caller, sequence, context, people, gateway) {
|
|
143
|
-
const candidates = await readOutreachEnrollCandidates(
|
|
139
|
+
const candidates = await readOutreachEnrollCandidates({
|
|
144
140
|
orgId: caller.orgId,
|
|
145
141
|
hostId: sequence.hostId,
|
|
146
142
|
contactGroupId: context.contactGroupId,
|
|
@@ -181,43 +177,6 @@ async function readPeople(firestore, caller, sequence, context, people, gateway)
|
|
|
181
177
|
const uniqueIds = (values)=>Array.isArray(values) ? [
|
|
182
178
|
...new Set(values.map(readOutreachDocumentId).filter((id)=>id !== null))
|
|
183
179
|
] : [];
|
|
184
|
-
/** The most leads a saved Leads view reads — the Leads list's own window. */ const LEADS_VIEW_WINDOW = 200;
|
|
185
|
-
/**
|
|
186
|
-
* The people a saved LEADS view selects (AGL-3234): the sequence's site's
|
|
187
|
-
* most recently seen leads, narrowed by the view's status clause the way
|
|
188
|
-
* the Leads list narrows its window — open leads when the view names no
|
|
189
|
-
* status, one status when it does, everything for `all`. The person key IS
|
|
190
|
-
* the id, so there is no address to resolve — and since AGL-3275 the window is
|
|
191
|
-
* narrowed by `visibleTo` rather than by the collection's parent.
|
|
192
|
-
*/ async function leadsViewPeople(firestore, orgId, sequence, filters) {
|
|
193
|
-
var _ref;
|
|
194
|
-
const statusClause = filters.find((clause)=>clause.field === 'status' && clause.op === 'equals');
|
|
195
|
-
const wanted = String((_ref = statusClause == null ? void 0 : statusClause.value) != null ? _ref : 'open');
|
|
196
|
-
/*
|
|
197
|
-
* SCOPED (AGL-3275). The collection is org-wide, so the window is narrowed
|
|
198
|
-
* to what the sequence's site may see before it is ordered — otherwise an
|
|
199
|
-
* agency's sequence would offer another client's people to enroll. The
|
|
200
|
-
* composite index for `visibleTo array-contains-any` + `lastSeenAtMs desc`
|
|
201
|
-
* is in `cloud/firestore.indexes.json`.
|
|
202
|
-
*/ const window = await firestore.collection('orgs').doc(orgId).collection('leads').where('visibleTo', 'array-contains-any', scopeTokensForHost(sequence.hostId)).orderBy('lastSeenAtMs', 'desc').limit(LEADS_VIEW_WINDOW + 1).get();
|
|
203
|
-
const matching = window.docs.slice(0, LEADS_VIEW_WINDOW).filter((doc)=>{
|
|
204
|
-
const lead = doc.data();
|
|
205
|
-
// Held, not merely seen: a lead shared with this site (AGL-3336) is not
|
|
206
|
-
// its to sequence.
|
|
207
|
-
if (seenOnlyThroughGrant(lead, sequence.hostId)) return false;
|
|
208
|
-
if (wanted === 'all') return true;
|
|
209
|
-
if (wanted === 'open') return isCrmLeadOpen(lead) && !lead['convertedContactId'];
|
|
210
|
-
return crmLeadStatus(lead) === wanted;
|
|
211
|
-
});
|
|
212
|
-
return {
|
|
213
|
-
people: matching.slice(0, OUTREACH_ENROLL_BATCH_MAX).map((doc)=>({
|
|
214
|
-
kind: 'lead',
|
|
215
|
-
id: doc.id
|
|
216
|
-
})),
|
|
217
|
-
total: matching.length,
|
|
218
|
-
truncated: matching.length > OUTREACH_ENROLL_BATCH_MAX || window.docs.length > LEADS_VIEW_WINDOW
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
180
|
export function createOutreachEnrollRoutes(deps) {
|
|
222
181
|
/**
|
|
223
182
|
* The person joins the sequence's campaigns (AGL-3254), the moment they
|
|
@@ -228,20 +187,25 @@ export function createOutreachEnrollRoutes(deps) {
|
|
|
228
187
|
* a person the create refused joins nothing. Neither write may fail the
|
|
229
188
|
* enrollment, which already exists; a membership that could not be
|
|
230
189
|
* stamped is one the list's bulk bar can add by hand.
|
|
231
|
-
*/ async function joinSequenceCampaigns(
|
|
190
|
+
*/ async function joinSequenceCampaigns(orgId, sequence, candidate, nowMs) {
|
|
232
191
|
const campaignIds = normalizeContainerIds(sequence.campaignIds);
|
|
233
192
|
if (!campaignIds.length) return;
|
|
234
193
|
try {
|
|
235
|
-
const
|
|
236
|
-
if (
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
194
|
+
const id = candidate.target === 'lead' ? candidate.leadId : candidate.contactId;
|
|
195
|
+
if (id) {
|
|
196
|
+
// Filed by the plugin that keeps people (AGL-3080), as the sequence's
|
|
197
|
+
// site holds the person: on a lead its own field, on a contact the
|
|
198
|
+
// site's facet — the field every other campaign member carries.
|
|
199
|
+
await filePluginPersonUnder({
|
|
200
|
+
hostId: sequence.hostId,
|
|
201
|
+
orgId,
|
|
202
|
+
record: {
|
|
203
|
+
kind: candidate.target,
|
|
204
|
+
id
|
|
205
|
+
},
|
|
206
|
+
containerKind: 'campaign',
|
|
207
|
+
ids: campaignIds
|
|
241
208
|
});
|
|
242
|
-
// A lead's campaigns are what the Leads list's Campaign filter reads
|
|
243
|
-
// under a site (AGL-3321): restamped from the lead as it now stands.
|
|
244
|
-
if (candidate.target === 'lead') await restampCrmListFieldsAt(ref, 'leads');
|
|
245
209
|
}
|
|
246
210
|
} catch (error) {
|
|
247
211
|
console.error('[outreach] the enrolled person could not join the sequence’s campaigns', error);
|
|
@@ -279,8 +243,7 @@ export function createOutreachEnrollRoutes(deps) {
|
|
|
279
243
|
* search picked, every lead picked from the sequence's site, or the
|
|
280
244
|
* people a saved Contacts or Leads view selects there — contacts found by
|
|
281
245
|
* address, leads by their own key.
|
|
282
|
-
*/ async function sourcePeople(
|
|
283
|
-
var _data_ownerUid;
|
|
246
|
+
*/ async function sourcePeople(caller, sequence, source) {
|
|
284
247
|
if (source['kind'] === 'contacts' || source['kind'] === 'leads') {
|
|
285
248
|
const kind = source['kind'] === 'leads' ? 'lead' : 'contact';
|
|
286
249
|
const ids = uniqueIds(kind === 'lead' ? source['leadIds'] : source['contactIds']);
|
|
@@ -298,48 +261,36 @@ export function createOutreachEnrollRoutes(deps) {
|
|
|
298
261
|
return outreachRefusal(400, 'invalid-request', 'Enroll people from a saved view or a search.');
|
|
299
262
|
}
|
|
300
263
|
const viewId = readOutreachDocumentId(source['viewId']);
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
264
|
+
// The record system takes the view for the sequence's site (AGL-3080):
|
|
265
|
+
// a colleague's private view is theirs, so it does not exist for this
|
|
266
|
+
// member; a Leads view takes the site's leads by the view's status, a
|
|
267
|
+
// Contacts view the contacts it selects there.
|
|
268
|
+
const taken = viewId ? await pluginPeopleInView({
|
|
269
|
+
orgId: caller.orgId,
|
|
270
|
+
hostId: sequence.hostId,
|
|
271
|
+
viewId,
|
|
272
|
+
viewerUid: caller.uid,
|
|
273
|
+
limit: OUTREACH_ENROLL_BATCH_MAX
|
|
274
|
+
}) : null;
|
|
275
|
+
if (!taken || taken.ok === false && taken.reason === 'not-found') {
|
|
309
276
|
return outreachRefusal(404, 'view-not-found', 'That saved view no longer exists.');
|
|
310
277
|
}
|
|
311
|
-
if (
|
|
312
|
-
return leadsViewPeople(firestore, caller.orgId, sequence, normalizeCrmViewFilters(data['filters']));
|
|
313
|
-
}
|
|
314
|
-
if (data['section'] !== 'contacts') {
|
|
278
|
+
if (taken.ok === false && taken.reason === 'not-people') {
|
|
315
279
|
return outreachRefusal(400, 'view-unsupported', 'Enroll from a saved view of Contacts or Leads.');
|
|
316
280
|
}
|
|
317
|
-
|
|
318
|
-
|
|
281
|
+
if (taken.ok === false) {
|
|
282
|
+
var _taken_unsupported;
|
|
319
283
|
// Dropping the filter enrolling cannot read would enroll more people
|
|
320
284
|
// than the view shows, so the view is refused whole.
|
|
321
|
-
return outreachRefusal(400, 'view-unsupported', `This view filters on ${
|
|
285
|
+
return outreachRefusal(400, 'view-unsupported', `This view filters on ${((_taken_unsupported = taken.unsupported) != null ? _taken_unsupported : []).join(', ') || 'something'}, which enrolling can't apply. Pick another view, or search.`);
|
|
322
286
|
}
|
|
323
|
-
const { emails, complete } = await deps.crmViewEmails({
|
|
324
|
-
hostId: sequence.hostId,
|
|
325
|
-
viewId
|
|
326
|
-
});
|
|
327
|
-
const ordered = [
|
|
328
|
-
...new Set(emails)
|
|
329
|
-
].sort();
|
|
330
|
-
const contacts = firestore.collection('orgs').doc(caller.orgId).collection('contacts');
|
|
331
|
-
const found = await Promise.all(ordered.slice(0, OUTREACH_ENROLL_BATCH_MAX).map((email)=>findContactByEmail(contacts, email, {
|
|
332
|
-
hostId: sequence.hostId
|
|
333
|
-
})));
|
|
334
287
|
return {
|
|
335
|
-
people:
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
kind: 'contact',
|
|
339
|
-
id
|
|
288
|
+
people: taken.people.map((person)=>({
|
|
289
|
+
kind: person.kind === 'lead' ? 'lead' : 'contact',
|
|
290
|
+
id: person.id
|
|
340
291
|
})),
|
|
341
|
-
total:
|
|
342
|
-
truncated:
|
|
292
|
+
total: taken.total,
|
|
293
|
+
truncated: taken.truncated
|
|
343
294
|
};
|
|
344
295
|
}
|
|
345
296
|
const preview = async (request)=>{
|
|
@@ -353,7 +304,7 @@ export function createOutreachEnrollRoutes(deps) {
|
|
|
353
304
|
const sequence = await loadActiveSequence(firestore, caller.orgId, body['sequenceId']);
|
|
354
305
|
if (sequence instanceof Response) return sequence;
|
|
355
306
|
const source = body['source'] && typeof body['source'] === 'object' ? body['source'] : {};
|
|
356
|
-
const named = await sourcePeople(
|
|
307
|
+
const named = await sourcePeople(caller, sequence, source);
|
|
357
308
|
if (named instanceof Response) return named;
|
|
358
309
|
const context = await enrollContext(firestore, caller, sequence);
|
|
359
310
|
const { candidates, enrolledHere, lookups } = await readPeople(firestore, caller, sequence, context, named.people, {
|
|
@@ -561,7 +512,16 @@ export function createOutreachEnrollRoutes(deps) {
|
|
|
561
512
|
]
|
|
562
513
|
});
|
|
563
514
|
}
|
|
564
|
-
await joinSequenceCampaigns(
|
|
515
|
+
await joinSequenceCampaigns(caller.orgId, sequence, candidate, nowMs);
|
|
516
|
+
// A person reached first by this sequence came from it (AGL-3519):
|
|
517
|
+
// the record system stamps its Sequence lead source on a record
|
|
518
|
+
// naming none, and leaves any other alone.
|
|
519
|
+
await stampRecordOrigin({
|
|
520
|
+
orgId: caller.orgId,
|
|
521
|
+
hostId: sequence.hostId,
|
|
522
|
+
email: decision.email,
|
|
523
|
+
origin: 'sequence'
|
|
524
|
+
});
|
|
565
525
|
// The person's record says so (AGL-3274): "Enrolled in <sequence>",
|
|
566
526
|
// with the campaigns it carried them into, once per enrollment.
|
|
567
527
|
const entry = outreachEnrolledEntry({
|