@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.
Files changed (116) hide show
  1. package/package.json +14 -14
  2. package/src/lib/components/do-not-contact-domains.d.ts +6 -2
  3. package/src/lib/components/do-not-contact-domains.js +73 -11
  4. package/src/lib/components/do-not-contact-domains.js.map +1 -1
  5. package/src/lib/components/enrollments-table.d.ts +6 -0
  6. package/src/lib/components/enrollments-table.js +77 -4
  7. package/src/lib/components/enrollments-table.js.map +1 -1
  8. package/src/lib/components/sequence-detail.js +3 -1
  9. package/src/lib/components/sequence-detail.js.map +1 -1
  10. package/src/lib/components/sequence-report-card.js +7 -3
  11. package/src/lib/components/sequence-report-card.js.map +1 -1
  12. package/src/lib/components/use-outreach-crm.d.ts +18 -15
  13. package/src/lib/components/use-outreach-crm.js +118 -87
  14. package/src/lib/components/use-outreach-crm.js.map +1 -1
  15. package/src/lib/components/use-outreach-data.d.ts +2 -2
  16. package/src/lib/components/use-outreach-data.js.map +1 -1
  17. package/src/lib/constants/transfer-resources.d.ts +33 -0
  18. package/src/lib/constants/transfer-resources.js +38 -0
  19. package/src/lib/constants/transfer-resources.js.map +1 -0
  20. package/src/lib/declarations.console-server.d.ts +7 -1
  21. package/src/lib/declarations.console-server.js +66 -1
  22. package/src/lib/declarations.console-server.js.map +1 -1
  23. package/src/lib/engine/click-tracking.js +1 -1
  24. package/src/lib/engine/click-tracking.js.map +1 -1
  25. package/src/lib/engine/ip-range.d.ts +26 -0
  26. package/src/lib/engine/ip-range.js +78 -0
  27. package/src/lib/engine/ip-range.js.map +1 -0
  28. package/src/lib/engine/open-source.d.ts +24 -0
  29. package/src/lib/engine/open-source.js +130 -0
  30. package/src/lib/engine/open-source.js.map +1 -0
  31. package/src/lib/engine/open-tracking.d.ts +15 -2
  32. package/src/lib/engine/open-tracking.js +59 -16
  33. package/src/lib/engine/open-tracking.js.map +1 -1
  34. package/src/lib/engine/outreach-access.d.ts +47 -0
  35. package/src/lib/engine/outreach-access.js +56 -0
  36. package/src/lib/engine/outreach-access.js.map +1 -0
  37. package/src/lib/enrollment/enroll-people.d.ts +10 -10
  38. package/src/lib/enrollment/enroll-people.js +30 -25
  39. package/src/lib/enrollment/enroll-people.js.map +1 -1
  40. package/src/lib/enrollment/gate-lookups.js +30 -15
  41. package/src/lib/enrollment/gate-lookups.js.map +1 -1
  42. package/src/lib/enrollment/message-templates.d.ts +24 -0
  43. package/src/lib/enrollment/message-templates.js +35 -0
  44. package/src/lib/enrollment/message-templates.js.map +1 -0
  45. package/src/lib/model/enrollment-engagement.d.ts +16 -0
  46. package/src/lib/model/enrollment-engagement.js +16 -0
  47. package/src/lib/model/enrollment-engagement.js.map +1 -1
  48. package/src/lib/model/enrollment-history.d.ts +8 -1
  49. package/src/lib/model/enrollment-history.js +17 -5
  50. package/src/lib/model/enrollment-history.js.map +1 -1
  51. package/src/lib/model/enrollment-timeline.d.ts +3 -0
  52. package/src/lib/model/enrollment-timeline.js +15 -4
  53. package/src/lib/model/enrollment-timeline.js.map +1 -1
  54. package/src/lib/model/outreach.types.d.ts +12 -3
  55. package/src/lib/model/outreach.types.js +12 -1
  56. package/src/lib/model/outreach.types.js.map +1 -1
  57. package/src/lib/model/sequence-report.d.ts +14 -7
  58. package/src/lib/model/sequence-report.js +12 -8
  59. package/src/lib/model/sequence-report.js.map +1 -1
  60. package/src/lib/plugin.js +18 -0
  61. package/src/lib/plugin.js.map +1 -1
  62. package/src/lib/routes/curate-routes.js +4 -6
  63. package/src/lib/routes/curate-routes.js.map +1 -1
  64. package/src/lib/routes/enroll-routes.d.ts +0 -12
  65. package/src/lib/routes/enroll-routes.js +52 -92
  66. package/src/lib/routes/enroll-routes.js.map +1 -1
  67. package/src/lib/routes/platform-deps.d.ts +4 -16
  68. package/src/lib/routes/platform-deps.js +5 -25
  69. package/src/lib/routes/platform-deps.js.map +1 -1
  70. package/src/lib/routes/preview-routes.js +25 -13
  71. package/src/lib/routes/preview-routes.js.map +1 -1
  72. package/src/lib/routes/register-routes.js +7 -5
  73. package/src/lib/routes/register-routes.js.map +1 -1
  74. package/src/lib/routes/route-gate.js +6 -17
  75. package/src/lib/routes/route-gate.js.map +1 -1
  76. package/src/lib/routes/sequence-routes.d.ts +46 -0
  77. package/src/lib/routes/sequence-routes.js +155 -63
  78. package/src/lib/routes/sequence-routes.js.map +1 -1
  79. package/src/lib/runtime/click-events.js +8 -8
  80. package/src/lib/runtime/click-events.js.map +1 -1
  81. package/src/lib/runtime/click-route.js +5 -2
  82. package/src/lib/runtime/click-route.js.map +1 -1
  83. package/src/lib/runtime/lead-records.d.ts +18 -10
  84. package/src/lib/runtime/lead-records.js +14 -60
  85. package/src/lib/runtime/lead-records.js.map +1 -1
  86. package/src/lib/runtime/open-events.d.ts +8 -0
  87. package/src/lib/runtime/open-events.js +10 -3
  88. package/src/lib/runtime/open-events.js.map +1 -1
  89. package/src/lib/runtime/platform-runtime-deps.d.ts +9 -5
  90. package/src/lib/runtime/platform-runtime-deps.js +31 -24
  91. package/src/lib/runtime/platform-runtime-deps.js.map +1 -1
  92. package/src/lib/runtime/send-job.js +20 -17
  93. package/src/lib/runtime/send-job.js.map +1 -1
  94. package/src/lib/runtime/sync-job.js +10 -5
  95. package/src/lib/runtime/sync-job.js.map +1 -1
  96. package/src/lib/storage/do-not-contact-store.d.ts +8 -0
  97. package/src/lib/storage/do-not-contact-store.js +13 -0
  98. package/src/lib/storage/do-not-contact-store.js.map +1 -1
  99. package/src/lib/testing/stand-in-record-system.d.ts +54 -0
  100. package/src/lib/testing/stand-in-record-system.js +243 -0
  101. package/src/lib/testing/stand-in-record-system.js.map +1 -0
  102. package/src/lib/transfer/do-not-contact-transfer.d.ts +129 -0
  103. package/src/lib/transfer/do-not-contact-transfer.js +801 -0
  104. package/src/lib/transfer/do-not-contact-transfer.js.map +1 -0
  105. package/src/lib/transfer/plan-gate.d.ts +25 -0
  106. package/src/lib/transfer/plan-gate.js +35 -0
  107. package/src/lib/transfer/plan-gate.js.map +1 -0
  108. package/src/lib/transfer/platform-transfer-deps.d.ts +24 -0
  109. package/src/lib/transfer/platform-transfer-deps.js +39 -0
  110. package/src/lib/transfer/platform-transfer-deps.js.map +1 -0
  111. package/src/lib/transfer/sequences-package.d.ts +48 -0
  112. package/src/lib/transfer/sequences-package.js +93 -0
  113. package/src/lib/transfer/sequences-package.js.map +1 -0
  114. package/src/lib/transfer/sequences-package.server.d.ts +31 -0
  115. package/src/lib/transfer/sequences-package.server.js +207 -0
  116. 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
- const template = await templates.doc(step.templateId).get();
216
- body = template.exists ? normalizeCrmEmailTemplate(template.data()).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(firestore, {
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 { containerMembershipField, contactContainerFieldPath, normalizeContainerIds } from "@aglyn/aglyn/app-utils/container-membership";
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 { scopeTokensForHost, seenOnlyThroughGrant } from "@aglyn/aglyn/app-utils/scope-tokens";
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(firestore, {
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(firestore, orgId, sequence, contactGroupId, candidate, nowMs) {
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 ref = candidate.target === 'lead' && candidate.leadId ? firestore.collection('orgs').doc(orgId).collection('leads').doc(candidate.leadId) : candidate.contactId ? firestore.collection('orgs').doc(orgId).collection('contacts').doc(candidate.contactId) : null;
236
- if (ref) {
237
- const field = candidate.target === 'lead' ? containerMembershipField('campaign') : contactContainerFieldPath(contactGroupId, 'campaign');
238
- await ref.update({
239
- [field]: FieldValue.arrayUnion(...campaignIds),
240
- updatedAt: FieldValue.serverTimestamp()
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(firestore, caller, sequence, source) {
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
- const view = viewId ? await firestore.collection('orgs').doc(caller.orgId).collection(CRM_COLLECTIONS.views).doc(viewId).get() : null;
302
- const data = (view == null ? void 0 : view.exists) ? view.data() : null;
303
- // A colleague's private view is theirs: it is not listed for this
304
- // reader, so it does not exist for them here either.
305
- if (!viewId || !data || !crmViewIsListed({
306
- shared: data['shared'] === true,
307
- ownerUid: String((_data_ownerUid = data['ownerUid']) != null ? _data_ownerUid : '')
308
- }, caller.uid)) {
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 (data['section'] === 'leads') {
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
- const { unsupported } = dynamicListDimensionsForCrmView(normalizeCrmViewFilters(data['filters']));
318
- if (unsupported.length) {
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 ${unsupported.map((clause)=>clause.label || clause.field).join(', ')}, which enrolling can't apply. Pick another view, or search.`);
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
- ...new Set(found.filter((snapshot)=>snapshot).map((snapshot)=>String(snapshot == null ? void 0 : snapshot.id)))
337
- ].map((id)=>({
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: ordered.length,
342
- truncated: ordered.length > OUTREACH_ENROLL_BATCH_MAX || !complete
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(firestore, caller, sequence, source);
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(firestore, caller.orgId, sequence, context.contactGroupId, candidate, nowMs);
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({