@aglyn/plugins-outreach 1.0.0-beta.161 → 1.0.0-beta.163
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 +13 -13
- package/src/lib/components/sequence-editor.js +24 -3
- package/src/lib/components/sequence-editor.js.map +1 -1
- package/src/lib/declarations.console-server.js +27 -3
- package/src/lib/declarations.console-server.js.map +1 -1
- package/src/lib/engine/enrollment-state.d.ts +1 -1
- package/src/lib/engine/enrollment-state.js +4 -0
- package/src/lib/engine/enrollment-state.js.map +1 -1
- package/src/lib/engine/gates.d.ts +1 -1
- package/src/lib/engine/gates.js +6 -1
- package/src/lib/engine/gates.js.map +1 -1
- package/src/lib/enrollment/enroll-people.js +6 -3
- package/src/lib/enrollment/enroll-people.js.map +1 -1
- package/src/lib/model/outreach.types.d.ts +15 -0
- package/src/lib/model/outreach.types.js.map +1 -1
- package/src/lib/model/sequence-draft.d.ts +8 -2
- package/src/lib/model/sequence-draft.js +6 -3
- package/src/lib/model/sequence-draft.js.map +1 -1
- package/src/lib/model/stored-records.js +6 -1
- package/src/lib/model/stored-records.js.map +1 -1
- package/src/lib/routes/enroll-routes.js +39 -1
- package/src/lib/routes/enroll-routes.js.map +1 -1
- package/src/lib/routes/enrollment-routes.js +13 -0
- package/src/lib/routes/enrollment-routes.js.map +1 -1
- package/src/lib/routes/platform-deps.d.ts +3 -1
- package/src/lib/routes/platform-deps.js +4 -2
- package/src/lib/routes/platform-deps.js.map +1 -1
- package/src/lib/routes/register-routes.js +12 -1
- package/src/lib/routes/register-routes.js.map +1 -1
- package/src/lib/routes/route-deps.d.ts +11 -0
- package/src/lib/routes/route-deps.js.map +1 -1
- package/src/lib/routes/sequence-routes.js +25 -2
- package/src/lib/routes/sequence-routes.js.map +1 -1
- package/src/lib/runtime/campaign-credit.d.ts +81 -0
- package/src/lib/runtime/campaign-credit.js +133 -0
- package/src/lib/runtime/campaign-credit.js.map +1 -0
- package/src/lib/runtime/campaign-detach.d.ts +21 -0
- package/src/lib/runtime/campaign-detach.js +85 -0
- package/src/lib/runtime/campaign-detach.js.map +1 -0
- package/src/lib/runtime/click-route.d.ts +1 -1
- package/src/lib/runtime/click-route.js +13 -1
- package/src/lib/runtime/click-route.js.map +1 -1
- package/src/lib/runtime/enrollment-events.d.ts +6 -2
- package/src/lib/runtime/enrollment-events.js +20 -3
- package/src/lib/runtime/enrollment-events.js.map +1 -1
- package/src/lib/runtime/platform-runtime-deps.d.ts +8 -1
- package/src/lib/runtime/platform-runtime-deps.js +52 -0
- package/src/lib/runtime/platform-runtime-deps.js.map +1 -1
- package/src/lib/runtime/runtime-deps.d.ts +73 -0
- package/src/lib/runtime/runtime-deps.js.map +1 -1
- package/src/lib/runtime/send-job.js +17 -2
- package/src/lib/runtime/send-job.js.map +1 -1
- package/src/lib/runtime/sync-job.js +59 -10
- package/src/lib/runtime/sync-job.js.map +1 -1
- package/src/lib/runtime/timeline.d.ts +10 -1
- package/src/lib/runtime/timeline.js +23 -1
- package/src/lib/runtime/timeline.js.map +1 -1
- package/src/lib/runtime/unsubscribe-route.d.ts +1 -1
- package/src/lib/runtime/unsubscribe-route.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/model/sequence-draft.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 * A SEQUENCE AS THE EDITOR HOLDS IT (AGL-2980).\n *\n * The editor builds a draft, the save route reads the draft it was sent\n * through the same reader, and the engine's validator judges both — so the\n * issues the editor shows beside a field are the ones the route would\n * refuse the save with. Client-safe.\n *\n * ## Once someone is enrolled, the steps they stand on stay put\n *\n * An enrollment points at its next step by POSITION (`stepIndex`), and it\n * was enrolled into a site's CRM from one mailbox. So after the first\n * enrollment, a save may edit any step's wording and delay and may add\n * steps at the end, but may not remove, reorder or change the kind of a\n * step that already exists, and may not move the sequence to another site\n * or mailbox: each of those would send an enrolled person a step they were\n * never going to get, from an address they never heard from. A new\n * sequence is the way to change them.\n *==========================================*/\n\nimport {\n readOutreachSequenceSettings,\n type OutreachValidationIssue,\n type OutreachValidationCode,\n} from '../engine/sequence-validation'\nimport {\n type OutreachEmailStep,\n type OutreachMailbox,\n type OutreachSendWindow,\n type OutreachSequence,\n type OutreachSequenceSettings,\n type OutreachSequenceStep,\n type OutreachTaskKind,\n type OutreachTaskStep,\n} from './outreach.types'\n\n/** A sequence as the editor holds it and the save route receives it. */\nexport interface OutreachSequenceDraft {\n name: string\n /** The site whose CRM the enrolled people are records of. */\n hostId: string\n /** `''` until a mailbox is chosen. */\n mailboxId: string\n steps: OutreachSequenceStep[]\n settings: OutreachSequenceSettings\n}\n\n/** The most steps a draft is read with; the validator refuses past the real limit. */\nconst STEPS_READ_MAX = 20\n\n/** The delay a new email after the first starts with, in business days. */\nexport const OUTREACH_DEFAULT_FOLLOW_UP_DAYS = 3\n\nconst text = (value: unknown): string => (typeof value === 'string' ? value : '')\n\n/** A whole number, or `NaN` for anything else, so the validator names it. */\nconst wholeNumber = (value: unknown): number => {\n const number = typeof value === 'number' ? value : Number(text(value) || NaN)\n return Number.isInteger(number) ? number : Number.NaN\n}\n\nfunction readWindow(value: unknown): OutreachSendWindow | null {\n if (!value || typeof value !== 'object') return null\n const raw = value as Record<string, unknown>\n return {\n days: Array.isArray(raw['days']) ? raw['days'].map((day) => wholeNumber(day)) : [],\n startMinute: wholeNumber(raw['startMinute']),\n endMinute: wholeNumber(raw['endMinute']),\n }\n}\n\nfunction readStep(value: unknown): OutreachSequenceStep {\n const raw = (value && typeof value === 'object' ? value : {}) as Record<string, unknown>\n const id = text(raw['id']).trim().slice(0, 64)\n const delayBusinessDays = wholeNumber(raw['delayBusinessDays'])\n if (raw['kind'] === 'email') {\n const templateId = text(raw['templateId']).trim()\n const step: OutreachEmailStep = {\n id,\n kind: 'email',\n delayBusinessDays,\n subject: text(raw['subject']),\n replyInThread: raw['replyInThread'] !== false,\n body: text(raw['body']),\n templateId: templateId || null,\n }\n return step\n }\n if (raw['kind'] === 'task') {\n const step: OutreachTaskStep = {\n id,\n kind: 'task',\n taskKind: text(raw['taskKind']) as OutreachTaskKind,\n title: text(raw['title']),\n delayBusinessDays,\n }\n return step\n }\n // Neither kind: kept, so the validator can say \"a step is an email or a\n // task\" beside it rather than the step silently disappearing.\n return { id, kind: text(raw['kind']) } as unknown as OutreachSequenceStep\n}\n\n/**\n * A draft read out of anything — a request body, a stored sequence — with\n * every field the model does not name left behind and every value coerced\n * to its type. Nothing is judged here; that is the validator's.\n */\nexport function readOutreachSequenceDraft(input: unknown): OutreachSequenceDraft {\n const raw = (input && typeof input === 'object' ? input : {}) as Record<string, unknown>\n const settings = readOutreachSequenceSettings(\n (raw['settings'] && typeof raw['settings'] === 'object'\n ? raw['settings']\n : {}) as Record<string, unknown>,\n )\n return {\n name: text(raw['name']).replace(/\\s+/g, ' ').trim(),\n hostId: text(raw['hostId']).trim(),\n mailboxId: text(raw['mailboxId']).trim(),\n steps: (Array.isArray(raw['steps']) ? raw['steps'] : []).slice(0, STEPS_READ_MAX).map(readStep),\n settings: {\n ...settings,\n window: readWindow((raw['settings'] as Record<string, unknown> | undefined)?.['window']),\n },\n }\n}\n\n/** The draft a stored sequence opens as in the editor. */\nexport function outreachSequenceDraftOf(\n sequence: Pick<OutreachSequence, 'name' | 'hostId' | 'mailboxId' | 'steps' | 'settings'>,\n): OutreachSequenceDraft {\n return readOutreachSequenceDraft(sequence)\n}\n\n/** A step id nothing else in the sequence carries. */\nexport function newOutreachStepId(\n steps: readonly Pick<OutreachSequenceStep, 'id'>[] = [],\n random: () => number = Math.random,\n): string {\n const taken = new Set(steps.map((step) => step.id))\n for (let attempt = 0; ; attempt += 1) {\n const drawn = `step-${Math.floor(random() * 36 ** 6).toString(36).padStart(6, '0')}`\n // A source that keeps answering the same number still ends: the attempt\n // count makes the id unique once the draws stop being.\n const id = attempt < 4 ? drawn : `${drawn}-${attempt}`\n if (!taken.has(id)) return id\n }\n}\n\n/**\n * A new email step for the end of `steps`: the first email waits for the\n * next opening; a later one waits a few business days and replies in the\n * thread the first started.\n */\nexport function newOutreachEmailStep(\n steps: readonly OutreachSequenceStep[],\n random?: () => number,\n): OutreachEmailStep {\n const follows = steps.some((step) => step.kind === 'email')\n return {\n id: newOutreachStepId(steps, random),\n kind: 'email',\n delayBusinessDays: follows ? OUTREACH_DEFAULT_FOLLOW_UP_DAYS : 0,\n subject: '',\n replyInThread: true,\n body: '',\n templateId: null,\n }\n}\n\n/** A new task step for the end of `steps`, titled for its kind. */\nexport function newOutreachTaskStep(\n steps: readonly OutreachSequenceStep[],\n taskKind: OutreachTaskKind,\n random?: () => number,\n): OutreachTaskStep {\n const titles: Record<OutreachTaskKind, string> = {\n linkedin: 'Connect on LinkedIn',\n call: 'Call',\n todo: '',\n }\n return {\n id: newOutreachStepId(steps, random),\n kind: 'task',\n taskKind,\n title: titles[taskKind] ?? '',\n delayBusinessDays: 1,\n }\n}\n\n/** A new sequence: one email, for `hostId`, sending to the countries given. */\nexport function emptyOutreachSequenceDraft(input: {\n hostId: string\n mailboxId?: string\n allowedCountries: readonly string[]\n random?: () => number\n}): OutreachSequenceDraft {\n return {\n name: '',\n hostId: input.hostId,\n mailboxId: input.mailboxId ?? '',\n steps: [newOutreachEmailStep([], input.random)],\n settings: {\n window: null,\n allowedCountries: [...input.allowedCountries],\n allowCustomers: false,\n // Off for a NEW sequence too (AGL-3239), not only for the ones that\n // predate it: a tracked link is visible in the body a cold recipient\n // reads, so it is turned on deliberately or not at all.\n trackClicks: false,\n },\n }\n}\n\n/** The issues a save can be refused with beyond the engine's own. */\nexport type OutreachSequenceIssueCode =\n | OutreachValidationCode\n | 'steps_locked'\n | 'host_locked'\n | 'mailbox_locked'\n | 'host_unknown'\n | 'mailbox_unknown'\n | 'mailbox_not_yours'\n | 'mailbox_not_sending'\n | 'country_not_in_org'\n\nexport interface OutreachSequenceIssue extends Omit<OutreachValidationIssue, 'code'> {\n code: OutreachSequenceIssueCode\n}\n\n/**\n * What a save may not change once someone is enrolled — see the module\n * note. `previous` is the stored sequence; `next` the draft being saved.\n */\nexport function outreachEnrolledSequenceIssues(\n previous: Pick<OutreachSequenceDraft, 'hostId' | 'mailboxId' | 'steps'>,\n next: Pick<OutreachSequenceDraft, 'hostId' | 'mailboxId' | 'steps'>,\n): OutreachSequenceIssue[] {\n const issues: OutreachSequenceIssue[] = []\n const locked = (path: string, code: OutreachSequenceIssueCode, message: string) =>\n issues.push({ path, code, message, severity: 'error' })\n const kept = previous.steps.every(\n (step, index) => next.steps[index]?.id === step.id && next.steps[index]?.kind === step.kind,\n )\n if (!kept) {\n locked(\n 'steps',\n 'steps_locked',\n \"People are enrolled in this sequence, so its steps can't be removed, reordered or changed to another kind. Edit their wording, add steps at the end, or start a new sequence.\",\n )\n }\n if (previous.hostId !== next.hostId) {\n locked('hostId', 'host_locked', \"People are enrolled from this site's CRM, so the site can't change.\")\n }\n if (previous.mailboxId !== next.mailboxId) {\n locked(\n 'mailboxId',\n 'mailbox_locked',\n \"People are enrolled from this mailbox, so it can't change. Start a new sequence to send from another.\",\n )\n }\n return issues\n}\n\n/**\n * Whether a sequence's mailbox can send right now, as activation asks it:\n * `none` when no mailbox is chosen, `gone` when it was disconnected or is\n * not this organization's, `paused` or `reconnect_required` while it is\n * connected but not sending, and `sending` when it is connected.\n */\nexport type OutreachSequenceMailboxState =\n | 'none'\n | 'gone'\n | 'paused'\n | 'reconnect_required'\n | 'sending'\n\nexport function outreachSequenceMailboxState(\n mailboxId: string,\n mailbox: Pick<OutreachMailbox, 'status'> | null | undefined,\n): OutreachSequenceMailboxState {\n if (!mailboxId) return 'none'\n if (!mailbox || mailbox.status === 'disconnected') return 'gone'\n if (mailbox.status === 'paused' || mailbox.status === 'reconnect_required') {\n return mailbox.status\n }\n return 'sending'\n}\n\n/** What activation refuses with for each mailbox that cannot send. */\nconst MAILBOX_ACTIVATION_ISSUES: Record<\n Exclude<OutreachSequenceMailboxState, 'sending'>,\n { code: OutreachSequenceIssueCode; message: string }\n> = {\n none: {\n code: 'mailbox_required',\n message: 'Choose the mailbox this sequence sends from before activating it.',\n },\n gone: {\n code: 'mailbox_unknown',\n message: \"This sequence's mailbox is no longer connected. Choose another before activating it.\",\n },\n paused: {\n code: 'mailbox_not_sending',\n message: \"This sequence's mailbox is paused. Resume it in Mailboxes, then activate the sequence.\",\n },\n reconnect_required: {\n code: 'mailbox_not_sending',\n message:\n \"Google stopped accepting this sequence's mailbox. Reconnect it in Mailboxes, then activate the sequence.\",\n },\n}\n\n/**\n * Why a sequence cannot be activated on its mailbox, or `null` when it can\n * (AGL-2980). A sequence is activated only onto a mailbox that is sending:\n * connected, not paused, and not waiting for its member to reconnect it —\n * whether or not its steps include an email, because every step's due time\n * is read in the mailbox's timezone and sending hours.\n *\n * The status route refuses with it, and the sequence page shows it beside\n * a disabled Activate button, so the page says why before anyone clicks.\n */\nexport function outreachMailboxActivationIssue(\n mailboxId: string,\n mailbox: Pick<OutreachMailbox, 'status'> | null | undefined,\n): OutreachSequenceIssue | null {\n const state = outreachSequenceMailboxState(mailboxId, mailbox)\n if (state === 'sending') return null\n const { code, message } = MAILBOX_ACTIVATION_ISSUES[state]\n return { path: 'mailboxId', code, message, severity: 'error' }\n}\n"],"names":["readOutreachSequenceSettings","STEPS_READ_MAX","OUTREACH_DEFAULT_FOLLOW_UP_DAYS","text","value","wholeNumber","number","Number","NaN","isInteger","readWindow","raw","days","Array","isArray","map","day","startMinute","endMinute","readStep","id","trim","slice","delayBusinessDays","templateId","step","kind","subject","replyInThread","body","taskKind","title","readOutreachSequenceDraft","input","settings","name","replace","hostId","mailboxId","steps","window","outreachSequenceDraftOf","sequence","newOutreachStepId","random","Math","taken","Set","attempt","drawn","floor","toString","padStart","has","newOutreachEmailStep","follows","some","newOutreachTaskStep","titles","linkedin","call","todo","emptyOutreachSequenceDraft","allowedCountries","allowCustomers","trackClicks","outreachEnrolledSequenceIssues","previous","next","issues","locked","path","code","message","push","severity","kept","every","index","outreachSequenceMailboxState","mailbox","status","MAILBOX_ACTIVATION_ISSUES","none","gone","paused","reconnect_required","outreachMailboxActivationIssue","state"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/model/sequence-draft.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 * A SEQUENCE AS THE EDITOR HOLDS IT (AGL-2980).\n *\n * The editor builds a draft, the save route reads the draft it was sent\n * through the same reader, and the engine's validator judges both — so the\n * issues the editor shows beside a field are the ones the route would\n * refuse the save with. Client-safe.\n *\n * ## Once someone is enrolled, the steps they stand on stay put\n *\n * An enrollment points at its next step by POSITION (`stepIndex`), and it\n * was enrolled into a site's CRM from one mailbox. So after the first\n * enrollment, a save may edit any step's wording and delay and may add\n * steps at the end, but may not remove, reorder or change the kind of a\n * step that already exists, and may not move the sequence to another site\n * or mailbox: each of those would send an enrolled person a step they were\n * never going to get, from an address they never heard from. A new\n * sequence is the way to change them.\n *==========================================*/\n\nimport { normalizeCampaignIds } from '@aglyn/aglyn/app-utils/campaign-membership'\nimport {\n readOutreachSequenceSettings,\n type OutreachValidationIssue,\n type OutreachValidationCode,\n} from '../engine/sequence-validation'\nimport {\n type OutreachEmailStep,\n type OutreachMailbox,\n type OutreachSendWindow,\n type OutreachSequence,\n type OutreachSequenceSettings,\n type OutreachSequenceStep,\n type OutreachTaskKind,\n type OutreachTaskStep,\n} from './outreach.types'\n\n/** A sequence as the editor holds it and the save route receives it. */\nexport interface OutreachSequenceDraft {\n name: string\n /** The site whose CRM the enrolled people are records of. */\n hostId: string\n /** `''` until a mailbox is chosen. */\n mailboxId: string\n steps: OutreachSequenceStep[]\n settings: OutreachSequenceSettings\n /**\n * The site's campaigns the sequence is in (AGL-3254), as the picker holds\n * them: clean ids, `[]` for none — stored as `[]` too, for the reason\n * `campaignMembershipValue` gives.\n */\n campaignIds: string[]\n}\n\n/** The most steps a draft is read with; the validator refuses past the real limit. */\nconst STEPS_READ_MAX = 20\n\n/** The delay a new email after the first starts with, in business days. */\nexport const OUTREACH_DEFAULT_FOLLOW_UP_DAYS = 3\n\nconst text = (value: unknown): string => (typeof value === 'string' ? value : '')\n\n/** A whole number, or `NaN` for anything else, so the validator names it. */\nconst wholeNumber = (value: unknown): number => {\n const number = typeof value === 'number' ? value : Number(text(value) || NaN)\n return Number.isInteger(number) ? number : Number.NaN\n}\n\nfunction readWindow(value: unknown): OutreachSendWindow | null {\n if (!value || typeof value !== 'object') return null\n const raw = value as Record<string, unknown>\n return {\n days: Array.isArray(raw['days']) ? raw['days'].map((day) => wholeNumber(day)) : [],\n startMinute: wholeNumber(raw['startMinute']),\n endMinute: wholeNumber(raw['endMinute']),\n }\n}\n\nfunction readStep(value: unknown): OutreachSequenceStep {\n const raw = (value && typeof value === 'object' ? value : {}) as Record<string, unknown>\n const id = text(raw['id']).trim().slice(0, 64)\n const delayBusinessDays = wholeNumber(raw['delayBusinessDays'])\n if (raw['kind'] === 'email') {\n const templateId = text(raw['templateId']).trim()\n const step: OutreachEmailStep = {\n id,\n kind: 'email',\n delayBusinessDays,\n subject: text(raw['subject']),\n replyInThread: raw['replyInThread'] !== false,\n body: text(raw['body']),\n templateId: templateId || null,\n }\n return step\n }\n if (raw['kind'] === 'task') {\n const step: OutreachTaskStep = {\n id,\n kind: 'task',\n taskKind: text(raw['taskKind']) as OutreachTaskKind,\n title: text(raw['title']),\n delayBusinessDays,\n }\n return step\n }\n // Neither kind: kept, so the validator can say \"a step is an email or a\n // task\" beside it rather than the step silently disappearing.\n return { id, kind: text(raw['kind']) } as unknown as OutreachSequenceStep\n}\n\n/**\n * A draft read out of anything — a request body, a stored sequence — with\n * every field the model does not name left behind and every value coerced\n * to its type. Nothing is judged here; that is the validator's.\n */\nexport function readOutreachSequenceDraft(input: unknown): OutreachSequenceDraft {\n const raw = (input && typeof input === 'object' ? input : {}) as Record<string, unknown>\n const settings = readOutreachSequenceSettings(\n (raw['settings'] && typeof raw['settings'] === 'object'\n ? raw['settings']\n : {}) as Record<string, unknown>,\n )\n return {\n name: text(raw['name']).replace(/\\s+/g, ' ').trim(),\n hostId: text(raw['hostId']).trim(),\n mailboxId: text(raw['mailboxId']).trim(),\n steps: (Array.isArray(raw['steps']) ? raw['steps'] : []).slice(0, STEPS_READ_MAX).map(readStep),\n settings: {\n ...settings,\n window: readWindow((raw['settings'] as Record<string, unknown> | undefined)?.['window']),\n },\n campaignIds: normalizeCampaignIds(raw['campaignIds']),\n }\n}\n\n/** The draft a stored sequence opens as in the editor. */\nexport function outreachSequenceDraftOf(\n sequence: Pick<OutreachSequence, 'name' | 'hostId' | 'mailboxId' | 'steps' | 'settings' | 'campaignIds'>,\n): OutreachSequenceDraft {\n return readOutreachSequenceDraft(sequence)\n}\n\n/** A step id nothing else in the sequence carries. */\nexport function newOutreachStepId(\n steps: readonly Pick<OutreachSequenceStep, 'id'>[] = [],\n random: () => number = Math.random,\n): string {\n const taken = new Set(steps.map((step) => step.id))\n for (let attempt = 0; ; attempt += 1) {\n const drawn = `step-${Math.floor(random() * 36 ** 6).toString(36).padStart(6, '0')}`\n // A source that keeps answering the same number still ends: the attempt\n // count makes the id unique once the draws stop being.\n const id = attempt < 4 ? drawn : `${drawn}-${attempt}`\n if (!taken.has(id)) return id\n }\n}\n\n/**\n * A new email step for the end of `steps`: the first email waits for the\n * next opening; a later one waits a few business days and replies in the\n * thread the first started.\n */\nexport function newOutreachEmailStep(\n steps: readonly OutreachSequenceStep[],\n random?: () => number,\n): OutreachEmailStep {\n const follows = steps.some((step) => step.kind === 'email')\n return {\n id: newOutreachStepId(steps, random),\n kind: 'email',\n delayBusinessDays: follows ? OUTREACH_DEFAULT_FOLLOW_UP_DAYS : 0,\n subject: '',\n replyInThread: true,\n body: '',\n templateId: null,\n }\n}\n\n/** A new task step for the end of `steps`, titled for its kind. */\nexport function newOutreachTaskStep(\n steps: readonly OutreachSequenceStep[],\n taskKind: OutreachTaskKind,\n random?: () => number,\n): OutreachTaskStep {\n const titles: Record<OutreachTaskKind, string> = {\n linkedin: 'Connect on LinkedIn',\n call: 'Call',\n todo: '',\n }\n return {\n id: newOutreachStepId(steps, random),\n kind: 'task',\n taskKind,\n title: titles[taskKind] ?? '',\n delayBusinessDays: 1,\n }\n}\n\n/** A new sequence: one email, for `hostId`, sending to the countries given. */\nexport function emptyOutreachSequenceDraft(input: {\n hostId: string\n mailboxId?: string\n allowedCountries: readonly string[]\n random?: () => number\n}): OutreachSequenceDraft {\n return {\n name: '',\n hostId: input.hostId,\n mailboxId: input.mailboxId ?? '',\n steps: [newOutreachEmailStep([], input.random)],\n settings: {\n window: null,\n allowedCountries: [...input.allowedCountries],\n allowCustomers: false,\n // Off for a NEW sequence too (AGL-3239), not only for the ones that\n // predate it: a tracked link is visible in the body a cold recipient\n // reads, so it is turned on deliberately or not at all.\n trackClicks: false,\n },\n campaignIds: [],\n }\n}\n\n/** The issues a save can be refused with beyond the engine's own. */\nexport type OutreachSequenceIssueCode =\n | OutreachValidationCode\n | 'steps_locked'\n | 'host_locked'\n | 'mailbox_locked'\n | 'host_unknown'\n | 'mailbox_unknown'\n | 'mailbox_not_yours'\n | 'mailbox_not_sending'\n | 'country_not_in_org'\n | 'campaign_unknown'\n\nexport interface OutreachSequenceIssue extends Omit<OutreachValidationIssue, 'code'> {\n code: OutreachSequenceIssueCode\n}\n\n/**\n * What a save may not change once someone is enrolled — see the module\n * note. `previous` is the stored sequence; `next` the draft being saved.\n */\nexport function outreachEnrolledSequenceIssues(\n previous: Pick<OutreachSequenceDraft, 'hostId' | 'mailboxId' | 'steps'>,\n next: Pick<OutreachSequenceDraft, 'hostId' | 'mailboxId' | 'steps'>,\n): OutreachSequenceIssue[] {\n const issues: OutreachSequenceIssue[] = []\n const locked = (path: string, code: OutreachSequenceIssueCode, message: string) =>\n issues.push({ path, code, message, severity: 'error' })\n const kept = previous.steps.every(\n (step, index) => next.steps[index]?.id === step.id && next.steps[index]?.kind === step.kind,\n )\n if (!kept) {\n locked(\n 'steps',\n 'steps_locked',\n \"People are enrolled in this sequence, so its steps can't be removed, reordered or changed to another kind. Edit their wording, add steps at the end, or start a new sequence.\",\n )\n }\n if (previous.hostId !== next.hostId) {\n locked('hostId', 'host_locked', \"People are enrolled from this site's CRM, so the site can't change.\")\n }\n if (previous.mailboxId !== next.mailboxId) {\n locked(\n 'mailboxId',\n 'mailbox_locked',\n \"People are enrolled from this mailbox, so it can't change. Start a new sequence to send from another.\",\n )\n }\n return issues\n}\n\n/**\n * Whether a sequence's mailbox can send right now, as activation asks it:\n * `none` when no mailbox is chosen, `gone` when it was disconnected or is\n * not this organization's, `paused` or `reconnect_required` while it is\n * connected but not sending, and `sending` when it is connected.\n */\nexport type OutreachSequenceMailboxState =\n | 'none'\n | 'gone'\n | 'paused'\n | 'reconnect_required'\n | 'sending'\n\nexport function outreachSequenceMailboxState(\n mailboxId: string,\n mailbox: Pick<OutreachMailbox, 'status'> | null | undefined,\n): OutreachSequenceMailboxState {\n if (!mailboxId) return 'none'\n if (!mailbox || mailbox.status === 'disconnected') return 'gone'\n if (mailbox.status === 'paused' || mailbox.status === 'reconnect_required') {\n return mailbox.status\n }\n return 'sending'\n}\n\n/** What activation refuses with for each mailbox that cannot send. */\nconst MAILBOX_ACTIVATION_ISSUES: Record<\n Exclude<OutreachSequenceMailboxState, 'sending'>,\n { code: OutreachSequenceIssueCode; message: string }\n> = {\n none: {\n code: 'mailbox_required',\n message: 'Choose the mailbox this sequence sends from before activating it.',\n },\n gone: {\n code: 'mailbox_unknown',\n message: \"This sequence's mailbox is no longer connected. Choose another before activating it.\",\n },\n paused: {\n code: 'mailbox_not_sending',\n message: \"This sequence's mailbox is paused. Resume it in Mailboxes, then activate the sequence.\",\n },\n reconnect_required: {\n code: 'mailbox_not_sending',\n message:\n \"Google stopped accepting this sequence's mailbox. Reconnect it in Mailboxes, then activate the sequence.\",\n },\n}\n\n/**\n * Why a sequence cannot be activated on its mailbox, or `null` when it can\n * (AGL-2980). A sequence is activated only onto a mailbox that is sending:\n * connected, not paused, and not waiting for its member to reconnect it —\n * whether or not its steps include an email, because every step's due time\n * is read in the mailbox's timezone and sending hours.\n *\n * The status route refuses with it, and the sequence page shows it beside\n * a disabled Activate button, so the page says why before anyone clicks.\n */\nexport function outreachMailboxActivationIssue(\n mailboxId: string,\n mailbox: Pick<OutreachMailbox, 'status'> | null | undefined,\n): OutreachSequenceIssue | null {\n const state = outreachSequenceMailboxState(mailboxId, mailbox)\n if (state === 'sending') return null\n const { code, message } = MAILBOX_ACTIVATION_ISSUES[state]\n return { path: 'mailboxId', code, message, severity: 'error' }\n}\n"],"names":["normalizeCampaignIds","readOutreachSequenceSettings","STEPS_READ_MAX","OUTREACH_DEFAULT_FOLLOW_UP_DAYS","text","value","wholeNumber","number","Number","NaN","isInteger","readWindow","raw","days","Array","isArray","map","day","startMinute","endMinute","readStep","id","trim","slice","delayBusinessDays","templateId","step","kind","subject","replyInThread","body","taskKind","title","readOutreachSequenceDraft","input","settings","name","replace","hostId","mailboxId","steps","window","campaignIds","outreachSequenceDraftOf","sequence","newOutreachStepId","random","Math","taken","Set","attempt","drawn","floor","toString","padStart","has","newOutreachEmailStep","follows","some","newOutreachTaskStep","titles","linkedin","call","todo","emptyOutreachSequenceDraft","allowedCountries","allowCustomers","trackClicks","outreachEnrolledSequenceIssues","previous","next","issues","locked","path","code","message","push","severity","kept","every","index","outreachSequenceMailboxState","mailbox","status","MAILBOX_ACTIVATION_ISSUES","none","gone","paused","reconnect_required","outreachMailboxActivationIssue","state"],"mappings":"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"}
|
|
@@ -21,7 +21,8 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
21
21
|
* Read DEFENSIVELY, a field at a time, because the readers act on what they
|
|
22
22
|
* read: a sequence's steps are what gets sent, and an enrollment's status is
|
|
23
23
|
* what decides whether it is sent at all.
|
|
24
|
-
*/ import {
|
|
24
|
+
*/ import { normalizeCampaignIds } from "@aglyn/aglyn/app-utils/campaign-membership";
|
|
25
|
+
import { readOutreachSequenceDraft } from "./sequence-draft.js";
|
|
25
26
|
import { OUTREACH_ENROLLMENT_STATUSES, OUTREACH_SEQUENCE_STATUSES, OUTREACH_STOP_REASONS } from "./outreach.types.js";
|
|
26
27
|
const text = (value)=>typeof value === 'string' ? value : '';
|
|
27
28
|
const ms = (value)=>typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
@@ -110,6 +111,10 @@ const texts = (value)=>Array.isArray(value) ? value.filter((entry)=>typeof entry
|
|
|
110
111
|
threadSubject: typeof data['threadSubject'] === 'string' ? data['threadSubject'] : null,
|
|
111
112
|
messageIds: texts(data['messageIds']),
|
|
112
113
|
lastSentAtMs: ms(data['lastSentAtMs']),
|
|
114
|
+
// Absent on an enrollment made before a sequence could join a campaign
|
|
115
|
+
// (AGL-3254), and read as none: nothing is credited to a campaign the
|
|
116
|
+
// enrollment does not name.
|
|
117
|
+
campaignIds: normalizeCampaignIds(data['campaignIds']),
|
|
113
118
|
createdAtMs: (_ms = ms(data['createdAtMs'])) != null ? _ms : 0,
|
|
114
119
|
updatedAtMs: (_ms1 = ms(data['updatedAtMs'])) != null ? _ms1 : 0
|
|
115
120
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/model/stored-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 * A stored Outreach document read back into its model shape (AGL-2980),\n * for the server's routes and the console's listeners alike. Client-safe.\n *\n * Read DEFENSIVELY, a field at a time, because the readers act on what they\n * read: a sequence's steps are what gets sent, and an enrollment's status is\n * what decides whether it is sent at all.\n */\n\nimport { readOutreachSequenceDraft } from './sequence-draft'\nimport {\n OUTREACH_ENROLLMENT_STATUSES,\n OUTREACH_SEQUENCE_STATUSES,\n OUTREACH_STOP_REASONS,\n type OutreachEnrollment,\n type OutreachEnrollmentStatus,\n type OutreachEnrollmentTarget,\n type OutreachMailbox,\n type OutreachSequence,\n type OutreachSequenceStats,\n type OutreachSequenceStatus,\n type OutreachStopReason,\n} from './outreach.types'\n\nconst text = (value: unknown): string =>\n typeof value === 'string' ? value : ''\nconst ms = (value: unknown): number | null =>\n typeof value === 'number' && Number.isFinite(value) ? value : null\nconst texts = (value: unknown): string[] =>\n Array.isArray(value)\n ? value.filter((entry): entry is string => typeof entry === 'string')\n : []\n\n/** A stored sequence in its model shape, or `null` for no document. */\nexport function readStoredOutreachSequence(\n id: string,\n data: Record<string, unknown> | undefined,\n): OutreachSequence | null {\n if (!data) return null\n const draft = readOutreachSequenceDraft(data)\n const status = data['status']\n const stats = readOutreachSequenceStats(data['stats'])\n return {\n id,\n ...draft,\n status: (OUTREACH_SEQUENCE_STATUSES as readonly unknown[]).includes(status)\n ? (status as OutreachSequenceStatus)\n : 'draft',\n // Absent rather than an empty object for a sequence that has none\n // (AGL-3239): the report reads an absent counter as \"not recorded\" and\n // a present one as a number, and `{}` would be neither.\n ...(stats ? { stats } : {}),\n createdAtMs: ms(data['createdAtMs']) ?? 0,\n updatedAtMs: ms(data['updatedAtMs']) ?? 0,\n }\n}\n\n/**\n * A sequence's stored counters (AGL-3239), or `null` when it has none.\n *\n * Every field is carried through only when it is READABLE, so an absence\n * survives the round trip as an absence: the whole of the report's honesty\n * is that a counter nobody wrote is not a zero, and a normalizer that\n * defaulted these to `0` would destroy the distinction here, before any\n * reader could make it.\n */\nexport function readOutreachSequenceStats(\n raw: unknown,\n): OutreachSequenceStats | null {\n if (!raw || typeof raw !== 'object') return null\n const data = raw as Record<string, unknown>\n const stats: OutreachSequenceStats = {}\n for (const key of ['sent', 'people', 'clicks', 'uniqueClicks', 'machineClicks'] as const) {\n const value = data[key]\n if (typeof value === 'number' && Number.isFinite(value) && value >= 0) {\n stats[key] = Math.floor(value)\n }\n }\n if (data['clickTracked'] === true) stats.clickTracked = true\n const last = data['lastClickAtMs']\n if (typeof last === 'number' && Number.isFinite(last)) stats.lastClickAtMs = last\n return Object.keys(stats).length ? stats : null\n}\n\n/** A stored enrollment in its model shape, or `null` for no document. */\nexport function readStoredOutreachEnrollment(\n id: string,\n data: Record<string, unknown> | undefined,\n): OutreachEnrollment | null {\n if (!data) return null\n const status = data['status']\n const stopReason = data['stopReason']\n const stepIndex = Number(data['stepIndex'])\n // An enrollment written before leads could be sequenced (AGL-3234) names\n // no target and is a contact's; one that names a lead and no target is a\n // lead's, whatever else it says.\n const leadId = text(data['leadId']) || null\n const target: OutreachEnrollmentTarget =\n data['target'] === 'lead' || (data['target'] === undefined && leadId && !text(data['contactId']))\n ? 'lead'\n : 'contact'\n return {\n ...(data as unknown as OutreachEnrollment),\n id,\n sequenceId: text(data['sequenceId']),\n target,\n contactId: text(data['contactId']),\n leadId,\n contactName: text(data['contactName']),\n email: text(data['email']),\n hostId: text(data['hostId']),\n mailboxId: text(data['mailboxId']),\n stepIndex: Number.isInteger(stepIndex) && stepIndex >= 0 ? stepIndex : 0,\n nextDueAtMs: ms(data['nextDueAtMs']),\n // A status the model does not name is read as stopped: nothing sends\n // from a document nobody can say the state of.\n status: (OUTREACH_ENROLLMENT_STATUSES as readonly unknown[]).includes(\n status,\n )\n ? (status as OutreachEnrollmentStatus)\n : 'stopped',\n stopReason: (OUTREACH_STOP_REASONS as readonly unknown[]).includes(\n stopReason,\n )\n ? (stopReason as OutreachStopReason)\n : null,\n stopDetail:\n typeof data['stopDetail'] === 'string' ? data['stopDetail'] : null,\n stoppedAtMs: ms(data['stoppedAtMs']),\n stoppedByUid:\n typeof data['stoppedByUid'] === 'string' ? data['stoppedByUid'] : null,\n personalLine: text(data['personalLine']),\n cold: data['cold'] === true,\n enrolledByUid: text(data['enrolledByUid']),\n gmailThreadId:\n typeof data['gmailThreadId'] === 'string' ? data['gmailThreadId'] : null,\n gmailThreadIds: texts(data['gmailThreadIds']),\n threadSubject:\n typeof data['threadSubject'] === 'string' ? data['threadSubject'] : null,\n messageIds: texts(data['messageIds']),\n lastSentAtMs: ms(data['lastSentAtMs']),\n createdAtMs: ms(data['createdAtMs']) ?? 0,\n updatedAtMs: ms(data['updatedAtMs']) ?? 0,\n }\n}\n\n/** A stored mailbox with its id, or `null` for no document. */\nexport function readStoredOutreachMailbox(\n id: string,\n data: Record<string, unknown> | undefined,\n): OutreachMailbox | null {\n if (!data) return null\n return { ...(data as unknown as OutreachMailbox), id }\n}\n"],"names":["readOutreachSequenceDraft","OUTREACH_ENROLLMENT_STATUSES","OUTREACH_SEQUENCE_STATUSES","OUTREACH_STOP_REASONS","text","value","ms","Number","isFinite","texts","Array","isArray","filter","entry","readStoredOutreachSequence","id","data","draft","status","stats","readOutreachSequenceStats","includes","createdAtMs","updatedAtMs","raw","key","Math","floor","clickTracked","last","lastClickAtMs","Object","keys","length","readStoredOutreachEnrollment","stopReason","stepIndex","leadId","target","undefined","sequenceId","contactId","contactName","email","hostId","mailboxId","isInteger","nextDueAtMs","stopDetail","stoppedAtMs","stoppedByUid","personalLine","cold","enrolledByUid","gmailThreadId","gmailThreadIds","threadSubject","messageIds","lastSentAtMs","readStoredOutreachMailbox"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/model/stored-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 * A stored Outreach document read back into its model shape (AGL-2980),\n * for the server's routes and the console's listeners alike. Client-safe.\n *\n * Read DEFENSIVELY, a field at a time, because the readers act on what they\n * read: a sequence's steps are what gets sent, and an enrollment's status is\n * what decides whether it is sent at all.\n */\n\nimport { normalizeCampaignIds } from '@aglyn/aglyn/app-utils/campaign-membership'\nimport { readOutreachSequenceDraft } from './sequence-draft'\nimport {\n OUTREACH_ENROLLMENT_STATUSES,\n OUTREACH_SEQUENCE_STATUSES,\n OUTREACH_STOP_REASONS,\n type OutreachEnrollment,\n type OutreachEnrollmentStatus,\n type OutreachEnrollmentTarget,\n type OutreachMailbox,\n type OutreachSequence,\n type OutreachSequenceStats,\n type OutreachSequenceStatus,\n type OutreachStopReason,\n} from './outreach.types'\n\nconst text = (value: unknown): string =>\n typeof value === 'string' ? value : ''\nconst ms = (value: unknown): number | null =>\n typeof value === 'number' && Number.isFinite(value) ? value : null\nconst texts = (value: unknown): string[] =>\n Array.isArray(value)\n ? value.filter((entry): entry is string => typeof entry === 'string')\n : []\n\n/** A stored sequence in its model shape, or `null` for no document. */\nexport function readStoredOutreachSequence(\n id: string,\n data: Record<string, unknown> | undefined,\n): OutreachSequence | null {\n if (!data) return null\n const draft = readOutreachSequenceDraft(data)\n const status = data['status']\n const stats = readOutreachSequenceStats(data['stats'])\n return {\n id,\n ...draft,\n status: (OUTREACH_SEQUENCE_STATUSES as readonly unknown[]).includes(status)\n ? (status as OutreachSequenceStatus)\n : 'draft',\n // Absent rather than an empty object for a sequence that has none\n // (AGL-3239): the report reads an absent counter as \"not recorded\" and\n // a present one as a number, and `{}` would be neither.\n ...(stats ? { stats } : {}),\n createdAtMs: ms(data['createdAtMs']) ?? 0,\n updatedAtMs: ms(data['updatedAtMs']) ?? 0,\n }\n}\n\n/**\n * A sequence's stored counters (AGL-3239), or `null` when it has none.\n *\n * Every field is carried through only when it is READABLE, so an absence\n * survives the round trip as an absence: the whole of the report's honesty\n * is that a counter nobody wrote is not a zero, and a normalizer that\n * defaulted these to `0` would destroy the distinction here, before any\n * reader could make it.\n */\nexport function readOutreachSequenceStats(\n raw: unknown,\n): OutreachSequenceStats | null {\n if (!raw || typeof raw !== 'object') return null\n const data = raw as Record<string, unknown>\n const stats: OutreachSequenceStats = {}\n for (const key of ['sent', 'people', 'clicks', 'uniqueClicks', 'machineClicks'] as const) {\n const value = data[key]\n if (typeof value === 'number' && Number.isFinite(value) && value >= 0) {\n stats[key] = Math.floor(value)\n }\n }\n if (data['clickTracked'] === true) stats.clickTracked = true\n const last = data['lastClickAtMs']\n if (typeof last === 'number' && Number.isFinite(last)) stats.lastClickAtMs = last\n return Object.keys(stats).length ? stats : null\n}\n\n/** A stored enrollment in its model shape, or `null` for no document. */\nexport function readStoredOutreachEnrollment(\n id: string,\n data: Record<string, unknown> | undefined,\n): OutreachEnrollment | null {\n if (!data) return null\n const status = data['status']\n const stopReason = data['stopReason']\n const stepIndex = Number(data['stepIndex'])\n // An enrollment written before leads could be sequenced (AGL-3234) names\n // no target and is a contact's; one that names a lead and no target is a\n // lead's, whatever else it says.\n const leadId = text(data['leadId']) || null\n const target: OutreachEnrollmentTarget =\n data['target'] === 'lead' || (data['target'] === undefined && leadId && !text(data['contactId']))\n ? 'lead'\n : 'contact'\n return {\n ...(data as unknown as OutreachEnrollment),\n id,\n sequenceId: text(data['sequenceId']),\n target,\n contactId: text(data['contactId']),\n leadId,\n contactName: text(data['contactName']),\n email: text(data['email']),\n hostId: text(data['hostId']),\n mailboxId: text(data['mailboxId']),\n stepIndex: Number.isInteger(stepIndex) && stepIndex >= 0 ? stepIndex : 0,\n nextDueAtMs: ms(data['nextDueAtMs']),\n // A status the model does not name is read as stopped: nothing sends\n // from a document nobody can say the state of.\n status: (OUTREACH_ENROLLMENT_STATUSES as readonly unknown[]).includes(\n status,\n )\n ? (status as OutreachEnrollmentStatus)\n : 'stopped',\n stopReason: (OUTREACH_STOP_REASONS as readonly unknown[]).includes(\n stopReason,\n )\n ? (stopReason as OutreachStopReason)\n : null,\n stopDetail:\n typeof data['stopDetail'] === 'string' ? data['stopDetail'] : null,\n stoppedAtMs: ms(data['stoppedAtMs']),\n stoppedByUid:\n typeof data['stoppedByUid'] === 'string' ? data['stoppedByUid'] : null,\n personalLine: text(data['personalLine']),\n cold: data['cold'] === true,\n enrolledByUid: text(data['enrolledByUid']),\n gmailThreadId:\n typeof data['gmailThreadId'] === 'string' ? data['gmailThreadId'] : null,\n gmailThreadIds: texts(data['gmailThreadIds']),\n threadSubject:\n typeof data['threadSubject'] === 'string' ? data['threadSubject'] : null,\n messageIds: texts(data['messageIds']),\n lastSentAtMs: ms(data['lastSentAtMs']),\n // Absent on an enrollment made before a sequence could join a campaign\n // (AGL-3254), and read as none: nothing is credited to a campaign the\n // enrollment does not name.\n campaignIds: normalizeCampaignIds(data['campaignIds']),\n createdAtMs: ms(data['createdAtMs']) ?? 0,\n updatedAtMs: ms(data['updatedAtMs']) ?? 0,\n }\n}\n\n/** A stored mailbox with its id, or `null` for no document. */\nexport function readStoredOutreachMailbox(\n id: string,\n data: Record<string, unknown> | undefined,\n): OutreachMailbox | null {\n if (!data) return null\n return { ...(data as unknown as OutreachMailbox), id }\n}\n"],"names":["normalizeCampaignIds","readOutreachSequenceDraft","OUTREACH_ENROLLMENT_STATUSES","OUTREACH_SEQUENCE_STATUSES","OUTREACH_STOP_REASONS","text","value","ms","Number","isFinite","texts","Array","isArray","filter","entry","readStoredOutreachSequence","id","data","draft","status","stats","readOutreachSequenceStats","includes","createdAtMs","updatedAtMs","raw","key","Math","floor","clickTracked","last","lastClickAtMs","Object","keys","length","readStoredOutreachEnrollment","stopReason","stepIndex","leadId","target","undefined","sequenceId","contactId","contactName","email","hostId","mailboxId","isInteger","nextDueAtMs","stopDetail","stoppedAtMs","stoppedByUid","personalLine","cold","enrolledByUid","gmailThreadId","gmailThreadIds","threadSubject","messageIds","lastSentAtMs","campaignIds","readStoredOutreachMailbox"],"mappings":"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"}
|
|
@@ -14,10 +14,12 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
14
14
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
|
-
*/ import {
|
|
17
|
+
*/ import { CAMPAIGN_MEMBERSHIP_FIELD, contactCampaignFieldPath, normalizeCampaignIds } from "@aglyn/aglyn/app-utils/campaign-membership";
|
|
18
|
+
import { consentGroupForHost } from "@aglyn/aglyn/app-utils/consent-groups";
|
|
18
19
|
import { CRM_COLLECTIONS, crmLeadStatus, crmViewIsListed, isCrmLeadOpen, normalizeCrmViewFilters } from "@aglyn/aglyn/app-utils/crm";
|
|
19
20
|
import { dynamicListDimensionsForCrmView } from "@aglyn/aglyn/app-utils/dynamic-list-rule";
|
|
20
21
|
import { findContactByEmail } from "@aglyn/tenant-data-admin/server/contact-email-index";
|
|
22
|
+
import { FieldValue } from "firebase-admin/firestore";
|
|
21
23
|
import { buildOutreachEnrollment, planOutreachFirstDue } from "../engine/enrollment-state.js";
|
|
22
24
|
import { decideOutreachEnrollment, previewOutreachPerson, readOutreachEnrollCandidates } from "../enrollment/enroll-people.js";
|
|
23
25
|
import { readOutreachGateLookups } from "../enrollment/gate-lookups.js";
|
|
@@ -172,6 +174,41 @@ const uniqueIds = (values)=>Array.isArray(values) ? [
|
|
|
172
174
|
}
|
|
173
175
|
export function createOutreachEnrollRoutes(deps) {
|
|
174
176
|
/**
|
|
177
|
+
* The person joins the sequence's campaigns (AGL-3254), the moment they
|
|
178
|
+
* are enrolled: `arrayUnion` on the lead's own `campaignIds`, or on the
|
|
179
|
+
* contact's facet for the sequence's site — the same field, the same
|
|
180
|
+
* shape, every other campaign member carries — and `enrolled` credited
|
|
181
|
+
* to each campaign. After the enrollment is created and never before:
|
|
182
|
+
* a person the create refused joins nothing. Neither write may fail the
|
|
183
|
+
* enrollment, which already exists; a membership that could not be
|
|
184
|
+
* stamped is one the list's bulk bar can add by hand.
|
|
185
|
+
*/ async function joinSequenceCampaigns(firestore, orgId, sequence, contactGroupId, candidate, nowMs) {
|
|
186
|
+
const campaignIds = normalizeCampaignIds(sequence.campaignIds);
|
|
187
|
+
if (!campaignIds.length) return;
|
|
188
|
+
try {
|
|
189
|
+
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;
|
|
190
|
+
if (ref) {
|
|
191
|
+
const field = candidate.target === 'lead' ? CAMPAIGN_MEMBERSHIP_FIELD : contactCampaignFieldPath(contactGroupId);
|
|
192
|
+
await ref.update({
|
|
193
|
+
[field]: FieldValue.arrayUnion(...campaignIds),
|
|
194
|
+
updatedAt: FieldValue.serverTimestamp()
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
} catch (error) {
|
|
198
|
+
console.error('[outreach] the enrolled person could not join the sequence’s campaigns', error);
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
await deps.creditCampaign({
|
|
202
|
+
hostId: sequence.hostId,
|
|
203
|
+
campaignIds,
|
|
204
|
+
outcome: 'enrolled',
|
|
205
|
+
atMs: nowMs
|
|
206
|
+
});
|
|
207
|
+
} catch (error) {
|
|
208
|
+
console.error('[outreach] the enrollment could not be credited to the sequence’s campaigns', error);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
175
212
|
* The people a source names, the batch limit applied: every contact a
|
|
176
213
|
* search picked, every lead picked from the sequence's site, or the
|
|
177
214
|
* people a saved Contacts or Leads view selects there — contacts found by
|
|
@@ -392,6 +429,7 @@ export function createOutreachEnrollRoutes(deps) {
|
|
|
392
429
|
]
|
|
393
430
|
});
|
|
394
431
|
}
|
|
432
|
+
await joinSequenceCampaigns(firestore, caller.orgId, sequence, context.contactGroupId, candidate, nowMs);
|
|
395
433
|
return _extends({}, named, {
|
|
396
434
|
email: decision.email,
|
|
397
435
|
outcome: 'enrolled',
|
|
@@ -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 { 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 { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport { findContactByEmail } from '@aglyn/tenant-data-admin/server/contact-email-index'\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 {\n outreachEnrollmentId,\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\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 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 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 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":["consentGroupForHost","CRM_COLLECTIONS","crmLeadStatus","crmViewIsListed","isCrmLeadOpen","normalizeCrmViewFilters","dynamicListDimensionsForCrmView","findContactByEmail","buildOutreachEnrollment","planOutreachFirstDue","decideOutreachEnrollment","previewOutreachPerson","readOutreachEnrollCandidates","readOutreachGateLookups","effectiveOutreachAllowedCountries","OUTREACH_ENROLL_BATCH_MAX","readOutreachComplianceSettingsDoc","outreachEnrollmentId","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","sourcePeople","source","ids","viewId","view","views","shared","ownerUid","uid","unsupported","label","join","emails","complete","crmViewEmails","ordered","sort","contacts","found","preview","request","method","body","gate","Response","named","alreadyInSequence","has","ok","confirm","requested","person","ref","set","personalLine","attestations","size","mailboxSnapshot","mailboxId","mailbox","nowMs","now","enrolledAtMs","random","results","target","supplied","decision","allowed","outcome","blocks","enrollment","contactName","name","cold","enrolledByUid","create","error","code","reason","enrollmentId","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 {\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 { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport { findContactByEmail } from '@aglyn/tenant-data-admin/server/contact-email-index'\nimport { FieldValue } from 'firebase-admin/firestore'\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 {\n outreachEnrollmentId,\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\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 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 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 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","buildOutreachEnrollment","planOutreachFirstDue","decideOutreachEnrollment","previewOutreachPerson","readOutreachEnrollCandidates","readOutreachGateLookups","effectiveOutreachAllowedCountries","OUTREACH_ENROLL_BATCH_MAX","readOutreachComplianceSettingsDoc","outreachEnrollmentId","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","sourcePeople","source","ids","viewId","view","views","shared","ownerUid","uid","unsupported","label","join","emails","complete","crmViewEmails","ordered","sort","contacts","found","preview","request","method","body","gate","Response","named","alreadyInSequence","has","ok","confirm","requested","person","set","personalLine","attestations","size","mailboxSnapshot","mailboxId","mailbox","now","enrolledAtMs","random","results","supplied","decision","allowed","blocks","enrollment","contactName","name","cold","enrolledByUid","create","code","reason","enrollmentId","enrolled","result","logOrgActivity","type"],"mappings":"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"}
|
|
@@ -134,6 +134,19 @@ export function createOutreachEnrollmentActionRoute(deps) {
|
|
|
134
134
|
detail
|
|
135
135
|
});
|
|
136
136
|
stoppedOthers = await optOutOtherEnrollments(firestore, enrollments, applied.enrollment, caller.uid, nowMs);
|
|
137
|
+
// The mark on the record the person is (AGL-3245): the member's verdict
|
|
138
|
+
// stands over every automatic one.
|
|
139
|
+
await deps.stampRecordEmailState({
|
|
140
|
+
orgId: caller.orgId,
|
|
141
|
+
email: applied.enrollment.email,
|
|
142
|
+
state: {
|
|
143
|
+
status: 'do_not_contact',
|
|
144
|
+
atMs: nowMs,
|
|
145
|
+
source: 'member',
|
|
146
|
+
detail,
|
|
147
|
+
enrollmentId
|
|
148
|
+
}
|
|
149
|
+
});
|
|
137
150
|
}
|
|
138
151
|
return outreachOk({
|
|
139
152
|
ok: true,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/enrollment-routes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport {\n applyOutreachEnrollmentEvent,\n type OutreachEnrollmentEvent,\n} from '../engine/enrollment-state'\nimport { OUTREACH_OPEN_ENROLLMENT_STATUSES } from '../engine/gates'\nimport type {\n OutreachEnrollmentAction,\n OutreachEnrollmentActionResponse,\n} from '../model/outreach-api'\nimport type { OutreachEnrollment } from '../model/outreach.types'\nimport { addOutreachDoNotContact } from '../storage/do-not-contact-store'\nimport { outreachOrgCollection, readStoredOutreachEnrollment } from '../storage/outreach-records'\nimport type { OutreachRouteDeps } from './route-deps'\nimport { outreachRouteGate } from './route-gate'\nimport {\n outreachMethodNotAllowed,\n outreachOk,\n outreachRefusal,\n readOutreachDocumentId,\n readOutreachJsonBody,\n} from './route-http'\n\n/**\n * WHAT A MEMBER DOES TO ONE ENROLLMENT (AGL-2980):\n * `outreach/enrollments/action` — pause, resume, stop, or mark the person\n * do-not-contact.\n *\n * Every transition is the engine's (`applyOutreachEnrollmentEvent`): the\n * route decides nothing about which status may follow which. Each write is\n * a transaction over the one document, so a member's pause and the sending\n * runtime's step completion cannot interleave and leave a paused enrollment\n * with a step it should not have run.\n *\n * MARK DO-NOT-CONTACT puts the address on the organization's list, with\n * the member as its author, and then opts out every enrollment that address\n * has in the organization — this one wherever it stood, and any other that\n * is still active or paused — so no sequence emails it again, whichever one\n * would have been next.\n */\n\nconst ACTIONS: readonly OutreachEnrollmentAction[] = ['pause', 'resume', 'stop', 'do_not_contact']\n\nfunction eventFor(\n action: OutreachEnrollmentAction,\n atMs: number,\n byUid: string,\n detail: string | null,\n): OutreachEnrollmentEvent {\n switch (action) {\n case 'pause':\n return { type: 'pause', atMs, byUid, detail }\n case 'resume':\n return { type: 'resume', atMs, byUid }\n case 'stop':\n return { type: 'stop', atMs, byUid, reason: 'manual', detail }\n case 'do_not_contact':\n return { type: 'opt_out', atMs, reason: 'do_not_contact', detail }\n }\n}\n\nexport function createOutreachEnrollmentActionRoute(deps: OutreachRouteDeps): PluginWebApiHandler {\n return async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate)\n if (caller instanceof Response) return caller\n const enrollmentId = readOutreachDocumentId(body['enrollmentId'])\n const action = body['action'] as OutreachEnrollmentAction\n if (!enrollmentId || !ACTIONS.includes(action)) {\n return outreachRefusal(400, 'invalid-request', 'Name the enrollment, and pause, resume, stop or mark it do-not-contact.')\n }\n const detail = typeof body['detail'] === 'string' ? body['detail'] : null\n const firestore = deps.firestore()\n const enrollments = outreachOrgCollection(firestore, caller.orgId, 'enrollments')\n const ref = enrollments.doc(enrollmentId)\n const nowMs = deps.now()\n\n const applied = await firestore.runTransaction(async (transaction) => {\n const snapshot = await transaction.get(ref)\n const enrollment = readStoredOutreachEnrollment(enrollmentId, snapshot.exists ? snapshot.data() : undefined)\n if (!enrollment) return null\n const { patch, error } = applyOutreachEnrollmentEvent(\n enrollment,\n eventFor(action, nowMs, caller.uid, detail),\n )\n if (!patch) return { enrollment, error, changed: false }\n // The opt-out event names no member; the person who marked it is\n // recorded here, beside the reason, and on the list entry.\n const written = {\n ...patch,\n stoppedByUid: action === 'do_not_contact' ? caller.uid : patch.stoppedByUid,\n updatedAtMs: nowMs,\n }\n transaction.update(ref, written)\n return { enrollment: { ...enrollment, ...written } as OutreachEnrollment, error: null, changed: true }\n })\n if (!applied) return outreachRefusal(404, 'enrollment-not-found', 'That enrollment no longer exists.')\n // An address that bounced still goes on the list when a member asks:\n // the enrollment's own status stays final, the list entry is what they\n // asked for.\n if (applied.error && action !== 'do_not_contact') {\n return outreachRefusal(409, 'transition-refused', applied.error)\n }\n\n let stoppedOthers = 0\n if (action === 'do_not_contact') {\n await addOutreachDoNotContact(firestore, {\n orgId: caller.orgId,\n email: applied.enrollment.email,\n reason: 'manual',\n source: 'member',\n addedByUid: caller.uid,\n nowMs,\n enrollmentId,\n sequenceId: applied.enrollment.sequenceId,\n detail,\n })\n stoppedOthers = await optOutOtherEnrollments(firestore, enrollments, applied.enrollment, caller.uid, nowMs)\n }\n return outreachOk({\n ok: true,\n changed: applied.changed,\n enrollment: applied.enrollment,\n stoppedOthers,\n } satisfies OutreachEnrollmentActionResponse)\n }\n}\n\n/** Opts out every OTHER open enrollment of the same address; returns how many. */\nasync function optOutOtherEnrollments(\n firestore: FirebaseFirestore.Firestore,\n enrollments: FirebaseFirestore.CollectionReference,\n marked: OutreachEnrollment,\n byUid: string,\n nowMs: number,\n): Promise<number> {\n const found = await enrollments.where('email', '==', marked.email).get()\n const batch = firestore.batch()\n let writes = 0\n for (const doc of found.docs) {\n if (doc.id === marked.id) continue\n const other = readStoredOutreachEnrollment(doc.id, doc.data())\n if (!other || !OUTREACH_OPEN_ENROLLMENT_STATUSES.includes(other.status)) continue\n const { patch } = applyOutreachEnrollmentEvent(other, {\n type: 'opt_out',\n atMs: nowMs,\n reason: 'do_not_contact',\n detail: 'Marked do-not-contact from another sequence.',\n })\n if (!patch) continue\n batch.update(doc.ref, { ...patch, stoppedByUid: byUid, updatedAtMs: nowMs })\n writes += 1\n }\n if (writes) await batch.commit()\n return writes\n}\n"],"names":["applyOutreachEnrollmentEvent","OUTREACH_OPEN_ENROLLMENT_STATUSES","addOutreachDoNotContact","outreachOrgCollection","readStoredOutreachEnrollment","outreachRouteGate","outreachMethodNotAllowed","outreachOk","outreachRefusal","readOutreachDocumentId","readOutreachJsonBody","ACTIONS","eventFor","action","atMs","byUid","detail","type","reason","createOutreachEnrollmentActionRoute","deps","request","method","body","caller","gate","Response","enrollmentId","includes","firestore","enrollments","orgId","ref","doc","nowMs","now","applied","runTransaction","transaction","snapshot","get","enrollment","exists","data","undefined","patch","error","uid","changed","written","stoppedByUid","updatedAtMs","update","stoppedOthers","email","source","addedByUid","sequenceId","optOutOtherEnrollments","ok","marked","found","where","batch","writes","docs","id","other","status","commit"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/enrollment-routes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport {\n applyOutreachEnrollmentEvent,\n type OutreachEnrollmentEvent,\n} from '../engine/enrollment-state'\nimport { OUTREACH_OPEN_ENROLLMENT_STATUSES } from '../engine/gates'\nimport type {\n OutreachEnrollmentAction,\n OutreachEnrollmentActionResponse,\n} from '../model/outreach-api'\nimport type { OutreachEnrollment } from '../model/outreach.types'\nimport { addOutreachDoNotContact } from '../storage/do-not-contact-store'\nimport { outreachOrgCollection, readStoredOutreachEnrollment } from '../storage/outreach-records'\nimport type { OutreachRouteDeps } from './route-deps'\nimport { outreachRouteGate } from './route-gate'\nimport {\n outreachMethodNotAllowed,\n outreachOk,\n outreachRefusal,\n readOutreachDocumentId,\n readOutreachJsonBody,\n} from './route-http'\n\n/**\n * WHAT A MEMBER DOES TO ONE ENROLLMENT (AGL-2980):\n * `outreach/enrollments/action` — pause, resume, stop, or mark the person\n * do-not-contact.\n *\n * Every transition is the engine's (`applyOutreachEnrollmentEvent`): the\n * route decides nothing about which status may follow which. Each write is\n * a transaction over the one document, so a member's pause and the sending\n * runtime's step completion cannot interleave and leave a paused enrollment\n * with a step it should not have run.\n *\n * MARK DO-NOT-CONTACT puts the address on the organization's list, with\n * the member as its author, and then opts out every enrollment that address\n * has in the organization — this one wherever it stood, and any other that\n * is still active or paused — so no sequence emails it again, whichever one\n * would have been next.\n */\n\nconst ACTIONS: readonly OutreachEnrollmentAction[] = ['pause', 'resume', 'stop', 'do_not_contact']\n\nfunction eventFor(\n action: OutreachEnrollmentAction,\n atMs: number,\n byUid: string,\n detail: string | null,\n): OutreachEnrollmentEvent {\n switch (action) {\n case 'pause':\n return { type: 'pause', atMs, byUid, detail }\n case 'resume':\n return { type: 'resume', atMs, byUid }\n case 'stop':\n return { type: 'stop', atMs, byUid, reason: 'manual', detail }\n case 'do_not_contact':\n return { type: 'opt_out', atMs, reason: 'do_not_contact', detail }\n }\n}\n\nexport function createOutreachEnrollmentActionRoute(deps: OutreachRouteDeps): PluginWebApiHandler {\n return async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate)\n if (caller instanceof Response) return caller\n const enrollmentId = readOutreachDocumentId(body['enrollmentId'])\n const action = body['action'] as OutreachEnrollmentAction\n if (!enrollmentId || !ACTIONS.includes(action)) {\n return outreachRefusal(400, 'invalid-request', 'Name the enrollment, and pause, resume, stop or mark it do-not-contact.')\n }\n const detail = typeof body['detail'] === 'string' ? body['detail'] : null\n const firestore = deps.firestore()\n const enrollments = outreachOrgCollection(firestore, caller.orgId, 'enrollments')\n const ref = enrollments.doc(enrollmentId)\n const nowMs = deps.now()\n\n const applied = await firestore.runTransaction(async (transaction) => {\n const snapshot = await transaction.get(ref)\n const enrollment = readStoredOutreachEnrollment(enrollmentId, snapshot.exists ? snapshot.data() : undefined)\n if (!enrollment) return null\n const { patch, error } = applyOutreachEnrollmentEvent(\n enrollment,\n eventFor(action, nowMs, caller.uid, detail),\n )\n if (!patch) return { enrollment, error, changed: false }\n // The opt-out event names no member; the person who marked it is\n // recorded here, beside the reason, and on the list entry.\n const written = {\n ...patch,\n stoppedByUid: action === 'do_not_contact' ? caller.uid : patch.stoppedByUid,\n updatedAtMs: nowMs,\n }\n transaction.update(ref, written)\n return { enrollment: { ...enrollment, ...written } as OutreachEnrollment, error: null, changed: true }\n })\n if (!applied) return outreachRefusal(404, 'enrollment-not-found', 'That enrollment no longer exists.')\n // An address that bounced still goes on the list when a member asks:\n // the enrollment's own status stays final, the list entry is what they\n // asked for.\n if (applied.error && action !== 'do_not_contact') {\n return outreachRefusal(409, 'transition-refused', applied.error)\n }\n\n let stoppedOthers = 0\n if (action === 'do_not_contact') {\n await addOutreachDoNotContact(firestore, {\n orgId: caller.orgId,\n email: applied.enrollment.email,\n reason: 'manual',\n source: 'member',\n addedByUid: caller.uid,\n nowMs,\n enrollmentId,\n sequenceId: applied.enrollment.sequenceId,\n detail,\n })\n stoppedOthers = await optOutOtherEnrollments(firestore, enrollments, applied.enrollment, caller.uid, nowMs)\n // The mark on the record the person is (AGL-3245): the member's verdict\n // stands over every automatic one.\n await deps.stampRecordEmailState({\n orgId: caller.orgId,\n email: applied.enrollment.email,\n state: { status: 'do_not_contact', atMs: nowMs, source: 'member', detail, enrollmentId },\n })\n }\n return outreachOk({\n ok: true,\n changed: applied.changed,\n enrollment: applied.enrollment,\n stoppedOthers,\n } satisfies OutreachEnrollmentActionResponse)\n }\n}\n\n/** Opts out every OTHER open enrollment of the same address; returns how many. */\nasync function optOutOtherEnrollments(\n firestore: FirebaseFirestore.Firestore,\n enrollments: FirebaseFirestore.CollectionReference,\n marked: OutreachEnrollment,\n byUid: string,\n nowMs: number,\n): Promise<number> {\n const found = await enrollments.where('email', '==', marked.email).get()\n const batch = firestore.batch()\n let writes = 0\n for (const doc of found.docs) {\n if (doc.id === marked.id) continue\n const other = readStoredOutreachEnrollment(doc.id, doc.data())\n if (!other || !OUTREACH_OPEN_ENROLLMENT_STATUSES.includes(other.status)) continue\n const { patch } = applyOutreachEnrollmentEvent(other, {\n type: 'opt_out',\n atMs: nowMs,\n reason: 'do_not_contact',\n detail: 'Marked do-not-contact from another sequence.',\n })\n if (!patch) continue\n batch.update(doc.ref, { ...patch, stoppedByUid: byUid, updatedAtMs: nowMs })\n writes += 1\n }\n if (writes) await batch.commit()\n return writes\n}\n"],"names":["applyOutreachEnrollmentEvent","OUTREACH_OPEN_ENROLLMENT_STATUSES","addOutreachDoNotContact","outreachOrgCollection","readStoredOutreachEnrollment","outreachRouteGate","outreachMethodNotAllowed","outreachOk","outreachRefusal","readOutreachDocumentId","readOutreachJsonBody","ACTIONS","eventFor","action","atMs","byUid","detail","type","reason","createOutreachEnrollmentActionRoute","deps","request","method","body","caller","gate","Response","enrollmentId","includes","firestore","enrollments","orgId","ref","doc","nowMs","now","applied","runTransaction","transaction","snapshot","get","enrollment","exists","data","undefined","patch","error","uid","changed","written","stoppedByUid","updatedAtMs","update","stoppedOthers","email","source","addedByUid","sequenceId","optOutOtherEnrollments","stampRecordEmailState","state","status","ok","marked","found","where","batch","writes","docs","id","other","commit"],"mappings":"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"}
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
+
import { creditCampaignSequenceOutcome } from '@aglyn/tenant-data-admin/server/campaign-conversion-attribution';
|
|
18
|
+
import { stampRecordEmailState } from '@aglyn/aglyn/plugin-manager/plugin-record-email-state';
|
|
17
19
|
import { lockdownRefusal } from '@aglyn/tenant-data-admin/server/lockdown';
|
|
18
20
|
import { logOrgActivity } from '@aglyn/tenant-data-admin/server/organizations';
|
|
19
21
|
import { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions';
|
|
@@ -30,7 +32,7 @@ import { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions';
|
|
|
30
32
|
* module of this plugin keeps every import of the platform's libraries a
|
|
31
33
|
* static one.
|
|
32
34
|
*/
|
|
33
|
-
export { lockdownRefusal, logOrgActivity, resolveOrgPermissions };
|
|
35
|
+
export { creditCampaignSequenceOutcome, lockdownRefusal, logOrgActivity, resolveOrgPermissions, stampRecordEmailState, };
|
|
34
36
|
/**
|
|
35
37
|
* Whether the member holds a CATALOG permission (`data.manage`) as the
|
|
36
38
|
* org's roles resolve it — custom role and per-member overrides included.
|
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
|
-
*/ import {
|
|
16
|
+
*/ import { creditCampaignSequenceOutcome } from "@aglyn/tenant-data-admin/server/campaign-conversion-attribution";
|
|
17
|
+
import { collectDynamicListCandidates } from "@aglyn/tenant-data-admin/server/dynamic-list-materialize";
|
|
18
|
+
import { stampRecordEmailState } from "@aglyn/aglyn/plugin-manager/plugin-record-email-state";
|
|
17
19
|
import { lockdownRefusal } from "@aglyn/tenant-data-admin/server/lockdown";
|
|
18
20
|
import { logOrgActivity, memberHasOrgPermission, resolveOrgMembership } from "@aglyn/tenant-data-admin/server/organizations";
|
|
19
21
|
import { resolveOrgPermissions } from "@aglyn/tenant-runtime/org-permissions";
|
|
@@ -29,7 +31,7 @@ import { resolveOrgPermissions } from "@aglyn/tenant-runtime/org-permissions";
|
|
|
29
31
|
* alone reaches the whole tenant data barrel. Loading them through one
|
|
30
32
|
* module of this plugin keeps every import of the platform's libraries a
|
|
31
33
|
* static one.
|
|
32
|
-
*/ export { lockdownRefusal, logOrgActivity, resolveOrgPermissions };
|
|
34
|
+
*/ export { creditCampaignSequenceOutcome, lockdownRefusal, logOrgActivity, resolveOrgPermissions, stampRecordEmailState, };
|
|
33
35
|
/**
|
|
34
36
|
* Whether the member holds a CATALOG permission (`data.manage`) as the
|
|
35
37
|
* org's roles resolve it — custom role and per-member overrides included.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/platform-deps.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { collectDynamicListCandidates } from '@aglyn/tenant-data-admin/server/dynamic-list-materialize'\nimport { lockdownRefusal } from '@aglyn/tenant-data-admin/server/lockdown'\nimport {\n logOrgActivity,\n memberHasOrgPermission,\n resolveOrgMembership,\n} from '@aglyn/tenant-data-admin/server/organizations'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\n\n/**\n * The platform's heavier server modules the Outreach routes reach\n * (AGL-2980): the permission resolver, the lockdown verdict, the activity\n * log and the saved-view sweep.\n *\n * Imported here statically, and this module is loaded by\n * `register-routes.ts` the first time a request needs one of them: the\n * manifest loads Outreach's server bundle into every console API process\n * whether or not an Outreach route is ever called, and `org-permissions`\n * alone reaches the whole tenant data barrel. Loading them through one\n * module of this plugin keeps every import of the platform's libraries a\n * static one.\n */\n\nexport {
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/platform-deps.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { creditCampaignSequenceOutcome } from '@aglyn/tenant-data-admin/server/campaign-conversion-attribution'\nimport { collectDynamicListCandidates } from '@aglyn/tenant-data-admin/server/dynamic-list-materialize'\nimport { stampRecordEmailState } from '@aglyn/aglyn/plugin-manager/plugin-record-email-state'\nimport { lockdownRefusal } from '@aglyn/tenant-data-admin/server/lockdown'\nimport {\n logOrgActivity,\n memberHasOrgPermission,\n resolveOrgMembership,\n} from '@aglyn/tenant-data-admin/server/organizations'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\n\n/**\n * The platform's heavier server modules the Outreach routes reach\n * (AGL-2980): the permission resolver, the lockdown verdict, the activity\n * log and the saved-view sweep.\n *\n * Imported here statically, and this module is loaded by\n * `register-routes.ts` the first time a request needs one of them: the\n * manifest loads Outreach's server bundle into every console API process\n * whether or not an Outreach route is ever called, and `org-permissions`\n * alone reaches the whole tenant data barrel. Loading them through one\n * module of this plugin keeps every import of the platform's libraries a\n * static one.\n */\n\nexport {\n creditCampaignSequenceOutcome,\n lockdownRefusal,\n logOrgActivity,\n resolveOrgPermissions,\n stampRecordEmailState,\n}\n\n/**\n * Whether the member holds a CATALOG permission (`data.manage`) as the\n * org's roles resolve it — custom role and per-member overrides included.\n * A lookup that failed has not shown the member holds it.\n */\nexport async function holdsOrgCatalogPermission(uid: string, orgId: string, key: string): Promise<boolean> {\n try {\n const membership = await resolveOrgMembership(uid, orgId)\n if (!membership?.member) return false\n return (\n (await memberHasOrgPermission(\n orgId,\n membership.member,\n key as Parameters<typeof memberHasOrgPermission>[2],\n )) === true\n )\n } catch {\n return false\n }\n}\n\n/**\n * The addresses a saved Contacts view selects among one site's contacts,\n * read by the dynamic-list sweep's own scan, and whether it reached the\n * whole view.\n */\nexport async function crmViewEmails(input: {\n hostId: string\n viewId: string\n}): Promise<{ emails: string[]; complete: boolean }> {\n const scan = await collectDynamicListCandidates({\n hostId: input.hostId,\n rule: { sources: ['contacts'], viewId: input.viewId },\n })\n return { emails: scan.candidates.map((candidate) => candidate.email), complete: scan.complete }\n}\n"],"names":["creditCampaignSequenceOutcome","collectDynamicListCandidates","stampRecordEmailState","lockdownRefusal","logOrgActivity","memberHasOrgPermission","resolveOrgMembership","resolveOrgPermissions","holdsOrgCatalogPermission","uid","orgId","key","membership","member","crmViewEmails","input","scan","hostId","rule","sources","viewId","emails","candidates","map","candidate","email","complete"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,6BAA6B,QAAQ,kEAAiE;AAC/G,SAASC,4BAA4B,QAAQ,2DAA0D;AACvG,SAASC,qBAAqB,QAAQ,wDAAuD;AAC7F,SAASC,eAAe,QAAQ,2CAA0C;AAC1E,SACEC,cAAc,EACdC,sBAAsB,EACtBC,oBAAoB,QACf,gDAA+C;AACtD,SAASC,qBAAqB,QAAQ,wCAAuC;AAE7E;;;;;;;;;;;;CAYC,GAED,SACEP,6BAA6B,EAC7BG,eAAe,EACfC,cAAc,EACdG,qBAAqB,EACrBL,qBAAqB,KACtB;AAED;;;;CAIC,GACD,OAAO,eAAeM,0BAA0BC,GAAW,EAAEC,KAAa,EAAEC,GAAW;IACrF,IAAI;QACF,MAAMC,aAAa,MAAMN,qBAAqBG,KAAKC;QACnD,IAAI,EAACE,8BAAAA,WAAYC,MAAM,GAAE,OAAO;QAChC,OACE,AAAC,MAAMR,uBACLK,OACAE,WAAWC,MAAM,EACjBF,SACK;IAEX,EAAE,eAAM;QACN,OAAO;IACT;AACF;AAEA;;;;CAIC,GACD,OAAO,eAAeG,cAAcC,KAGnC;IACC,MAAMC,OAAO,MAAMf,6BAA6B;QAC9CgB,QAAQF,MAAME,MAAM;QACpBC,MAAM;YAAEC,SAAS;gBAAC;aAAW;YAAEC,QAAQL,MAAMK,MAAM;QAAC;IACtD;IACA,OAAO;QAAEC,QAAQL,KAAKM,UAAU,CAACC,GAAG,CAAC,CAACC,YAAcA,UAAUC,KAAK;QAAGC,UAAUV,KAAKU,QAAQ;IAAC;AAChG"}
|