@aglyn/plugins-outreach 1.0.0-beta.165 → 1.0.0-beta.166

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/plugins-outreach",
3
- "version": "1.0.0-beta.165",
3
+ "version": "1.0.0-beta.166",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,18 +25,18 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.165",
29
- "@aglyn/shared-data-mdi": "1.0.0-beta.165",
30
- "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.165",
31
- "@aglyn/shared-ui-jsx": "1.0.0-beta.165",
32
- "@aglyn/shared-ui-next": "1.0.0-beta.165",
33
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.165",
34
- "@aglyn/shared-util-http": "1.0.0-beta.165",
35
- "@aglyn/shared-util-timestamp": "1.0.0-beta.165",
36
- "@aglyn/shared-util-tools": "1.0.0-beta.165",
37
- "@aglyn/tenant-data-admin": "1.0.0-beta.165",
38
- "@aglyn/tenant-feature-instance": "1.0.0-beta.165",
39
- "@aglyn/tenant-runtime": "1.0.0-beta.165",
28
+ "@aglyn/aglyn": "1.0.0-beta.166",
29
+ "@aglyn/shared-data-mdi": "1.0.0-beta.166",
30
+ "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.166",
31
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.166",
32
+ "@aglyn/shared-ui-next": "1.0.0-beta.166",
33
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.166",
34
+ "@aglyn/shared-util-http": "1.0.0-beta.166",
35
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.166",
36
+ "@aglyn/shared-util-tools": "1.0.0-beta.166",
37
+ "@aglyn/tenant-data-admin": "1.0.0-beta.166",
38
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.166",
39
+ "@aglyn/tenant-runtime": "1.0.0-beta.166",
40
40
  "@swc/helpers": "0.5.23"
41
41
  },
42
42
  "peerDependencies": {
@@ -107,7 +107,8 @@ async function getAllChunked(firestore, refs) {
107
107
  */ export async function readOutreachEnrollCandidates(firestore, input) {
108
108
  if (!input.people.length) return [];
109
109
  const org = firestore.collection('orgs').doc(input.orgId);
110
- const leads = firestore.collection('hosts').doc(input.hostId).collection('leads');
110
+ // Beside the contacts, on the org (AGL-3275).
111
+ const leads = org.collection('leads');
111
112
  const snapshots = await getAllChunked(firestore, input.people.map((person)=>person.kind === 'lead' ? leads.doc(person.id) : org.collection('contacts').doc(person.id)));
112
113
  const read = await Promise.all(input.people.map(async (person, index)=>{
113
114
  var _readContactFacet_companyId;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/enrollment/enroll-people.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*==========================================\n * WHO CAN BE ENROLLED, AND WHAT THE REP STILL OWES (AGL-2980).\n *\n * The enroll dialog shows every person a view or a search named, each\n * marked eligible, blocked, or waiting on the rep; Confirm asks the same\n * question again, on the server, with what the rep supplied. Both answers\n * come from the engine's `evaluateOutreachGates` — this module only reads\n * the people and asks it the right questions.\n *\n * ## Three questions of one pure function\n *\n * The gates refuse a cold contact until the rep writes a personal line and\n * ticks the attestations, and refuse an address in an unknown country until\n * the rep confirms it is a US business address. So a preview asks:\n *\n * 1. as the person stands, with nothing confirmed — allowed means\n * ELIGIBLE;\n * 2. as if the rep confirmed everything they can — refused means BLOCKED,\n * for the reasons that remain, which no tick can clear;\n * 3. otherwise the person is WAITING ON THE REP, for what (1) found\n * missing.\n *\n * The confirmation in (2) is hypothetical and never stored: Confirm stamps\n * only what the rep ticked, with their uid and the time, and asks again.\n *\n * ## One enrollment per person per sequence, ever\n *\n * The enrollment's id is the sequence and the contact\n * (`outreachEnrollmentId`), and it is written with `create()`. A person who\n * finished, replied or was stopped is not sent the same emails a second\n * time by being enrolled again, and two members confirming the same person\n * at once write one enrollment.\n *==========================================*/\n\nimport { normalizeContactEmail, contactDisplayName, readContactFacet } from '@aglyn/aglyn/app-utils/contacts'\nimport { crmLeadDisplayName, crmLeadStatus } from '@aglyn/aglyn/app-utils/crm'\nimport { visibleToHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport { findContactByEmail } from '@aglyn/tenant-data-admin/server/contact-email-index'\nimport {\n evaluateOutreachGates,\n type OutreachGateBlock,\n type OutreachGateLookups,\n} from '../engine/gates'\nimport { outreachAttestationsFrom } from '../engine/enrollment-state'\nimport {\n OUTREACH_ATTESTATION_KINDS,\n type OutreachAttestationKind,\n type OutreachAttestations,\n type OutreachSequenceSettings,\n} from '../model/outreach.types'\nimport type {\n OutreachEnrollBlock,\n OutreachEnrollPreviewPerson,\n OutreachPersonRef,\n} from '../model/outreach-api'\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/**\n * One person as the preview and the enroll read them: a contact, or a lead\n * the sequence's site holds (AGL-3234).\n *\n * ## A lead is judged as the contact it would be\n *\n * The gates, the merge fields and the country rule all read a CONTACT\n * document — its facet's sources, stage, address, company and title. A lead\n * carries the same facts in its own shape, so rather than teach every gate\n * two records, a lead is handed to them as {@link leadAsContact}: a\n * contact-shaped view whose one facet, under the sequence's group, holds\n * what the lead holds. An imported lead has no inbound source and is cold;\n * one that wrote in through a form is not; a lead is never a customer; its\n * address decides its country; and `{{contact.firstName}}` or\n * `{{contact.company}}` in a step reads the lead's name and company text,\n * so a sequence written for contacts sends to leads unchanged — which is\n * also what lets an enrollment made on a lead keep sending after the lead\n * converts and the view becomes the real contact.\n */\nexport interface OutreachEnrollCandidate {\n /** The record's own id: the contact's, or the lead's person key. */\n personId: string\n target: 'contact' | 'lead'\n /** The contact's id; `''` for a lead. */\n contactId: string\n /** The lead's person key; `null` for a contact. */\n leadId: string | null\n /**\n * The contact document (`orgs/{orgId}/contacts/{id}`), or for a lead the\n * contact-shaped view of it — see the module note. `null` when the record\n * no longer exists.\n */\n contact: Record<string, unknown> | null\n /** The lead document as stored, for a lead; `null` for a contact. */\n lead: Record<string, unknown> | null\n /** Whether the sequence's site may see the record. A lead is the site's own. */\n visible: boolean\n /** The name the sequence's site knows them by; `''` for none. */\n name: string\n /** The primary address, normalized, or `null` when there is none. */\n email: string | null\n /** The company the contact is filed under at that site, when there is one. */\n company: Record<string, unknown> | null\n /** A lead already converted: the contact it became. Enroll that instead. */\n convertedContactId: string | null\n /** A lead whose address the workspace already holds as a contact. Enroll that instead. */\n heldAsContactId: string | null\n}\n\n/**\n * A lead as the gates and the merge fields read a contact — see the\n * module note. The facet is the sequence's group's, which is the one every\n * reader asks for. The lead's `sources` — `form:{id}`, `booking`, `import`,\n * `manual`, `api`, `signup` — become the facet's source flags by kind, so\n * the inbound rule reads them as it reads a contact's.\n */\nexport function leadAsContact(\n lead: Record<string, unknown>,\n contactGroupId: string,\n): Record<string, unknown> {\n const sources: Record<string, true> = {}\n for (const raw of Array.isArray(lead['sources']) ? lead['sources'] : []) {\n const source = String(raw)\n const kind = source.startsWith('form:') ? 'form' : source === 'signup' ? 'member' : source\n if (kind) sources[kind] = true\n }\n return {\n email: lead['email'],\n name: lead['name'],\n // The verdict on the address (AGL-3245) is the lead's own top-level\n // field, as it is the contact's, so the gates quote it either way.\n ...(lead['emailState'] && typeof lead['emailState'] === 'object' ? { emailState: lead['emailState'] } : {}),\n facets: {\n [contactGroupId]: {\n sources,\n ...(lead['address'] && typeof lead['address'] === 'object' ? { address: lead['address'] } : {}),\n ...(typeof lead['company'] === 'string' ? { companyName: lead['company'] } : {}),\n ...(typeof lead['jobTitle'] === 'string' ? { jobTitle: lead['jobTitle'] } : {}),\n ...(typeof lead['phone'] === 'string' ? { phone: lead['phone'] } : {}),\n ...(Array.isArray(lead['tags']) ? { tags: lead['tags'] } : {}),\n },\n },\n }\n}\n\n/** `getAll` takes this many references at most per call. */\nconst GET_ALL_CHUNK = 300\n\nasync function getAllChunked(\n firestore: Firestore,\n refs: readonly FirebaseFirestore.DocumentReference[],\n): Promise<FirebaseFirestore.DocumentSnapshot[]> {\n const snapshots: FirebaseFirestore.DocumentSnapshot[] = []\n for (let start = 0; start < refs.length; start += GET_ALL_CHUNK) {\n snapshots.push(...(await firestore.getAll(...refs.slice(start, start + GET_ALL_CHUNK))))\n }\n return snapshots\n}\n\n/**\n * The people, as the sequence's site knows them, and the company each\n * contact is filed under there — batched reads for everyone: the contacts\n * and the leads by id, the companies once, and for each lead one address\n * lookup, because a lead the workspace already holds as a contact is that\n * contact's to enroll.\n */\nexport async function readOutreachEnrollCandidates(\n firestore: Firestore,\n input: { orgId: string; hostId: string; contactGroupId: string; people: readonly OutreachPersonRef[] },\n): Promise<OutreachEnrollCandidate[]> {\n if (!input.people.length) return []\n const org = firestore.collection('orgs').doc(input.orgId)\n const leads = firestore.collection('hosts').doc(input.hostId).collection('leads')\n const snapshots = await getAllChunked(\n firestore,\n input.people.map((person) =>\n person.kind === 'lead' ? leads.doc(person.id) : org.collection('contacts').doc(person.id),\n ),\n )\n const read = await Promise.all(\n input.people.map(async (person, index) => {\n const data = snapshots[index]?.exists ? (snapshots[index].data() as Record<string, unknown>) : null\n if (person.kind === 'lead') {\n const email = data ? normalizeContactEmail(data['email']) : null\n const held =\n data && email ? await findContactByEmail(org.collection('contacts'), email) : null\n return {\n personId: person.id,\n target: 'lead' as const,\n contactId: '',\n leadId: person.id,\n contact: data ? leadAsContact(data, input.contactGroupId) : null,\n lead: data,\n // A lead is the site's own record: no scope to check.\n visible: data !== null,\n name: data ? crmLeadDisplayName(data) : '',\n email,\n companyId: '',\n convertedContactId: data && typeof data['convertedContactId'] === 'string' ? data['convertedContactId'] : null,\n heldAsContactId: held ? held.id : null,\n }\n }\n return {\n personId: person.id,\n target: 'contact' as const,\n contactId: person.id,\n leadId: null,\n contact: data,\n lead: null,\n visible: data ? visibleToHost(data['visibleTo'] as string[] | undefined, input.hostId) : false,\n name: data ? contactDisplayName(data, input.contactGroupId).trim() : '',\n email: data ? normalizeContactEmail(data['email']) : null,\n companyId: data ? String(readContactFacet(data, input.contactGroupId).companyId ?? '') : '',\n convertedContactId: null,\n heldAsContactId: null,\n }\n }),\n )\n const companyIds = [...new Set(read.map((entry) => entry.companyId).filter(Boolean))]\n const companies = new Map<string, Record<string, unknown>>()\n if (companyIds.length) {\n const found = await getAllChunked(\n firestore,\n companyIds.map((id) => org.collection('companies').doc(id)),\n )\n found.forEach((snapshot, index) => {\n if (snapshot.exists) companies.set(companyIds[index], snapshot.data() as Record<string, unknown>)\n })\n }\n return read.map(({ companyId, ...candidate }) => ({\n ...candidate,\n company: companyId ? (companies.get(companyId) ?? null) : null,\n }))\n}\n\n/** The person as every answer names them. */\nfunction personOf(candidate: OutreachEnrollCandidate) {\n return {\n personId: candidate.personId,\n target: candidate.target,\n contactId: candidate.contactId,\n leadId: candidate.leadId,\n }\n}\n\n/** What stands in the way before the gates are asked anything. */\nexport function outreachCandidateBlocks(\n candidate: OutreachEnrollCandidate,\n facts: { siteName: string; alreadyInSequence: boolean },\n): OutreachEnrollBlock[] {\n if (!candidate.contact) {\n return candidate.target === 'lead'\n ? [{ code: 'lead_missing', reason: \"This lead no longer exists in this sequence's site's CRM.\" }]\n : [{ code: 'contact_missing', reason: 'This contact no longer exists in the CRM.' }]\n }\n const blocks: OutreachEnrollBlock[] = []\n if (candidate.target === 'lead') {\n // A lead that converted, or whose address the workspace already holds\n // as a contact, is that contact's to enroll — one person, one record.\n if (candidate.convertedContactId) {\n blocks.push({\n code: 'lead_converted',\n reason: 'This lead was converted into a contact. Enroll the contact instead.',\n })\n } else if (candidate.heldAsContactId) {\n blocks.push({\n code: 'lead_is_contact',\n reason: 'This address is already a contact in your CRM. Enroll the contact instead.',\n })\n }\n if (crmLeadStatus(candidate.lead) === 'unqualified') {\n blocks.push({\n code: 'lead_unqualified',\n reason: 'This lead was closed as unqualified. Reopen it before enrolling.',\n })\n }\n }\n if (!candidate.visible) {\n blocks.push({\n code: 'not_in_site',\n reason: `This contact isn't in ${facts.siteName || \"this sequence's site\"}'s CRM.`,\n })\n }\n if (!candidate.email) {\n blocks.push({ code: 'no_email', reason: 'This contact has no email address.' })\n }\n if (facts.alreadyInSequence) {\n blocks.push({\n code: 'already_in_sequence',\n reason: \"This person has already been in this sequence, so they won't be sent its emails again.\",\n })\n }\n return blocks\n}\n\n/** Every attestation, stamped for a hypothetical question only — never stored. */\nconst EVERYTHING_CONFIRMED: OutreachAttestations = Object.fromEntries(\n OUTREACH_ATTESTATION_KINDS.map((kind) => [kind, { uid: 'preview', atMs: 1 }]),\n)\n\nconst blocksOf = (blocks: readonly OutreachGateBlock[]): OutreachEnrollBlock[] =>\n blocks.map(({ code, reason }) => ({ code, reason }))\n\nexport interface OutreachGateQuestion {\n candidate: OutreachEnrollCandidate\n lookups: OutreachGateLookups\n /** The sequence's settings, its countries already capped by the organization's. */\n settings: OutreachSequenceSettings\n contactGroupId: string\n}\n\n/** Where one person stands before the rep confirms anything — see the module note. */\nexport function previewOutreachPerson(\n question: OutreachGateQuestion,\n facts: { siteName: string; alreadyInSequence: boolean },\n): OutreachEnrollPreviewPerson {\n const { candidate } = question\n const person = {\n ...personOf(candidate),\n name: candidate.name,\n email: candidate.email,\n }\n const early = outreachCandidateBlocks(candidate, facts)\n if (early.length) {\n return {\n ...person,\n cold: false,\n country: null,\n status: 'blocked',\n blocks: early,\n requires: { personalLine: false, attestations: [] },\n }\n }\n const ask = (personalLine: string, attestations: OutreachAttestations) =>\n evaluateOutreachGates({\n email: candidate.email ?? '',\n contact: candidate.contact,\n contactGroupId: question.contactGroupId,\n company: candidate.company,\n settings: question.settings,\n personalLine,\n attestations,\n lookups: question.lookups,\n })\n const asIs = ask('', {})\n const known = { cold: asIs.cold, country: asIs.country.country }\n if (asIs.allowed) {\n return {\n ...person,\n ...known,\n status: 'eligible',\n blocks: [],\n requires: { personalLine: false, attestations: [] },\n }\n }\n // The line is only checked for being present, so any sentence stands in.\n const confirmed = ask('A personal line.', EVERYTHING_CONFIRMED)\n if (!confirmed.allowed) {\n return {\n ...person,\n ...known,\n status: 'blocked',\n blocks: blocksOf(confirmed.blocks),\n requires: { personalLine: false, attestations: [] },\n }\n }\n const attestations: OutreachAttestationKind[] = asIs.cold\n ? [...asIs.missingAttestations]\n : asIs.blocks.some((block) => block.code === 'country_unknown')\n ? ['us_business_address']\n : []\n return {\n ...person,\n ...known,\n status: 'needs_confirmation',\n blocks: [],\n requires: {\n personalLine: asIs.blocks.some((block) => block.code === 'personal_line_missing'),\n attestations,\n },\n }\n}\n\nexport interface OutreachEnrollDecision {\n allowed: boolean\n blocks: OutreachEnrollBlock[]\n cold: boolean\n /** The address as the gates normalized it. */\n email: string | null\n /** What the rep confirmed, stamped with who and when. */\n attestations: OutreachAttestations\n}\n\n/** Confirm's answer for one person, with what the rep supplied — see the module note. */\nexport function decideOutreachEnrollment(\n question: OutreachGateQuestion,\n facts: { siteName: string; alreadyInSequence: boolean },\n supplied: {\n personalLine: string\n attestations: readonly unknown[]\n uid: string\n nowMs: number\n },\n): OutreachEnrollDecision {\n const attestations = outreachAttestationsFrom(supplied.attestations, supplied.uid, supplied.nowMs)\n const early = outreachCandidateBlocks(question.candidate, facts)\n if (early.length) {\n return { allowed: false, blocks: early, cold: false, email: question.candidate.email, attestations }\n }\n const result = evaluateOutreachGates({\n email: question.candidate.email ?? '',\n contact: question.candidate.contact,\n contactGroupId: question.contactGroupId,\n company: question.candidate.company,\n settings: question.settings,\n personalLine: supplied.personalLine,\n attestations,\n lookups: question.lookups,\n })\n return {\n allowed: result.allowed,\n blocks: blocksOf(result.blocks),\n cold: result.cold,\n email: result.email,\n attestations,\n }\n}\n"],"names":["normalizeContactEmail","contactDisplayName","readContactFacet","crmLeadDisplayName","crmLeadStatus","visibleToHost","findContactByEmail","evaluateOutreachGates","outreachAttestationsFrom","OUTREACH_ATTESTATION_KINDS","leadAsContact","lead","contactGroupId","sources","raw","Array","isArray","source","String","kind","startsWith","email","name","emailState","facets","address","companyName","jobTitle","phone","tags","GET_ALL_CHUNK","getAllChunked","firestore","refs","snapshots","start","length","push","getAll","slice","readOutreachEnrollCandidates","input","people","org","collection","doc","orgId","leads","hostId","map","person","id","read","Promise","all","index","data","exists","held","personId","target","contactId","leadId","contact","visible","companyId","convertedContactId","heldAsContactId","trim","companyIds","Set","entry","filter","Boolean","companies","Map","found","forEach","snapshot","set","candidate","company","get","personOf","outreachCandidateBlocks","facts","code","reason","blocks","siteName","alreadyInSequence","EVERYTHING_CONFIRMED","Object","fromEntries","uid","atMs","blocksOf","previewOutreachPerson","question","early","cold","country","status","requires","personalLine","attestations","ask","settings","lookups","asIs","known","allowed","confirmed","missingAttestations","some","block","decideOutreachEnrollment","supplied","nowMs","result"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/enrollment/enroll-people.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*==========================================\n * WHO CAN BE ENROLLED, AND WHAT THE REP STILL OWES (AGL-2980).\n *\n * The enroll dialog shows every person a view or a search named, each\n * marked eligible, blocked, or waiting on the rep; Confirm asks the same\n * question again, on the server, with what the rep supplied. Both answers\n * come from the engine's `evaluateOutreachGates` — this module only reads\n * the people and asks it the right questions.\n *\n * ## Three questions of one pure function\n *\n * The gates refuse a cold contact until the rep writes a personal line and\n * ticks the attestations, and refuse an address in an unknown country until\n * the rep confirms it is a US business address. So a preview asks:\n *\n * 1. as the person stands, with nothing confirmed — allowed means\n * ELIGIBLE;\n * 2. as if the rep confirmed everything they can — refused means BLOCKED,\n * for the reasons that remain, which no tick can clear;\n * 3. otherwise the person is WAITING ON THE REP, for what (1) found\n * missing.\n *\n * The confirmation in (2) is hypothetical and never stored: Confirm stamps\n * only what the rep ticked, with their uid and the time, and asks again.\n *\n * ## One enrollment per person per sequence, ever\n *\n * The enrollment's id is the sequence and the contact\n * (`outreachEnrollmentId`), and it is written with `create()`. A person who\n * finished, replied or was stopped is not sent the same emails a second\n * time by being enrolled again, and two members confirming the same person\n * at once write one enrollment.\n *==========================================*/\n\nimport { normalizeContactEmail, contactDisplayName, readContactFacet } from '@aglyn/aglyn/app-utils/contacts'\nimport { crmLeadDisplayName, crmLeadStatus } from '@aglyn/aglyn/app-utils/crm'\nimport { visibleToHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport { findContactByEmail } from '@aglyn/tenant-data-admin/server/contact-email-index'\nimport {\n evaluateOutreachGates,\n type OutreachGateBlock,\n type OutreachGateLookups,\n} from '../engine/gates'\nimport { outreachAttestationsFrom } from '../engine/enrollment-state'\nimport {\n OUTREACH_ATTESTATION_KINDS,\n type OutreachAttestationKind,\n type OutreachAttestations,\n type OutreachSequenceSettings,\n} from '../model/outreach.types'\nimport type {\n OutreachEnrollBlock,\n OutreachEnrollPreviewPerson,\n OutreachPersonRef,\n} from '../model/outreach-api'\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/**\n * One person as the preview and the enroll read them: a contact, or a lead\n * the sequence's site holds (AGL-3234).\n *\n * ## A lead is judged as the contact it would be\n *\n * The gates, the merge fields and the country rule all read a CONTACT\n * document — its facet's sources, stage, address, company and title. A lead\n * carries the same facts in its own shape, so rather than teach every gate\n * two records, a lead is handed to them as {@link leadAsContact}: a\n * contact-shaped view whose one facet, under the sequence's group, holds\n * what the lead holds. An imported lead has no inbound source and is cold;\n * one that wrote in through a form is not; a lead is never a customer; its\n * address decides its country; and `{{contact.firstName}}` or\n * `{{contact.company}}` in a step reads the lead's name and company text,\n * so a sequence written for contacts sends to leads unchanged — which is\n * also what lets an enrollment made on a lead keep sending after the lead\n * converts and the view becomes the real contact.\n */\nexport interface OutreachEnrollCandidate {\n /** The record's own id: the contact's, or the lead's person key. */\n personId: string\n target: 'contact' | 'lead'\n /** The contact's id; `''` for a lead. */\n contactId: string\n /** The lead's person key; `null` for a contact. */\n leadId: string | null\n /**\n * The contact document (`orgs/{orgId}/contacts/{id}`), or for a lead the\n * contact-shaped view of it — see the module note. `null` when the record\n * no longer exists.\n */\n contact: Record<string, unknown> | null\n /** The lead document as stored, for a lead; `null` for a contact. */\n lead: Record<string, unknown> | null\n /** Whether the sequence's site may see the record. A lead is the site's own. */\n visible: boolean\n /** The name the sequence's site knows them by; `''` for none. */\n name: string\n /** The primary address, normalized, or `null` when there is none. */\n email: string | null\n /** The company the contact is filed under at that site, when there is one. */\n company: Record<string, unknown> | null\n /** A lead already converted: the contact it became. Enroll that instead. */\n convertedContactId: string | null\n /** A lead whose address the workspace already holds as a contact. Enroll that instead. */\n heldAsContactId: string | null\n}\n\n/**\n * A lead as the gates and the merge fields read a contact — see the\n * module note. The facet is the sequence's group's, which is the one every\n * reader asks for. The lead's `sources` — `form:{id}`, `booking`, `import`,\n * `manual`, `api`, `signup` — become the facet's source flags by kind, so\n * the inbound rule reads them as it reads a contact's.\n */\nexport function leadAsContact(\n lead: Record<string, unknown>,\n contactGroupId: string,\n): Record<string, unknown> {\n const sources: Record<string, true> = {}\n for (const raw of Array.isArray(lead['sources']) ? lead['sources'] : []) {\n const source = String(raw)\n const kind = source.startsWith('form:') ? 'form' : source === 'signup' ? 'member' : source\n if (kind) sources[kind] = true\n }\n return {\n email: lead['email'],\n name: lead['name'],\n // The verdict on the address (AGL-3245) is the lead's own top-level\n // field, as it is the contact's, so the gates quote it either way.\n ...(lead['emailState'] && typeof lead['emailState'] === 'object' ? { emailState: lead['emailState'] } : {}),\n facets: {\n [contactGroupId]: {\n sources,\n ...(lead['address'] && typeof lead['address'] === 'object' ? { address: lead['address'] } : {}),\n ...(typeof lead['company'] === 'string' ? { companyName: lead['company'] } : {}),\n ...(typeof lead['jobTitle'] === 'string' ? { jobTitle: lead['jobTitle'] } : {}),\n ...(typeof lead['phone'] === 'string' ? { phone: lead['phone'] } : {}),\n ...(Array.isArray(lead['tags']) ? { tags: lead['tags'] } : {}),\n },\n },\n }\n}\n\n/** `getAll` takes this many references at most per call. */\nconst GET_ALL_CHUNK = 300\n\nasync function getAllChunked(\n firestore: Firestore,\n refs: readonly FirebaseFirestore.DocumentReference[],\n): Promise<FirebaseFirestore.DocumentSnapshot[]> {\n const snapshots: FirebaseFirestore.DocumentSnapshot[] = []\n for (let start = 0; start < refs.length; start += GET_ALL_CHUNK) {\n snapshots.push(...(await firestore.getAll(...refs.slice(start, start + GET_ALL_CHUNK))))\n }\n return snapshots\n}\n\n/**\n * The people, as the sequence's site knows them, and the company each\n * contact is filed under there — batched reads for everyone: the contacts\n * and the leads by id, the companies once, and for each lead one address\n * lookup, because a lead the workspace already holds as a contact is that\n * contact's to enroll.\n */\nexport async function readOutreachEnrollCandidates(\n firestore: Firestore,\n input: { orgId: string; hostId: string; contactGroupId: string; people: readonly OutreachPersonRef[] },\n): Promise<OutreachEnrollCandidate[]> {\n if (!input.people.length) return []\n const org = firestore.collection('orgs').doc(input.orgId)\n // Beside the contacts, on the org (AGL-3275).\n const leads = org.collection('leads')\n const snapshots = await getAllChunked(\n firestore,\n input.people.map((person) =>\n person.kind === 'lead' ? leads.doc(person.id) : org.collection('contacts').doc(person.id),\n ),\n )\n const read = await Promise.all(\n input.people.map(async (person, index) => {\n const data = snapshots[index]?.exists ? (snapshots[index].data() as Record<string, unknown>) : null\n if (person.kind === 'lead') {\n const email = data ? normalizeContactEmail(data['email']) : null\n const held =\n data && email ? await findContactByEmail(org.collection('contacts'), email) : null\n return {\n personId: person.id,\n target: 'lead' as const,\n contactId: '',\n leadId: person.id,\n contact: data ? leadAsContact(data, input.contactGroupId) : null,\n lead: data,\n // A lead is the site's own record: no scope to check.\n visible: data !== null,\n name: data ? crmLeadDisplayName(data) : '',\n email,\n companyId: '',\n convertedContactId: data && typeof data['convertedContactId'] === 'string' ? data['convertedContactId'] : null,\n heldAsContactId: held ? held.id : null,\n }\n }\n return {\n personId: person.id,\n target: 'contact' as const,\n contactId: person.id,\n leadId: null,\n contact: data,\n lead: null,\n visible: data ? visibleToHost(data['visibleTo'] as string[] | undefined, input.hostId) : false,\n name: data ? contactDisplayName(data, input.contactGroupId).trim() : '',\n email: data ? normalizeContactEmail(data['email']) : null,\n companyId: data ? String(readContactFacet(data, input.contactGroupId).companyId ?? '') : '',\n convertedContactId: null,\n heldAsContactId: null,\n }\n }),\n )\n const companyIds = [...new Set(read.map((entry) => entry.companyId).filter(Boolean))]\n const companies = new Map<string, Record<string, unknown>>()\n if (companyIds.length) {\n const found = await getAllChunked(\n firestore,\n companyIds.map((id) => org.collection('companies').doc(id)),\n )\n found.forEach((snapshot, index) => {\n if (snapshot.exists) companies.set(companyIds[index], snapshot.data() as Record<string, unknown>)\n })\n }\n return read.map(({ companyId, ...candidate }) => ({\n ...candidate,\n company: companyId ? (companies.get(companyId) ?? null) : null,\n }))\n}\n\n/** The person as every answer names them. */\nfunction personOf(candidate: OutreachEnrollCandidate) {\n return {\n personId: candidate.personId,\n target: candidate.target,\n contactId: candidate.contactId,\n leadId: candidate.leadId,\n }\n}\n\n/** What stands in the way before the gates are asked anything. */\nexport function outreachCandidateBlocks(\n candidate: OutreachEnrollCandidate,\n facts: { siteName: string; alreadyInSequence: boolean },\n): OutreachEnrollBlock[] {\n if (!candidate.contact) {\n return candidate.target === 'lead'\n ? [{ code: 'lead_missing', reason: \"This lead no longer exists in this sequence's site's CRM.\" }]\n : [{ code: 'contact_missing', reason: 'This contact no longer exists in the CRM.' }]\n }\n const blocks: OutreachEnrollBlock[] = []\n if (candidate.target === 'lead') {\n // A lead that converted, or whose address the workspace already holds\n // as a contact, is that contact's to enroll — one person, one record.\n if (candidate.convertedContactId) {\n blocks.push({\n code: 'lead_converted',\n reason: 'This lead was converted into a contact. Enroll the contact instead.',\n })\n } else if (candidate.heldAsContactId) {\n blocks.push({\n code: 'lead_is_contact',\n reason: 'This address is already a contact in your CRM. Enroll the contact instead.',\n })\n }\n if (crmLeadStatus(candidate.lead) === 'unqualified') {\n blocks.push({\n code: 'lead_unqualified',\n reason: 'This lead was closed as unqualified. Reopen it before enrolling.',\n })\n }\n }\n if (!candidate.visible) {\n blocks.push({\n code: 'not_in_site',\n reason: `This contact isn't in ${facts.siteName || \"this sequence's site\"}'s CRM.`,\n })\n }\n if (!candidate.email) {\n blocks.push({ code: 'no_email', reason: 'This contact has no email address.' })\n }\n if (facts.alreadyInSequence) {\n blocks.push({\n code: 'already_in_sequence',\n reason: \"This person has already been in this sequence, so they won't be sent its emails again.\",\n })\n }\n return blocks\n}\n\n/** Every attestation, stamped for a hypothetical question only — never stored. */\nconst EVERYTHING_CONFIRMED: OutreachAttestations = Object.fromEntries(\n OUTREACH_ATTESTATION_KINDS.map((kind) => [kind, { uid: 'preview', atMs: 1 }]),\n)\n\nconst blocksOf = (blocks: readonly OutreachGateBlock[]): OutreachEnrollBlock[] =>\n blocks.map(({ code, reason }) => ({ code, reason }))\n\nexport interface OutreachGateQuestion {\n candidate: OutreachEnrollCandidate\n lookups: OutreachGateLookups\n /** The sequence's settings, its countries already capped by the organization's. */\n settings: OutreachSequenceSettings\n contactGroupId: string\n}\n\n/** Where one person stands before the rep confirms anything — see the module note. */\nexport function previewOutreachPerson(\n question: OutreachGateQuestion,\n facts: { siteName: string; alreadyInSequence: boolean },\n): OutreachEnrollPreviewPerson {\n const { candidate } = question\n const person = {\n ...personOf(candidate),\n name: candidate.name,\n email: candidate.email,\n }\n const early = outreachCandidateBlocks(candidate, facts)\n if (early.length) {\n return {\n ...person,\n cold: false,\n country: null,\n status: 'blocked',\n blocks: early,\n requires: { personalLine: false, attestations: [] },\n }\n }\n const ask = (personalLine: string, attestations: OutreachAttestations) =>\n evaluateOutreachGates({\n email: candidate.email ?? '',\n contact: candidate.contact,\n contactGroupId: question.contactGroupId,\n company: candidate.company,\n settings: question.settings,\n personalLine,\n attestations,\n lookups: question.lookups,\n })\n const asIs = ask('', {})\n const known = { cold: asIs.cold, country: asIs.country.country }\n if (asIs.allowed) {\n return {\n ...person,\n ...known,\n status: 'eligible',\n blocks: [],\n requires: { personalLine: false, attestations: [] },\n }\n }\n // The line is only checked for being present, so any sentence stands in.\n const confirmed = ask('A personal line.', EVERYTHING_CONFIRMED)\n if (!confirmed.allowed) {\n return {\n ...person,\n ...known,\n status: 'blocked',\n blocks: blocksOf(confirmed.blocks),\n requires: { personalLine: false, attestations: [] },\n }\n }\n const attestations: OutreachAttestationKind[] = asIs.cold\n ? [...asIs.missingAttestations]\n : asIs.blocks.some((block) => block.code === 'country_unknown')\n ? ['us_business_address']\n : []\n return {\n ...person,\n ...known,\n status: 'needs_confirmation',\n blocks: [],\n requires: {\n personalLine: asIs.blocks.some((block) => block.code === 'personal_line_missing'),\n attestations,\n },\n }\n}\n\nexport interface OutreachEnrollDecision {\n allowed: boolean\n blocks: OutreachEnrollBlock[]\n cold: boolean\n /** The address as the gates normalized it. */\n email: string | null\n /** What the rep confirmed, stamped with who and when. */\n attestations: OutreachAttestations\n}\n\n/** Confirm's answer for one person, with what the rep supplied — see the module note. */\nexport function decideOutreachEnrollment(\n question: OutreachGateQuestion,\n facts: { siteName: string; alreadyInSequence: boolean },\n supplied: {\n personalLine: string\n attestations: readonly unknown[]\n uid: string\n nowMs: number\n },\n): OutreachEnrollDecision {\n const attestations = outreachAttestationsFrom(supplied.attestations, supplied.uid, supplied.nowMs)\n const early = outreachCandidateBlocks(question.candidate, facts)\n if (early.length) {\n return { allowed: false, blocks: early, cold: false, email: question.candidate.email, attestations }\n }\n const result = evaluateOutreachGates({\n email: question.candidate.email ?? '',\n contact: question.candidate.contact,\n contactGroupId: question.contactGroupId,\n company: question.candidate.company,\n settings: question.settings,\n personalLine: supplied.personalLine,\n attestations,\n lookups: question.lookups,\n })\n return {\n allowed: result.allowed,\n blocks: blocksOf(result.blocks),\n cold: result.cold,\n email: result.email,\n attestations,\n }\n}\n"],"names":["normalizeContactEmail","contactDisplayName","readContactFacet","crmLeadDisplayName","crmLeadStatus","visibleToHost","findContactByEmail","evaluateOutreachGates","outreachAttestationsFrom","OUTREACH_ATTESTATION_KINDS","leadAsContact","lead","contactGroupId","sources","raw","Array","isArray","source","String","kind","startsWith","email","name","emailState","facets","address","companyName","jobTitle","phone","tags","GET_ALL_CHUNK","getAllChunked","firestore","refs","snapshots","start","length","push","getAll","slice","readOutreachEnrollCandidates","input","people","org","collection","doc","orgId","leads","map","person","id","read","Promise","all","index","data","exists","held","personId","target","contactId","leadId","contact","visible","companyId","convertedContactId","heldAsContactId","hostId","trim","companyIds","Set","entry","filter","Boolean","companies","Map","found","forEach","snapshot","set","candidate","company","get","personOf","outreachCandidateBlocks","facts","code","reason","blocks","siteName","alreadyInSequence","EVERYTHING_CONFIRMED","Object","fromEntries","uid","atMs","blocksOf","previewOutreachPerson","question","early","cold","country","status","requires","personalLine","attestations","ask","settings","lookups","asIs","known","allowed","confirmed","missingAttestations","some","block","decideOutreachEnrollment","supplied","nowMs","result"],"mappings":"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"}
@@ -16,6 +16,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
16
16
  * limitations under the License.
17
17
  */ import { CAMPAIGN_MEMBERSHIP_FIELD, contactCampaignFieldPath, normalizeCampaignIds } from "@aglyn/aglyn/app-utils/campaign-membership";
18
18
  import { consentGroupForHost } from "@aglyn/aglyn/app-utils/consent-groups";
19
+ import { scopeTokensForHost } from "@aglyn/aglyn/app-utils/scope-tokens";
19
20
  import { CRM_COLLECTIONS, crmLeadStatus, crmViewIsListed, isCrmLeadOpen, normalizeCrmViewFilters } from "@aglyn/aglyn/app-utils/crm";
20
21
  import { dynamicListDimensionsForCrmView } from "@aglyn/aglyn/app-utils/dynamic-list-rule";
21
22
  import { findContactByEmail } from "@aglyn/tenant-data-admin/server/contact-email-index";
@@ -152,13 +153,20 @@ const uniqueIds = (values)=>Array.isArray(values) ? [
152
153
  * The people a saved LEADS view selects (AGL-3234): the sequence's site's
153
154
  * most recently seen leads, narrowed by the view's status clause the way
154
155
  * the Leads list narrows its window — open leads when the view names no
155
- * status, one status when it does, everything for `all`. A lead is the
156
- * site's own, so there is no address to resolve: the person key IS the id.
157
- */ async function leadsViewPeople(firestore, sequence, filters) {
156
+ * status, one status when it does, everything for `all`. The person key IS
157
+ * the id, so there is no address to resolve — and since AGL-3275 the window is
158
+ * narrowed by `visibleTo` rather than by the collection's parent.
159
+ */ async function leadsViewPeople(firestore, orgId, sequence, filters) {
158
160
  var _ref;
159
161
  const statusClause = filters.find((clause)=>clause.field === 'status' && clause.op === 'equals');
160
162
  const wanted = String((_ref = statusClause == null ? void 0 : statusClause.value) != null ? _ref : 'open');
161
- const window = await firestore.collection('hosts').doc(sequence.hostId).collection('leads').orderBy('lastSeenAtMs', 'desc').limit(LEADS_VIEW_WINDOW + 1).get();
163
+ /*
164
+ * SCOPED (AGL-3275). The collection is org-wide, so the window is narrowed
165
+ * to what the sequence's site may see before it is ordered — otherwise an
166
+ * agency's sequence would offer another client's people to enroll. The
167
+ * composite index for `visibleTo array-contains-any` + `lastSeenAtMs desc`
168
+ * is in `cloud/firestore.indexes.json`.
169
+ */ 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();
162
170
  const matching = window.docs.slice(0, LEADS_VIEW_WINDOW).filter((doc)=>{
163
171
  const lead = doc.data();
164
172
  if (wanted === 'all') return true;
@@ -188,7 +196,7 @@ export function createOutreachEnrollRoutes(deps) {
188
196
  const campaignIds = normalizeCampaignIds(sequence.campaignIds);
189
197
  if (!campaignIds.length) return;
190
198
  try {
191
- const ref = candidate.target === 'lead' && candidate.leadId ? firestore.collection('hosts').doc(sequence.hostId).collection('leads').doc(candidate.leadId) : candidate.contactId ? firestore.collection('orgs').doc(orgId).collection('contacts').doc(candidate.contactId) : null;
199
+ 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;
192
200
  if (ref) {
193
201
  const field = candidate.target === 'lead' ? CAMPAIGN_MEMBERSHIP_FIELD : contactCampaignFieldPath(contactGroupId);
194
202
  await ref.update({
@@ -265,7 +273,7 @@ export function createOutreachEnrollRoutes(deps) {
265
273
  return outreachRefusal(404, 'view-not-found', 'That saved view no longer exists.');
266
274
  }
267
275
  if (data['section'] === 'leads') {
268
- return leadsViewPeople(firestore, sequence, normalizeCrmViewFilters(data['filters']));
276
+ return leadsViewPeople(firestore, caller.orgId, sequence, normalizeCrmViewFilters(data['filters']));
269
277
  }
270
278
  if (data['section'] !== 'contacts') {
271
279
  return outreachRefusal(400, 'view-unsupported', 'Enroll from a saved view of Contacts or Leads.');
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/enroll-routes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n CAMPAIGN_MEMBERSHIP_FIELD,\n contactCampaignFieldPath,\n normalizeCampaignIds,\n} from '@aglyn/aglyn/app-utils/campaign-membership'\nimport { consentGroupForHost } from '@aglyn/aglyn/app-utils/consent-groups'\nimport {\n CRM_COLLECTIONS,\n crmLeadStatus,\n crmViewIsListed,\n isCrmLeadOpen,\n normalizeCrmViewFilters,\n} from '@aglyn/aglyn/app-utils/crm'\nimport { dynamicListDimensionsForCrmView } from '@aglyn/aglyn/app-utils/dynamic-list-rule'\nimport type { PluginRecordTimelineWriter } from '@aglyn/aglyn/plugin-manager/plugin-record-timeline'\nimport type { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport { findContactByEmail } from '@aglyn/tenant-data-admin/server/contact-email-index'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { outreachEnrolledEntry } from '../engine/enrollment-activity'\nimport { buildOutreachEnrollment, planOutreachFirstDue } from '../engine/enrollment-state'\nimport type { OutreachGateLookups } from '../engine/gates'\nimport {\n decideOutreachEnrollment,\n previewOutreachPerson,\n readOutreachEnrollCandidates,\n type OutreachEnrollCandidate,\n type OutreachGateQuestion,\n} from '../enrollment/enroll-people'\nimport { readOutreachGateLookups } from '../enrollment/gate-lookups'\nimport { effectiveOutreachAllowedCountries } from '../model/compliance-settings'\nimport {\n OUTREACH_ENROLL_BATCH_MAX,\n type OutreachEnrollOutcome,\n type OutreachEnrollPreviewResponse,\n type OutreachEnrollResponse,\n type OutreachPersonRef,\n} from '../model/outreach-api'\nimport type { OutreachSequence } from '../model/outreach.types'\nimport { readOutreachComplianceSettingsDoc } from '../storage/compliance-settings-store'\nimport { fileOutreachNote } from '../runtime/timeline'\nimport {\n outreachEnrollmentId,\n outreachEnrollmentLink,\n outreachOrgCollection,\n readStoredOutreachMailbox,\n readStoredOutreachSequence,\n} from '../storage/outreach-records'\nimport { OUTREACH_SEQUENCE_ACTIVITY_TARGET, type OutreachRouteDeps } from './route-deps'\nimport { outreachRouteGate, type OutreachRouteCaller, type OutreachRouteExtraPermission } from './route-gate'\nimport {\n outreachMethodNotAllowed,\n outreachOk,\n outreachRefusal,\n readOutreachDocumentId,\n readOutreachJsonBody,\n} from './route-http'\n\n/**\n * ENROLLING PEOPLE IN A SEQUENCE (AGL-2980): `outreach/enroll/preview` and\n * `outreach/enroll`.\n *\n * The preview reads the people a saved Contacts view or a search named —\n * at most {@link OUTREACH_ENROLL_BATCH_MAX} — and says where each stands:\n * eligible, blocked with the gate's own sentence, or waiting on the rep for\n * a personal line and the attestations only they can make. Confirm reads\n * everything again and asks the gates again with what the rep supplied,\n * because a preview is a picture of a moment: someone can unsubscribe\n * between the two, and a request can claim anything. Nothing the preview\n * answered is trusted by the enroll.\n *\n * Both read the CRM's people, so both ask for `data.manage` beside\n * `outreach.use` — the permission the contacts' own rules read with, so\n * Outreach is no side door into the address book.\n *\n * Only an ACTIVE sequence takes people: enrolling schedules the first step\n * in the mailbox's hours, and a draft's steps and a paused sequence's timing\n * are not yet what will be sent.\n */\n\n/** The permission enrolling reads the CRM with. */\nexport const OUTREACH_READS_PEOPLE: OutreachRouteExtraPermission = {\n key: 'data.manage',\n refusal: \"Enrolling reads your CRM's contacts, and your role does not include Manage data.\",\n}\n\n/** Everything enrolling reaches beyond the routes' own dependencies. */\nexport interface OutreachEnrollRouteDeps extends OutreachRouteDeps {\n /**\n * The addresses a saved Contacts view selects among one site's contacts,\n * read the way the dynamic-list sweep reads one, and whether the read\n * reached the whole view before its budget.\n */\n crmViewEmails(input: { hostId: string; viewId: string }): Promise<{ emails: string[]; complete: boolean }>\n /**\n * The workspace's record system on the timeline seam (AGL-3274), when a\n * plugin keeps one: the enroll files \"Enrolled in\" on the person's record\n * — the runtime's `timeline`, reached the same way.\n */\n timeline(): PluginRecordTimelineWriter | null\n}\n\nexport interface OutreachEnrollRoutes {\n preview: PluginWebApiHandler\n confirm: PluginWebApiHandler\n}\n\n/** The activity line an enroll writes. */\nexport function outreachEnrolledActivity(count: number): string {\n return count === 1\n ? 'Enrolled 1 person in a sequence'\n : `Enrolled ${count} people in a sequence`\n}\n\n/** Lookups for a person the gates will never be asked about: every one unchecked. */\nconst UNCHECKED: OutreachGateLookups = {\n platformSuppressed: null,\n hostSuppressed: null,\n salesTopicState: null,\n doNotContact: null,\n doNotContactDomain: null,\n workspaceMembers: null,\n openEnrollments: null,\n hasInboundEmail: null,\n}\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** gRPC `ALREADY_EXISTS`, which `create()` rejects with when the document is there. */\nconst ALREADY_EXISTS = 6\n\nasync function loadActiveSequence(\n firestore: Firestore,\n orgId: string,\n rawSequenceId: unknown,\n): Promise<Response | OutreachSequence> {\n const sequenceId = readOutreachDocumentId(rawSequenceId)\n if (!sequenceId) return outreachRefusal(400, 'invalid-request', 'Name the sequence to enroll people in.')\n const snapshot = await outreachOrgCollection(firestore, orgId, 'sequences').doc(sequenceId).get()\n const sequence = readStoredOutreachSequence(sequenceId, snapshot.exists ? snapshot.data() : undefined)\n if (!sequence) return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n if (sequence.status === 'archived') {\n return outreachRefusal(409, 'sequence-archived', 'This sequence is archived and takes no one new.')\n }\n if (sequence.status !== 'active') {\n return outreachRefusal(409, 'sequence-not-active', 'Activate this sequence before enrolling people.')\n }\n return sequence\n}\n\n/** What every person in one request is judged against. */\ninterface EnrollContext {\n settings: OutreachGateQuestion['settings']\n contactGroupId: string\n siteName: string\n}\n\nasync function enrollContext(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n sequence: OutreachSequence,\n): Promise<EnrollContext> {\n const [orgSettings, host] = await Promise.all([\n readOutreachComplianceSettingsDoc(firestore, caller.orgId),\n firestore.collection('hosts').doc(sequence.hostId).get(),\n ])\n return {\n // The organization's countries are a ceiling over the sequence's.\n settings: {\n ...sequence.settings,\n allowedCountries: effectiveOutreachAllowedCountries(\n sequence.settings.allowedCountries,\n orgSettings.allowedCountries,\n ),\n },\n contactGroupId: consentGroupForHost(caller.org, sequence.hostId).groupId,\n siteName: host.exists ? String(host.get('name') ?? '') : '',\n }\n}\n\n/**\n * The candidates, the enrollments they already have here, and the gates'\n * lookups — every map keyed by the person's own id.\n *\n * \"Already in this sequence\" is answered two ways (AGL-3234): by the id the\n * person's enrollment would have, and by any enrollment in this sequence\n * that carries the person's ADDRESS. The second is what keeps a contact\n * from being enrolled beside the lead they were converted from: the lead's\n * enrollment followed them, under the lead's key, and still names them.\n */\nasync function readPeople(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n sequence: OutreachSequence,\n context: EnrollContext,\n people: readonly OutreachPersonRef[],\n): Promise<{\n candidates: OutreachEnrollCandidate[]\n enrolledHere: Set<string>\n lookups: Map<string, OutreachGateLookups>\n}> {\n const candidates = await readOutreachEnrollCandidates(firestore, {\n orgId: caller.orgId,\n hostId: sequence.hostId,\n contactGroupId: context.contactGroupId,\n people,\n })\n const enrollments = outreachOrgCollection(firestore, caller.orgId, 'enrollments')\n const existing = candidates.length\n ? await firestore.getAll(\n ...candidates.map((candidate) => enrollments.doc(outreachEnrollmentId(sequence.id, candidate.personId))),\n )\n : []\n const enrolledHere = new Set(\n candidates.filter((_candidate, index) => existing[index]?.exists).map((candidate) => candidate.personId),\n )\n const askable = candidates.filter((candidate) => candidate.contact && candidate.visible && candidate.email)\n const lookups = askable.length\n ? await readOutreachGateLookups(firestore, {\n orgId: caller.orgId,\n hostId: sequence.hostId,\n people: askable.map((candidate) => ({\n personId: candidate.personId,\n contactId: candidate.contactId || null,\n leadId: candidate.leadId,\n email: String(candidate.email),\n })),\n })\n : new Map<string, OutreachGateLookups>()\n for (const candidate of askable) {\n const open = lookups.get(candidate.personId)?.openEnrollments ?? []\n if (open.some((entry) => entry.sequenceId === sequence.id)) enrolledHere.add(candidate.personId)\n }\n return { candidates, enrolledHere, lookups }\n}\n\nconst uniqueIds = (values: unknown): string[] =>\n Array.isArray(values)\n ? [...new Set(values.map(readOutreachDocumentId).filter((id): id is string => id !== null))]\n : []\n\n/** The most leads a saved Leads view reads — the Leads list's own window. */\nconst LEADS_VIEW_WINDOW = 200\n\n/**\n * The people a saved LEADS view selects (AGL-3234): the sequence's site's\n * most recently seen leads, narrowed by the view's status clause the way\n * the Leads list narrows its window — open leads when the view names no\n * status, one status when it does, everything for `all`. A lead is the\n * site's own, so there is no address to resolve: the person key IS the id.\n */\nasync function leadsViewPeople(\n firestore: Firestore,\n sequence: OutreachSequence,\n filters: ReturnType<typeof normalizeCrmViewFilters>,\n): Promise<{ people: OutreachPersonRef[]; total: number; truncated: boolean }> {\n const statusClause = filters.find((clause) => clause.field === 'status' && clause.op === 'equals')\n const wanted = String(statusClause?.value ?? 'open')\n const window = await firestore\n .collection('hosts')\n .doc(sequence.hostId)\n .collection('leads')\n .orderBy('lastSeenAtMs', 'desc')\n .limit(LEADS_VIEW_WINDOW + 1)\n .get()\n const matching = window.docs.slice(0, LEADS_VIEW_WINDOW).filter((doc) => {\n const lead = doc.data() as Record<string, unknown>\n if (wanted === 'all') return true\n if (wanted === 'open') return isCrmLeadOpen(lead as never) && !lead['convertedContactId']\n return crmLeadStatus(lead as never) === wanted\n })\n return {\n people: matching.slice(0, OUTREACH_ENROLL_BATCH_MAX).map((doc) => ({ kind: 'lead', id: doc.id })),\n total: matching.length,\n truncated: matching.length > OUTREACH_ENROLL_BATCH_MAX || window.docs.length > LEADS_VIEW_WINDOW,\n }\n}\n\nexport function createOutreachEnrollRoutes(deps: OutreachEnrollRouteDeps): OutreachEnrollRoutes {\n /**\n * The person joins the sequence's campaigns (AGL-3254), the moment they\n * are enrolled: `arrayUnion` on the lead's own `campaignIds`, or on the\n * contact's facet for the sequence's site — the same field, the same\n * shape, every other campaign member carries — and `enrolled` credited\n * to each campaign. After the enrollment is created and never before:\n * a person the create refused joins nothing. Neither write may fail the\n * enrollment, which already exists; a membership that could not be\n * stamped is one the list's bulk bar can add by hand.\n */\n async function joinSequenceCampaigns(\n firestore: Firestore,\n orgId: string,\n sequence: OutreachSequence,\n contactGroupId: string,\n candidate: OutreachEnrollCandidate,\n nowMs: number,\n ): Promise<void> {\n const campaignIds = normalizeCampaignIds(sequence.campaignIds)\n if (!campaignIds.length) return\n try {\n const ref =\n candidate.target === 'lead' && candidate.leadId\n ? firestore.collection('hosts').doc(sequence.hostId).collection('leads').doc(candidate.leadId)\n : candidate.contactId\n ? firestore.collection('orgs').doc(orgId).collection('contacts').doc(candidate.contactId)\n : null\n if (ref) {\n const field =\n candidate.target === 'lead' ? CAMPAIGN_MEMBERSHIP_FIELD : contactCampaignFieldPath(contactGroupId)\n await ref.update({ [field]: FieldValue.arrayUnion(...campaignIds), updatedAt: FieldValue.serverTimestamp() })\n }\n } catch (error) {\n console.error('[outreach] the enrolled person could not join the sequence’s campaigns', error)\n }\n try {\n await deps.creditCampaign({ hostId: sequence.hostId, campaignIds, outcome: 'enrolled', atMs: nowMs })\n } catch (error) {\n console.error('[outreach] the enrollment could not be credited to the sequence’s campaigns', error)\n }\n }\n\n /**\n * The names of the sequence's campaigns as they stand, for the entry the\n * enroll files on the person's record (AGL-3274). Read once per request,\n * not per person; a container that is gone answers nothing and the entry\n * names the rest.\n */\n async function sequenceCampaignNames(firestore: Firestore, sequence: OutreachSequence): Promise<string[]> {\n const campaignIds = normalizeCampaignIds(sequence.campaignIds)\n if (!campaignIds.length) return []\n try {\n const containers = firestore.collection('hosts').doc(sequence.hostId).collection('emailCampaigns')\n const found = await firestore.getAll(...campaignIds.map((id) => containers.doc(id)))\n return found.map((snapshot) => String(snapshot.get('name') ?? '').trim()).filter(Boolean)\n } catch (error) {\n console.error('[outreach] the sequence’s campaigns could not be named for the record', error)\n return []\n }\n }\n\n /**\n * The people a source names, the batch limit applied: every contact a\n * search picked, every lead picked from the sequence's site, or the\n * people a saved Contacts or Leads view selects there — contacts found by\n * address, leads by their own key.\n */\n async function sourcePeople(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n sequence: OutreachSequence,\n source: Record<string, unknown>,\n ): Promise<Response | { people: OutreachPersonRef[]; total: number; truncated: boolean }> {\n if (source['kind'] === 'contacts' || source['kind'] === 'leads') {\n const kind = source['kind'] === 'leads' ? 'lead' : 'contact'\n const ids = uniqueIds(kind === 'lead' ? source['leadIds'] : source['contactIds'])\n if (!ids.length) return outreachRefusal(400, 'invalid-request', 'Pick the people to enroll.')\n return {\n people: ids.slice(0, OUTREACH_ENROLL_BATCH_MAX).map((id) => ({ kind, id })),\n total: ids.length,\n truncated: ids.length > OUTREACH_ENROLL_BATCH_MAX,\n }\n }\n if (source['kind'] !== 'view') {\n return outreachRefusal(400, 'invalid-request', 'Enroll people from a saved view or a search.')\n }\n const viewId = readOutreachDocumentId(source['viewId'])\n const view = viewId\n ? await firestore.collection('orgs').doc(caller.orgId).collection(CRM_COLLECTIONS.views).doc(viewId).get()\n : null\n const data = view?.exists ? (view.data() as Record<string, unknown>) : null\n // A colleague's private view is theirs: it is not listed for this\n // reader, so it does not exist for them here either.\n if (\n !viewId ||\n !data ||\n !crmViewIsListed({ shared: data['shared'] === true, ownerUid: String(data['ownerUid'] ?? '') }, caller.uid)\n ) {\n return outreachRefusal(404, 'view-not-found', 'That saved view no longer exists.')\n }\n if (data['section'] === 'leads') {\n return leadsViewPeople(firestore, sequence, normalizeCrmViewFilters(data['filters']))\n }\n if (data['section'] !== 'contacts') {\n return outreachRefusal(400, 'view-unsupported', 'Enroll from a saved view of Contacts or Leads.')\n }\n const { unsupported } = dynamicListDimensionsForCrmView(normalizeCrmViewFilters(data['filters']))\n if (unsupported.length) {\n // Dropping the filter enrolling cannot read would enroll more people\n // than the view shows, so the view is refused whole.\n return outreachRefusal(\n 400,\n 'view-unsupported',\n `This view filters on ${unsupported.map((clause) => clause.label || clause.field).join(', ')}, which enrolling can't apply. Pick another view, or search.`,\n )\n }\n const { emails, complete } = await deps.crmViewEmails({ hostId: sequence.hostId, viewId })\n const ordered = [...new Set(emails)].sort()\n const contacts = firestore.collection('orgs').doc(caller.orgId).collection('contacts')\n const found = await Promise.all(\n ordered\n .slice(0, OUTREACH_ENROLL_BATCH_MAX)\n .map((email) => findContactByEmail(contacts, email, { hostId: sequence.hostId })),\n )\n return {\n people: [...new Set(found.filter((snapshot) => snapshot).map((snapshot) => String(snapshot?.id)))].map(\n (id) => ({ kind: 'contact', id }),\n ),\n total: ordered.length,\n truncated: ordered.length > OUTREACH_ENROLL_BATCH_MAX || !complete,\n }\n }\n\n const preview: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate, [OUTREACH_READS_PEOPLE])\n if (caller instanceof Response) return caller\n const firestore = deps.firestore()\n const sequence = await loadActiveSequence(firestore, caller.orgId, body['sequenceId'])\n if (sequence instanceof Response) return sequence\n const source = body['source'] && typeof body['source'] === 'object' ? (body['source'] as Record<string, unknown>) : {}\n const named = await sourcePeople(firestore, caller, sequence, source)\n if (named instanceof Response) return named\n const context = await enrollContext(firestore, caller, sequence)\n const { candidates, enrolledHere, lookups } = await readPeople(\n firestore,\n caller,\n sequence,\n context,\n named.people,\n )\n const people = candidates.map((candidate) =>\n previewOutreachPerson(\n {\n candidate,\n lookups: lookups.get(candidate.personId) ?? UNCHECKED,\n settings: context.settings,\n contactGroupId: context.contactGroupId,\n },\n { siteName: context.siteName, alreadyInSequence: enrolledHere.has(candidate.personId) },\n ),\n )\n return outreachOk({\n ok: true,\n people,\n total: named.total,\n truncated: named.truncated,\n } satisfies OutreachEnrollPreviewResponse)\n }\n\n const confirm: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate, [OUTREACH_READS_PEOPLE])\n if (caller instanceof Response) return caller\n const firestore = deps.firestore()\n const sequence = await loadActiveSequence(firestore, caller.orgId, body['sequenceId'])\n if (sequence instanceof Response) return sequence\n\n // Each person once, by whichever record they are: a contact by id, or a\n // lead by its key (AGL-3234). A person named both ways is a contact.\n const requested = new Map<\n string,\n { ref: OutreachPersonRef; personalLine: string; attestations: unknown[] }\n >()\n for (const entry of Array.isArray(body['people']) ? body['people'] : []) {\n const person = (entry && typeof entry === 'object' ? entry : {}) as Record<string, unknown>\n const contactId = readOutreachDocumentId(person['contactId'])\n const leadId = readOutreachDocumentId(person['leadId'])\n const ref: OutreachPersonRef | null = contactId\n ? { kind: 'contact', id: contactId }\n : leadId\n ? { kind: 'lead', id: leadId }\n : null\n if (!ref || requested.has(ref.id)) continue\n requested.set(ref.id, {\n ref,\n personalLine: typeof person['personalLine'] === 'string' ? person['personalLine'] : '',\n attestations: Array.isArray(person['attestations']) ? person['attestations'] : [],\n })\n }\n if (!requested.size) return outreachRefusal(400, 'invalid-request', 'Pick the people to enroll.')\n if (requested.size > OUTREACH_ENROLL_BATCH_MAX) {\n return outreachRefusal(\n 400,\n 'invalid-request',\n `Enroll at most ${OUTREACH_ENROLL_BATCH_MAX} people at a time.`,\n )\n }\n\n const mailboxSnapshot = sequence.mailboxId\n ? await outreachOrgCollection(firestore, caller.orgId, 'mailboxes').doc(sequence.mailboxId).get()\n : null\n const mailbox = readStoredOutreachMailbox(\n sequence.mailboxId,\n mailboxSnapshot?.exists ? mailboxSnapshot.data() : undefined,\n )\n if (!mailbox || mailbox.status === 'disconnected') {\n return outreachRefusal(\n 409,\n 'mailbox-unavailable',\n \"This sequence's mailbox is no longer connected. Choose another mailbox for it before enrolling people.\",\n )\n }\n const nowMs = deps.now()\n if (planOutreachFirstDue({ sequence, mailbox, enrolledAtMs: nowMs, random: deps.random }) === null) {\n return outreachRefusal(\n 409,\n 'mailbox-unavailable',\n \"This sequence's sending hours never open in its mailbox's timezone. Fix the hours before enrolling people.\",\n )\n }\n\n const context = await enrollContext(firestore, caller, sequence)\n const { candidates, enrolledHere, lookups } = await readPeople(\n firestore,\n caller,\n sequence,\n context,\n [...requested.values()].map((entry) => entry.ref),\n )\n const enrollments = outreachOrgCollection(firestore, caller.orgId, 'enrollments')\n const campaignNames = await sequenceCampaignNames(firestore, sequence)\n const results: OutreachEnrollOutcome[] = await Promise.all(\n candidates.map(async (candidate): Promise<OutreachEnrollOutcome> => {\n const named = {\n personId: candidate.personId,\n target: candidate.target,\n contactId: candidate.contactId,\n leadId: candidate.leadId,\n }\n const supplied = requested.get(candidate.personId) ?? { personalLine: '', attestations: [] }\n const decision = decideOutreachEnrollment(\n {\n candidate,\n lookups: lookups.get(candidate.personId) ?? UNCHECKED,\n settings: context.settings,\n contactGroupId: context.contactGroupId,\n },\n { siteName: context.siteName, alreadyInSequence: enrolledHere.has(candidate.personId) },\n { personalLine: supplied.personalLine, attestations: supplied.attestations, uid: caller.uid, nowMs },\n )\n if (!decision.allowed || !decision.email) {\n return { ...named, email: candidate.email, outcome: 'blocked', blocks: decision.blocks }\n }\n const id = outreachEnrollmentId(sequence.id, candidate.personId)\n const enrollment = buildOutreachEnrollment({\n id,\n sequence,\n mailbox,\n target: candidate.target,\n contactId: candidate.contactId,\n leadId: candidate.leadId,\n contactName: candidate.name,\n email: decision.email,\n cold: decision.cold,\n personalLine: supplied.personalLine,\n attestations: decision.attestations,\n enrolledByUid: caller.uid,\n nowMs,\n random: deps.random,\n })\n try {\n await enrollments.doc(id).create(enrollment)\n } catch (error) {\n if ((error as { code?: unknown })?.code !== ALREADY_EXISTS) throw error\n return {\n ...named,\n email: candidate.email,\n outcome: 'blocked',\n blocks: [\n {\n code: 'already_in_sequence',\n reason: \"This person has already been in this sequence, so they won't be sent its emails again.\",\n },\n ],\n }\n }\n await joinSequenceCampaigns(firestore, caller.orgId, sequence, context.contactGroupId, candidate, nowMs)\n // The person's record says so (AGL-3274): \"Enrolled in <sequence>\",\n // with the campaigns it carried them into, once per enrollment.\n const entry = outreachEnrolledEntry({ enrollmentId: id, sequenceName: sequence.name, campaignNames })\n await fileOutreachNote(deps, {\n orgId: caller.orgId,\n hostId: sequence.hostId,\n link: outreachEnrollmentLink(enrollment),\n dedupeKey: entry.dedupeKey,\n body: entry.body,\n atMs: nowMs,\n })\n return { ...named, email: decision.email, outcome: 'enrolled', enrollmentId: id }\n }),\n )\n const enrolled = results.filter((result) => result.outcome === 'enrolled').length\n if (enrolled) {\n await deps.logOrgActivity(\n caller.orgId,\n { uid: caller.uid, email: caller.email },\n outreachEnrolledActivity(enrolled),\n { type: OUTREACH_SEQUENCE_ACTIVITY_TARGET, id: sequence.id, name: sequence.name },\n )\n }\n return outreachOk({ ok: true, results, enrolled } satisfies OutreachEnrollResponse)\n }\n\n return { preview, confirm }\n}\n"],"names":["CAMPAIGN_MEMBERSHIP_FIELD","contactCampaignFieldPath","normalizeCampaignIds","consentGroupForHost","CRM_COLLECTIONS","crmLeadStatus","crmViewIsListed","isCrmLeadOpen","normalizeCrmViewFilters","dynamicListDimensionsForCrmView","findContactByEmail","FieldValue","outreachEnrolledEntry","buildOutreachEnrollment","planOutreachFirstDue","decideOutreachEnrollment","previewOutreachPerson","readOutreachEnrollCandidates","readOutreachGateLookups","effectiveOutreachAllowedCountries","OUTREACH_ENROLL_BATCH_MAX","readOutreachComplianceSettingsDoc","fileOutreachNote","outreachEnrollmentId","outreachEnrollmentLink","outreachOrgCollection","readStoredOutreachMailbox","readStoredOutreachSequence","OUTREACH_SEQUENCE_ACTIVITY_TARGET","outreachRouteGate","outreachMethodNotAllowed","outreachOk","outreachRefusal","readOutreachDocumentId","readOutreachJsonBody","OUTREACH_READS_PEOPLE","key","refusal","outreachEnrolledActivity","count","UNCHECKED","platformSuppressed","hostSuppressed","salesTopicState","doNotContact","doNotContactDomain","workspaceMembers","openEnrollments","hasInboundEmail","ALREADY_EXISTS","loadActiveSequence","firestore","orgId","rawSequenceId","sequenceId","snapshot","doc","get","sequence","exists","data","undefined","status","enrollContext","caller","host","orgSettings","Promise","all","collection","hostId","settings","allowedCountries","contactGroupId","org","groupId","siteName","String","readPeople","context","people","candidates","enrollments","existing","length","getAll","map","candidate","id","personId","enrolledHere","Set","filter","_candidate","index","askable","contact","visible","email","lookups","contactId","leadId","Map","open","some","entry","add","uniqueIds","values","Array","isArray","LEADS_VIEW_WINDOW","leadsViewPeople","filters","statusClause","find","clause","field","op","wanted","value","window","orderBy","limit","matching","docs","slice","lead","kind","total","truncated","createOutreachEnrollRoutes","deps","joinSequenceCampaigns","nowMs","campaignIds","ref","target","update","arrayUnion","updatedAt","serverTimestamp","error","console","creditCampaign","outcome","atMs","sequenceCampaignNames","containers","found","trim","Boolean","sourcePeople","source","ids","viewId","view","views","shared","ownerUid","uid","unsupported","label","join","emails","complete","crmViewEmails","ordered","sort","contacts","preview","request","method","body","gate","Response","named","alreadyInSequence","has","ok","confirm","requested","person","set","personalLine","attestations","size","mailboxSnapshot","mailboxId","mailbox","now","enrolledAtMs","random","campaignNames","results","supplied","decision","allowed","blocks","enrollment","contactName","name","cold","enrolledByUid","create","code","reason","enrollmentId","sequenceName","link","dedupeKey","enrolled","result","logOrgActivity","type"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/enroll-routes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n CAMPAIGN_MEMBERSHIP_FIELD,\n contactCampaignFieldPath,\n normalizeCampaignIds,\n} from '@aglyn/aglyn/app-utils/campaign-membership'\nimport { consentGroupForHost } from '@aglyn/aglyn/app-utils/consent-groups'\nimport { scopeTokensForHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport {\n CRM_COLLECTIONS,\n crmLeadStatus,\n crmViewIsListed,\n isCrmLeadOpen,\n normalizeCrmViewFilters,\n} from '@aglyn/aglyn/app-utils/crm'\nimport { dynamicListDimensionsForCrmView } from '@aglyn/aglyn/app-utils/dynamic-list-rule'\nimport type { PluginRecordTimelineWriter } from '@aglyn/aglyn/plugin-manager/plugin-record-timeline'\nimport type { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport { findContactByEmail } from '@aglyn/tenant-data-admin/server/contact-email-index'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { outreachEnrolledEntry } from '../engine/enrollment-activity'\nimport { buildOutreachEnrollment, planOutreachFirstDue } from '../engine/enrollment-state'\nimport type { OutreachGateLookups } from '../engine/gates'\nimport {\n decideOutreachEnrollment,\n previewOutreachPerson,\n readOutreachEnrollCandidates,\n type OutreachEnrollCandidate,\n type OutreachGateQuestion,\n} from '../enrollment/enroll-people'\nimport { readOutreachGateLookups } from '../enrollment/gate-lookups'\nimport { effectiveOutreachAllowedCountries } from '../model/compliance-settings'\nimport {\n OUTREACH_ENROLL_BATCH_MAX,\n type OutreachEnrollOutcome,\n type OutreachEnrollPreviewResponse,\n type OutreachEnrollResponse,\n type OutreachPersonRef,\n} from '../model/outreach-api'\nimport type { OutreachSequence } from '../model/outreach.types'\nimport { readOutreachComplianceSettingsDoc } from '../storage/compliance-settings-store'\nimport { fileOutreachNote } from '../runtime/timeline'\nimport {\n outreachEnrollmentId,\n outreachEnrollmentLink,\n outreachOrgCollection,\n readStoredOutreachMailbox,\n readStoredOutreachSequence,\n} from '../storage/outreach-records'\nimport { OUTREACH_SEQUENCE_ACTIVITY_TARGET, type OutreachRouteDeps } from './route-deps'\nimport { outreachRouteGate, type OutreachRouteCaller, type OutreachRouteExtraPermission } from './route-gate'\nimport {\n outreachMethodNotAllowed,\n outreachOk,\n outreachRefusal,\n readOutreachDocumentId,\n readOutreachJsonBody,\n} from './route-http'\n\n/**\n * ENROLLING PEOPLE IN A SEQUENCE (AGL-2980): `outreach/enroll/preview` and\n * `outreach/enroll`.\n *\n * The preview reads the people a saved Contacts view or a search named —\n * at most {@link OUTREACH_ENROLL_BATCH_MAX} — and says where each stands:\n * eligible, blocked with the gate's own sentence, or waiting on the rep for\n * a personal line and the attestations only they can make. Confirm reads\n * everything again and asks the gates again with what the rep supplied,\n * because a preview is a picture of a moment: someone can unsubscribe\n * between the two, and a request can claim anything. Nothing the preview\n * answered is trusted by the enroll.\n *\n * Both read the CRM's people, so both ask for `data.manage` beside\n * `outreach.use` — the permission the contacts' own rules read with, so\n * Outreach is no side door into the address book.\n *\n * Only an ACTIVE sequence takes people: enrolling schedules the first step\n * in the mailbox's hours, and a draft's steps and a paused sequence's timing\n * are not yet what will be sent.\n */\n\n/** The permission enrolling reads the CRM with. */\nexport const OUTREACH_READS_PEOPLE: OutreachRouteExtraPermission = {\n key: 'data.manage',\n refusal: \"Enrolling reads your CRM's contacts, and your role does not include Manage data.\",\n}\n\n/** Everything enrolling reaches beyond the routes' own dependencies. */\nexport interface OutreachEnrollRouteDeps extends OutreachRouteDeps {\n /**\n * The addresses a saved Contacts view selects among one site's contacts,\n * read the way the dynamic-list sweep reads one, and whether the read\n * reached the whole view before its budget.\n */\n crmViewEmails(input: { hostId: string; viewId: string }): Promise<{ emails: string[]; complete: boolean }>\n /**\n * The workspace's record system on the timeline seam (AGL-3274), when a\n * plugin keeps one: the enroll files \"Enrolled in\" on the person's record\n * — the runtime's `timeline`, reached the same way.\n */\n timeline(): PluginRecordTimelineWriter | null\n}\n\nexport interface OutreachEnrollRoutes {\n preview: PluginWebApiHandler\n confirm: PluginWebApiHandler\n}\n\n/** The activity line an enroll writes. */\nexport function outreachEnrolledActivity(count: number): string {\n return count === 1\n ? 'Enrolled 1 person in a sequence'\n : `Enrolled ${count} people in a sequence`\n}\n\n/** Lookups for a person the gates will never be asked about: every one unchecked. */\nconst UNCHECKED: OutreachGateLookups = {\n platformSuppressed: null,\n hostSuppressed: null,\n salesTopicState: null,\n doNotContact: null,\n doNotContactDomain: null,\n workspaceMembers: null,\n openEnrollments: null,\n hasInboundEmail: null,\n}\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** gRPC `ALREADY_EXISTS`, which `create()` rejects with when the document is there. */\nconst ALREADY_EXISTS = 6\n\nasync function loadActiveSequence(\n firestore: Firestore,\n orgId: string,\n rawSequenceId: unknown,\n): Promise<Response | OutreachSequence> {\n const sequenceId = readOutreachDocumentId(rawSequenceId)\n if (!sequenceId) return outreachRefusal(400, 'invalid-request', 'Name the sequence to enroll people in.')\n const snapshot = await outreachOrgCollection(firestore, orgId, 'sequences').doc(sequenceId).get()\n const sequence = readStoredOutreachSequence(sequenceId, snapshot.exists ? snapshot.data() : undefined)\n if (!sequence) return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n if (sequence.status === 'archived') {\n return outreachRefusal(409, 'sequence-archived', 'This sequence is archived and takes no one new.')\n }\n if (sequence.status !== 'active') {\n return outreachRefusal(409, 'sequence-not-active', 'Activate this sequence before enrolling people.')\n }\n return sequence\n}\n\n/** What every person in one request is judged against. */\ninterface EnrollContext {\n settings: OutreachGateQuestion['settings']\n contactGroupId: string\n siteName: string\n}\n\nasync function enrollContext(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n sequence: OutreachSequence,\n): Promise<EnrollContext> {\n const [orgSettings, host] = await Promise.all([\n readOutreachComplianceSettingsDoc(firestore, caller.orgId),\n firestore.collection('hosts').doc(sequence.hostId).get(),\n ])\n return {\n // The organization's countries are a ceiling over the sequence's.\n settings: {\n ...sequence.settings,\n allowedCountries: effectiveOutreachAllowedCountries(\n sequence.settings.allowedCountries,\n orgSettings.allowedCountries,\n ),\n },\n contactGroupId: consentGroupForHost(caller.org, sequence.hostId).groupId,\n siteName: host.exists ? String(host.get('name') ?? '') : '',\n }\n}\n\n/**\n * The candidates, the enrollments they already have here, and the gates'\n * lookups — every map keyed by the person's own id.\n *\n * \"Already in this sequence\" is answered two ways (AGL-3234): by the id the\n * person's enrollment would have, and by any enrollment in this sequence\n * that carries the person's ADDRESS. The second is what keeps a contact\n * from being enrolled beside the lead they were converted from: the lead's\n * enrollment followed them, under the lead's key, and still names them.\n */\nasync function readPeople(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n sequence: OutreachSequence,\n context: EnrollContext,\n people: readonly OutreachPersonRef[],\n): Promise<{\n candidates: OutreachEnrollCandidate[]\n enrolledHere: Set<string>\n lookups: Map<string, OutreachGateLookups>\n}> {\n const candidates = await readOutreachEnrollCandidates(firestore, {\n orgId: caller.orgId,\n hostId: sequence.hostId,\n contactGroupId: context.contactGroupId,\n people,\n })\n const enrollments = outreachOrgCollection(firestore, caller.orgId, 'enrollments')\n const existing = candidates.length\n ? await firestore.getAll(\n ...candidates.map((candidate) => enrollments.doc(outreachEnrollmentId(sequence.id, candidate.personId))),\n )\n : []\n const enrolledHere = new Set(\n candidates.filter((_candidate, index) => existing[index]?.exists).map((candidate) => candidate.personId),\n )\n const askable = candidates.filter((candidate) => candidate.contact && candidate.visible && candidate.email)\n const lookups = askable.length\n ? await readOutreachGateLookups(firestore, {\n orgId: caller.orgId,\n hostId: sequence.hostId,\n people: askable.map((candidate) => ({\n personId: candidate.personId,\n contactId: candidate.contactId || null,\n leadId: candidate.leadId,\n email: String(candidate.email),\n })),\n })\n : new Map<string, OutreachGateLookups>()\n for (const candidate of askable) {\n const open = lookups.get(candidate.personId)?.openEnrollments ?? []\n if (open.some((entry) => entry.sequenceId === sequence.id)) enrolledHere.add(candidate.personId)\n }\n return { candidates, enrolledHere, lookups }\n}\n\nconst uniqueIds = (values: unknown): string[] =>\n Array.isArray(values)\n ? [...new Set(values.map(readOutreachDocumentId).filter((id): id is string => id !== null))]\n : []\n\n/** The most leads a saved Leads view reads — the Leads list's own window. */\nconst LEADS_VIEW_WINDOW = 200\n\n/**\n * The people a saved LEADS view selects (AGL-3234): the sequence's site's\n * most recently seen leads, narrowed by the view's status clause the way\n * the Leads list narrows its window — open leads when the view names no\n * status, one status when it does, everything for `all`. The person key IS\n * the id, so there is no address to resolve — and since AGL-3275 the window is\n * narrowed by `visibleTo` rather than by the collection's parent.\n */\nasync function leadsViewPeople(\n firestore: Firestore,\n orgId: string,\n sequence: OutreachSequence,\n filters: ReturnType<typeof normalizeCrmViewFilters>,\n): Promise<{ people: OutreachPersonRef[]; total: number; truncated: boolean }> {\n const statusClause = filters.find((clause) => clause.field === 'status' && clause.op === 'equals')\n const wanted = String(statusClause?.value ?? 'open')\n /*\n * SCOPED (AGL-3275). The collection is org-wide, so the window is narrowed\n * to what the sequence's site may see before it is ordered — otherwise an\n * agency's sequence would offer another client's people to enroll. The\n * composite index for `visibleTo array-contains-any` + `lastSeenAtMs desc`\n * is in `cloud/firestore.indexes.json`.\n */\n const window = await firestore\n .collection('orgs')\n .doc(orgId)\n .collection('leads')\n .where('visibleTo', 'array-contains-any', scopeTokensForHost(sequence.hostId))\n .orderBy('lastSeenAtMs', 'desc')\n .limit(LEADS_VIEW_WINDOW + 1)\n .get()\n const matching = window.docs.slice(0, LEADS_VIEW_WINDOW).filter((doc) => {\n const lead = doc.data() as Record<string, unknown>\n if (wanted === 'all') return true\n if (wanted === 'open') return isCrmLeadOpen(lead as never) && !lead['convertedContactId']\n return crmLeadStatus(lead as never) === wanted\n })\n return {\n people: matching.slice(0, OUTREACH_ENROLL_BATCH_MAX).map((doc) => ({ kind: 'lead', id: doc.id })),\n total: matching.length,\n truncated: matching.length > OUTREACH_ENROLL_BATCH_MAX || window.docs.length > LEADS_VIEW_WINDOW,\n }\n}\n\nexport function createOutreachEnrollRoutes(deps: OutreachEnrollRouteDeps): OutreachEnrollRoutes {\n /**\n * The person joins the sequence's campaigns (AGL-3254), the moment they\n * are enrolled: `arrayUnion` on the lead's own `campaignIds`, or on the\n * contact's facet for the sequence's site — the same field, the same\n * shape, every other campaign member carries — and `enrolled` credited\n * to each campaign. After the enrollment is created and never before:\n * a person the create refused joins nothing. Neither write may fail the\n * enrollment, which already exists; a membership that could not be\n * stamped is one the list's bulk bar can add by hand.\n */\n async function joinSequenceCampaigns(\n firestore: Firestore,\n orgId: string,\n sequence: OutreachSequence,\n contactGroupId: string,\n candidate: OutreachEnrollCandidate,\n nowMs: number,\n ): Promise<void> {\n const campaignIds = normalizeCampaignIds(sequence.campaignIds)\n if (!campaignIds.length) return\n try {\n const ref =\n candidate.target === 'lead' && candidate.leadId\n ? firestore.collection('orgs').doc(orgId).collection('leads').doc(candidate.leadId)\n : candidate.contactId\n ? firestore.collection('orgs').doc(orgId).collection('contacts').doc(candidate.contactId)\n : null\n if (ref) {\n const field =\n candidate.target === 'lead' ? CAMPAIGN_MEMBERSHIP_FIELD : contactCampaignFieldPath(contactGroupId)\n await ref.update({ [field]: FieldValue.arrayUnion(...campaignIds), updatedAt: FieldValue.serverTimestamp() })\n }\n } catch (error) {\n console.error('[outreach] the enrolled person could not join the sequence’s campaigns', error)\n }\n try {\n await deps.creditCampaign({ hostId: sequence.hostId, campaignIds, outcome: 'enrolled', atMs: nowMs })\n } catch (error) {\n console.error('[outreach] the enrollment could not be credited to the sequence’s campaigns', error)\n }\n }\n\n /**\n * The names of the sequence's campaigns as they stand, for the entry the\n * enroll files on the person's record (AGL-3274). Read once per request,\n * not per person; a container that is gone answers nothing and the entry\n * names the rest.\n */\n async function sequenceCampaignNames(firestore: Firestore, sequence: OutreachSequence): Promise<string[]> {\n const campaignIds = normalizeCampaignIds(sequence.campaignIds)\n if (!campaignIds.length) return []\n try {\n const containers = firestore.collection('hosts').doc(sequence.hostId).collection('emailCampaigns')\n const found = await firestore.getAll(...campaignIds.map((id) => containers.doc(id)))\n return found.map((snapshot) => String(snapshot.get('name') ?? '').trim()).filter(Boolean)\n } catch (error) {\n console.error('[outreach] the sequence’s campaigns could not be named for the record', error)\n return []\n }\n }\n\n /**\n * The people a source names, the batch limit applied: every contact a\n * search picked, every lead picked from the sequence's site, or the\n * people a saved Contacts or Leads view selects there — contacts found by\n * address, leads by their own key.\n */\n async function sourcePeople(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n sequence: OutreachSequence,\n source: Record<string, unknown>,\n ): Promise<Response | { people: OutreachPersonRef[]; total: number; truncated: boolean }> {\n if (source['kind'] === 'contacts' || source['kind'] === 'leads') {\n const kind = source['kind'] === 'leads' ? 'lead' : 'contact'\n const ids = uniqueIds(kind === 'lead' ? source['leadIds'] : source['contactIds'])\n if (!ids.length) return outreachRefusal(400, 'invalid-request', 'Pick the people to enroll.')\n return {\n people: ids.slice(0, OUTREACH_ENROLL_BATCH_MAX).map((id) => ({ kind, id })),\n total: ids.length,\n truncated: ids.length > OUTREACH_ENROLL_BATCH_MAX,\n }\n }\n if (source['kind'] !== 'view') {\n return outreachRefusal(400, 'invalid-request', 'Enroll people from a saved view or a search.')\n }\n const viewId = readOutreachDocumentId(source['viewId'])\n const view = viewId\n ? await firestore.collection('orgs').doc(caller.orgId).collection(CRM_COLLECTIONS.views).doc(viewId).get()\n : null\n const data = view?.exists ? (view.data() as Record<string, unknown>) : null\n // A colleague's private view is theirs: it is not listed for this\n // reader, so it does not exist for them here either.\n if (\n !viewId ||\n !data ||\n !crmViewIsListed({ shared: data['shared'] === true, ownerUid: String(data['ownerUid'] ?? '') }, caller.uid)\n ) {\n return outreachRefusal(404, 'view-not-found', 'That saved view no longer exists.')\n }\n if (data['section'] === 'leads') {\n return leadsViewPeople(firestore, caller.orgId, sequence, normalizeCrmViewFilters(data['filters']))\n }\n if (data['section'] !== 'contacts') {\n return outreachRefusal(400, 'view-unsupported', 'Enroll from a saved view of Contacts or Leads.')\n }\n const { unsupported } = dynamicListDimensionsForCrmView(normalizeCrmViewFilters(data['filters']))\n if (unsupported.length) {\n // Dropping the filter enrolling cannot read would enroll more people\n // than the view shows, so the view is refused whole.\n return outreachRefusal(\n 400,\n 'view-unsupported',\n `This view filters on ${unsupported.map((clause) => clause.label || clause.field).join(', ')}, which enrolling can't apply. Pick another view, or search.`,\n )\n }\n const { emails, complete } = await deps.crmViewEmails({ hostId: sequence.hostId, viewId })\n const ordered = [...new Set(emails)].sort()\n const contacts = firestore.collection('orgs').doc(caller.orgId).collection('contacts')\n const found = await Promise.all(\n ordered\n .slice(0, OUTREACH_ENROLL_BATCH_MAX)\n .map((email) => findContactByEmail(contacts, email, { hostId: sequence.hostId })),\n )\n return {\n people: [...new Set(found.filter((snapshot) => snapshot).map((snapshot) => String(snapshot?.id)))].map(\n (id) => ({ kind: 'contact', id }),\n ),\n total: ordered.length,\n truncated: ordered.length > OUTREACH_ENROLL_BATCH_MAX || !complete,\n }\n }\n\n const preview: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate, [OUTREACH_READS_PEOPLE])\n if (caller instanceof Response) return caller\n const firestore = deps.firestore()\n const sequence = await loadActiveSequence(firestore, caller.orgId, body['sequenceId'])\n if (sequence instanceof Response) return sequence\n const source = body['source'] && typeof body['source'] === 'object' ? (body['source'] as Record<string, unknown>) : {}\n const named = await sourcePeople(firestore, caller, sequence, source)\n if (named instanceof Response) return named\n const context = await enrollContext(firestore, caller, sequence)\n const { candidates, enrolledHere, lookups } = await readPeople(\n firestore,\n caller,\n sequence,\n context,\n named.people,\n )\n const people = candidates.map((candidate) =>\n previewOutreachPerson(\n {\n candidate,\n lookups: lookups.get(candidate.personId) ?? UNCHECKED,\n settings: context.settings,\n contactGroupId: context.contactGroupId,\n },\n { siteName: context.siteName, alreadyInSequence: enrolledHere.has(candidate.personId) },\n ),\n )\n return outreachOk({\n ok: true,\n people,\n total: named.total,\n truncated: named.truncated,\n } satisfies OutreachEnrollPreviewResponse)\n }\n\n const confirm: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate, [OUTREACH_READS_PEOPLE])\n if (caller instanceof Response) return caller\n const firestore = deps.firestore()\n const sequence = await loadActiveSequence(firestore, caller.orgId, body['sequenceId'])\n if (sequence instanceof Response) return sequence\n\n // Each person once, by whichever record they are: a contact by id, or a\n // lead by its key (AGL-3234). A person named both ways is a contact.\n const requested = new Map<\n string,\n { ref: OutreachPersonRef; personalLine: string; attestations: unknown[] }\n >()\n for (const entry of Array.isArray(body['people']) ? body['people'] : []) {\n const person = (entry && typeof entry === 'object' ? entry : {}) as Record<string, unknown>\n const contactId = readOutreachDocumentId(person['contactId'])\n const leadId = readOutreachDocumentId(person['leadId'])\n const ref: OutreachPersonRef | null = contactId\n ? { kind: 'contact', id: contactId }\n : leadId\n ? { kind: 'lead', id: leadId }\n : null\n if (!ref || requested.has(ref.id)) continue\n requested.set(ref.id, {\n ref,\n personalLine: typeof person['personalLine'] === 'string' ? person['personalLine'] : '',\n attestations: Array.isArray(person['attestations']) ? person['attestations'] : [],\n })\n }\n if (!requested.size) return outreachRefusal(400, 'invalid-request', 'Pick the people to enroll.')\n if (requested.size > OUTREACH_ENROLL_BATCH_MAX) {\n return outreachRefusal(\n 400,\n 'invalid-request',\n `Enroll at most ${OUTREACH_ENROLL_BATCH_MAX} people at a time.`,\n )\n }\n\n const mailboxSnapshot = sequence.mailboxId\n ? await outreachOrgCollection(firestore, caller.orgId, 'mailboxes').doc(sequence.mailboxId).get()\n : null\n const mailbox = readStoredOutreachMailbox(\n sequence.mailboxId,\n mailboxSnapshot?.exists ? mailboxSnapshot.data() : undefined,\n )\n if (!mailbox || mailbox.status === 'disconnected') {\n return outreachRefusal(\n 409,\n 'mailbox-unavailable',\n \"This sequence's mailbox is no longer connected. Choose another mailbox for it before enrolling people.\",\n )\n }\n const nowMs = deps.now()\n if (planOutreachFirstDue({ sequence, mailbox, enrolledAtMs: nowMs, random: deps.random }) === null) {\n return outreachRefusal(\n 409,\n 'mailbox-unavailable',\n \"This sequence's sending hours never open in its mailbox's timezone. Fix the hours before enrolling people.\",\n )\n }\n\n const context = await enrollContext(firestore, caller, sequence)\n const { candidates, enrolledHere, lookups } = await readPeople(\n firestore,\n caller,\n sequence,\n context,\n [...requested.values()].map((entry) => entry.ref),\n )\n const enrollments = outreachOrgCollection(firestore, caller.orgId, 'enrollments')\n const campaignNames = await sequenceCampaignNames(firestore, sequence)\n const results: OutreachEnrollOutcome[] = await Promise.all(\n candidates.map(async (candidate): Promise<OutreachEnrollOutcome> => {\n const named = {\n personId: candidate.personId,\n target: candidate.target,\n contactId: candidate.contactId,\n leadId: candidate.leadId,\n }\n const supplied = requested.get(candidate.personId) ?? { personalLine: '', attestations: [] }\n const decision = decideOutreachEnrollment(\n {\n candidate,\n lookups: lookups.get(candidate.personId) ?? UNCHECKED,\n settings: context.settings,\n contactGroupId: context.contactGroupId,\n },\n { siteName: context.siteName, alreadyInSequence: enrolledHere.has(candidate.personId) },\n { personalLine: supplied.personalLine, attestations: supplied.attestations, uid: caller.uid, nowMs },\n )\n if (!decision.allowed || !decision.email) {\n return { ...named, email: candidate.email, outcome: 'blocked', blocks: decision.blocks }\n }\n const id = outreachEnrollmentId(sequence.id, candidate.personId)\n const enrollment = buildOutreachEnrollment({\n id,\n sequence,\n mailbox,\n target: candidate.target,\n contactId: candidate.contactId,\n leadId: candidate.leadId,\n contactName: candidate.name,\n email: decision.email,\n cold: decision.cold,\n personalLine: supplied.personalLine,\n attestations: decision.attestations,\n enrolledByUid: caller.uid,\n nowMs,\n random: deps.random,\n })\n try {\n await enrollments.doc(id).create(enrollment)\n } catch (error) {\n if ((error as { code?: unknown })?.code !== ALREADY_EXISTS) throw error\n return {\n ...named,\n email: candidate.email,\n outcome: 'blocked',\n blocks: [\n {\n code: 'already_in_sequence',\n reason: \"This person has already been in this sequence, so they won't be sent its emails again.\",\n },\n ],\n }\n }\n await joinSequenceCampaigns(firestore, caller.orgId, sequence, context.contactGroupId, candidate, nowMs)\n // The person's record says so (AGL-3274): \"Enrolled in <sequence>\",\n // with the campaigns it carried them into, once per enrollment.\n const entry = outreachEnrolledEntry({ enrollmentId: id, sequenceName: sequence.name, campaignNames })\n await fileOutreachNote(deps, {\n orgId: caller.orgId,\n hostId: sequence.hostId,\n link: outreachEnrollmentLink(enrollment),\n dedupeKey: entry.dedupeKey,\n body: entry.body,\n atMs: nowMs,\n })\n return { ...named, email: decision.email, outcome: 'enrolled', enrollmentId: id }\n }),\n )\n const enrolled = results.filter((result) => result.outcome === 'enrolled').length\n if (enrolled) {\n await deps.logOrgActivity(\n caller.orgId,\n { uid: caller.uid, email: caller.email },\n outreachEnrolledActivity(enrolled),\n { type: OUTREACH_SEQUENCE_ACTIVITY_TARGET, id: sequence.id, name: sequence.name },\n )\n }\n return outreachOk({ ok: true, results, enrolled } satisfies OutreachEnrollResponse)\n }\n\n return { preview, confirm }\n}\n"],"names":["CAMPAIGN_MEMBERSHIP_FIELD","contactCampaignFieldPath","normalizeCampaignIds","consentGroupForHost","scopeTokensForHost","CRM_COLLECTIONS","crmLeadStatus","crmViewIsListed","isCrmLeadOpen","normalizeCrmViewFilters","dynamicListDimensionsForCrmView","findContactByEmail","FieldValue","outreachEnrolledEntry","buildOutreachEnrollment","planOutreachFirstDue","decideOutreachEnrollment","previewOutreachPerson","readOutreachEnrollCandidates","readOutreachGateLookups","effectiveOutreachAllowedCountries","OUTREACH_ENROLL_BATCH_MAX","readOutreachComplianceSettingsDoc","fileOutreachNote","outreachEnrollmentId","outreachEnrollmentLink","outreachOrgCollection","readStoredOutreachMailbox","readStoredOutreachSequence","OUTREACH_SEQUENCE_ACTIVITY_TARGET","outreachRouteGate","outreachMethodNotAllowed","outreachOk","outreachRefusal","readOutreachDocumentId","readOutreachJsonBody","OUTREACH_READS_PEOPLE","key","refusal","outreachEnrolledActivity","count","UNCHECKED","platformSuppressed","hostSuppressed","salesTopicState","doNotContact","doNotContactDomain","workspaceMembers","openEnrollments","hasInboundEmail","ALREADY_EXISTS","loadActiveSequence","firestore","orgId","rawSequenceId","sequenceId","snapshot","doc","get","sequence","exists","data","undefined","status","enrollContext","caller","host","orgSettings","Promise","all","collection","hostId","settings","allowedCountries","contactGroupId","org","groupId","siteName","String","readPeople","context","people","candidates","enrollments","existing","length","getAll","map","candidate","id","personId","enrolledHere","Set","filter","_candidate","index","askable","contact","visible","email","lookups","contactId","leadId","Map","open","some","entry","add","uniqueIds","values","Array","isArray","LEADS_VIEW_WINDOW","leadsViewPeople","filters","statusClause","find","clause","field","op","wanted","value","window","where","orderBy","limit","matching","docs","slice","lead","kind","total","truncated","createOutreachEnrollRoutes","deps","joinSequenceCampaigns","nowMs","campaignIds","ref","target","update","arrayUnion","updatedAt","serverTimestamp","error","console","creditCampaign","outcome","atMs","sequenceCampaignNames","containers","found","trim","Boolean","sourcePeople","source","ids","viewId","view","views","shared","ownerUid","uid","unsupported","label","join","emails","complete","crmViewEmails","ordered","sort","contacts","preview","request","method","body","gate","Response","named","alreadyInSequence","has","ok","confirm","requested","person","set","personalLine","attestations","size","mailboxSnapshot","mailboxId","mailbox","now","enrolledAtMs","random","campaignNames","results","supplied","decision","allowed","blocks","enrollment","contactName","name","cold","enrolledByUid","create","code","reason","enrollmentId","sequenceName","link","dedupeKey","enrolled","result","logOrgActivity","type"],"mappings":"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"}
@@ -77,7 +77,9 @@ import { outreachMethodNotAllowed, outreachOk, outreachRefusal, readOutreachDocu
77
77
  sequence.hostId ? firestore.collection('hosts').doc(sequence.hostId).get() : null,
78
78
  step.templateId ? org.collection(CRM_COLLECTIONS.emailTemplates).doc(step.templateId).get() : null,
79
79
  contactId ? org.collection('contacts').doc(contactId).get() : null,
80
- leadId && sequence.hostId ? firestore.collection('hosts').doc(sequence.hostId).collection('leads').doc(leadId).get() : null
80
+ // One org row (AGL-3275); the site is still required because a lead
81
+ // preview is rendered in a site's context.
82
+ leadId && sequence.hostId ? org.collection('leads').doc(leadId).get() : null
81
83
  ]);
82
84
  const leadData = (lead == null ? void 0 : lead.exists) ? lead.data() : null;
83
85
  if (leadId && !leadData) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/preview-routes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { consentGroupForHost } from '@aglyn/aglyn/app-utils/consent-groups'\nimport { normalizeContactEmail } from '@aglyn/aglyn/app-utils/contacts'\nimport { CRM_COLLECTIONS } from '@aglyn/aglyn/app-utils/crm'\nimport { normalizeCrmEmailTemplate } from '@aglyn/aglyn/app-utils/crm-email-templates'\nimport { visibleToHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport type { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport { firstEmailStepIndex } from '../engine/sequence-validation'\nimport { leadAsContact } from '../enrollment/enroll-people'\nimport type { OutreachPreviewResponse } from '../model/outreach-api'\nimport {\n OUTREACH_SAMPLE_PERSON,\n outreachSampleMergeContext,\n previewOutreachStep,\n} from '../model/step-preview'\nimport { readOutreachComplianceSettingsDoc } from '../storage/compliance-settings-store'\nimport {\n outreachOrgCollection,\n readStoredOutreachMailbox,\n readStoredOutreachSequence,\n} from '../storage/outreach-records'\nimport { OUTREACH_READS_PEOPLE } from './enroll-routes'\nimport type { OutreachRouteDeps } from './route-deps'\nimport { outreachRouteGate } from './route-gate'\nimport {\n outreachMethodNotAllowed,\n outreachOk,\n outreachRefusal,\n readOutreachDocumentId,\n readOutreachJsonBody,\n} from './route-http'\n\n/**\n * ONE EMAIL OF A SAVED SEQUENCE, AS A PERSON WOULD GET IT (AGL-2980):\n * `outreach/preview`.\n *\n * Written from what is stored — the sequence, the CRM template a step names,\n * the mailbox it sends as, the organization's footer — by the engine's own\n * composer (`previewOutreachStep`), for a real contact with the personal\n * line the rep is writing, or for a sample person. The enroll dialog shows\n * it beside each person before Confirm, so the rep reads the sentence they\n * wrote in the email it goes into.\n *\n * A real contact is read from the CRM, so naming one asks for `data.manage`\n * as enrolling does; the sample person asks for nothing more.\n */\n\nexport function createOutreachPreviewRoute(deps: OutreachRouteDeps): PluginWebApiHandler {\n return async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const contactId = body['contactId'] === undefined ? null : readOutreachDocumentId(body['contactId'])\n if (body['contactId'] !== undefined && !contactId) {\n return outreachRefusal(400, 'invalid-request', 'Name the contact to preview for.')\n }\n // A lead the sequence's site holds (AGL-3234), previewed as the contact it would be.\n const leadId = body['leadId'] === undefined ? null : readOutreachDocumentId(body['leadId'])\n if (body['leadId'] !== undefined && !leadId) {\n return outreachRefusal(400, 'invalid-request', 'Name the lead to preview for.')\n }\n const caller = await outreachRouteGate(\n request,\n body['orgId'],\n deps.gate,\n contactId || leadId ? [OUTREACH_READS_PEOPLE] : [],\n )\n if (caller instanceof Response) return caller\n const sequenceId = readOutreachDocumentId(body['sequenceId'])\n if (!sequenceId) return outreachRefusal(400, 'invalid-request', 'Name the sequence to preview.')\n const firestore = deps.firestore()\n const org = firestore.collection('orgs').doc(caller.orgId)\n const snapshot = await outreachOrgCollection(firestore, caller.orgId, 'sequences').doc(sequenceId).get()\n const sequence = readStoredOutreachSequence(sequenceId, snapshot.exists ? snapshot.data() : undefined)\n if (!sequence) return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n\n const requested = body['stepIndex']\n const stepIndex = typeof requested === 'number' ? requested : firstEmailStepIndex(sequence.steps)\n const step = sequence.steps[stepIndex]\n if (step?.kind !== 'email') {\n return outreachRefusal(400, 'invalid-request', 'That step does not send an email.')\n }\n\n const [orgSettings, mailboxSnapshot, host, template, contact, lead] = await Promise.all([\n readOutreachComplianceSettingsDoc(firestore, caller.orgId),\n sequence.mailboxId ? outreachOrgCollection(firestore, caller.orgId, 'mailboxes').doc(sequence.mailboxId).get() : null,\n sequence.hostId ? firestore.collection('hosts').doc(sequence.hostId).get() : null,\n step.templateId ? org.collection(CRM_COLLECTIONS.emailTemplates).doc(step.templateId).get() : null,\n contactId ? org.collection('contacts').doc(contactId).get() : null,\n leadId && sequence.hostId\n ? firestore.collection('hosts').doc(sequence.hostId).collection('leads').doc(leadId).get()\n : null,\n ])\n const leadData = lead?.exists ? (lead.data() as Record<string, unknown>) : null\n if (leadId && !leadData) {\n return outreachRefusal(404, 'contact-not-found', \"That lead isn't in this sequence's site's CRM.\")\n }\n const contactGroupId = consentGroupForHost(caller.org, sequence.hostId).groupId\n const contactData = contact?.exists\n ? (contact.data() as Record<string, unknown>)\n : leadData\n ? leadAsContact(leadData, contactGroupId)\n : null\n if (\n contactId &&\n (!contactData || !visibleToHost(contactData['visibleTo'] as string[] | undefined, sequence.hostId))\n ) {\n return outreachRefusal(404, 'contact-not-found', \"That contact isn't in this sequence's site's CRM.\")\n }\n const mailbox = readStoredOutreachMailbox(\n sequence.mailboxId,\n mailboxSnapshot?.exists ? mailboxSnapshot.data() : undefined,\n )\n const sender = mailbox ? { name: mailbox.displayName, email: mailbox.sendAs || mailbox.email } : null\n const siteName = host?.exists ? String(host.get('name') ?? '') : ''\n const merge = contactData\n ? {\n contact: contactData,\n contactGroupId,\n lead: leadData,\n sender,\n site: { name: siteName },\n }\n : outreachSampleMergeContext({ sender, siteName })\n const personalLine =\n typeof body['personalLine'] === 'string'\n ? body['personalLine']\n : contactData\n ? ''\n : OUTREACH_SAMPLE_PERSON.personalLine\n const result = previewOutreachStep({\n steps: sequence.steps,\n stepIndex,\n orgSettings,\n merge,\n email: contactData ? (normalizeContactEmail(contactData['email']) ?? '') : OUTREACH_SAMPLE_PERSON.email,\n personalLine,\n templateBody: template?.exists\n ? normalizeCrmEmailTemplate(template.data() as Record<string, unknown>).body\n : null,\n })\n return outreachOk({\n ok: true,\n stepIndex,\n subject: result.email?.subject ?? '',\n text: result.email?.text ?? '',\n unresolvedFields: result.unresolvedFields,\n error: result.error,\n } satisfies OutreachPreviewResponse)\n }\n}\n"],"names":["consentGroupForHost","normalizeContactEmail","CRM_COLLECTIONS","normalizeCrmEmailTemplate","visibleToHost","firstEmailStepIndex","leadAsContact","OUTREACH_SAMPLE_PERSON","outreachSampleMergeContext","previewOutreachStep","readOutreachComplianceSettingsDoc","outreachOrgCollection","readStoredOutreachMailbox","readStoredOutreachSequence","OUTREACH_READS_PEOPLE","outreachRouteGate","outreachMethodNotAllowed","outreachOk","outreachRefusal","readOutreachDocumentId","readOutreachJsonBody","createOutreachPreviewRoute","deps","request","host","result","method","body","contactId","undefined","leadId","caller","gate","Response","sequenceId","firestore","org","collection","doc","orgId","snapshot","get","sequence","exists","data","requested","stepIndex","steps","step","kind","orgSettings","mailboxSnapshot","template","contact","lead","Promise","all","mailboxId","hostId","templateId","emailTemplates","leadData","contactGroupId","groupId","contactData","mailbox","sender","name","displayName","email","sendAs","siteName","String","merge","site","personalLine","templateBody","ok","subject","text","unresolvedFields","error"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/preview-routes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { consentGroupForHost } from '@aglyn/aglyn/app-utils/consent-groups'\nimport { normalizeContactEmail } from '@aglyn/aglyn/app-utils/contacts'\nimport { CRM_COLLECTIONS } from '@aglyn/aglyn/app-utils/crm'\nimport { normalizeCrmEmailTemplate } from '@aglyn/aglyn/app-utils/crm-email-templates'\nimport { visibleToHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport type { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport { firstEmailStepIndex } from '../engine/sequence-validation'\nimport { leadAsContact } from '../enrollment/enroll-people'\nimport type { OutreachPreviewResponse } from '../model/outreach-api'\nimport {\n OUTREACH_SAMPLE_PERSON,\n outreachSampleMergeContext,\n previewOutreachStep,\n} from '../model/step-preview'\nimport { readOutreachComplianceSettingsDoc } from '../storage/compliance-settings-store'\nimport {\n outreachOrgCollection,\n readStoredOutreachMailbox,\n readStoredOutreachSequence,\n} from '../storage/outreach-records'\nimport { OUTREACH_READS_PEOPLE } from './enroll-routes'\nimport type { OutreachRouteDeps } from './route-deps'\nimport { outreachRouteGate } from './route-gate'\nimport {\n outreachMethodNotAllowed,\n outreachOk,\n outreachRefusal,\n readOutreachDocumentId,\n readOutreachJsonBody,\n} from './route-http'\n\n/**\n * ONE EMAIL OF A SAVED SEQUENCE, AS A PERSON WOULD GET IT (AGL-2980):\n * `outreach/preview`.\n *\n * Written from what is stored — the sequence, the CRM template a step names,\n * the mailbox it sends as, the organization's footer — by the engine's own\n * composer (`previewOutreachStep`), for a real contact with the personal\n * line the rep is writing, or for a sample person. The enroll dialog shows\n * it beside each person before Confirm, so the rep reads the sentence they\n * wrote in the email it goes into.\n *\n * A real contact is read from the CRM, so naming one asks for `data.manage`\n * as enrolling does; the sample person asks for nothing more.\n */\n\nexport function createOutreachPreviewRoute(deps: OutreachRouteDeps): PluginWebApiHandler {\n return async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const contactId = body['contactId'] === undefined ? null : readOutreachDocumentId(body['contactId'])\n if (body['contactId'] !== undefined && !contactId) {\n return outreachRefusal(400, 'invalid-request', 'Name the contact to preview for.')\n }\n // A lead the sequence's site holds (AGL-3234), previewed as the contact it would be.\n const leadId = body['leadId'] === undefined ? null : readOutreachDocumentId(body['leadId'])\n if (body['leadId'] !== undefined && !leadId) {\n return outreachRefusal(400, 'invalid-request', 'Name the lead to preview for.')\n }\n const caller = await outreachRouteGate(\n request,\n body['orgId'],\n deps.gate,\n contactId || leadId ? [OUTREACH_READS_PEOPLE] : [],\n )\n if (caller instanceof Response) return caller\n const sequenceId = readOutreachDocumentId(body['sequenceId'])\n if (!sequenceId) return outreachRefusal(400, 'invalid-request', 'Name the sequence to preview.')\n const firestore = deps.firestore()\n const org = firestore.collection('orgs').doc(caller.orgId)\n const snapshot = await outreachOrgCollection(firestore, caller.orgId, 'sequences').doc(sequenceId).get()\n const sequence = readStoredOutreachSequence(sequenceId, snapshot.exists ? snapshot.data() : undefined)\n if (!sequence) return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n\n const requested = body['stepIndex']\n const stepIndex = typeof requested === 'number' ? requested : firstEmailStepIndex(sequence.steps)\n const step = sequence.steps[stepIndex]\n if (step?.kind !== 'email') {\n return outreachRefusal(400, 'invalid-request', 'That step does not send an email.')\n }\n\n const [orgSettings, mailboxSnapshot, host, template, contact, lead] = await Promise.all([\n readOutreachComplianceSettingsDoc(firestore, caller.orgId),\n sequence.mailboxId ? outreachOrgCollection(firestore, caller.orgId, 'mailboxes').doc(sequence.mailboxId).get() : null,\n sequence.hostId ? firestore.collection('hosts').doc(sequence.hostId).get() : null,\n step.templateId ? org.collection(CRM_COLLECTIONS.emailTemplates).doc(step.templateId).get() : null,\n contactId ? org.collection('contacts').doc(contactId).get() : null,\n // One org row (AGL-3275); the site is still required because a lead\n // preview is rendered in a site's context.\n leadId && sequence.hostId ? org.collection('leads').doc(leadId).get() : null,\n ])\n const leadData = lead?.exists ? (lead.data() as Record<string, unknown>) : null\n if (leadId && !leadData) {\n return outreachRefusal(404, 'contact-not-found', \"That lead isn't in this sequence's site's CRM.\")\n }\n const contactGroupId = consentGroupForHost(caller.org, sequence.hostId).groupId\n const contactData = contact?.exists\n ? (contact.data() as Record<string, unknown>)\n : leadData\n ? leadAsContact(leadData, contactGroupId)\n : null\n if (\n contactId &&\n (!contactData || !visibleToHost(contactData['visibleTo'] as string[] | undefined, sequence.hostId))\n ) {\n return outreachRefusal(404, 'contact-not-found', \"That contact isn't in this sequence's site's CRM.\")\n }\n const mailbox = readStoredOutreachMailbox(\n sequence.mailboxId,\n mailboxSnapshot?.exists ? mailboxSnapshot.data() : undefined,\n )\n const sender = mailbox ? { name: mailbox.displayName, email: mailbox.sendAs || mailbox.email } : null\n const siteName = host?.exists ? String(host.get('name') ?? '') : ''\n const merge = contactData\n ? {\n contact: contactData,\n contactGroupId,\n lead: leadData,\n sender,\n site: { name: siteName },\n }\n : outreachSampleMergeContext({ sender, siteName })\n const personalLine =\n typeof body['personalLine'] === 'string'\n ? body['personalLine']\n : contactData\n ? ''\n : OUTREACH_SAMPLE_PERSON.personalLine\n const result = previewOutreachStep({\n steps: sequence.steps,\n stepIndex,\n orgSettings,\n merge,\n email: contactData ? (normalizeContactEmail(contactData['email']) ?? '') : OUTREACH_SAMPLE_PERSON.email,\n personalLine,\n templateBody: template?.exists\n ? normalizeCrmEmailTemplate(template.data() as Record<string, unknown>).body\n : null,\n })\n return outreachOk({\n ok: true,\n stepIndex,\n subject: result.email?.subject ?? '',\n text: result.email?.text ?? '',\n unresolvedFields: result.unresolvedFields,\n error: result.error,\n } satisfies OutreachPreviewResponse)\n }\n}\n"],"names":["consentGroupForHost","normalizeContactEmail","CRM_COLLECTIONS","normalizeCrmEmailTemplate","visibleToHost","firstEmailStepIndex","leadAsContact","OUTREACH_SAMPLE_PERSON","outreachSampleMergeContext","previewOutreachStep","readOutreachComplianceSettingsDoc","outreachOrgCollection","readStoredOutreachMailbox","readStoredOutreachSequence","OUTREACH_READS_PEOPLE","outreachRouteGate","outreachMethodNotAllowed","outreachOk","outreachRefusal","readOutreachDocumentId","readOutreachJsonBody","createOutreachPreviewRoute","deps","request","host","result","method","body","contactId","undefined","leadId","caller","gate","Response","sequenceId","firestore","org","collection","doc","orgId","snapshot","get","sequence","exists","data","requested","stepIndex","steps","step","kind","orgSettings","mailboxSnapshot","template","contact","lead","Promise","all","mailboxId","hostId","templateId","emailTemplates","leadData","contactGroupId","groupId","contactData","mailbox","sender","name","displayName","email","sendAs","siteName","String","merge","site","personalLine","templateBody","ok","subject","text","unresolvedFields","error"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,mBAAmB,QAAQ,wCAAuC;AAC3E,SAASC,qBAAqB,QAAQ,kCAAiC;AACvE,SAASC,eAAe,QAAQ,6BAA4B;AAC5D,SAASC,yBAAyB,QAAQ,6CAA4C;AACtF,SAASC,aAAa,QAAQ,sCAAqC;AAEnE,SAASC,mBAAmB,QAAQ,mCAA+B;AACnE,SAASC,aAAa,QAAQ,iCAA6B;AAE3D,SACEC,sBAAsB,EACtBC,0BAA0B,EAC1BC,mBAAmB,QACd,2BAAuB;AAC9B,SAASC,iCAAiC,QAAQ,0CAAsC;AACxF,SACEC,qBAAqB,EACrBC,yBAAyB,EACzBC,0BAA0B,QACrB,iCAA6B;AACpC,SAASC,qBAAqB,QAAQ,qBAAiB;AAEvD,SAASC,iBAAiB,QAAQ,kBAAc;AAChD,SACEC,wBAAwB,EACxBC,UAAU,EACVC,eAAe,EACfC,sBAAsB,EACtBC,oBAAoB,QACf,kBAAc;AAErB;;;;;;;;;;;;;CAaC,GAED,OAAO,SAASC,2BAA2BC,IAAuB;IAChE,OAAO,OAAOC;YAiE2BC,WAqBfvB;YASbwB,eACHA;QA/FR,IAAIF,QAAQG,MAAM,KAAK,QAAQ,OAAOV,yBAAyB;QAC/D,MAAMW,OAAO,MAAMP,qBAAqBG;QACxC,MAAMK,YAAYD,IAAI,CAAC,YAAY,KAAKE,YAAY,OAAOV,uBAAuBQ,IAAI,CAAC,YAAY;QACnG,IAAIA,IAAI,CAAC,YAAY,KAAKE,aAAa,CAACD,WAAW;YACjD,OAAOV,gBAAgB,KAAK,mBAAmB;QACjD;QACA,qFAAqF;QACrF,MAAMY,SAASH,IAAI,CAAC,SAAS,KAAKE,YAAY,OAAOV,uBAAuBQ,IAAI,CAAC,SAAS;QAC1F,IAAIA,IAAI,CAAC,SAAS,KAAKE,aAAa,CAACC,QAAQ;YAC3C,OAAOZ,gBAAgB,KAAK,mBAAmB;QACjD;QACA,MAAMa,SAAS,MAAMhB,kBACnBQ,SACAI,IAAI,CAAC,QAAQ,EACbL,KAAKU,IAAI,EACTJ,aAAaE,SAAS;YAAChB;SAAsB,GAAG,EAAE;QAEpD,IAAIiB,kBAAkBE,UAAU,OAAOF;QACvC,MAAMG,aAAaf,uBAAuBQ,IAAI,CAAC,aAAa;QAC5D,IAAI,CAACO,YAAY,OAAOhB,gBAAgB,KAAK,mBAAmB;QAChE,MAAMiB,YAAYb,KAAKa,SAAS;QAChC,MAAMC,MAAMD,UAAUE,UAAU,CAAC,QAAQC,GAAG,CAACP,OAAOQ,KAAK;QACzD,MAAMC,WAAW,MAAM7B,sBAAsBwB,WAAWJ,OAAOQ,KAAK,EAAE,aAAaD,GAAG,CAACJ,YAAYO,GAAG;QACtG,MAAMC,WAAW7B,2BAA2BqB,YAAYM,SAASG,MAAM,GAAGH,SAASI,IAAI,KAAKf;QAC5F,IAAI,CAACa,UAAU,OAAOxB,gBAAgB,KAAK,sBAAsB;QAEjE,MAAM2B,YAAYlB,IAAI,CAAC,YAAY;QACnC,MAAMmB,YAAY,OAAOD,cAAc,WAAWA,YAAYxC,oBAAoBqC,SAASK,KAAK;QAChG,MAAMC,OAAON,SAASK,KAAK,CAACD,UAAU;QACtC,IAAIE,CAAAA,wBAAAA,KAAMC,IAAI,MAAK,SAAS;YAC1B,OAAO/B,gBAAgB,KAAK,mBAAmB;QACjD;QAEA,MAAM,CAACgC,aAAaC,iBAAiB3B,MAAM4B,UAAUC,SAASC,KAAK,GAAG,MAAMC,QAAQC,GAAG,CAAC;YACtF9C,kCAAkCyB,WAAWJ,OAAOQ,KAAK;YACzDG,SAASe,SAAS,GAAG9C,sBAAsBwB,WAAWJ,OAAOQ,KAAK,EAAE,aAAaD,GAAG,CAACI,SAASe,SAAS,EAAEhB,GAAG,KAAK;YACjHC,SAASgB,MAAM,GAAGvB,UAAUE,UAAU,CAAC,SAASC,GAAG,CAACI,SAASgB,MAAM,EAAEjB,GAAG,KAAK;YAC7EO,KAAKW,UAAU,GAAGvB,IAAIC,UAAU,CAACnC,gBAAgB0D,cAAc,EAAEtB,GAAG,CAACU,KAAKW,UAAU,EAAElB,GAAG,KAAK;YAC9Fb,YAAYQ,IAAIC,UAAU,CAAC,YAAYC,GAAG,CAACV,WAAWa,GAAG,KAAK;YAC9D,oEAAoE;YACpE,2CAA2C;YAC3CX,UAAUY,SAASgB,MAAM,GAAGtB,IAAIC,UAAU,CAAC,SAASC,GAAG,CAACR,QAAQW,GAAG,KAAK;SACzE;QACD,MAAMoB,WAAWP,CAAAA,wBAAAA,KAAMX,MAAM,IAAIW,KAAKV,IAAI,KAAiC;QAC3E,IAAId,UAAU,CAAC+B,UAAU;YACvB,OAAO3C,gBAAgB,KAAK,qBAAqB;QACnD;QACA,MAAM4C,iBAAiB9D,oBAAoB+B,OAAOK,GAAG,EAAEM,SAASgB,MAAM,EAAEK,OAAO;QAC/E,MAAMC,cAAcX,CAAAA,2BAAAA,QAASV,MAAM,IAC9BU,QAAQT,IAAI,KACbiB,WACEvD,cAAcuD,UAAUC,kBACxB;QACN,IACElC,aACC,CAAA,CAACoC,eAAe,CAAC5D,cAAc4D,WAAW,CAAC,YAAY,EAA0BtB,SAASgB,MAAM,CAAA,GACjG;YACA,OAAOxC,gBAAgB,KAAK,qBAAqB;QACnD;QACA,MAAM+C,UAAUrD,0BACd8B,SAASe,SAAS,EAClBN,CAAAA,mCAAAA,gBAAiBR,MAAM,IAAGQ,gBAAgBP,IAAI,KAAKf;QAErD,MAAMqC,SAASD,UAAU;YAAEE,MAAMF,QAAQG,WAAW;YAAEC,OAAOJ,QAAQK,MAAM,IAAIL,QAAQI,KAAK;QAAC,IAAI;QACjG,MAAME,WAAW/C,CAAAA,wBAAAA,KAAMmB,MAAM,IAAG6B,QAAOhD,YAAAA,KAAKiB,GAAG,CAAC,mBAATjB,YAAoB,MAAM;QACjE,MAAMiD,QAAQT,cACV;YACEX,SAASW;YACTF;YACAR,MAAMO;YACNK;YACAQ,MAAM;gBAAEP,MAAMI;YAAS;QACzB,IACA/D,2BAA2B;YAAE0D;YAAQK;QAAS;QAClD,MAAMI,eACJ,OAAOhD,IAAI,CAAC,eAAe,KAAK,WAC5BA,IAAI,CAAC,eAAe,GACpBqC,cACE,KACAzD,uBAAuBoE,YAAY;QAC3C,MAAMlD,SAAShB,oBAAoB;YACjCsC,OAAOL,SAASK,KAAK;YACrBD;YACAI;YACAuB;YACAJ,OAAOL,eAAe/D,yBAAAA,sBAAsB+D,WAAW,CAAC,QAAQ,aAA1C/D,yBAA+C,KAAMM,uBAAuB8D,KAAK;YACvGM;YACAC,cAAcxB,CAAAA,4BAAAA,SAAUT,MAAM,IAC1BxC,0BAA0BiD,SAASR,IAAI,IAA+BjB,IAAI,GAC1E;QACN;QACA,OAAOV,WAAW;YAChB4D,IAAI;YACJ/B;YACAgC,OAAO,WAAErD,gBAAAA,OAAO4C,KAAK,qBAAZ5C,cAAcqD,OAAO,mBAAI;YAClCC,IAAI,YAAEtD,iBAAAA,OAAO4C,KAAK,qBAAZ5C,eAAcsD,IAAI,oBAAI;YAC5BC,kBAAkBvD,OAAOuD,gBAAgB;YACzCC,OAAOxD,OAAOwD,KAAK;QACrB;IACF;AACF"}
@@ -18,7 +18,7 @@ import type { PluginLeadConversionReport, PluginLeadConversionRequest } from '@a
18
18
  type Firestore = FirebaseFirestore.Firestore;
19
19
  /** A lead nobody has touched, once somebody replied: Working. `true` when it moved. */
20
20
  export declare function markOutreachLeadWorking(firestore: Firestore, input: {
21
- hostId: string;
21
+ orgId: string;
22
22
  leadId: string;
23
23
  }): Promise<boolean>;
24
24
  /** Every enrollment made on the lead now names the contact it became. */
@@ -41,7 +41,8 @@ import { outreachOrgCollection } from "../storage/outreach-records.js";
41
41
  /** A lead nobody has touched, once somebody replied: Working. `true` when it moved. */ export async function markOutreachLeadWorking(firestore, input) {
42
42
  try {
43
43
  var _snapshot_data;
44
- const ref = firestore.collection('hosts').doc(input.hostId).collection('leads').doc(input.leadId);
44
+ // A lead is one org row (AGL-3275), so the site no longer names it.
45
+ const ref = firestore.collection('orgs').doc(input.orgId).collection('leads').doc(input.leadId);
45
46
  const snapshot = await ref.get();
46
47
  if (!snapshot.exists) return false;
47
48
  const lead = (_snapshot_data = snapshot.data()) != null ? _snapshot_data : {};
@@ -54,7 +55,7 @@ import { outreachOrgCollection } from "../storage/outreach-records.js";
54
55
  });
55
56
  return true;
56
57
  } catch (error) {
57
- console.error('[outreach] the lead could not be marked working', input.hostId, input.leadId, error);
58
+ console.error('[outreach] the lead could not be marked working', input.orgId, input.leadId, error);
58
59
  return false;
59
60
  }
60
61
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/runtime/lead-records.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * WHAT A SEQUENCE DOES TO A LEAD'S OWN RECORD (AGL-3234).\n *\n * Two touches, both on `hosts/{hostId}/leads/{leadId}` — the record system's\n * document, written in the record system's vocabulary (`@aglyn/aglyn`'s lead\n * statuses), never through an import of the CRM plugin:\n *\n * - a REPLY moves a lead nobody has touched from New to Working. Somebody\n * wrote back; the lead is being worked, and a queue that still showed it\n * as New would send a rep to open a conversation already open. A lead\n * already Working, closed, or converted is left as it is — a reply does\n * not reopen a verdict or move a contact's record.\n * - a CONVERSION re-points every enrollment made on the lead at the contact\n * it became: the record system tells every plugin through its\n * lead-conversion seam, and this is Sequences' share of it. The\n * enrollment keeps its id, its thread and its place in the steps, and\n * keeps naming the lead beside the contact, so both pages list it.\n *\n * Neither throws: a status that could not be moved is one a rep moves by\n * hand, and an enrollment that could not follow its lead is one the next\n * send stops as `contact_missing` and a rep re-enrolls.\n */\n\nimport { crmLeadStatus } from '@aglyn/aglyn/app-utils/crm'\nimport type {\n PluginLeadConversionReport,\n PluginLeadConversionRequest,\n} from '@aglyn/aglyn/plugin-manager/plugin-lead-conversion'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { outreachOrgCollection } from '../storage/outreach-records'\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** Firestore refuses a batch of more than 500 writes. */\nconst BATCH_LIMIT = 450\n\n/** A lead nobody has touched, once somebody replied: Working. `true` when it moved. */\nexport async function markOutreachLeadWorking(\n firestore: Firestore,\n input: { hostId: string; leadId: string },\n): Promise<boolean> {\n try {\n const ref = firestore.collection('hosts').doc(input.hostId).collection('leads').doc(input.leadId)\n const snapshot = await ref.get()\n if (!snapshot.exists) return false\n const lead = snapshot.data() ?? {}\n if (lead['convertedContactId'] || crmLeadStatus(lead as never) !== 'new') return false\n await ref.set({ status: 'working', updatedAt: FieldValue.serverTimestamp() }, { merge: true })\n return true\n } catch (error) {\n console.error('[outreach] the lead could not be marked working', input.hostId, input.leadId, error)\n return false\n }\n}\n\n/** Every enrollment made on the lead now names the contact it became. */\nexport async function followOutreachLeadToContact(\n firestore: Firestore,\n input: Pick<PluginLeadConversionRequest, 'orgId' | 'leadId' | 'contactId'>,\n): Promise<PluginLeadConversionReport> {\n const enrollments = outreachOrgCollection(firestore, input.orgId, 'enrollments')\n const rows = await enrollments.where('leadId', '==', input.leadId).get()\n const refs = rows.docs\n .filter((row) => row.get('target') !== 'contact' || String(row.get('contactId') ?? '') !== input.contactId)\n .map((row) => row.ref)\n for (let start = 0; start < refs.length; start += BATCH_LIMIT) {\n const batch = firestore.batch()\n for (const ref of refs.slice(start, start + BATCH_LIMIT)) {\n batch.update(ref, { target: 'contact', contactId: input.contactId, updatedAtMs: Date.now() })\n }\n await batch.commit()\n }\n return { enrollments: refs.length }\n}\n"],"names":["crmLeadStatus","FieldValue","outreachOrgCollection","BATCH_LIMIT","markOutreachLeadWorking","firestore","input","snapshot","ref","collection","doc","hostId","leadId","get","exists","lead","data","set","status","updatedAt","serverTimestamp","merge","error","console","followOutreachLeadToContact","enrollments","orgId","rows","where","refs","docs","filter","row","String","contactId","map","start","length","batch","slice","update","target","updatedAtMs","Date","now","commit"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;CAqBC,GAED,SAASA,aAAa,QAAQ,6BAA4B;AAK1D,SAASC,UAAU,QAAQ,2BAA0B;AACrD,SAASC,qBAAqB,QAAQ,iCAA6B;AAInE,uDAAuD,GACvD,MAAMC,cAAc;AAEpB,qFAAqF,GACrF,OAAO,eAAeC,wBACpBC,SAAoB,EACpBC,KAAyC;IAEzC,IAAI;YAIWC;QAHb,MAAMC,MAAMH,UAAUI,UAAU,CAAC,SAASC,GAAG,CAACJ,MAAMK,MAAM,EAAEF,UAAU,CAAC,SAASC,GAAG,CAACJ,MAAMM,MAAM;QAChG,MAAML,WAAW,MAAMC,IAAIK,GAAG;QAC9B,IAAI,CAACN,SAASO,MAAM,EAAE,OAAO;QAC7B,MAAMC,QAAOR,iBAAAA,SAASS,IAAI,cAAbT,iBAAmB,CAAC;QACjC,IAAIQ,IAAI,CAAC,qBAAqB,IAAIf,cAAce,UAAmB,OAAO,OAAO;QACjF,MAAMP,IAAIS,GAAG,CAAC;YAAEC,QAAQ;YAAWC,WAAWlB,WAAWmB,eAAe;QAAG,GAAG;YAAEC,OAAO;QAAK;QAC5F,OAAO;IACT,EAAE,OAAOC,OAAO;QACdC,QAAQD,KAAK,CAAC,mDAAmDhB,MAAMK,MAAM,EAAEL,MAAMM,MAAM,EAAEU;QAC7F,OAAO;IACT;AACF;AAEA,uEAAuE,GACvE,OAAO,eAAeE,4BACpBnB,SAAoB,EACpBC,KAA0E;IAE1E,MAAMmB,cAAcvB,sBAAsBG,WAAWC,MAAMoB,KAAK,EAAE;IAClE,MAAMC,OAAO,MAAMF,YAAYG,KAAK,CAAC,UAAU,MAAMtB,MAAMM,MAAM,EAAEC,GAAG;IACtE,MAAMgB,OAAOF,KAAKG,IAAI,CACnBC,MAAM,CAAC,CAACC;YAAkDA;eAA1CA,IAAInB,GAAG,CAAC,cAAc,aAAaoB,QAAOD,WAAAA,IAAInB,GAAG,CAAC,wBAARmB,WAAwB,QAAQ1B,MAAM4B,SAAS;OACzGC,GAAG,CAAC,CAACH,MAAQA,IAAIxB,GAAG;IACvB,IAAK,IAAI4B,QAAQ,GAAGA,QAAQP,KAAKQ,MAAM,EAAED,SAASjC,YAAa;QAC7D,MAAMmC,QAAQjC,UAAUiC,KAAK;QAC7B,KAAK,MAAM9B,OAAOqB,KAAKU,KAAK,CAACH,OAAOA,QAAQjC,aAAc;YACxDmC,MAAME,MAAM,CAAChC,KAAK;gBAAEiC,QAAQ;gBAAWP,WAAW5B,MAAM4B,SAAS;gBAAEQ,aAAaC,KAAKC,GAAG;YAAG;QAC7F;QACA,MAAMN,MAAMO,MAAM;IACpB;IACA,OAAO;QAAEpB,aAAaI,KAAKQ,MAAM;IAAC;AACpC"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/runtime/lead-records.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * WHAT A SEQUENCE DOES TO A LEAD'S OWN RECORD (AGL-3234).\n *\n * Two touches, both on `hosts/{hostId}/leads/{leadId}` — the record system's\n * document, written in the record system's vocabulary (`@aglyn/aglyn`'s lead\n * statuses), never through an import of the CRM plugin:\n *\n * - a REPLY moves a lead nobody has touched from New to Working. Somebody\n * wrote back; the lead is being worked, and a queue that still showed it\n * as New would send a rep to open a conversation already open. A lead\n * already Working, closed, or converted is left as it is — a reply does\n * not reopen a verdict or move a contact's record.\n * - a CONVERSION re-points every enrollment made on the lead at the contact\n * it became: the record system tells every plugin through its\n * lead-conversion seam, and this is Sequences' share of it. The\n * enrollment keeps its id, its thread and its place in the steps, and\n * keeps naming the lead beside the contact, so both pages list it.\n *\n * Neither throws: a status that could not be moved is one a rep moves by\n * hand, and an enrollment that could not follow its lead is one the next\n * send stops as `contact_missing` and a rep re-enrolls.\n */\n\nimport { crmLeadStatus } from '@aglyn/aglyn/app-utils/crm'\nimport type {\n PluginLeadConversionReport,\n PluginLeadConversionRequest,\n} from '@aglyn/aglyn/plugin-manager/plugin-lead-conversion'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { outreachOrgCollection } from '../storage/outreach-records'\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** Firestore refuses a batch of more than 500 writes. */\nconst BATCH_LIMIT = 450\n\n/** A lead nobody has touched, once somebody replied: Working. `true` when it moved. */\nexport async function markOutreachLeadWorking(\n firestore: Firestore,\n input: { orgId: string; leadId: string },\n): Promise<boolean> {\n try {\n // A lead is one org row (AGL-3275), so the site no longer names it.\n const ref = firestore.collection('orgs').doc(input.orgId).collection('leads').doc(input.leadId)\n const snapshot = await ref.get()\n if (!snapshot.exists) return false\n const lead = snapshot.data() ?? {}\n if (lead['convertedContactId'] || crmLeadStatus(lead as never) !== 'new') return false\n await ref.set({ status: 'working', updatedAt: FieldValue.serverTimestamp() }, { merge: true })\n return true\n } catch (error) {\n console.error('[outreach] the lead could not be marked working', input.orgId, input.leadId, error)\n return false\n }\n}\n\n/** Every enrollment made on the lead now names the contact it became. */\nexport async function followOutreachLeadToContact(\n firestore: Firestore,\n input: Pick<PluginLeadConversionRequest, 'orgId' | 'leadId' | 'contactId'>,\n): Promise<PluginLeadConversionReport> {\n const enrollments = outreachOrgCollection(firestore, input.orgId, 'enrollments')\n const rows = await enrollments.where('leadId', '==', input.leadId).get()\n const refs = rows.docs\n .filter((row) => row.get('target') !== 'contact' || String(row.get('contactId') ?? '') !== input.contactId)\n .map((row) => row.ref)\n for (let start = 0; start < refs.length; start += BATCH_LIMIT) {\n const batch = firestore.batch()\n for (const ref of refs.slice(start, start + BATCH_LIMIT)) {\n batch.update(ref, { target: 'contact', contactId: input.contactId, updatedAtMs: Date.now() })\n }\n await batch.commit()\n }\n return { enrollments: refs.length }\n}\n"],"names":["crmLeadStatus","FieldValue","outreachOrgCollection","BATCH_LIMIT","markOutreachLeadWorking","firestore","input","snapshot","ref","collection","doc","orgId","leadId","get","exists","lead","data","set","status","updatedAt","serverTimestamp","merge","error","console","followOutreachLeadToContact","enrollments","rows","where","refs","docs","filter","row","String","contactId","map","start","length","batch","slice","update","target","updatedAtMs","Date","now","commit"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;CAqBC,GAED,SAASA,aAAa,QAAQ,6BAA4B;AAK1D,SAASC,UAAU,QAAQ,2BAA0B;AACrD,SAASC,qBAAqB,QAAQ,iCAA6B;AAInE,uDAAuD,GACvD,MAAMC,cAAc;AAEpB,qFAAqF,GACrF,OAAO,eAAeC,wBACpBC,SAAoB,EACpBC,KAAwC;IAExC,IAAI;YAKWC;QAJb,oEAAoE;QACpE,MAAMC,MAAMH,UAAUI,UAAU,CAAC,QAAQC,GAAG,CAACJ,MAAMK,KAAK,EAAEF,UAAU,CAAC,SAASC,GAAG,CAACJ,MAAMM,MAAM;QAC9F,MAAML,WAAW,MAAMC,IAAIK,GAAG;QAC9B,IAAI,CAACN,SAASO,MAAM,EAAE,OAAO;QAC7B,MAAMC,QAAOR,iBAAAA,SAASS,IAAI,cAAbT,iBAAmB,CAAC;QACjC,IAAIQ,IAAI,CAAC,qBAAqB,IAAIf,cAAce,UAAmB,OAAO,OAAO;QACjF,MAAMP,IAAIS,GAAG,CAAC;YAAEC,QAAQ;YAAWC,WAAWlB,WAAWmB,eAAe;QAAG,GAAG;YAAEC,OAAO;QAAK;QAC5F,OAAO;IACT,EAAE,OAAOC,OAAO;QACdC,QAAQD,KAAK,CAAC,mDAAmDhB,MAAMK,KAAK,EAAEL,MAAMM,MAAM,EAAEU;QAC5F,OAAO;IACT;AACF;AAEA,uEAAuE,GACvE,OAAO,eAAeE,4BACpBnB,SAAoB,EACpBC,KAA0E;IAE1E,MAAMmB,cAAcvB,sBAAsBG,WAAWC,MAAMK,KAAK,EAAE;IAClE,MAAMe,OAAO,MAAMD,YAAYE,KAAK,CAAC,UAAU,MAAMrB,MAAMM,MAAM,EAAEC,GAAG;IACtE,MAAMe,OAAOF,KAAKG,IAAI,CACnBC,MAAM,CAAC,CAACC;YAAkDA;eAA1CA,IAAIlB,GAAG,CAAC,cAAc,aAAamB,QAAOD,WAAAA,IAAIlB,GAAG,CAAC,wBAARkB,WAAwB,QAAQzB,MAAM2B,SAAS;OACzGC,GAAG,CAAC,CAACH,MAAQA,IAAIvB,GAAG;IACvB,IAAK,IAAI2B,QAAQ,GAAGA,QAAQP,KAAKQ,MAAM,EAAED,SAAShC,YAAa;QAC7D,MAAMkC,QAAQhC,UAAUgC,KAAK;QAC7B,KAAK,MAAM7B,OAAOoB,KAAKU,KAAK,CAACH,OAAOA,QAAQhC,aAAc;YACxDkC,MAAME,MAAM,CAAC/B,KAAK;gBAAEgC,QAAQ;gBAAWP,WAAW3B,MAAM2B,SAAS;gBAAEQ,aAAaC,KAAKC,GAAG;YAAG;QAC7F;QACA,MAAMN,MAAMO,MAAM;IACpB;IACA,OAAO;QAAEnB,aAAaG,KAAKQ,MAAM;IAAC;AACpC"}
@@ -432,7 +432,7 @@ async function syncMailbox(deps, firestore, input) {
432
432
  // A lead somebody wrote back to is being worked (AGL-3234).
433
433
  if (enrollment.target === 'lead' && enrollment.leadId) {
434
434
  await markOutreachLeadWorking(firestore, {
435
- hostId: enrollment.hostId,
435
+ orgId: context.orgId,
436
436
  leadId: enrollment.leadId
437
437
  });
438
438
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/runtime/sync-job.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 { normalizeContactEmail } from '@aglyn/aglyn/app-utils/contacts'\nimport { isPublicMailboxDomain } from '@aglyn/aglyn/app-utils/crm'\nimport { crmInboundExcerpt, emailAddressOf } from '@aglyn/aglyn/app-utils/crm-inbound'\nimport { outreachMessageIdHeader } from '../engine/compose'\nimport { readOutreachDeliveryReport } from '../engine/delivery-status'\nimport { outreachEmailDomain } from '../engine/do-not-contact-domain'\nimport { outreachStoppedEntry } from '../engine/enrollment-activity'\nimport type { OutreachEnrollmentEvent } from '../engine/enrollment-state'\nimport { isOutreachGatewayBlock, outreachGatewayBlockDetail } from '../engine/gateway-block'\nimport { effectiveOutreachWindow, postponeOutreachForAutoReply } from '../engine/schedule'\nimport {\n decideOutreachThread,\n type OutreachMessageClassification,\n type OutreachThreadDecision,\n} from '../engine/thread-classification'\nimport { outreachHeader, outreachThreadMessageFromGmail, type OutreachThreadMessage } from '../engine/thread-message'\nimport { mailboxRef } from '../mailboxes/mailbox-credentials'\nimport {\n OUTREACH_COLLECTIONS,\n type OutreachEnrollment,\n type OutreachMailbox,\n type OutreachSequence,\n} from '../model/outreach.types'\nimport { addOutreachDoNotContact, addOutreachDoNotContactDomain } from '../storage/do-not-contact-store'\nimport {\n outreachEnrollmentLink,\n outreachOrgCollection,\n readStoredOutreachEnrollment,\n readStoredOutreachMailbox,\n readStoredOutreachSequence,\n} from '../storage/outreach-records'\nimport type { GmailClient } from '../transport/gmail-client'\nimport { GmailTransportError, isReconnectRequired } from '../transport/gmail-errors'\nimport { creditOutreachReply } from './campaign-credit'\nimport { applyOutreachEvent, recordOutreachOptOut } from './enrollment-events'\nimport {\n applyOutreachMailboxHealth,\n emptyOutreachHealthDelta,\n type OutreachMailboxHealthDelta,\n} from './mailbox-health-store'\nimport { noteOutreachMailboxReconnectRequired } from './mailbox-notices'\nimport type { OutreachRuntimeDeps } from './runtime-deps'\nimport { markOutreachLeadWorking } from './lead-records'\nimport { fileOutreachEmail, fileOutreachNote, fileOutreachTask, markOutreachEmailDelivery } from './timeline'\nimport { outreachUnsubscribeMailbox } from './unsubscribe-link'\n\n/*==========================================\n * THE SYNC JOB (AGL-2981): every fifteen minutes, on the console.\n *\n * For each connected or paused mailbox, what came back since the last run —\n * read with `format=full`, turned into the engine's message shape and\n * classified by `decideOutreachThread`:\n *\n * - a REPLY stops the enrollment, files the message on the contact's\n * timeline (once per `Message-ID`, under the capture address's own id for\n * it) and gives the rep the task \"Reply from <name>\";\n * - an OUT-OF-OFFICE postpones the next step a working week from the reply;\n * - an OPT-OUT — a reply asking to be left alone, or a message to the\n * `+unsubscribe` address the `mailto:` names — puts the address on the\n * organization's do-not-contact list and off the site's `sales` topic,\n * and stops every open enrollment of it;\n * - a HARD BOUNCE stops the enrollment, files the address on the platform's\n * suppression list and the do-not-contact list — and, when its diagnostic\n * reads as the domain's mail gateway refusing the sender rather than one\n * address being unknown (`engine/gateway-block`, AGL-3244), files the\n * DOMAIN on the do-not-contact list too, and says so on the enrollment.\n *\n * Every verdict a list takes is said on the record the person is as well\n * (AGL-3245): the lead and the contact carry `emailState` through the\n * platform's stamp, and a bounce or a complaint lands on the send's own\n * timeline entry, so a member reading the record sees what the runtime saw.\n *\n * Four reads find those, each narrowed by Gmail rather than read in full:\n * the threads of this mailbox's enrollments that have new mail; delivery\n * reports outside those threads (a receiving server's own bounce), by\n * searching for the mail systems that send them; messages to the\n * unsubscribe address; and messages from the people this mailbox is\n * writing to, in a thread of their own.\n *\n * Every write it makes is idempotent — the do-not-contact list keeps its\n * first entry, a topic opt-out keeps its first moment, a timeline entry is\n * filed once per message — and the messages it handled are marked AFTER\n * their effects, on the enrollment and on the mailbox, so a run that dies\n * part-way repeats harmless work rather than skipping some. The mailbox's\n * health takes the run's bounces and complaints in one write at the end,\n * which is where it pauses itself.\n *==========================================*/\n\nexport { OUTREACH_SYNC_JOB_ID } from '../constants/runtime-jobs'\n\n/** The furthest back a run reads: a mailbox's first run, or one that fell behind. */\nexport const OUTREACH_SYNC_LOOKBACK_MS = 7 * 24 * 60 * 60_000\n\n/** How far a run reads back past the previous one, for mail Gmail indexed late. */\nexport const OUTREACH_SYNC_OVERLAP_MS = 2 * 60 * 60_000\n\n/** How long after its last send an enrollment's mail is still watched. */\nexport const OUTREACH_SYNC_WATCH_MS = 45 * 24 * 60 * 60_000\n\n/** The most pages of one search a run reads. */\nconst SEARCH_PAGES = 5\n/** Handled message ids a mailbox keeps, and an enrollment keeps. */\nconst MAILBOX_HANDLED_MAX = 500\nconst ENROLLMENT_HANDLED_MAX = 100\n/** Addresses in one `from:` search. */\nconst FROM_CHUNK = 15\n\ntype Firestore = FirebaseFirestore.Firestore\n\nexport interface OutreachSyncReport {\n [key: string]: number\n mailboxes: number\n threads: number\n replies: number\n optOuts: number\n bounces: number\n /** Hard bounces that read as a gateway block, each filing its domain (AGL-3244). */\n gatewayBlocks: number\n postponed: number\n paused: number\n}\n\ninterface SyncContext {\n deps: OutreachRuntimeDeps\n firestore: Firestore\n orgId: string\n mailbox: OutreachMailbox\n client: GmailClient\n selfAddresses: string[]\n unsubscribeAddress: string | null\n sequences: Map<string, OutreachSequence | null>\n delta: OutreachMailboxHealthDelta\n report: OutreachSyncReport\n nowMs: number\n}\n\n/** Runs the sync job once — see the module note. */\nexport async function runOutreachSyncJob(\n deps: OutreachRuntimeDeps,\n context: { nowMs: number; deadlineMs: number },\n): Promise<OutreachSyncReport> {\n const firestore = deps.firestore()\n const report: OutreachSyncReport = {\n mailboxes: 0,\n threads: 0,\n replies: 0,\n optOuts: 0,\n bounces: 0,\n gatewayBlocks: 0,\n postponed: 0,\n paused: 0,\n }\n const all = await firestore.collectionGroup(OUTREACH_COLLECTIONS.mailboxes).get()\n const byOrg = new Map<string, OutreachMailbox[]>()\n for (const doc of all.docs) {\n const orgId = doc.ref.parent.parent?.id\n const mailbox = readStoredOutreachMailbox(doc.id, doc.data())\n if (!orgId || !mailbox || (mailbox.status !== 'connected' && mailbox.status !== 'paused')) continue\n byOrg.set(orgId, [...(byOrg.get(orgId) ?? []), mailbox])\n }\n for (const [orgId, mailboxes] of byOrg) {\n if (deps.now() >= context.deadlineMs) break\n const orgSnapshot = await firestore.collection('orgs').doc(orgId).get()\n const org = (orgSnapshot.exists ? orgSnapshot.data() : null) as Record<string, unknown> | null\n if (!org || (await deps.orgRefusal(orgId, org))) continue\n for (const mailbox of mailboxes) {\n if (deps.now() >= context.deadlineMs) break\n try {\n await syncMailbox(deps, firestore, { orgId, mailbox, report, nowMs: context.nowMs })\n report.mailboxes += 1\n } catch (error) {\n // One mailbox's failure is its own; the next mailbox is read. A grant\n // Google refused is the mailbox's to reconnect.\n if (isReconnectRequired(error)) {\n await noteOutreachMailboxReconnectRequired(deps, {\n orgId,\n mailboxId: mailbox.id,\n errorCode: error instanceof GmailTransportError ? error.code : 'invalid_grant',\n nowMs: context.nowMs,\n })\n }\n console.error('[outreach] syncing a mailbox failed', error)\n }\n }\n }\n return report\n}\n\n/** Every stub a search finds, a few pages deep. */\nasync function search(\n client: GmailClient,\n q: string,\n): Promise<Array<{ id: string; threadId: string }>> {\n const found: Array<{ id: string; threadId: string }> = []\n let pageToken: string | null = null\n for (let page = 0; page < SEARCH_PAGES; page += 1) {\n const list = await client.listMessages({ q, maxResults: 100, pageToken, includeSpamTrash: true })\n found.push(...list.messages.filter((message) => message.id))\n pageToken = list.nextPageToken\n if (!pageToken) break\n }\n return found\n}\n\nasync function syncMailbox(\n deps: OutreachRuntimeDeps,\n firestore: Firestore,\n input: { orgId: string; mailbox: OutreachMailbox; report: OutreachSyncReport; nowMs: number },\n): Promise<void> {\n const { orgId, mailbox, report, nowMs } = input\n const opened = await deps.openMailbox(mailbox.id)\n if (opened.ok === false) {\n if (opened.reason !== 'not-configured') {\n await noteOutreachMailboxReconnectRequired(deps, {\n orgId,\n mailboxId: mailbox.id,\n errorCode: opened.reason,\n nowMs,\n })\n }\n return\n }\n const unsubscribe = outreachUnsubscribeMailbox(mailbox.email)\n const context: SyncContext = {\n deps,\n firestore,\n orgId,\n mailbox,\n client: opened.client,\n selfAddresses: [\n mailbox.email,\n mailbox.sendAs,\n ...(mailbox.sendAsOptions ?? []).map((option) => option.email),\n ].filter(Boolean),\n unsubscribeAddress: unsubscribe?.address ?? null,\n sequences: new Map(),\n delta: emptyOutreachHealthDelta(),\n report,\n nowMs,\n }\n\n // The enrollments this mailbox is writing to, by thread and by address.\n const watched = await outreachOrgCollection(firestore, orgId, 'enrollments')\n .where('mailboxId', '==', mailbox.id)\n .where('lastSentAtMs', '>=', nowMs - OUTREACH_SYNC_WATCH_MS)\n .get()\n const byThread = new Map<string, OutreachEnrollment>()\n const byAddress = new Map<string, OutreachEnrollment>()\n for (const doc of watched.docs) {\n const enrollment = readStoredOutreachEnrollment(doc.id, doc.data())\n if (!enrollment) continue\n for (const threadId of enrollment.gmailThreadIds) byThread.set(threadId, enrollment)\n const known = byAddress.get(enrollment.email)\n if (!known || (enrollment.lastSentAtMs ?? 0) > (known.lastSentAtMs ?? 0)) byAddress.set(enrollment.email, enrollment)\n }\n\n const sinceMs = Math.max(Number(mailbox.sync?.throughMs) || 0, nowMs - OUTREACH_SYNC_LOOKBACK_MS) - OUTREACH_SYNC_OVERLAP_MS\n const after = `after:${Math.floor(sinceMs / 1000)}`\n // Every message this run or an earlier one handled outside an enrollment's\n // thread; one message is handled once however many searches find it.\n const handled = new Set(mailbox.sync?.handledMessageIds ?? [])\n const newlyHandled: string[] = []\n const markHandled = (id: string) => {\n if (handled.has(id)) return\n handled.add(id)\n newlyHandled.push(id)\n }\n const fresh = (stub: { id: string; threadId: string }) => !handled.has(stub.id) && !byThread.has(stub.threadId)\n\n // 1. The enrollments' own threads that have new mail.\n const inbound = await search(context.client, `${after} -from:me`)\n for (const threadId of new Set(inbound.map((stub) => stub.threadId))) {\n const enrollment = byThread.get(threadId)\n if (!enrollment) continue\n const thread = await context.client.getFullThread(threadId)\n report.threads += 1\n await applyMessages(context, enrollment, thread.messages.map(outreachThreadMessageFromGmail))\n }\n\n // 2. Delivery reports outside those threads: a receiving server's bounce.\n for (const stub of await search(context.client, `${after} from:(mailer-daemon OR postmaster)`)) {\n if (!fresh(stub)) continue\n const message = outreachThreadMessageFromGmail(await context.client.getFullMessage(stub.id))\n const delivery = readOutreachDeliveryReport(message)\n if (delivery?.kind === 'hard') {\n for (const address of delivery.failedAddresses) {\n const enrollment = byAddress.get(address)\n if (enrollment) await applyMessages(context, enrollment, [message])\n }\n }\n markHandled(stub.id)\n }\n\n // 3. Messages to the unsubscribe address: whoever wrote asked to leave.\n if (unsubscribe) {\n for (const stub of await search(context.client, `${after} to:${unsubscribe.address}`)) {\n if (handled.has(stub.id)) continue\n const metadata = await context.client.getMessage(stub.id, { metadataHeaders: ['From'] })\n const from = emailAddressOf(metadata.headers.find((header) => header.name.toLowerCase() === 'from')?.value ?? '')\n const address = normalizeContactEmail(from)\n if (address && !context.selfAddresses.includes(address)) {\n const enrollment = byAddress.get(address) ?? null\n await recordOutreachOptOut(deps, {\n orgId,\n email: address,\n source: 'unsubscribe',\n enrollment,\n hostIds: enrollment ? [enrollment.hostId] : [],\n detail: 'Wrote to the unsubscribe address.',\n nowMs,\n })\n report.optOuts += 1\n }\n markHandled(stub.id)\n }\n }\n\n // 4. The people this mailbox writes to, answering in a thread of their own.\n const addresses = [...byAddress.keys()]\n for (let start = 0; start < addresses.length; start += FROM_CHUNK) {\n const chunk = addresses.slice(start, start + FROM_CHUNK)\n for (const stub of await search(context.client, `${after} from:(${chunk.join(' OR ')})`)) {\n if (!fresh(stub)) continue\n const message = outreachThreadMessageFromGmail(await context.client.getFullMessage(stub.id))\n const enrollment = byAddress.get(normalizeContactEmail(emailAddressOf(message.from)) ?? '')\n if (enrollment) await applyMessages(context, enrollment, [message])\n markHandled(stub.id)\n }\n }\n\n const paused = await applyOutreachMailboxHealth(deps, { orgId, mailboxId: mailbox.id, delta: context.delta })\n if (paused) report.paused += 1\n await mailboxRef(firestore, orgId, mailbox.id).update({\n sync: {\n throughMs: nowMs,\n handledMessageIds: [...(mailbox.sync?.handledMessageIds ?? []), ...newlyHandled].slice(-MAILBOX_HANDLED_MAX),\n },\n })\n}\n\n/** The sequence an enrollment runs through, read once per run. */\nasync function sequenceOf(context: SyncContext, sequenceId: string): Promise<OutreachSequence | null> {\n if (!context.sequences.has(sequenceId)) {\n const snapshot = await outreachOrgCollection(context.firestore, context.orgId, 'sequences').doc(sequenceId).get()\n context.sequences.set(sequenceId, readStoredOutreachSequence(sequenceId, snapshot.exists ? snapshot.data() : undefined))\n }\n return context.sequences.get(sequenceId) ?? null\n}\n\n/** A message's `Message-ID` in header form, or `null` when it carries none. */\nconst messageIdOf = (message: OutreachThreadMessage) => outreachMessageIdHeader(outreachHeader(message, 'Message-ID'))\n\n/** A bounce's diagnostic with the recipient's address taken out of it. */\nexport function scrubbedOutreachDiagnostic(diagnostic: string | null, email: string): string | null {\n if (!diagnostic) return null\n const pattern = new RegExp(email.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'), 'gi')\n return diagnostic.replace(pattern, 'the address')\n}\n\n/** Files a message the person wrote on their timeline. */\nasync function fileInbound(context: SyncContext, enrollment: OutreachEnrollment, message: OutreachThreadMessage) {\n const messageId = messageIdOf(message)\n if (!messageId) return\n await fileOutreachEmail(context.deps, {\n orgId: context.orgId,\n hostId: enrollment.hostId,\n link: outreachEnrollmentLink(enrollment),\n direction: 'inbound',\n subject: message.subject,\n from: emailAddressOf(message.from) ?? message.from,\n to: context.mailbox.sendAs || context.mailbox.email,\n messageId,\n inReplyTo: outreachMessageIdHeader(outreachHeader(message, 'In-Reply-To')),\n body: crmInboundExcerpt(message.textBody ?? message.snippet ?? '', message.htmlBody ?? ''),\n atMs: message.internalDateMs,\n byUid: '',\n })\n}\n\n/**\n * What the new messages of one enrollment mean, done: the effects first,\n * each idempotent, then the event and the handled ids in one transaction.\n */\nasync function applyMessages(\n context: SyncContext,\n enrollment: OutreachEnrollment,\n messages: readonly OutreachThreadMessage[],\n): Promise<void> {\n const { deps, firestore, report, nowMs } = context\n const decision: OutreachThreadDecision = decideOutreachThread({\n messages,\n recipient: enrollment.email,\n handledMessageIds: enrollment.syncedMessageIds ?? [],\n selfAddresses: context.selfAddresses,\n unsubscribeAddress: context.unsubscribeAddress,\n threadSubject: enrollment.threadSubject,\n })\n const freshIds = decision.classifications.map((classification) => classification.messageId).filter(Boolean)\n if (!freshIds.length) return\n const decidedBy: OutreachMessageClassification | null = decision.decidedBy\n const decidingMessage = decidedBy ? messages.find((message) => message.id === decidedBy.messageId) ?? null : null\n const lastSentMessageId = enrollment.messageIds[enrollment.messageIds.length - 1] ?? null\n\n for (const classification of decision.classifications) {\n const aboutRecipient =\n !classification.bounce?.recipients.length || classification.bounce.recipients.includes(enrollment.email)\n if (classification.kind === 'hard_bounce' && aboutRecipient) {\n context.delta.bounces.push({ atMs: classification.atMs || nowMs, messageId: lastSentMessageId })\n }\n if (classification.complaint) {\n context.delta.complaintAtMs = Math.max(context.delta.complaintAtMs ?? 0, classification.atMs || nowMs)\n }\n }\n\n let event: OutreachEnrollmentEvent | null = null\n let postponedTo: number | null = null\n switch (decision.outcome) {\n case 'opted_out': {\n if (decidingMessage) await fileInbound(context, enrollment, decidingMessage)\n const complaint = decidedBy?.complaint === true\n await recordOutreachOptOut(deps, {\n orgId: context.orgId,\n email: enrollment.email,\n source: decidedBy?.evidence?.startsWith('To:') ? 'unsubscribe' : 'opt_out_reply',\n enrollment,\n hostIds: [enrollment.hostId],\n detail: decidedBy?.evidence ?? null,\n nowMs,\n complaint,\n })\n // A reply that called the email spam marks the send it answered\n // (AGL-3245), as the campaign webhook would mark a reported one.\n if (complaint) {\n await markOutreachEmailDelivery(deps, {\n orgId: context.orgId,\n messageId: lastSentMessageId,\n state: 'complained',\n atMs: decidedBy?.atMs || nowMs,\n detail: decidedBy?.evidence ?? null,\n })\n }\n report.optOuts += 1\n break\n }\n case 'replied': {\n if (decidingMessage) {\n await fileInbound(context, enrollment, decidingMessage)\n try {\n await fileOutreachTask(deps, {\n orgId: context.orgId,\n hostId: enrollment.hostId,\n link: outreachEnrollmentLink(enrollment),\n dedupeKey: `reply:${messageIdOf(decidingMessage) ?? decidingMessage.id}`,\n title: `Reply from ${enrollment.contactName || enrollment.email}`,\n notes: crmInboundExcerpt(decidingMessage.textBody ?? decidingMessage.snippet ?? '', decidingMessage.htmlBody ?? '').slice(0, 500),\n kind: 'email',\n dueAtMs: nowMs,\n assigneeUid: context.mailbox.connectedByUid,\n })\n } catch (error) {\n console.error('[outreach] the reply task could not be filed', error)\n }\n }\n // A lead somebody wrote back to is being worked (AGL-3234).\n if (enrollment.target === 'lead' && enrollment.leadId) {\n await markOutreachLeadWorking(firestore, { hostId: enrollment.hostId, leadId: enrollment.leadId })\n }\n context.delta.replies += 1\n event = { type: 'reply', atMs: decidedBy?.atMs || nowMs, detail: decidedBy?.evidence ?? null }\n report.replies += 1\n // Once per enrollment (AGL-3254): a reply moves the status to\n // `replied`, and nothing is synced for it again.\n await creditOutreachReply(deps, { enrollment, atMs: event.atMs })\n break\n }\n case 'bounced': {\n const diagnostic = scrubbedOutreachDiagnostic(decidedBy?.bounce?.diagnostic ?? decidedBy?.bounce?.status ?? null, enrollment.email)\n await deps.suppressBouncedEmail({ email: enrollment.email, hostId: enrollment.hostId })\n await addOutreachDoNotContact(firestore, {\n orgId: context.orgId,\n email: enrollment.email,\n reason: 'hard_bounce',\n source: 'runtime',\n nowMs,\n enrollmentId: enrollment.id,\n sequenceId: enrollment.sequenceId,\n detail: diagnostic,\n })\n let detail = diagnostic\n // A gateway block is the domain's verdict on the sender (AGL-3244):\n // the domain is filed beside the address, and the enrollment says so.\n // Never a public mailbox provider's domain — Gmail refusing one\n // message on policy is not a reason to stop writing to Gmail.\n const domain = outreachEmailDomain(enrollment.email)\n const gateway = isOutreachGatewayBlock(decidedBy?.bounce ?? null)\n if (domain && !isPublicMailboxDomain(domain) && gateway) {\n await addOutreachDoNotContactDomain(firestore, {\n orgId: context.orgId,\n domain,\n reason: 'gateway_block',\n source: 'runtime',\n nowMs,\n enrollmentId: enrollment.id,\n sequenceId: enrollment.sequenceId,\n detail: diagnostic,\n })\n detail = outreachGatewayBlockDetail(domain, diagnostic)\n report.gatewayBlocks += 1\n }\n const bouncedAtMs = decidedBy?.atMs || nowMs\n // The record the person is says so too (AGL-3245): the verdict on the\n // lead and the contact, and the bounce on the send's own timeline\n // entry, so the record reads Sent, then Bounced.\n await deps.stampRecordEmailState({\n orgId: context.orgId,\n email: enrollment.email,\n state: {\n status: gateway ? 'blocked' : 'bounced',\n atMs: bouncedAtMs,\n source: 'sequence',\n detail: diagnostic,\n enrollmentId: enrollment.id,\n },\n })\n await markOutreachEmailDelivery(deps, {\n orgId: context.orgId,\n messageId: lastSentMessageId,\n state: 'bounced',\n atMs: bouncedAtMs,\n detail: diagnostic,\n })\n event = { type: 'bounce', atMs: bouncedAtMs, detail }\n report.bounces += 1\n break\n }\n case 'postpone': {\n const sequence = await sequenceOf(context, enrollment.sequenceId)\n postponedTo = postponeOutreachForAutoReply({\n nextDueAtMs: enrollment.nextDueAtMs,\n receivedAtMs: decidedBy?.atMs || nowMs,\n timeZone: context.mailbox.timezone,\n window: effectiveOutreachWindow(sequence?.settings.window, context.mailbox.window),\n random: deps.random,\n })\n break\n }\n default:\n break\n }\n\n const outcome = await applyOutreachEvent(firestore, {\n orgId: context.orgId,\n enrollmentId: enrollment.id,\n event,\n extra: (current) => {\n const extra: Record<string, unknown> = {\n syncedMessageIds: [...(current.syncedMessageIds ?? []), ...freshIds].slice(-ENROLLMENT_HANDLED_MAX),\n }\n const waiting = current.status === 'active' || current.status === 'paused'\n if (postponedTo !== null && waiting && current.nextDueAtMs !== null && postponedTo > current.nextDueAtMs) {\n extra['nextDueAtMs'] = postponedTo\n }\n return extra\n },\n nowMs,\n })\n if (postponedTo !== null && outcome.enrollment?.nextDueAtMs === postponedTo) report.postponed += 1\n // The person's record says the sequence ended (AGL-3274), once, under the\n // reply that ended it — after the transaction moved the status, so a run\n // that reads the same reply again finds nothing to file. An opt-out's\n // line is `recordOutreachOptOut`'s, beside the lists it writes.\n if (event?.type === 'reply' && outcome.changed) {\n const sequence = await sequenceOf(context, enrollment.sequenceId)\n const entry = outreachStoppedEntry({\n enrollmentId: enrollment.id,\n sequenceName: sequence?.name ?? '',\n reason: 'replied',\n })\n await fileOutreachNote(deps, {\n orgId: context.orgId,\n hostId: enrollment.hostId,\n link: outreachEnrollmentLink(enrollment),\n dedupeKey: entry.dedupeKey,\n body: entry.body,\n atMs: event.atMs,\n })\n }\n // The run's own view of the enrollment moves with it, so a second message\n // of it in this run is judged against what this one did.\n if (outcome.enrollment) Object.assign(enrollment, outcome.enrollment)\n}\n"],"names":["normalizeContactEmail","isPublicMailboxDomain","crmInboundExcerpt","emailAddressOf","outreachMessageIdHeader","readOutreachDeliveryReport","outreachEmailDomain","outreachStoppedEntry","isOutreachGatewayBlock","outreachGatewayBlockDetail","effectiveOutreachWindow","postponeOutreachForAutoReply","decideOutreachThread","outreachHeader","outreachThreadMessageFromGmail","mailboxRef","OUTREACH_COLLECTIONS","addOutreachDoNotContact","addOutreachDoNotContactDomain","outreachEnrollmentLink","outreachOrgCollection","readStoredOutreachEnrollment","readStoredOutreachMailbox","readStoredOutreachSequence","GmailTransportError","isReconnectRequired","creditOutreachReply","applyOutreachEvent","recordOutreachOptOut","applyOutreachMailboxHealth","emptyOutreachHealthDelta","noteOutreachMailboxReconnectRequired","markOutreachLeadWorking","fileOutreachEmail","fileOutreachNote","fileOutreachTask","markOutreachEmailDelivery","outreachUnsubscribeMailbox","OUTREACH_SYNC_JOB_ID","OUTREACH_SYNC_LOOKBACK_MS","OUTREACH_SYNC_OVERLAP_MS","OUTREACH_SYNC_WATCH_MS","SEARCH_PAGES","MAILBOX_HANDLED_MAX","ENROLLMENT_HANDLED_MAX","FROM_CHUNK","runOutreachSyncJob","deps","context","firestore","report","mailboxes","threads","replies","optOuts","bounces","gatewayBlocks","postponed","paused","all","collectionGroup","get","byOrg","Map","doc","docs","orgId","ref","parent","id","mailbox","data","status","set","now","deadlineMs","orgSnapshot","collection","org","exists","orgRefusal","syncMailbox","nowMs","error","mailboxId","errorCode","code","console","search","client","q","found","pageToken","page","list","listMessages","maxResults","includeSpamTrash","push","messages","filter","message","nextPageToken","input","opened","openMailbox","ok","reason","unsubscribe","email","selfAddresses","sendAs","sendAsOptions","map","option","Boolean","unsubscribeAddress","address","sequences","delta","watched","where","byThread","byAddress","enrollment","known","threadId","gmailThreadIds","lastSentAtMs","sinceMs","Math","max","Number","sync","throughMs","after","floor","handled","Set","handledMessageIds","newlyHandled","markHandled","has","add","fresh","stub","inbound","thread","getFullThread","applyMessages","getFullMessage","delivery","kind","failedAddresses","metadata","getMessage","metadataHeaders","from","headers","find","header","name","toLowerCase","value","includes","source","hostIds","hostId","detail","addresses","keys","start","length","chunk","slice","join","update","sequenceOf","sequenceId","snapshot","undefined","messageIdOf","scrubbedOutreachDiagnostic","diagnostic","pattern","RegExp","replace","fileInbound","messageId","link","direction","subject","to","inReplyTo","body","textBody","snippet","htmlBody","atMs","internalDateMs","byUid","outcome","decision","recipient","syncedMessageIds","threadSubject","freshIds","classifications","classification","decidedBy","decidingMessage","lastSentMessageId","messageIds","aboutRecipient","bounce","recipients","complaint","complaintAtMs","event","postponedTo","evidence","startsWith","state","dedupeKey","title","contactName","notes","dueAtMs","assigneeUid","connectedByUid","target","leadId","type","suppressBouncedEmail","enrollmentId","domain","gateway","bouncedAtMs","stampRecordEmailState","sequence","nextDueAtMs","receivedAtMs","timeZone","timezone","window","settings","random","extra","current","waiting","changed","entry","sequenceName","Object","assign"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/runtime/sync-job.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 { normalizeContactEmail } from '@aglyn/aglyn/app-utils/contacts'\nimport { isPublicMailboxDomain } from '@aglyn/aglyn/app-utils/crm'\nimport { crmInboundExcerpt, emailAddressOf } from '@aglyn/aglyn/app-utils/crm-inbound'\nimport { outreachMessageIdHeader } from '../engine/compose'\nimport { readOutreachDeliveryReport } from '../engine/delivery-status'\nimport { outreachEmailDomain } from '../engine/do-not-contact-domain'\nimport { outreachStoppedEntry } from '../engine/enrollment-activity'\nimport type { OutreachEnrollmentEvent } from '../engine/enrollment-state'\nimport { isOutreachGatewayBlock, outreachGatewayBlockDetail } from '../engine/gateway-block'\nimport { effectiveOutreachWindow, postponeOutreachForAutoReply } from '../engine/schedule'\nimport {\n decideOutreachThread,\n type OutreachMessageClassification,\n type OutreachThreadDecision,\n} from '../engine/thread-classification'\nimport { outreachHeader, outreachThreadMessageFromGmail, type OutreachThreadMessage } from '../engine/thread-message'\nimport { mailboxRef } from '../mailboxes/mailbox-credentials'\nimport {\n OUTREACH_COLLECTIONS,\n type OutreachEnrollment,\n type OutreachMailbox,\n type OutreachSequence,\n} from '../model/outreach.types'\nimport { addOutreachDoNotContact, addOutreachDoNotContactDomain } from '../storage/do-not-contact-store'\nimport {\n outreachEnrollmentLink,\n outreachOrgCollection,\n readStoredOutreachEnrollment,\n readStoredOutreachMailbox,\n readStoredOutreachSequence,\n} from '../storage/outreach-records'\nimport type { GmailClient } from '../transport/gmail-client'\nimport { GmailTransportError, isReconnectRequired } from '../transport/gmail-errors'\nimport { creditOutreachReply } from './campaign-credit'\nimport { applyOutreachEvent, recordOutreachOptOut } from './enrollment-events'\nimport {\n applyOutreachMailboxHealth,\n emptyOutreachHealthDelta,\n type OutreachMailboxHealthDelta,\n} from './mailbox-health-store'\nimport { noteOutreachMailboxReconnectRequired } from './mailbox-notices'\nimport type { OutreachRuntimeDeps } from './runtime-deps'\nimport { markOutreachLeadWorking } from './lead-records'\nimport { fileOutreachEmail, fileOutreachNote, fileOutreachTask, markOutreachEmailDelivery } from './timeline'\nimport { outreachUnsubscribeMailbox } from './unsubscribe-link'\n\n/*==========================================\n * THE SYNC JOB (AGL-2981): every fifteen minutes, on the console.\n *\n * For each connected or paused mailbox, what came back since the last run —\n * read with `format=full`, turned into the engine's message shape and\n * classified by `decideOutreachThread`:\n *\n * - a REPLY stops the enrollment, files the message on the contact's\n * timeline (once per `Message-ID`, under the capture address's own id for\n * it) and gives the rep the task \"Reply from <name>\";\n * - an OUT-OF-OFFICE postpones the next step a working week from the reply;\n * - an OPT-OUT — a reply asking to be left alone, or a message to the\n * `+unsubscribe` address the `mailto:` names — puts the address on the\n * organization's do-not-contact list and off the site's `sales` topic,\n * and stops every open enrollment of it;\n * - a HARD BOUNCE stops the enrollment, files the address on the platform's\n * suppression list and the do-not-contact list — and, when its diagnostic\n * reads as the domain's mail gateway refusing the sender rather than one\n * address being unknown (`engine/gateway-block`, AGL-3244), files the\n * DOMAIN on the do-not-contact list too, and says so on the enrollment.\n *\n * Every verdict a list takes is said on the record the person is as well\n * (AGL-3245): the lead and the contact carry `emailState` through the\n * platform's stamp, and a bounce or a complaint lands on the send's own\n * timeline entry, so a member reading the record sees what the runtime saw.\n *\n * Four reads find those, each narrowed by Gmail rather than read in full:\n * the threads of this mailbox's enrollments that have new mail; delivery\n * reports outside those threads (a receiving server's own bounce), by\n * searching for the mail systems that send them; messages to the\n * unsubscribe address; and messages from the people this mailbox is\n * writing to, in a thread of their own.\n *\n * Every write it makes is idempotent — the do-not-contact list keeps its\n * first entry, a topic opt-out keeps its first moment, a timeline entry is\n * filed once per message — and the messages it handled are marked AFTER\n * their effects, on the enrollment and on the mailbox, so a run that dies\n * part-way repeats harmless work rather than skipping some. The mailbox's\n * health takes the run's bounces and complaints in one write at the end,\n * which is where it pauses itself.\n *==========================================*/\n\nexport { OUTREACH_SYNC_JOB_ID } from '../constants/runtime-jobs'\n\n/** The furthest back a run reads: a mailbox's first run, or one that fell behind. */\nexport const OUTREACH_SYNC_LOOKBACK_MS = 7 * 24 * 60 * 60_000\n\n/** How far a run reads back past the previous one, for mail Gmail indexed late. */\nexport const OUTREACH_SYNC_OVERLAP_MS = 2 * 60 * 60_000\n\n/** How long after its last send an enrollment's mail is still watched. */\nexport const OUTREACH_SYNC_WATCH_MS = 45 * 24 * 60 * 60_000\n\n/** The most pages of one search a run reads. */\nconst SEARCH_PAGES = 5\n/** Handled message ids a mailbox keeps, and an enrollment keeps. */\nconst MAILBOX_HANDLED_MAX = 500\nconst ENROLLMENT_HANDLED_MAX = 100\n/** Addresses in one `from:` search. */\nconst FROM_CHUNK = 15\n\ntype Firestore = FirebaseFirestore.Firestore\n\nexport interface OutreachSyncReport {\n [key: string]: number\n mailboxes: number\n threads: number\n replies: number\n optOuts: number\n bounces: number\n /** Hard bounces that read as a gateway block, each filing its domain (AGL-3244). */\n gatewayBlocks: number\n postponed: number\n paused: number\n}\n\ninterface SyncContext {\n deps: OutreachRuntimeDeps\n firestore: Firestore\n orgId: string\n mailbox: OutreachMailbox\n client: GmailClient\n selfAddresses: string[]\n unsubscribeAddress: string | null\n sequences: Map<string, OutreachSequence | null>\n delta: OutreachMailboxHealthDelta\n report: OutreachSyncReport\n nowMs: number\n}\n\n/** Runs the sync job once — see the module note. */\nexport async function runOutreachSyncJob(\n deps: OutreachRuntimeDeps,\n context: { nowMs: number; deadlineMs: number },\n): Promise<OutreachSyncReport> {\n const firestore = deps.firestore()\n const report: OutreachSyncReport = {\n mailboxes: 0,\n threads: 0,\n replies: 0,\n optOuts: 0,\n bounces: 0,\n gatewayBlocks: 0,\n postponed: 0,\n paused: 0,\n }\n const all = await firestore.collectionGroup(OUTREACH_COLLECTIONS.mailboxes).get()\n const byOrg = new Map<string, OutreachMailbox[]>()\n for (const doc of all.docs) {\n const orgId = doc.ref.parent.parent?.id\n const mailbox = readStoredOutreachMailbox(doc.id, doc.data())\n if (!orgId || !mailbox || (mailbox.status !== 'connected' && mailbox.status !== 'paused')) continue\n byOrg.set(orgId, [...(byOrg.get(orgId) ?? []), mailbox])\n }\n for (const [orgId, mailboxes] of byOrg) {\n if (deps.now() >= context.deadlineMs) break\n const orgSnapshot = await firestore.collection('orgs').doc(orgId).get()\n const org = (orgSnapshot.exists ? orgSnapshot.data() : null) as Record<string, unknown> | null\n if (!org || (await deps.orgRefusal(orgId, org))) continue\n for (const mailbox of mailboxes) {\n if (deps.now() >= context.deadlineMs) break\n try {\n await syncMailbox(deps, firestore, { orgId, mailbox, report, nowMs: context.nowMs })\n report.mailboxes += 1\n } catch (error) {\n // One mailbox's failure is its own; the next mailbox is read. A grant\n // Google refused is the mailbox's to reconnect.\n if (isReconnectRequired(error)) {\n await noteOutreachMailboxReconnectRequired(deps, {\n orgId,\n mailboxId: mailbox.id,\n errorCode: error instanceof GmailTransportError ? error.code : 'invalid_grant',\n nowMs: context.nowMs,\n })\n }\n console.error('[outreach] syncing a mailbox failed', error)\n }\n }\n }\n return report\n}\n\n/** Every stub a search finds, a few pages deep. */\nasync function search(\n client: GmailClient,\n q: string,\n): Promise<Array<{ id: string; threadId: string }>> {\n const found: Array<{ id: string; threadId: string }> = []\n let pageToken: string | null = null\n for (let page = 0; page < SEARCH_PAGES; page += 1) {\n const list = await client.listMessages({ q, maxResults: 100, pageToken, includeSpamTrash: true })\n found.push(...list.messages.filter((message) => message.id))\n pageToken = list.nextPageToken\n if (!pageToken) break\n }\n return found\n}\n\nasync function syncMailbox(\n deps: OutreachRuntimeDeps,\n firestore: Firestore,\n input: { orgId: string; mailbox: OutreachMailbox; report: OutreachSyncReport; nowMs: number },\n): Promise<void> {\n const { orgId, mailbox, report, nowMs } = input\n const opened = await deps.openMailbox(mailbox.id)\n if (opened.ok === false) {\n if (opened.reason !== 'not-configured') {\n await noteOutreachMailboxReconnectRequired(deps, {\n orgId,\n mailboxId: mailbox.id,\n errorCode: opened.reason,\n nowMs,\n })\n }\n return\n }\n const unsubscribe = outreachUnsubscribeMailbox(mailbox.email)\n const context: SyncContext = {\n deps,\n firestore,\n orgId,\n mailbox,\n client: opened.client,\n selfAddresses: [\n mailbox.email,\n mailbox.sendAs,\n ...(mailbox.sendAsOptions ?? []).map((option) => option.email),\n ].filter(Boolean),\n unsubscribeAddress: unsubscribe?.address ?? null,\n sequences: new Map(),\n delta: emptyOutreachHealthDelta(),\n report,\n nowMs,\n }\n\n // The enrollments this mailbox is writing to, by thread and by address.\n const watched = await outreachOrgCollection(firestore, orgId, 'enrollments')\n .where('mailboxId', '==', mailbox.id)\n .where('lastSentAtMs', '>=', nowMs - OUTREACH_SYNC_WATCH_MS)\n .get()\n const byThread = new Map<string, OutreachEnrollment>()\n const byAddress = new Map<string, OutreachEnrollment>()\n for (const doc of watched.docs) {\n const enrollment = readStoredOutreachEnrollment(doc.id, doc.data())\n if (!enrollment) continue\n for (const threadId of enrollment.gmailThreadIds) byThread.set(threadId, enrollment)\n const known = byAddress.get(enrollment.email)\n if (!known || (enrollment.lastSentAtMs ?? 0) > (known.lastSentAtMs ?? 0)) byAddress.set(enrollment.email, enrollment)\n }\n\n const sinceMs = Math.max(Number(mailbox.sync?.throughMs) || 0, nowMs - OUTREACH_SYNC_LOOKBACK_MS) - OUTREACH_SYNC_OVERLAP_MS\n const after = `after:${Math.floor(sinceMs / 1000)}`\n // Every message this run or an earlier one handled outside an enrollment's\n // thread; one message is handled once however many searches find it.\n const handled = new Set(mailbox.sync?.handledMessageIds ?? [])\n const newlyHandled: string[] = []\n const markHandled = (id: string) => {\n if (handled.has(id)) return\n handled.add(id)\n newlyHandled.push(id)\n }\n const fresh = (stub: { id: string; threadId: string }) => !handled.has(stub.id) && !byThread.has(stub.threadId)\n\n // 1. The enrollments' own threads that have new mail.\n const inbound = await search(context.client, `${after} -from:me`)\n for (const threadId of new Set(inbound.map((stub) => stub.threadId))) {\n const enrollment = byThread.get(threadId)\n if (!enrollment) continue\n const thread = await context.client.getFullThread(threadId)\n report.threads += 1\n await applyMessages(context, enrollment, thread.messages.map(outreachThreadMessageFromGmail))\n }\n\n // 2. Delivery reports outside those threads: a receiving server's bounce.\n for (const stub of await search(context.client, `${after} from:(mailer-daemon OR postmaster)`)) {\n if (!fresh(stub)) continue\n const message = outreachThreadMessageFromGmail(await context.client.getFullMessage(stub.id))\n const delivery = readOutreachDeliveryReport(message)\n if (delivery?.kind === 'hard') {\n for (const address of delivery.failedAddresses) {\n const enrollment = byAddress.get(address)\n if (enrollment) await applyMessages(context, enrollment, [message])\n }\n }\n markHandled(stub.id)\n }\n\n // 3. Messages to the unsubscribe address: whoever wrote asked to leave.\n if (unsubscribe) {\n for (const stub of await search(context.client, `${after} to:${unsubscribe.address}`)) {\n if (handled.has(stub.id)) continue\n const metadata = await context.client.getMessage(stub.id, { metadataHeaders: ['From'] })\n const from = emailAddressOf(metadata.headers.find((header) => header.name.toLowerCase() === 'from')?.value ?? '')\n const address = normalizeContactEmail(from)\n if (address && !context.selfAddresses.includes(address)) {\n const enrollment = byAddress.get(address) ?? null\n await recordOutreachOptOut(deps, {\n orgId,\n email: address,\n source: 'unsubscribe',\n enrollment,\n hostIds: enrollment ? [enrollment.hostId] : [],\n detail: 'Wrote to the unsubscribe address.',\n nowMs,\n })\n report.optOuts += 1\n }\n markHandled(stub.id)\n }\n }\n\n // 4. The people this mailbox writes to, answering in a thread of their own.\n const addresses = [...byAddress.keys()]\n for (let start = 0; start < addresses.length; start += FROM_CHUNK) {\n const chunk = addresses.slice(start, start + FROM_CHUNK)\n for (const stub of await search(context.client, `${after} from:(${chunk.join(' OR ')})`)) {\n if (!fresh(stub)) continue\n const message = outreachThreadMessageFromGmail(await context.client.getFullMessage(stub.id))\n const enrollment = byAddress.get(normalizeContactEmail(emailAddressOf(message.from)) ?? '')\n if (enrollment) await applyMessages(context, enrollment, [message])\n markHandled(stub.id)\n }\n }\n\n const paused = await applyOutreachMailboxHealth(deps, { orgId, mailboxId: mailbox.id, delta: context.delta })\n if (paused) report.paused += 1\n await mailboxRef(firestore, orgId, mailbox.id).update({\n sync: {\n throughMs: nowMs,\n handledMessageIds: [...(mailbox.sync?.handledMessageIds ?? []), ...newlyHandled].slice(-MAILBOX_HANDLED_MAX),\n },\n })\n}\n\n/** The sequence an enrollment runs through, read once per run. */\nasync function sequenceOf(context: SyncContext, sequenceId: string): Promise<OutreachSequence | null> {\n if (!context.sequences.has(sequenceId)) {\n const snapshot = await outreachOrgCollection(context.firestore, context.orgId, 'sequences').doc(sequenceId).get()\n context.sequences.set(sequenceId, readStoredOutreachSequence(sequenceId, snapshot.exists ? snapshot.data() : undefined))\n }\n return context.sequences.get(sequenceId) ?? null\n}\n\n/** A message's `Message-ID` in header form, or `null` when it carries none. */\nconst messageIdOf = (message: OutreachThreadMessage) => outreachMessageIdHeader(outreachHeader(message, 'Message-ID'))\n\n/** A bounce's diagnostic with the recipient's address taken out of it. */\nexport function scrubbedOutreachDiagnostic(diagnostic: string | null, email: string): string | null {\n if (!diagnostic) return null\n const pattern = new RegExp(email.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'), 'gi')\n return diagnostic.replace(pattern, 'the address')\n}\n\n/** Files a message the person wrote on their timeline. */\nasync function fileInbound(context: SyncContext, enrollment: OutreachEnrollment, message: OutreachThreadMessage) {\n const messageId = messageIdOf(message)\n if (!messageId) return\n await fileOutreachEmail(context.deps, {\n orgId: context.orgId,\n hostId: enrollment.hostId,\n link: outreachEnrollmentLink(enrollment),\n direction: 'inbound',\n subject: message.subject,\n from: emailAddressOf(message.from) ?? message.from,\n to: context.mailbox.sendAs || context.mailbox.email,\n messageId,\n inReplyTo: outreachMessageIdHeader(outreachHeader(message, 'In-Reply-To')),\n body: crmInboundExcerpt(message.textBody ?? message.snippet ?? '', message.htmlBody ?? ''),\n atMs: message.internalDateMs,\n byUid: '',\n })\n}\n\n/**\n * What the new messages of one enrollment mean, done: the effects first,\n * each idempotent, then the event and the handled ids in one transaction.\n */\nasync function applyMessages(\n context: SyncContext,\n enrollment: OutreachEnrollment,\n messages: readonly OutreachThreadMessage[],\n): Promise<void> {\n const { deps, firestore, report, nowMs } = context\n const decision: OutreachThreadDecision = decideOutreachThread({\n messages,\n recipient: enrollment.email,\n handledMessageIds: enrollment.syncedMessageIds ?? [],\n selfAddresses: context.selfAddresses,\n unsubscribeAddress: context.unsubscribeAddress,\n threadSubject: enrollment.threadSubject,\n })\n const freshIds = decision.classifications.map((classification) => classification.messageId).filter(Boolean)\n if (!freshIds.length) return\n const decidedBy: OutreachMessageClassification | null = decision.decidedBy\n const decidingMessage = decidedBy ? messages.find((message) => message.id === decidedBy.messageId) ?? null : null\n const lastSentMessageId = enrollment.messageIds[enrollment.messageIds.length - 1] ?? null\n\n for (const classification of decision.classifications) {\n const aboutRecipient =\n !classification.bounce?.recipients.length || classification.bounce.recipients.includes(enrollment.email)\n if (classification.kind === 'hard_bounce' && aboutRecipient) {\n context.delta.bounces.push({ atMs: classification.atMs || nowMs, messageId: lastSentMessageId })\n }\n if (classification.complaint) {\n context.delta.complaintAtMs = Math.max(context.delta.complaintAtMs ?? 0, classification.atMs || nowMs)\n }\n }\n\n let event: OutreachEnrollmentEvent | null = null\n let postponedTo: number | null = null\n switch (decision.outcome) {\n case 'opted_out': {\n if (decidingMessage) await fileInbound(context, enrollment, decidingMessage)\n const complaint = decidedBy?.complaint === true\n await recordOutreachOptOut(deps, {\n orgId: context.orgId,\n email: enrollment.email,\n source: decidedBy?.evidence?.startsWith('To:') ? 'unsubscribe' : 'opt_out_reply',\n enrollment,\n hostIds: [enrollment.hostId],\n detail: decidedBy?.evidence ?? null,\n nowMs,\n complaint,\n })\n // A reply that called the email spam marks the send it answered\n // (AGL-3245), as the campaign webhook would mark a reported one.\n if (complaint) {\n await markOutreachEmailDelivery(deps, {\n orgId: context.orgId,\n messageId: lastSentMessageId,\n state: 'complained',\n atMs: decidedBy?.atMs || nowMs,\n detail: decidedBy?.evidence ?? null,\n })\n }\n report.optOuts += 1\n break\n }\n case 'replied': {\n if (decidingMessage) {\n await fileInbound(context, enrollment, decidingMessage)\n try {\n await fileOutreachTask(deps, {\n orgId: context.orgId,\n hostId: enrollment.hostId,\n link: outreachEnrollmentLink(enrollment),\n dedupeKey: `reply:${messageIdOf(decidingMessage) ?? decidingMessage.id}`,\n title: `Reply from ${enrollment.contactName || enrollment.email}`,\n notes: crmInboundExcerpt(decidingMessage.textBody ?? decidingMessage.snippet ?? '', decidingMessage.htmlBody ?? '').slice(0, 500),\n kind: 'email',\n dueAtMs: nowMs,\n assigneeUid: context.mailbox.connectedByUid,\n })\n } catch (error) {\n console.error('[outreach] the reply task could not be filed', error)\n }\n }\n // A lead somebody wrote back to is being worked (AGL-3234).\n if (enrollment.target === 'lead' && enrollment.leadId) {\n await markOutreachLeadWorking(firestore, { orgId: context.orgId, leadId: enrollment.leadId })\n }\n context.delta.replies += 1\n event = { type: 'reply', atMs: decidedBy?.atMs || nowMs, detail: decidedBy?.evidence ?? null }\n report.replies += 1\n // Once per enrollment (AGL-3254): a reply moves the status to\n // `replied`, and nothing is synced for it again.\n await creditOutreachReply(deps, { enrollment, atMs: event.atMs })\n break\n }\n case 'bounced': {\n const diagnostic = scrubbedOutreachDiagnostic(decidedBy?.bounce?.diagnostic ?? decidedBy?.bounce?.status ?? null, enrollment.email)\n await deps.suppressBouncedEmail({ email: enrollment.email, hostId: enrollment.hostId })\n await addOutreachDoNotContact(firestore, {\n orgId: context.orgId,\n email: enrollment.email,\n reason: 'hard_bounce',\n source: 'runtime',\n nowMs,\n enrollmentId: enrollment.id,\n sequenceId: enrollment.sequenceId,\n detail: diagnostic,\n })\n let detail = diagnostic\n // A gateway block is the domain's verdict on the sender (AGL-3244):\n // the domain is filed beside the address, and the enrollment says so.\n // Never a public mailbox provider's domain — Gmail refusing one\n // message on policy is not a reason to stop writing to Gmail.\n const domain = outreachEmailDomain(enrollment.email)\n const gateway = isOutreachGatewayBlock(decidedBy?.bounce ?? null)\n if (domain && !isPublicMailboxDomain(domain) && gateway) {\n await addOutreachDoNotContactDomain(firestore, {\n orgId: context.orgId,\n domain,\n reason: 'gateway_block',\n source: 'runtime',\n nowMs,\n enrollmentId: enrollment.id,\n sequenceId: enrollment.sequenceId,\n detail: diagnostic,\n })\n detail = outreachGatewayBlockDetail(domain, diagnostic)\n report.gatewayBlocks += 1\n }\n const bouncedAtMs = decidedBy?.atMs || nowMs\n // The record the person is says so too (AGL-3245): the verdict on the\n // lead and the contact, and the bounce on the send's own timeline\n // entry, so the record reads Sent, then Bounced.\n await deps.stampRecordEmailState({\n orgId: context.orgId,\n email: enrollment.email,\n state: {\n status: gateway ? 'blocked' : 'bounced',\n atMs: bouncedAtMs,\n source: 'sequence',\n detail: diagnostic,\n enrollmentId: enrollment.id,\n },\n })\n await markOutreachEmailDelivery(deps, {\n orgId: context.orgId,\n messageId: lastSentMessageId,\n state: 'bounced',\n atMs: bouncedAtMs,\n detail: diagnostic,\n })\n event = { type: 'bounce', atMs: bouncedAtMs, detail }\n report.bounces += 1\n break\n }\n case 'postpone': {\n const sequence = await sequenceOf(context, enrollment.sequenceId)\n postponedTo = postponeOutreachForAutoReply({\n nextDueAtMs: enrollment.nextDueAtMs,\n receivedAtMs: decidedBy?.atMs || nowMs,\n timeZone: context.mailbox.timezone,\n window: effectiveOutreachWindow(sequence?.settings.window, context.mailbox.window),\n random: deps.random,\n })\n break\n }\n default:\n break\n }\n\n const outcome = await applyOutreachEvent(firestore, {\n orgId: context.orgId,\n enrollmentId: enrollment.id,\n event,\n extra: (current) => {\n const extra: Record<string, unknown> = {\n syncedMessageIds: [...(current.syncedMessageIds ?? []), ...freshIds].slice(-ENROLLMENT_HANDLED_MAX),\n }\n const waiting = current.status === 'active' || current.status === 'paused'\n if (postponedTo !== null && waiting && current.nextDueAtMs !== null && postponedTo > current.nextDueAtMs) {\n extra['nextDueAtMs'] = postponedTo\n }\n return extra\n },\n nowMs,\n })\n if (postponedTo !== null && outcome.enrollment?.nextDueAtMs === postponedTo) report.postponed += 1\n // The person's record says the sequence ended (AGL-3274), once, under the\n // reply that ended it — after the transaction moved the status, so a run\n // that reads the same reply again finds nothing to file. An opt-out's\n // line is `recordOutreachOptOut`'s, beside the lists it writes.\n if (event?.type === 'reply' && outcome.changed) {\n const sequence = await sequenceOf(context, enrollment.sequenceId)\n const entry = outreachStoppedEntry({\n enrollmentId: enrollment.id,\n sequenceName: sequence?.name ?? '',\n reason: 'replied',\n })\n await fileOutreachNote(deps, {\n orgId: context.orgId,\n hostId: enrollment.hostId,\n link: outreachEnrollmentLink(enrollment),\n dedupeKey: entry.dedupeKey,\n body: entry.body,\n atMs: event.atMs,\n })\n }\n // The run's own view of the enrollment moves with it, so a second message\n // of it in this run is judged against what this one did.\n if (outcome.enrollment) Object.assign(enrollment, outcome.enrollment)\n}\n"],"names":["normalizeContactEmail","isPublicMailboxDomain","crmInboundExcerpt","emailAddressOf","outreachMessageIdHeader","readOutreachDeliveryReport","outreachEmailDomain","outreachStoppedEntry","isOutreachGatewayBlock","outreachGatewayBlockDetail","effectiveOutreachWindow","postponeOutreachForAutoReply","decideOutreachThread","outreachHeader","outreachThreadMessageFromGmail","mailboxRef","OUTREACH_COLLECTIONS","addOutreachDoNotContact","addOutreachDoNotContactDomain","outreachEnrollmentLink","outreachOrgCollection","readStoredOutreachEnrollment","readStoredOutreachMailbox","readStoredOutreachSequence","GmailTransportError","isReconnectRequired","creditOutreachReply","applyOutreachEvent","recordOutreachOptOut","applyOutreachMailboxHealth","emptyOutreachHealthDelta","noteOutreachMailboxReconnectRequired","markOutreachLeadWorking","fileOutreachEmail","fileOutreachNote","fileOutreachTask","markOutreachEmailDelivery","outreachUnsubscribeMailbox","OUTREACH_SYNC_JOB_ID","OUTREACH_SYNC_LOOKBACK_MS","OUTREACH_SYNC_OVERLAP_MS","OUTREACH_SYNC_WATCH_MS","SEARCH_PAGES","MAILBOX_HANDLED_MAX","ENROLLMENT_HANDLED_MAX","FROM_CHUNK","runOutreachSyncJob","deps","context","firestore","report","mailboxes","threads","replies","optOuts","bounces","gatewayBlocks","postponed","paused","all","collectionGroup","get","byOrg","Map","doc","docs","orgId","ref","parent","id","mailbox","data","status","set","now","deadlineMs","orgSnapshot","collection","org","exists","orgRefusal","syncMailbox","nowMs","error","mailboxId","errorCode","code","console","search","client","q","found","pageToken","page","list","listMessages","maxResults","includeSpamTrash","push","messages","filter","message","nextPageToken","input","opened","openMailbox","ok","reason","unsubscribe","email","selfAddresses","sendAs","sendAsOptions","map","option","Boolean","unsubscribeAddress","address","sequences","delta","watched","where","byThread","byAddress","enrollment","known","threadId","gmailThreadIds","lastSentAtMs","sinceMs","Math","max","Number","sync","throughMs","after","floor","handled","Set","handledMessageIds","newlyHandled","markHandled","has","add","fresh","stub","inbound","thread","getFullThread","applyMessages","getFullMessage","delivery","kind","failedAddresses","metadata","getMessage","metadataHeaders","from","headers","find","header","name","toLowerCase","value","includes","source","hostIds","hostId","detail","addresses","keys","start","length","chunk","slice","join","update","sequenceOf","sequenceId","snapshot","undefined","messageIdOf","scrubbedOutreachDiagnostic","diagnostic","pattern","RegExp","replace","fileInbound","messageId","link","direction","subject","to","inReplyTo","body","textBody","snippet","htmlBody","atMs","internalDateMs","byUid","outcome","decision","recipient","syncedMessageIds","threadSubject","freshIds","classifications","classification","decidedBy","decidingMessage","lastSentMessageId","messageIds","aboutRecipient","bounce","recipients","complaint","complaintAtMs","event","postponedTo","evidence","startsWith","state","dedupeKey","title","contactName","notes","dueAtMs","assigneeUid","connectedByUid","target","leadId","type","suppressBouncedEmail","enrollmentId","domain","gateway","bouncedAtMs","stampRecordEmailState","sequence","nextDueAtMs","receivedAtMs","timeZone","timezone","window","settings","random","extra","current","waiting","changed","entry","sequenceName","Object","assign"],"mappings":"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"}