@aglyn/plugins-outreach 1.0.0-beta.219 → 1.0.0-beta.220

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/package.json +15 -15
  2. package/src/lib/components/compliance-section.d.ts +5 -0
  3. package/src/lib/components/compliance-section.js +233 -149
  4. package/src/lib/components/compliance-section.js.map +1 -1
  5. package/src/lib/components/enrollment-detail.js +2 -2
  6. package/src/lib/components/enrollment-detail.js.map +1 -1
  7. package/src/lib/components/sequence-editor.d.ts +13 -0
  8. package/src/lib/components/sequence-editor.js +508 -302
  9. package/src/lib/components/sequence-editor.js.map +1 -1
  10. package/src/lib/components/sequence-preview.js +25 -10
  11. package/src/lib/components/sequence-preview.js.map +1 -1
  12. package/src/lib/components/sequence-step-card.d.ts +10 -0
  13. package/src/lib/components/sequence-step-card.js +320 -202
  14. package/src/lib/components/sequence-step-card.js.map +1 -1
  15. package/src/lib/engine/compose.js +2 -2
  16. package/src/lib/engine/compose.js.map +1 -1
  17. package/src/lib/engine/delivery-status.js +1 -1
  18. package/src/lib/engine/delivery-status.js.map +1 -1
  19. package/src/lib/engine/enrollment-state.js +2 -2
  20. package/src/lib/engine/enrollment-state.js.map +1 -1
  21. package/src/lib/engine/gates.d.ts +2 -2
  22. package/src/lib/engine/gates.js.map +1 -1
  23. package/src/lib/engine/opt-out-intent.js +1 -1
  24. package/src/lib/engine/opt-out-intent.js.map +1 -1
  25. package/src/lib/engine/sequence-validation.js +2 -2
  26. package/src/lib/engine/sequence-validation.js.map +1 -1
  27. package/src/lib/engine/thread-classification.js +3 -3
  28. package/src/lib/engine/thread-classification.js.map +1 -1
  29. package/src/lib/enrollment/gate-lookups.js +5 -4
  30. package/src/lib/enrollment/gate-lookups.js.map +1 -1
  31. package/src/lib/model/outreach.types.d.ts +1 -1
  32. package/src/lib/model/outreach.types.js.map +1 -1
  33. package/src/lib/model/sales-topic.d.ts +27 -0
  34. package/src/lib/model/sales-topic.js +27 -0
  35. package/src/lib/model/sales-topic.js.map +1 -0
  36. package/src/lib/model/sequence-draft.d.ts +1 -1
  37. package/src/lib/model/sequence-draft.js +2 -2
  38. package/src/lib/model/sequence-draft.js.map +1 -1
  39. package/src/lib/model/stored-records.js +2 -2
  40. package/src/lib/model/stored-records.js.map +1 -1
  41. package/src/lib/routes/curate-routes.js +7 -7
  42. package/src/lib/routes/curate-routes.js.map +1 -1
  43. package/src/lib/routes/enroll-routes.js +10 -13
  44. package/src/lib/routes/enroll-routes.js.map +1 -1
  45. package/src/lib/routes/sequence-routes.js +9 -9
  46. package/src/lib/routes/sequence-routes.js.map +1 -1
  47. package/src/lib/runtime/campaign-credit.js +2 -2
  48. package/src/lib/runtime/campaign-credit.js.map +1 -1
  49. package/src/lib/runtime/campaign-detach.js +3 -2
  50. package/src/lib/runtime/campaign-detach.js.map +1 -1
  51. package/src/lib/runtime/lead-records.d.ts +6 -1
  52. package/src/lib/runtime/lead-records.js +25 -6
  53. package/src/lib/runtime/lead-records.js.map +1 -1
  54. package/src/lib/runtime/platform-runtime-deps.js +3 -3
  55. package/src/lib/runtime/platform-runtime-deps.js.map +1 -1
  56. package/src/lib/runtime/send-job.js +20 -4
  57. package/src/lib/runtime/send-job.js.map +1 -1
  58. package/src/lib/runtime/sync-job.js +3 -3
  59. package/src/lib/runtime/sync-job.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 { 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 // Off for a new sequence (AGL-3395): counting opens adds an HTML part\n // and an image to a one-to-one email, which is chosen, not defaulted.\n countOpens: false,\n // Off for a new sequence (AGL-3296): a sequence is one-to-one mail,\n // and the header makes mail clients present it as a mailing list.\n listUnsubscribe: 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","countOpens","listUnsubscribe","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 { normalizeContainerIds } from '@aglyn/aglyn/app-utils/container-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 * `containerMembershipValue` 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: normalizeContainerIds(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 // Off for a new sequence (AGL-3395): counting opens adds an HTML part\n // and an image to a one-to-one email, which is chosen, not defaulted.\n countOpens: false,\n // Off for a new sequence (AGL-3296): a sequence is one-to-one mail,\n // and the header makes mail clients present it as a mailing list.\n listUnsubscribe: 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":["normalizeContainerIds","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","countOpens","listUnsubscribe","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,7 @@ 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 { normalizeCampaignIds } from "@aglyn/aglyn/app-utils/campaign-membership";
24
+ */ import { normalizeContainerIds } from "@aglyn/aglyn/app-utils/container-membership";
25
25
  import { readOutreachSequenceDraft } from "./sequence-draft.js";
26
26
  import { OUTREACH_ENROLLMENT_STATUSES, OUTREACH_SEQUENCE_STATUSES, OUTREACH_STEP_OVERRIDE_SOURCES, OUTREACH_STOP_REASONS } from "./outreach.types.js";
27
27
  const text = (value)=>typeof value === 'string' ? value : '';
@@ -179,7 +179,7 @@ const texts = (value)=>Array.isArray(value) ? value.filter((entry)=>typeof entry
179
179
  // Absent on an enrollment made before a sequence could join a campaign
180
180
  // (AGL-3254), and read as none: nothing is credited to a campaign the
181
181
  // enrollment does not name.
182
- campaignIds: normalizeCampaignIds(data['campaignIds']),
182
+ campaignIds: normalizeContainerIds(data['campaignIds']),
183
183
  createdAtMs: (_ms = ms(data['createdAtMs'])) != null ? _ms : 0,
184
184
  updatedAtMs: (_ms1 = ms(data['updatedAtMs'])) != null ? _ms1 : 0
185
185
  });
@@ -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 { normalizeCampaignIds } from '@aglyn/aglyn/app-utils/campaign-membership'\nimport { readOutreachSequenceDraft } from './sequence-draft'\nimport {\n OUTREACH_ENROLLMENT_STATUSES,\n OUTREACH_SEQUENCE_STATUSES,\n OUTREACH_STEP_OVERRIDE_SOURCES,\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 OutreachSkippedStep,\n type OutreachStepOverride,\n type OutreachStepOverrides,\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 [\n 'sent',\n 'people',\n 'clicks',\n 'uniqueClicks',\n 'machineClicks',\n 'openPeople',\n 'opens',\n 'uniqueOpens',\n 'machineOpens',\n 'proxyOpens',\n ] 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 if (data['openTracked'] === true) stats.openTracked = true\n const lastOpen = data['lastOpenAtMs']\n if (typeof lastOpen === 'number' && Number.isFinite(lastOpen)) stats.lastOpenAtMs = lastOpen\n return Object.keys(stats).length ? stats : null\n}\n\n/**\n * A stored enrollment's curated steps (AGL-3324), or `undefined` when it\n * carries none worth reading. Read a field at a time, because what is read\n * here is what gets SENT in place of the step: an entry with neither a\n * subject nor a body, or one keyed by something that is not a step index,\n * is dropped rather than sent as an empty email.\n */\nexport function readOutreachStepOverrides(raw: unknown): OutreachStepOverrides | undefined {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return undefined\n const overrides: OutreachStepOverrides = {}\n for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {\n if (!/^(0|[1-9]\\d*)$/.test(key) || !value || typeof value !== 'object') continue\n const entry = value as Record<string, unknown>\n const subject = typeof entry['subject'] === 'string' ? entry['subject'] : undefined\n const body = typeof entry['body'] === 'string' ? entry['body'] : undefined\n if (subject === undefined && body === undefined) continue\n const source = entry['source']\n const override: OutreachStepOverride = {\n ...(subject !== undefined ? { subject } : {}),\n ...(body !== undefined ? { body } : {}),\n // Words nobody can say the source of are read as the member's: the\n // audit line then claims no AI wrote them.\n source: (OUTREACH_STEP_OVERRIDE_SOURCES as readonly unknown[]).includes(source)\n ? (source as OutreachStepOverride['source'])\n : 'member',\n draftedAtMs: ms(entry['draftedAtMs']) ?? 0,\n ...(typeof entry['draftedByUid'] === 'string' ? { draftedByUid: entry['draftedByUid'] } : {}),\n ...(entry['edited'] === true ? { edited: true } : {}),\n ...(typeof entry['prompt'] === 'string' ? { prompt: entry['prompt'] } : {}),\n ...(typeof entry['model'] === 'string' ? { model: entry['model'] } : {}),\n }\n overrides[key] = override\n }\n return Object.keys(overrides).length ? overrides : undefined\n}\n\n/** The steps a stored enrollment skipped (AGL-3228), each as the model reads one; the malformed are dropped. */\nfunction readOutreachSkippedSteps(value: unknown): OutreachSkippedStep[] {\n if (!Array.isArray(value)) return []\n const steps: OutreachSkippedStep[] = []\n for (const entry of value) {\n if (!entry || typeof entry !== 'object') continue\n const raw = entry as Record<string, unknown>\n const stepIndex = Number(raw['stepIndex'])\n if (!Number.isInteger(stepIndex) || stepIndex < 0) continue\n steps.push({\n stepIndex,\n stepId: text(raw['stepId']),\n kind: raw['kind'] === 'task' ? 'task' : 'email',\n atMs: ms(raw['atMs']) ?? 0,\n })\n }\n return steps\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 const enrollment: OutreachEnrollment = {\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 // Absent rather than an empty map for an enrollment nobody curated\n // (AGL-3324): the spread above would otherwise carry whatever was stored.\n const stepOverrides = readOutreachStepOverrides(data['stepOverrides'])\n if (stepOverrides) enrollment.stepOverrides = stepOverrides\n else delete enrollment.stepOverrides\n // Where it began, and what it skipped (AGL-3228) — absent on one that\n // began at step 1, which is every enrollment made before either existed.\n const startStepIndex = Number(data['startStepIndex'])\n if (Number.isInteger(startStepIndex) && startStepIndex > 0) enrollment.startStepIndex = startStepIndex\n else delete enrollment.startStepIndex\n const skippedSteps = readOutreachSkippedSteps(data['skippedSteps'])\n if (skippedSteps.length) enrollment.skippedSteps = skippedSteps\n else delete enrollment.skippedSteps\n return enrollment\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_STEP_OVERRIDE_SOURCES","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","openTracked","lastOpen","lastOpenAtMs","Object","keys","length","readOutreachStepOverrides","undefined","overrides","entries","test","subject","body","source","override","draftedAtMs","draftedByUid","edited","prompt","model","readOutreachSkippedSteps","steps","stepIndex","isInteger","push","stepId","kind","atMs","readStoredOutreachEnrollment","stopReason","leadId","target","enrollment","sequenceId","contactId","contactName","email","hostId","mailboxId","nextDueAtMs","stopDetail","stoppedAtMs","stoppedByUid","personalLine","cold","enrolledByUid","gmailThreadId","gmailThreadIds","threadSubject","messageIds","lastSentAtMs","campaignIds","stepOverrides","startStepIndex","skippedSteps","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 { normalizeContainerIds } from '@aglyn/aglyn/app-utils/container-membership'\nimport { readOutreachSequenceDraft } from './sequence-draft'\nimport {\n OUTREACH_ENROLLMENT_STATUSES,\n OUTREACH_SEQUENCE_STATUSES,\n OUTREACH_STEP_OVERRIDE_SOURCES,\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 OutreachSkippedStep,\n type OutreachStepOverride,\n type OutreachStepOverrides,\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 [\n 'sent',\n 'people',\n 'clicks',\n 'uniqueClicks',\n 'machineClicks',\n 'openPeople',\n 'opens',\n 'uniqueOpens',\n 'machineOpens',\n 'proxyOpens',\n ] 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 if (data['openTracked'] === true) stats.openTracked = true\n const lastOpen = data['lastOpenAtMs']\n if (typeof lastOpen === 'number' && Number.isFinite(lastOpen)) stats.lastOpenAtMs = lastOpen\n return Object.keys(stats).length ? stats : null\n}\n\n/**\n * A stored enrollment's curated steps (AGL-3324), or `undefined` when it\n * carries none worth reading. Read a field at a time, because what is read\n * here is what gets SENT in place of the step: an entry with neither a\n * subject nor a body, or one keyed by something that is not a step index,\n * is dropped rather than sent as an empty email.\n */\nexport function readOutreachStepOverrides(raw: unknown): OutreachStepOverrides | undefined {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return undefined\n const overrides: OutreachStepOverrides = {}\n for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {\n if (!/^(0|[1-9]\\d*)$/.test(key) || !value || typeof value !== 'object') continue\n const entry = value as Record<string, unknown>\n const subject = typeof entry['subject'] === 'string' ? entry['subject'] : undefined\n const body = typeof entry['body'] === 'string' ? entry['body'] : undefined\n if (subject === undefined && body === undefined) continue\n const source = entry['source']\n const override: OutreachStepOverride = {\n ...(subject !== undefined ? { subject } : {}),\n ...(body !== undefined ? { body } : {}),\n // Words nobody can say the source of are read as the member's: the\n // audit line then claims no AI wrote them.\n source: (OUTREACH_STEP_OVERRIDE_SOURCES as readonly unknown[]).includes(source)\n ? (source as OutreachStepOverride['source'])\n : 'member',\n draftedAtMs: ms(entry['draftedAtMs']) ?? 0,\n ...(typeof entry['draftedByUid'] === 'string' ? { draftedByUid: entry['draftedByUid'] } : {}),\n ...(entry['edited'] === true ? { edited: true } : {}),\n ...(typeof entry['prompt'] === 'string' ? { prompt: entry['prompt'] } : {}),\n ...(typeof entry['model'] === 'string' ? { model: entry['model'] } : {}),\n }\n overrides[key] = override\n }\n return Object.keys(overrides).length ? overrides : undefined\n}\n\n/** The steps a stored enrollment skipped (AGL-3228), each as the model reads one; the malformed are dropped. */\nfunction readOutreachSkippedSteps(value: unknown): OutreachSkippedStep[] {\n if (!Array.isArray(value)) return []\n const steps: OutreachSkippedStep[] = []\n for (const entry of value) {\n if (!entry || typeof entry !== 'object') continue\n const raw = entry as Record<string, unknown>\n const stepIndex = Number(raw['stepIndex'])\n if (!Number.isInteger(stepIndex) || stepIndex < 0) continue\n steps.push({\n stepIndex,\n stepId: text(raw['stepId']),\n kind: raw['kind'] === 'task' ? 'task' : 'email',\n atMs: ms(raw['atMs']) ?? 0,\n })\n }\n return steps\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 const enrollment: OutreachEnrollment = {\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: normalizeContainerIds(data['campaignIds']),\n createdAtMs: ms(data['createdAtMs']) ?? 0,\n updatedAtMs: ms(data['updatedAtMs']) ?? 0,\n }\n // Absent rather than an empty map for an enrollment nobody curated\n // (AGL-3324): the spread above would otherwise carry whatever was stored.\n const stepOverrides = readOutreachStepOverrides(data['stepOverrides'])\n if (stepOverrides) enrollment.stepOverrides = stepOverrides\n else delete enrollment.stepOverrides\n // Where it began, and what it skipped (AGL-3228) — absent on one that\n // began at step 1, which is every enrollment made before either existed.\n const startStepIndex = Number(data['startStepIndex'])\n if (Number.isInteger(startStepIndex) && startStepIndex > 0) enrollment.startStepIndex = startStepIndex\n else delete enrollment.startStepIndex\n const skippedSteps = readOutreachSkippedSteps(data['skippedSteps'])\n if (skippedSteps.length) enrollment.skippedSteps = skippedSteps\n else delete enrollment.skippedSteps\n return enrollment\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":["normalizeContainerIds","readOutreachSequenceDraft","OUTREACH_ENROLLMENT_STATUSES","OUTREACH_SEQUENCE_STATUSES","OUTREACH_STEP_OVERRIDE_SOURCES","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","openTracked","lastOpen","lastOpenAtMs","Object","keys","length","readOutreachStepOverrides","undefined","overrides","entries","test","subject","body","source","override","draftedAtMs","draftedByUid","edited","prompt","model","readOutreachSkippedSteps","steps","stepIndex","isInteger","push","stepId","kind","atMs","readStoredOutreachEnrollment","stopReason","leadId","target","enrollment","sequenceId","contactId","contactName","email","hostId","mailboxId","nextDueAtMs","stopDetail","stoppedAtMs","stoppedByUid","personalLine","cold","enrolledByUid","gmailThreadId","gmailThreadIds","threadSubject","messageIds","lastSentAtMs","campaignIds","stepOverrides","startStepIndex","skippedSteps","readStoredOutreachMailbox"],"mappings":"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"}
@@ -14,11 +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 { readCampaignIds, readContactCampaignIds } from "@aglyn/aglyn/app-utils/campaign-membership";
17
+ */ import { readContainerIds, readContactContainerIds } from "@aglyn/aglyn/app-utils/container-membership";
18
18
  import { consentGroupForHost } from "@aglyn/aglyn/app-utils/consent-groups";
19
19
  import { readContactFacet } from "@aglyn/aglyn/app-utils/contacts";
20
20
  import { CRM_COLLECTIONS } from "@aglyn/aglyn/app-utils/crm";
21
21
  import { normalizeCrmEmailTemplate } from "@aglyn/aglyn/app-utils/crm-email-templates";
22
+ import { readOrgContainers } from "@aglyn/tenant-data-admin/server/org-containers";
22
23
  import { FieldValue } from "firebase-admin/firestore";
23
24
  import { OUTREACH_CURATION_SYSTEM, outreachCurationPrompt, parseOutreachCurationAnswer } from "../engine/curation-prompt.js";
24
25
  import { outreachCuratedEntry } from "../engine/enrollment-activity.js";
@@ -158,14 +159,13 @@ const text = (value)=>typeof value === 'string' ? value.trim() : '';
158
159
  const texts = (value)=>Array.isArray(value) ? value.map(text).filter(Boolean) : [];
159
160
  /**
160
161
  * The names of the campaigns the person is filed under — the org's
161
- * containers, `orgs/{orgId}/emailCampaigns`. A container that is gone names
162
- * nothing; a read that fails names none, and the draft goes without.
162
+ * containers of the `campaign` kind. A container that is gone names nothing;
163
+ * a read that fails names none, and the draft goes without.
163
164
  */ async function campaignNames(firestore, orgId, campaignIds) {
164
165
  if (!campaignIds.length) return [];
165
166
  try {
166
- const containers = firestore.collection('orgs').doc(orgId).collection('emailCampaigns');
167
- const found = await firestore.getAll(...campaignIds.map((id)=>containers.doc(id)));
168
- return found.map((snapshot)=>text(snapshot.get('name'))).filter(Boolean);
167
+ const found = await readOrgContainers(firestore, 'campaign', orgId, campaignIds);
168
+ return found.map((container)=>container.name).filter(Boolean);
169
169
  } catch (error) {
170
170
  console.error('[outreach] the person’s campaigns could not be named for the draft', error);
171
171
  return [];
@@ -177,7 +177,7 @@ const texts = (value)=>Array.isArray(value) ? value.map(text).filter(Boolean) :
177
177
  const facet = readContactFacet(contact, contactGroupId);
178
178
  const lead = (_candidate_lead = candidate.lead) != null ? _candidate_lead : {};
179
179
  const company = (_candidate_company = candidate.company) != null ? _candidate_company : {};
180
- const campaignIds = candidate.target === 'lead' ? readCampaignIds(lead) : readContactCampaignIds(contact, contactGroupId);
180
+ const campaignIds = candidate.target === 'lead' ? readContainerIds(lead, 'campaign') : readContactContainerIds(contact, contactGroupId, 'campaign');
181
181
  const [mailboxSnapshot, host, campaigns] = await Promise.all([
182
182
  subject.sequence.mailboxId ? outreachOrgCollection(firestore, caller.orgId, 'mailboxes').doc(subject.sequence.mailboxId).get() : null,
183
183
  subject.sequence.hostId ? firestore.collection('hosts').doc(subject.sequence.hostId).get() : null,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/curate-routes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { readCampaignIds, readContactCampaignIds } from '@aglyn/aglyn/app-utils/campaign-membership'\nimport { consentGroupForHost } from '@aglyn/aglyn/app-utils/consent-groups'\nimport { readContactFacet } from '@aglyn/aglyn/app-utils/contacts'\nimport { CRM_COLLECTIONS } from '@aglyn/aglyn/app-utils/crm'\nimport { normalizeCrmEmailTemplate } from '@aglyn/aglyn/app-utils/crm-email-templates'\nimport type { PluginTextGenerator } from '@aglyn/aglyn/plugin-manager/plugin-text-generation'\nimport type { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport {\n OUTREACH_CURATION_SYSTEM,\n outreachCurationPrompt,\n parseOutreachCurationAnswer,\n type OutreachCurationFacts,\n type OutreachCurationStep,\n} from '../engine/curation-prompt'\nimport { outreachCuratedEntry } from '../engine/enrollment-activity'\nimport { OUTREACH_OPEN_ENROLLMENT_STATUSES } from '../engine/gates'\nimport {\n hasOutreachValidationErrors,\n isInThreadEmailStep,\n validateOutreachCuratedStep,\n} from '../engine/sequence-validation'\nimport { readOutreachEnrollCandidates, type OutreachEnrollCandidate } from '../enrollment/enroll-people'\nimport { readOutreachStepOverrideRequest } from '../enrollment/step-overrides'\nimport type {\n OutreachCurateDraft,\n OutreachCurateDraftResponse,\n OutreachCurateSaveResponse,\n OutreachPersonRef,\n} from '../model/outreach-api'\nimport {\n OUTREACH_MAX_EMAIL_STEPS,\n type OutreachEnrollment,\n type OutreachSequence,\n type OutreachStepOverride,\n} from '../model/outreach.types'\nimport { fileOutreachNote } from '../runtime/timeline'\nimport {\n outreachEnrollmentLink,\n outreachEnrollmentPerson,\n outreachOrgCollection,\n readStoredOutreachEnrollment,\n readStoredOutreachMailbox,\n readStoredOutreachSequence,\n} from '../storage/outreach-records'\nimport { OUTREACH_READS_PEOPLE, type OutreachEnrollRouteDeps } from './enroll-routes'\nimport { OUTREACH_SEQUENCE_ACTIVITY_TARGET } from './route-deps'\nimport { outreachRouteGate, type OutreachRouteCaller } from './route-gate'\nimport {\n outreachMethodNotAllowed,\n outreachOk,\n outreachRefusal,\n readOutreachDocumentId,\n readOutreachJsonBody,\n} from './route-http'\n\n/**\n * CURATING ONE PERSON'S EMAILS (AGL-3324): `outreach/curate/draft` and\n * `outreach/curate/save`.\n *\n * A sequence's step is one template for everyone plus the personal line;\n * what an agent-run outbound tool does instead is write each send for each\n * prospect from what it knows about them, and let the seller approve or\n * edit. These two routes are that, with the member's confirmation kept as\n * the one thing that stores anything.\n *\n * DRAFT reads the person the way enrolling reads them — the contact, or the\n * lead as the contact it would be, with the company filed beside them — and\n * asks the workspace's text generator, on the core's seam, for this\n * person's copy of the email steps named: every email step for a person\n * about to be enrolled, the next one for an enrollment. The answer is\n * validated against the playbook's rules and returned with each draft's\n * issues, the prompt and the model. NOTHING IS STORED: the member reads it\n * in the dialog, edits it, ticks the confirmation, and then either enrolls\n * (the enroll route stores the copies with the enrollment) or saves.\n *\n * SAVE stores one confirmed copy on an open enrollment, for a step that\n * has not gone out, validated again — the request is not trusted to have\n * shown the member what it sends — and files \"Curated step N\" on the\n * person's record with who wrote the words. `override: null` clears a copy,\n * so the step goes back to the sequence's own words.\n *\n * Both read the CRM, so both ask for `data.manage` beside `outreach.use`;\n * the generator applies its own rules — the generation permission, the\n * plan, the switches, the allotments — to the member named, and meters the\n * draft on the member's month and the org's. Outreach never reaches the AI\n * plugin: the seam is the platform's, and a workspace where no plugin\n * generates text is told so in the route's words.\n */\n\n/** The purpose the generator's ledger files a draft under. */\nexport const OUTREACH_CURATION_PURPOSE = 'outreach-curate'\n\n/** The most tokens one drafting answer may run to: four short emails, with room. */\nconst CURATION_MAX_TOKENS = 2_400\n\n/** The org activity line a saved copy writes. */\nexport function outreachCuratedActivity(stepIndex: number, cleared: boolean): string {\n return cleared\n ? `Cleared the curated copy of step ${stepIndex + 1} of a sequence`\n : `Curated step ${stepIndex + 1} of a sequence for one person`\n}\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** The person's copies of steps a draft covers, and the enrollment they belong to when one exists. */\ninterface DraftSubject {\n sequence: OutreachSequence\n person: OutreachPersonRef\n personalLine: string\n enrollment: OutreachEnrollment | null\n /** The steps to draft, email steps only, unique, in order. */\n stepIndexes: number[]\n}\n\n/** Every email step's index, in order. */\nfunction emailStepIndexes(sequence: Pick<OutreachSequence, 'steps'>): number[] {\n return sequence.steps.map((step, index) => (step.kind === 'email' ? index : -1)).filter((index) => index >= 0)\n}\n\n/** The step indexes a request names, or the default for its subject; a refusal for anything else. */\nfunction readStepIndexes(\n raw: unknown,\n sequence: OutreachSequence,\n fallback: number[],\n): number[] | Response {\n if (raw === undefined) return fallback\n if (!Array.isArray(raw) || !raw.length) {\n return outreachRefusal(400, 'invalid-request', 'Name the email steps to draft.')\n }\n const wanted = [...new Set(raw.map((value) => Number(value)))]\n const emails = emailStepIndexes(sequence)\n if (wanted.some((index) => !emails.includes(index))) {\n return outreachRefusal(400, 'invalid-request', 'A curated step is one of the sequence’s email steps.')\n }\n if (wanted.length > OUTREACH_MAX_EMAIL_STEPS) {\n return outreachRefusal(400, 'invalid-request', `Draft at most ${OUTREACH_MAX_EMAIL_STEPS} emails at a time.`)\n }\n return wanted.sort((a, b) => a - b)\n}\n\n/** What the request is about: a person to enroll, or an enrollment's next step. */\nasync function draftSubject(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n body: Record<string, unknown>,\n): Promise<DraftSubject | Response> {\n const enrollmentId = body['enrollmentId'] === undefined ? null : readOutreachDocumentId(body['enrollmentId'])\n if (body['enrollmentId'] !== undefined && !enrollmentId) {\n return outreachRefusal(400, 'invalid-request', 'Name the enrollment to curate.')\n }\n if (enrollmentId) {\n const snapshot = await outreachOrgCollection(firestore, caller.orgId, 'enrollments').doc(enrollmentId).get()\n const enrollment = readStoredOutreachEnrollment(enrollmentId, snapshot.exists ? snapshot.data() : undefined)\n if (!enrollment) return outreachRefusal(404, 'enrollment-not-found', 'That enrollment no longer exists.')\n if (!OUTREACH_OPEN_ENROLLMENT_STATUSES.includes(enrollment.status)) {\n return outreachRefusal(409, 'transition-refused', 'This enrollment has ended, so nothing more is sent to them.')\n }\n const sequenceSnapshot = await outreachOrgCollection(firestore, caller.orgId, 'sequences')\n .doc(enrollment.sequenceId)\n .get()\n const sequence = readStoredOutreachSequence(\n enrollment.sequenceId,\n sequenceSnapshot.exists ? sequenceSnapshot.data() : undefined,\n )\n if (!sequence) return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n // The next email step that has not gone out: the one the enrollment is\n // on when it is an email, else the first email after it.\n const next = emailStepIndexes(sequence).find((index) => index >= enrollment.stepIndex)\n const stepIndexes = readStepIndexes(body['stepIndexes'], sequence, next === undefined ? [] : [next])\n if (stepIndexes instanceof Response) return stepIndexes\n if (stepIndexes.some((index) => index < enrollment.stepIndex)) {\n return outreachRefusal(409, 'transition-refused', 'That step already went out to this person.')\n }\n if (!stepIndexes.length) {\n return outreachRefusal(409, 'transition-refused', 'Every email in this sequence has gone out to this person.')\n }\n return {\n sequence,\n person: outreachEnrollmentPerson(enrollment),\n personalLine: enrollment.personalLine,\n enrollment,\n stepIndexes,\n }\n }\n const sequenceId = readOutreachDocumentId(body['sequenceId'])\n if (!sequenceId) return outreachRefusal(400, 'invalid-request', 'Name the sequence to draft for.')\n const contactId = body['contactId'] === undefined ? null : readOutreachDocumentId(body['contactId'])\n const leadId = body['leadId'] === undefined ? null : readOutreachDocumentId(body['leadId'])\n const person: OutreachPersonRef | null = contactId\n ? { kind: 'contact', id: contactId }\n : leadId\n ? { kind: 'lead', id: leadId }\n : null\n if (!person) return outreachRefusal(400, 'invalid-request', 'Name the contact or the lead to draft for.')\n const snapshot = await outreachOrgCollection(firestore, caller.orgId, 'sequences').doc(sequenceId).get()\n const sequence = readStoredOutreachSequence(sequenceId, snapshot.exists ? snapshot.data() : undefined)\n if (!sequence) return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n const stepIndexes = readStepIndexes(body['stepIndexes'], sequence, emailStepIndexes(sequence))\n if (stepIndexes instanceof Response) return stepIndexes\n if (!stepIndexes.length) {\n return outreachRefusal(400, 'invalid-request', 'This sequence sends no email, so there is nothing to draft.')\n }\n return {\n sequence,\n person,\n personalLine: typeof body['personalLine'] === 'string' ? body['personalLine'] : '',\n enrollment: null,\n stepIndexes,\n }\n}\n\nconst text = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\nconst texts = (value: unknown): string[] =>\n Array.isArray(value) ? value.map(text).filter(Boolean) : []\n\n/**\n * The names of the campaigns the person is filed under — the org's\n * containers, `orgs/{orgId}/emailCampaigns`. A container that is gone names\n * nothing; a read that fails names none, and the draft goes without.\n */\nasync function campaignNames(firestore: Firestore, orgId: string, campaignIds: readonly string[]): Promise<string[]> {\n if (!campaignIds.length) return []\n try {\n const containers = firestore.collection('orgs').doc(orgId).collection('emailCampaigns')\n const found = await firestore.getAll(...campaignIds.map((id) => containers.doc(id)))\n return found.map((snapshot) => text(snapshot.get('name'))).filter(Boolean)\n } catch (error) {\n console.error('[outreach] the person’s campaigns could not be named for the draft', error)\n return []\n }\n}\n\n/** What the CRM holds about the person, for the prompt. */\nasync function curationFacts(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n subject: DraftSubject,\n candidate: OutreachEnrollCandidate,\n contactGroupId: string,\n): Promise<OutreachCurationFacts> {\n const contact = candidate.contact ?? {}\n const facet = readContactFacet(contact, contactGroupId) as unknown as Record<string, unknown>\n const lead = candidate.lead ?? {}\n const company = candidate.company ?? {}\n const campaignIds =\n candidate.target === 'lead' ? readCampaignIds(lead) : readContactCampaignIds(contact, contactGroupId)\n const [mailboxSnapshot, host, campaigns] = await Promise.all([\n subject.sequence.mailboxId\n ? outreachOrgCollection(firestore, caller.orgId, 'mailboxes').doc(subject.sequence.mailboxId).get()\n : null,\n subject.sequence.hostId ? firestore.collection('hosts').doc(subject.sequence.hostId).get() : null,\n campaignNames(firestore, caller.orgId, campaignIds),\n ])\n const mailbox = readStoredOutreachMailbox(\n subject.sequence.mailboxId,\n mailboxSnapshot?.exists ? mailboxSnapshot.data() : undefined,\n )\n return {\n name: candidate.name || text(contact['name']),\n company: text(facet['companyName']) || text(lead['company']) || text(company['name']),\n title: text(facet['jobTitle']) || text(lead['jobTitle']),\n website: text(company['website']) || text(company['domain']) || text(lead['website']) || text(lead['domain']),\n tags: [...new Set([...texts(facet['tags']), ...texts(company['tags'])])],\n sources: Object.entries((facet['sources'] ?? {}) as Record<string, unknown>)\n .filter(([, on]) => on === true)\n .map(([source]) => source)\n .sort(),\n campaigns,\n notes: candidate.target === 'lead' ? text(lead['notes']) : '',\n record: candidate.target,\n siteName: host?.exists ? text(host.get('name')) : '',\n senderName: mailbox?.displayName || '',\n personalLine: subject.personalLine,\n }\n}\n\n/** The email steps to draft, as the prompt states them: the step's words, or its template's. */\nasync function curationSteps(\n firestore: Firestore,\n orgId: string,\n sequence: OutreachSequence,\n stepIndexes: readonly number[],\n): Promise<OutreachCurationStep[]> {\n const emails = emailStepIndexes(sequence)\n const templates = firestore.collection('orgs').doc(orgId).collection(CRM_COLLECTIONS.emailTemplates)\n return Promise.all(\n stepIndexes.map(async (stepIndex) => {\n const step = sequence.steps[stepIndex]\n if (step?.kind !== 'email') throw new Error(`step ${stepIndex} is not an email`)\n let body = step.body\n if (step.templateId) {\n const template = await templates.doc(step.templateId).get()\n body = template.exists ? normalizeCrmEmailTemplate(template.data() as Record<string, unknown>).body : ''\n }\n return {\n stepIndex,\n position: emails.indexOf(stepIndex) + 1,\n count: emails.length,\n subject: step.subject,\n body,\n startsThread: !isInThreadEmailStep(sequence.steps, stepIndex),\n }\n }),\n )\n}\n\n/** The member's name for the record's entry: as the roster lists them, else their address. */\nasync function memberName(firestore: Firestore, caller: OutreachRouteCaller): Promise<string | null> {\n try {\n const member = await firestore.collection('orgs').doc(caller.orgId).collection('members').doc(caller.uid).get()\n return (member.exists ? text(member.get('displayName')) || text(member.get('email')) : '') || caller.email\n } catch {\n return caller.email\n }\n}\n\nexport interface OutreachCurateRoutes {\n draft: PluginWebApiHandler\n save: PluginWebApiHandler\n}\n\nexport function createOutreachCurateRoutes(deps: OutreachEnrollRouteDeps): OutreachCurateRoutes {\n const draft: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate, [OUTREACH_READS_PEOPLE])\n if (caller instanceof Response) return caller\n const generator = deps.textGenerator()\n if (!generator) {\n return outreachRefusal(503, 'curation-unavailable', 'AI drafting is not available in this workspace.')\n }\n const firestore = deps.firestore()\n const subject = await draftSubject(firestore, caller, body)\n if (subject instanceof Response) return subject\n const contactGroupId = consentGroupForHost(caller.org, subject.sequence.hostId).groupId\n const [candidate] = await readOutreachEnrollCandidates(firestore, {\n orgId: caller.orgId,\n hostId: subject.sequence.hostId,\n contactGroupId,\n people: [subject.person],\n })\n if (!candidate?.contact || !candidate.visible) {\n return outreachRefusal(404, 'contact-not-found', \"That person isn't in this sequence's site's CRM.\")\n }\n const [facts, steps] = await Promise.all([\n curationFacts(firestore, caller, subject, candidate, contactGroupId),\n curationSteps(firestore, caller.orgId, subject.sequence, subject.stepIndexes),\n ])\n const prompt = outreachCurationPrompt(facts, steps)\n const answer = await generator.generate({\n orgId: caller.orgId,\n hostId: subject.sequence.hostId || null,\n uid: caller.uid,\n staff: caller.staff,\n org: caller.org,\n purpose: OUTREACH_CURATION_PURPOSE,\n system: OUTREACH_CURATION_SYSTEM,\n prompt,\n maxTokens: CURATION_MAX_TOKENS,\n now: new Date(deps.now()),\n })\n // Compared to `false` rather than negated: the union narrows on the\n // literal, and this tree compiles without strict null checks.\n if (answer.ok === false) {\n return outreachRefusal(\n answer.status,\n answer.reason === 'unavailable' ? 'curation-unavailable' : 'curation-refused',\n answer.error,\n )\n }\n const parsed = parseOutreachCurationAnswer(answer.text, steps)\n if (!parsed) {\n return outreachRefusal(\n 502,\n 'curation-refused',\n 'The draft came back in a shape that can’t be read. Try again.',\n )\n }\n const drafts: OutreachCurateDraft[] = parsed.map((entry) => {\n const step = steps.find((candidateStep) => candidateStep.stepIndex === entry.stepIndex)\n return {\n ...entry,\n issues: validateOutreachCuratedStep(\n { subject: entry.subject, body: entry.body, startsThread: step?.startsThread === true },\n `stepOverrides.${entry.stepIndex}`,\n ),\n }\n })\n return outreachOk({ ok: true, drafts, prompt, model: answer.model } satisfies OutreachCurateDraftResponse)\n }\n\n const save: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate)\n if (caller instanceof Response) return caller\n const enrollmentId = readOutreachDocumentId(body['enrollmentId'])\n const stepIndex = Number(body['stepIndex'])\n if (!enrollmentId || !Number.isInteger(stepIndex) || stepIndex < 0) {\n return outreachRefusal(400, 'invalid-request', 'Name the enrollment and the step to curate.')\n }\n const firestore = deps.firestore()\n const enrollments = outreachOrgCollection(firestore, caller.orgId, 'enrollments')\n const current = await enrollments.doc(enrollmentId).get()\n const stored = readStoredOutreachEnrollment(enrollmentId, current.exists ? current.data() : undefined)\n if (!stored) return outreachRefusal(404, 'enrollment-not-found', 'That enrollment no longer exists.')\n const sequenceSnapshot = await outreachOrgCollection(firestore, caller.orgId, 'sequences').doc(stored.sequenceId).get()\n const sequence = readStoredOutreachSequence(stored.sequenceId, sequenceSnapshot.exists ? sequenceSnapshot.data() : undefined)\n if (!sequence) return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n if (sequence.steps[stepIndex]?.kind !== 'email') {\n return outreachRefusal(400, 'invalid-request', 'A curated step is one of the sequence’s email steps.')\n }\n\n const cleared = body['override'] === null\n let override: OutreachStepOverride | null = null\n if (!cleared) {\n const read = readOutreachStepOverrideRequest({ ...(body['override'] as object), stepIndex }, sequence.steps)\n if ('refusal' in read) return outreachRefusal(400, 'invalid-override', read.refusal)\n if (hasOutreachValidationErrors(read.issues)) {\n return outreachRefusal(400, 'invalid-override', 'This draft breaks a rule every sequence email keeps.', {\n issues: read.issues,\n })\n }\n override = { ...read.override, draftedAtMs: deps.now(), draftedByUid: caller.uid }\n }\n\n const nowMs = deps.now()\n const written = await firestore.runTransaction(async (transaction) => {\n const snapshot = await transaction.get(enrollments.doc(enrollmentId))\n const enrollment = readStoredOutreachEnrollment(enrollmentId, snapshot.exists ? snapshot.data() : undefined)\n if (!enrollment) return { refusal: outreachRefusal(404, 'enrollment-not-found', 'That enrollment no longer exists.') }\n if (!OUTREACH_OPEN_ENROLLMENT_STATUSES.includes(enrollment.status)) {\n return {\n refusal: outreachRefusal(409, 'transition-refused', 'This enrollment has ended, so nothing more is sent to them.'),\n }\n }\n // A step that already went out, or is going out now, is not rewritten\n // under the run: what was sent is what was sent.\n if (stepIndex < enrollment.stepIndex || (enrollment.sendClaim && enrollment.sendClaim.stepIndex === stepIndex)) {\n return { refusal: outreachRefusal(409, 'transition-refused', 'That step already went out to this person.') }\n }\n transaction.update(enrollments.doc(enrollmentId), {\n [`stepOverrides.${stepIndex}`]: override ?? FieldValue.delete(),\n updatedAtMs: nowMs,\n })\n const stepOverrides = { ...(enrollment.stepOverrides ?? {}) }\n if (override) stepOverrides[String(stepIndex)] = override\n else delete stepOverrides[String(stepIndex)]\n // Absent rather than empty once the last copy is cleared, as the\n // stored reader answers it.\n const next: OutreachEnrollment = { ...enrollment, updatedAtMs: nowMs }\n delete next.stepOverrides\n if (Object.keys(stepOverrides).length) next.stepOverrides = stepOverrides\n return { enrollment: next }\n })\n if ('refusal' in written) return written.refusal\n const { enrollment } = written\n\n if (override) {\n // The person's record says so (AGL-3274, AGL-3324): who wrote this\n // person's copy of the step, under the send it shapes.\n const entry = outreachCuratedEntry({\n enrollmentId,\n stepIndex,\n source: override.source,\n edited: override.edited === true,\n memberName: await memberName(firestore, caller),\n atMs: nowMs,\n })\n await fileOutreachNote(deps, {\n orgId: caller.orgId,\n hostId: enrollment.hostId,\n link: outreachEnrollmentLink(enrollment),\n dedupeKey: entry.dedupeKey,\n body: entry.body,\n atMs: nowMs,\n })\n }\n await deps.logOrgActivity(\n caller.orgId,\n { uid: caller.uid, email: caller.email },\n outreachCuratedActivity(stepIndex, cleared),\n { type: OUTREACH_SEQUENCE_ACTIVITY_TARGET, id: sequence.id, name: sequence.name },\n )\n return outreachOk({ ok: true, enrollment } satisfies OutreachCurateSaveResponse)\n }\n\n return { draft, save }\n}\n\n/** The text generator on the core's seam, for the default route deps; `null` when no plugin generates. */\nexport type OutreachTextGeneratorReader = () => PluginTextGenerator | null\n"],"names":["readCampaignIds","readContactCampaignIds","consentGroupForHost","readContactFacet","CRM_COLLECTIONS","normalizeCrmEmailTemplate","FieldValue","OUTREACH_CURATION_SYSTEM","outreachCurationPrompt","parseOutreachCurationAnswer","outreachCuratedEntry","OUTREACH_OPEN_ENROLLMENT_STATUSES","hasOutreachValidationErrors","isInThreadEmailStep","validateOutreachCuratedStep","readOutreachEnrollCandidates","readOutreachStepOverrideRequest","OUTREACH_MAX_EMAIL_STEPS","fileOutreachNote","outreachEnrollmentLink","outreachEnrollmentPerson","outreachOrgCollection","readStoredOutreachEnrollment","readStoredOutreachMailbox","readStoredOutreachSequence","OUTREACH_READS_PEOPLE","OUTREACH_SEQUENCE_ACTIVITY_TARGET","outreachRouteGate","outreachMethodNotAllowed","outreachOk","outreachRefusal","readOutreachDocumentId","readOutreachJsonBody","OUTREACH_CURATION_PURPOSE","CURATION_MAX_TOKENS","outreachCuratedActivity","stepIndex","cleared","emailStepIndexes","sequence","steps","map","step","index","kind","filter","readStepIndexes","raw","fallback","undefined","Array","isArray","length","wanted","Set","value","Number","emails","some","includes","sort","a","b","draftSubject","firestore","caller","body","enrollmentId","snapshot","orgId","doc","get","enrollment","exists","data","status","sequenceSnapshot","sequenceId","next","find","stepIndexes","Response","person","personalLine","contactId","leadId","id","text","trim","texts","Boolean","campaignNames","campaignIds","containers","collection","found","getAll","error","console","curationFacts","subject","candidate","contactGroupId","facet","contact","lead","company","target","mailboxSnapshot","host","campaigns","Promise","all","mailboxId","hostId","mailbox","name","title","website","tags","sources","Object","entries","on","source","notes","record","siteName","senderName","displayName","curationSteps","templates","emailTemplates","Error","templateId","template","position","indexOf","count","startsThread","memberName","member","uid","email","createOutreachCurateRoutes","deps","draft","request","method","gate","generator","textGenerator","org","groupId","people","visible","facts","prompt","answer","generate","staff","purpose","system","maxTokens","now","Date","ok","reason","parsed","drafts","entry","candidateStep","issues","model","save","isInteger","enrollments","current","stored","override","read","refusal","draftedAtMs","draftedByUid","nowMs","written","runTransaction","transaction","sendClaim","update","delete","updatedAtMs","stepOverrides","String","keys","edited","atMs","link","dedupeKey","logOrgActivity","type"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,eAAe,EAAEC,sBAAsB,QAAQ,6CAA4C;AACpG,SAASC,mBAAmB,QAAQ,wCAAuC;AAC3E,SAASC,gBAAgB,QAAQ,kCAAiC;AAClE,SAASC,eAAe,QAAQ,6BAA4B;AAC5D,SAASC,yBAAyB,QAAQ,6CAA4C;AAGtF,SAASC,UAAU,QAAQ,2BAA0B;AACrD,SACEC,wBAAwB,EACxBC,sBAAsB,EACtBC,2BAA2B,QAGtB,+BAA2B;AAClC,SAASC,oBAAoB,QAAQ,mCAA+B;AACpE,SAASC,iCAAiC,QAAQ,qBAAiB;AACnE,SACEC,2BAA2B,EAC3BC,mBAAmB,EACnBC,2BAA2B,QACtB,mCAA+B;AACtC,SAASC,4BAA4B,QAAsC,iCAA6B;AACxG,SAASC,+BAA+B,QAAQ,kCAA8B;AAO9E,SACEC,wBAAwB,QAInB,6BAAyB;AAChC,SAASC,gBAAgB,QAAQ,yBAAqB;AACtD,SACEC,sBAAsB,EACtBC,wBAAwB,EACxBC,qBAAqB,EACrBC,4BAA4B,EAC5BC,yBAAyB,EACzBC,0BAA0B,QACrB,iCAA6B;AACpC,SAASC,qBAAqB,QAAsC,qBAAiB;AACrF,SAASC,iCAAiC,QAAQ,kBAAc;AAChE,SAASC,iBAAiB,QAAkC,kBAAc;AAC1E,SACEC,wBAAwB,EACxBC,UAAU,EACVC,eAAe,EACfC,sBAAsB,EACtBC,oBAAoB,QACf,kBAAc;AAErB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgCC,GAED,4DAA4D,GAC5D,OAAO,MAAMC,4BAA4B,kBAAiB;AAE1D,kFAAkF,GAClF,MAAMC,sBAAsB;AAE5B,+CAA+C,GAC/C,OAAO,SAASC,wBAAwBC,SAAiB,EAAEC,OAAgB;IACzE,OAAOA,UACH,CAAC,iCAAiC,EAAED,YAAY,EAAE,cAAc,CAAC,GACjE,CAAC,aAAa,EAAEA,YAAY,EAAE,6BAA6B,CAAC;AAClE;AAcA,wCAAwC,GACxC,SAASE,iBAAiBC,QAAyC;IACjE,OAAOA,SAASC,KAAK,CAACC,GAAG,CAAC,CAACC,MAAMC,QAAWD,KAAKE,IAAI,KAAK,UAAUD,QAAQ,CAAC,GAAIE,MAAM,CAAC,CAACF,QAAUA,SAAS;AAC9G;AAEA,mGAAmG,GACnG,SAASG,gBACPC,GAAY,EACZR,QAA0B,EAC1BS,QAAkB;IAElB,IAAID,QAAQE,WAAW,OAAOD;IAC9B,IAAI,CAACE,MAAMC,OAAO,CAACJ,QAAQ,CAACA,IAAIK,MAAM,EAAE;QACtC,OAAOtB,gBAAgB,KAAK,mBAAmB;IACjD;IACA,MAAMuB,SAAS;WAAI,IAAIC,IAAIP,IAAIN,GAAG,CAAC,CAACc,QAAUC,OAAOD;KAAS;IAC9D,MAAME,SAASnB,iBAAiBC;IAChC,IAAIc,OAAOK,IAAI,CAAC,CAACf,QAAU,CAACc,OAAOE,QAAQ,CAAChB,SAAS;QACnD,OAAOb,gBAAgB,KAAK,mBAAmB;IACjD;IACA,IAAIuB,OAAOD,MAAM,GAAGnC,0BAA0B;QAC5C,OAAOa,gBAAgB,KAAK,mBAAmB,CAAC,cAAc,EAAEb,yBAAyB,kBAAkB,CAAC;IAC9G;IACA,OAAOoC,OAAOO,IAAI,CAAC,CAACC,GAAGC,IAAMD,IAAIC;AACnC;AAEA,iFAAiF,GACjF,eAAeC,aACbC,SAAoB,EACpBC,MAA2B,EAC3BC,IAA6B;IAE7B,MAAMC,eAAeD,IAAI,CAAC,eAAe,KAAKjB,YAAY,OAAOlB,uBAAuBmC,IAAI,CAAC,eAAe;IAC5G,IAAIA,IAAI,CAAC,eAAe,KAAKjB,aAAa,CAACkB,cAAc;QACvD,OAAOrC,gBAAgB,KAAK,mBAAmB;IACjD;IACA,IAAIqC,cAAc;QAChB,MAAMC,WAAW,MAAM/C,sBAAsB2C,WAAWC,OAAOI,KAAK,EAAE,eAAeC,GAAG,CAACH,cAAcI,GAAG;QAC1G,MAAMC,aAAalD,6BAA6B6C,cAAcC,SAASK,MAAM,GAAGL,SAASM,IAAI,KAAKzB;QAClG,IAAI,CAACuB,YAAY,OAAO1C,gBAAgB,KAAK,wBAAwB;QACrE,IAAI,CAACnB,kCAAkCgD,QAAQ,CAACa,WAAWG,MAAM,GAAG;YAClE,OAAO7C,gBAAgB,KAAK,sBAAsB;QACpD;QACA,MAAM8C,mBAAmB,MAAMvD,sBAAsB2C,WAAWC,OAAOI,KAAK,EAAE,aAC3EC,GAAG,CAACE,WAAWK,UAAU,EACzBN,GAAG;QACN,MAAMhC,WAAWf,2BACfgD,WAAWK,UAAU,EACrBD,iBAAiBH,MAAM,GAAGG,iBAAiBF,IAAI,KAAKzB;QAEtD,IAAI,CAACV,UAAU,OAAOT,gBAAgB,KAAK,sBAAsB;QACjE,uEAAuE;QACvE,yDAAyD;QACzD,MAAMgD,OAAOxC,iBAAiBC,UAAUwC,IAAI,CAAC,CAACpC,QAAUA,SAAS6B,WAAWpC,SAAS;QACrF,MAAM4C,cAAclC,gBAAgBoB,IAAI,CAAC,cAAc,EAAE3B,UAAUuC,SAAS7B,YAAY,EAAE,GAAG;YAAC6B;SAAK;QACnG,IAAIE,uBAAuBC,UAAU,OAAOD;QAC5C,IAAIA,YAAYtB,IAAI,CAAC,CAACf,QAAUA,QAAQ6B,WAAWpC,SAAS,GAAG;YAC7D,OAAON,gBAAgB,KAAK,sBAAsB;QACpD;QACA,IAAI,CAACkD,YAAY5B,MAAM,EAAE;YACvB,OAAOtB,gBAAgB,KAAK,sBAAsB;QACpD;QACA,OAAO;YACLS;YACA2C,QAAQ9D,yBAAyBoD;YACjCW,cAAcX,WAAWW,YAAY;YACrCX;YACAQ;QACF;IACF;IACA,MAAMH,aAAa9C,uBAAuBmC,IAAI,CAAC,aAAa;IAC5D,IAAI,CAACW,YAAY,OAAO/C,gBAAgB,KAAK,mBAAmB;IAChE,MAAMsD,YAAYlB,IAAI,CAAC,YAAY,KAAKjB,YAAY,OAAOlB,uBAAuBmC,IAAI,CAAC,YAAY;IACnG,MAAMmB,SAASnB,IAAI,CAAC,SAAS,KAAKjB,YAAY,OAAOlB,uBAAuBmC,IAAI,CAAC,SAAS;IAC1F,MAAMgB,SAAmCE,YACrC;QAAExC,MAAM;QAAW0C,IAAIF;IAAU,IACjCC,SACE;QAAEzC,MAAM;QAAQ0C,IAAID;IAAO,IAC3B;IACN,IAAI,CAACH,QAAQ,OAAOpD,gBAAgB,KAAK,mBAAmB;IAC5D,MAAMsC,WAAW,MAAM/C,sBAAsB2C,WAAWC,OAAOI,KAAK,EAAE,aAAaC,GAAG,CAACO,YAAYN,GAAG;IACtG,MAAMhC,WAAWf,2BAA2BqD,YAAYT,SAASK,MAAM,GAAGL,SAASM,IAAI,KAAKzB;IAC5F,IAAI,CAACV,UAAU,OAAOT,gBAAgB,KAAK,sBAAsB;IACjE,MAAMkD,cAAclC,gBAAgBoB,IAAI,CAAC,cAAc,EAAE3B,UAAUD,iBAAiBC;IACpF,IAAIyC,uBAAuBC,UAAU,OAAOD;IAC5C,IAAI,CAACA,YAAY5B,MAAM,EAAE;QACvB,OAAOtB,gBAAgB,KAAK,mBAAmB;IACjD;IACA,OAAO;QACLS;QACA2C;QACAC,cAAc,OAAOjB,IAAI,CAAC,eAAe,KAAK,WAAWA,IAAI,CAAC,eAAe,GAAG;QAChFM,YAAY;QACZQ;IACF;AACF;AAEA,MAAMO,OAAO,CAAChC,QAA4B,OAAOA,UAAU,WAAWA,MAAMiC,IAAI,KAAK;AACrF,MAAMC,QAAQ,CAAClC,QACbL,MAAMC,OAAO,CAACI,SAASA,MAAMd,GAAG,CAAC8C,MAAM1C,MAAM,CAAC6C,WAAW,EAAE;AAE7D;;;;CAIC,GACD,eAAeC,cAAc3B,SAAoB,EAAEK,KAAa,EAAEuB,WAA8B;IAC9F,IAAI,CAACA,YAAYxC,MAAM,EAAE,OAAO,EAAE;IAClC,IAAI;QACF,MAAMyC,aAAa7B,UAAU8B,UAAU,CAAC,QAAQxB,GAAG,CAACD,OAAOyB,UAAU,CAAC;QACtE,MAAMC,QAAQ,MAAM/B,UAAUgC,MAAM,IAAIJ,YAAYnD,GAAG,CAAC,CAAC6C,KAAOO,WAAWvB,GAAG,CAACgB;QAC/E,OAAOS,MAAMtD,GAAG,CAAC,CAAC2B,WAAamB,KAAKnB,SAASG,GAAG,CAAC,UAAU1B,MAAM,CAAC6C;IACpE,EAAE,OAAOO,OAAO;QACdC,QAAQD,KAAK,CAAC,sEAAsEA;QACpF,OAAO,EAAE;IACX;AACF;AAEA,yDAAyD,GACzD,eAAeE,cACbnC,SAAoB,EACpBC,MAA2B,EAC3BmC,OAAqB,EACrBC,SAAkC,EAClCC,cAAsB;QAEND,oBAEHA,iBACGA,oBAoBWE;IAvB3B,MAAMC,WAAUH,qBAAAA,UAAUG,OAAO,YAAjBH,qBAAqB,CAAC;IACtC,MAAME,QAAQpG,iBAAiBqG,SAASF;IACxC,MAAMG,QAAOJ,kBAAAA,UAAUI,IAAI,YAAdJ,kBAAkB,CAAC;IAChC,MAAMK,WAAUL,qBAAAA,UAAUK,OAAO,YAAjBL,qBAAqB,CAAC;IACtC,MAAMT,cACJS,UAAUM,MAAM,KAAK,SAAS3G,gBAAgByG,QAAQxG,uBAAuBuG,SAASF;IACxF,MAAM,CAACM,iBAAiBC,MAAMC,UAAU,GAAG,MAAMC,QAAQC,GAAG,CAAC;QAC3DZ,QAAQ7D,QAAQ,CAAC0E,SAAS,GACtB5F,sBAAsB2C,WAAWC,OAAOI,KAAK,EAAE,aAAaC,GAAG,CAAC8B,QAAQ7D,QAAQ,CAAC0E,SAAS,EAAE1C,GAAG,KAC/F;QACJ6B,QAAQ7D,QAAQ,CAAC2E,MAAM,GAAGlD,UAAU8B,UAAU,CAAC,SAASxB,GAAG,CAAC8B,QAAQ7D,QAAQ,CAAC2E,MAAM,EAAE3C,GAAG,KAAK;QAC7FoB,cAAc3B,WAAWC,OAAOI,KAAK,EAAEuB;KACxC;IACD,MAAMuB,UAAU5F,0BACd6E,QAAQ7D,QAAQ,CAAC0E,SAAS,EAC1BL,CAAAA,mCAAAA,gBAAiBnC,MAAM,IAAGmC,gBAAgBlC,IAAI,KAAKzB;IAErD,OAAO;QACLmE,MAAMf,UAAUe,IAAI,IAAI7B,KAAKiB,OAAO,CAAC,OAAO;QAC5CE,SAASnB,KAAKgB,KAAK,CAAC,cAAc,KAAKhB,KAAKkB,IAAI,CAAC,UAAU,KAAKlB,KAAKmB,OAAO,CAAC,OAAO;QACpFW,OAAO9B,KAAKgB,KAAK,CAAC,WAAW,KAAKhB,KAAKkB,IAAI,CAAC,WAAW;QACvDa,SAAS/B,KAAKmB,OAAO,CAAC,UAAU,KAAKnB,KAAKmB,OAAO,CAAC,SAAS,KAAKnB,KAAKkB,IAAI,CAAC,UAAU,KAAKlB,KAAKkB,IAAI,CAAC,SAAS;QAC5Gc,MAAM;eAAI,IAAIjE,IAAI;mBAAImC,MAAMc,KAAK,CAAC,OAAO;mBAAMd,MAAMiB,OAAO,CAAC,OAAO;aAAE;SAAE;QACxEc,SAASC,OAAOC,OAAO,EAAEnB,iBAAAA,KAAK,CAAC,UAAU,YAAhBA,iBAAoB,CAAC,GAC3C1D,MAAM,CAAC,CAAC,GAAG8E,GAAG,GAAKA,OAAO,MAC1BlF,GAAG,CAAC,CAAC,CAACmF,OAAO,GAAKA,QAClBhE,IAAI;QACPkD;QACAe,OAAOxB,UAAUM,MAAM,KAAK,SAASpB,KAAKkB,IAAI,CAAC,QAAQ,IAAI;QAC3DqB,QAAQzB,UAAUM,MAAM;QACxBoB,UAAUlB,CAAAA,wBAAAA,KAAMpC,MAAM,IAAGc,KAAKsB,KAAKtC,GAAG,CAAC,WAAW;QAClDyD,YAAYb,CAAAA,2BAAAA,QAASc,WAAW,KAAI;QACpC9C,cAAciB,QAAQjB,YAAY;IACpC;AACF;AAEA,8FAA8F,GAC9F,eAAe+C,cACblE,SAAoB,EACpBK,KAAa,EACb9B,QAA0B,EAC1ByC,WAA8B;IAE9B,MAAMvB,SAASnB,iBAAiBC;IAChC,MAAM4F,YAAYnE,UAAU8B,UAAU,CAAC,QAAQxB,GAAG,CAACD,OAAOyB,UAAU,CAAC1F,gBAAgBgI,cAAc;IACnG,OAAOrB,QAAQC,GAAG,CAChBhC,YAAYvC,GAAG,CAAC,OAAOL;QACrB,MAAMM,OAAOH,SAASC,KAAK,CAACJ,UAAU;QACtC,IAAIM,CAAAA,wBAAAA,KAAME,IAAI,MAAK,SAAS,MAAM,IAAIyF,MAAM,CAAC,KAAK,EAAEjG,UAAU,gBAAgB,CAAC;QAC/E,IAAI8B,OAAOxB,KAAKwB,IAAI;QACpB,IAAIxB,KAAK4F,UAAU,EAAE;YACnB,MAAMC,WAAW,MAAMJ,UAAU7D,GAAG,CAAC5B,KAAK4F,UAAU,EAAE/D,GAAG;YACzDL,OAAOqE,SAAS9D,MAAM,GAAGpE,0BAA0BkI,SAAS7D,IAAI,IAA+BR,IAAI,GAAG;QACxG;QACA,OAAO;YACL9B;YACAoG,UAAU/E,OAAOgF,OAAO,CAACrG,aAAa;YACtCsG,OAAOjF,OAAOL,MAAM;YACpBgD,SAAS1D,KAAK0D,OAAO;YACrBlC;YACAyE,cAAc,CAAC9H,oBAAoB0B,SAASC,KAAK,EAAEJ;QACrD;IACF;AAEJ;AAEA,4FAA4F,GAC5F,eAAewG,WAAW5E,SAAoB,EAAEC,MAA2B;IACzE,IAAI;QACF,MAAM4E,SAAS,MAAM7E,UAAU8B,UAAU,CAAC,QAAQxB,GAAG,CAACL,OAAOI,KAAK,EAAEyB,UAAU,CAAC,WAAWxB,GAAG,CAACL,OAAO6E,GAAG,EAAEvE,GAAG;QAC7G,OAAO,AAACsE,CAAAA,OAAOpE,MAAM,GAAGc,KAAKsD,OAAOtE,GAAG,CAAC,mBAAmBgB,KAAKsD,OAAOtE,GAAG,CAAC,YAAY,EAAC,KAAMN,OAAO8E,KAAK;IAC5G,EAAE,eAAM;QACN,OAAO9E,OAAO8E,KAAK;IACrB;AACF;AAOA,OAAO,SAASC,2BAA2BC,IAA6B;IACtE,MAAMC,QAA6B,OAAOC;QACxC,IAAIA,QAAQC,MAAM,KAAK,QAAQ,OAAOxH,yBAAyB;QAC/D,MAAMsC,OAAO,MAAMlC,qBAAqBmH;QACxC,MAAMlF,SAAS,MAAMtC,kBAAkBwH,SAASjF,IAAI,CAAC,QAAQ,EAAE+E,KAAKI,IAAI,EAAE;YAAC5H;SAAsB;QACjG,IAAIwC,kBAAkBgB,UAAU,OAAOhB;QACvC,MAAMqF,YAAYL,KAAKM,aAAa;QACpC,IAAI,CAACD,WAAW;YACd,OAAOxH,gBAAgB,KAAK,wBAAwB;QACtD;QACA,MAAMkC,YAAYiF,KAAKjF,SAAS;QAChC,MAAMoC,UAAU,MAAMrC,aAAaC,WAAWC,QAAQC;QACtD,IAAIkC,mBAAmBnB,UAAU,OAAOmB;QACxC,MAAME,iBAAiBpG,oBAAoB+D,OAAOuF,GAAG,EAAEpD,QAAQ7D,QAAQ,CAAC2E,MAAM,EAAEuC,OAAO;QACvF,MAAM,CAACpD,UAAU,GAAG,MAAMtF,6BAA6BiD,WAAW;YAChEK,OAAOJ,OAAOI,KAAK;YACnB6C,QAAQd,QAAQ7D,QAAQ,CAAC2E,MAAM;YAC/BZ;YACAoD,QAAQ;gBAACtD,QAAQlB,MAAM;aAAC;QAC1B;QACA,IAAI,EAACmB,6BAAAA,UAAWG,OAAO,KAAI,CAACH,UAAUsD,OAAO,EAAE;YAC7C,OAAO7H,gBAAgB,KAAK,qBAAqB;QACnD;QACA,MAAM,CAAC8H,OAAOpH,MAAM,GAAG,MAAMuE,QAAQC,GAAG,CAAC;YACvCb,cAAcnC,WAAWC,QAAQmC,SAASC,WAAWC;YACrD4B,cAAclE,WAAWC,OAAOI,KAAK,EAAE+B,QAAQ7D,QAAQ,EAAE6D,QAAQpB,WAAW;SAC7E;QACD,MAAM6E,SAASrJ,uBAAuBoJ,OAAOpH;QAC7C,MAAMsH,SAAS,MAAMR,UAAUS,QAAQ,CAAC;YACtC1F,OAAOJ,OAAOI,KAAK;YACnB6C,QAAQd,QAAQ7D,QAAQ,CAAC2E,MAAM,IAAI;YACnC4B,KAAK7E,OAAO6E,GAAG;YACfkB,OAAO/F,OAAO+F,KAAK;YACnBR,KAAKvF,OAAOuF,GAAG;YACfS,SAAShI;YACTiI,QAAQ3J;YACRsJ;YACAM,WAAWjI;YACXkI,KAAK,IAAIC,KAAKpB,KAAKmB,GAAG;QACxB;QACA,oEAAoE;QACpE,8DAA8D;QAC9D,IAAIN,OAAOQ,EAAE,KAAK,OAAO;YACvB,OAAOxI,gBACLgI,OAAOnF,MAAM,EACbmF,OAAOS,MAAM,KAAK,gBAAgB,yBAAyB,oBAC3DT,OAAO7D,KAAK;QAEhB;QACA,MAAMuE,SAAS/J,4BAA4BqJ,OAAOvE,IAAI,EAAE/C;QACxD,IAAI,CAACgI,QAAQ;YACX,OAAO1I,gBACL,KACA,oBACA;QAEJ;QACA,MAAM2I,SAAgCD,OAAO/H,GAAG,CAAC,CAACiI;YAChD,MAAMhI,OAAOF,MAAMuC,IAAI,CAAC,CAAC4F,gBAAkBA,cAAcvI,SAAS,KAAKsI,MAAMtI,SAAS;YACtF,OAAO,aACFsI;gBACHE,QAAQ9J,4BACN;oBAAEsF,SAASsE,MAAMtE,OAAO;oBAAElC,MAAMwG,MAAMxG,IAAI;oBAAEyE,cAAcjG,CAAAA,wBAAAA,KAAMiG,YAAY,MAAK;gBAAK,GACtF,CAAC,cAAc,EAAE+B,MAAMtI,SAAS,EAAE;;QAGxC;QACA,OAAOP,WAAW;YAAEyI,IAAI;YAAMG;YAAQZ;YAAQgB,OAAOf,OAAOe,KAAK;QAAC;IACpE;IAEA,MAAMC,OAA4B,OAAO3B;YAkBnC5G;QAjBJ,IAAI4G,QAAQC,MAAM,KAAK,QAAQ,OAAOxH,yBAAyB;QAC/D,MAAMsC,OAAO,MAAMlC,qBAAqBmH;QACxC,MAAMlF,SAAS,MAAMtC,kBAAkBwH,SAASjF,IAAI,CAAC,QAAQ,EAAE+E,KAAKI,IAAI;QACxE,IAAIpF,kBAAkBgB,UAAU,OAAOhB;QACvC,MAAME,eAAepC,uBAAuBmC,IAAI,CAAC,eAAe;QAChE,MAAM9B,YAAYoB,OAAOU,IAAI,CAAC,YAAY;QAC1C,IAAI,CAACC,gBAAgB,CAACX,OAAOuH,SAAS,CAAC3I,cAAcA,YAAY,GAAG;YAClE,OAAON,gBAAgB,KAAK,mBAAmB;QACjD;QACA,MAAMkC,YAAYiF,KAAKjF,SAAS;QAChC,MAAMgH,cAAc3J,sBAAsB2C,WAAWC,OAAOI,KAAK,EAAE;QACnE,MAAM4G,UAAU,MAAMD,YAAY1G,GAAG,CAACH,cAAcI,GAAG;QACvD,MAAM2G,SAAS5J,6BAA6B6C,cAAc8G,QAAQxG,MAAM,GAAGwG,QAAQvG,IAAI,KAAKzB;QAC5F,IAAI,CAACiI,QAAQ,OAAOpJ,gBAAgB,KAAK,wBAAwB;QACjE,MAAM8C,mBAAmB,MAAMvD,sBAAsB2C,WAAWC,OAAOI,KAAK,EAAE,aAAaC,GAAG,CAAC4G,OAAOrG,UAAU,EAAEN,GAAG;QACrH,MAAMhC,WAAWf,2BAA2B0J,OAAOrG,UAAU,EAAED,iBAAiBH,MAAM,GAAGG,iBAAiBF,IAAI,KAAKzB;QACnH,IAAI,CAACV,UAAU,OAAOT,gBAAgB,KAAK,sBAAsB;QACjE,IAAIS,EAAAA,4BAAAA,SAASC,KAAK,CAACJ,UAAU,qBAAzBG,0BAA2BK,IAAI,MAAK,SAAS;YAC/C,OAAOd,gBAAgB,KAAK,mBAAmB;QACjD;QAEA,MAAMO,UAAU6B,IAAI,CAAC,WAAW,KAAK;QACrC,IAAIiH,WAAwC;QAC5C,IAAI,CAAC9I,SAAS;YACZ,MAAM+I,OAAOpK,gCAAgC,aAAMkD,IAAI,CAAC,WAAW;gBAAa9B;gBAAaG,SAASC,KAAK;YAC3G,IAAI,aAAa4I,MAAM,OAAOtJ,gBAAgB,KAAK,oBAAoBsJ,KAAKC,OAAO;YACnF,IAAIzK,4BAA4BwK,KAAKR,MAAM,GAAG;gBAC5C,OAAO9I,gBAAgB,KAAK,oBAAoB,wDAAwD;oBACtG8I,QAAQQ,KAAKR,MAAM;gBACrB;YACF;YACAO,WAAW,aAAKC,KAAKD,QAAQ;gBAAEG,aAAarC,KAAKmB,GAAG;gBAAImB,cAActH,OAAO6E,GAAG;;QAClF;QAEA,MAAM0C,QAAQvC,KAAKmB,GAAG;QACtB,MAAMqB,UAAU,MAAMzH,UAAU0H,cAAc,CAAC,OAAOC;gBAkBxBnH;YAjB5B,MAAMJ,WAAW,MAAMuH,YAAYpH,GAAG,CAACyG,YAAY1G,GAAG,CAACH;YACvD,MAAMK,aAAalD,6BAA6B6C,cAAcC,SAASK,MAAM,GAAGL,SAASM,IAAI,KAAKzB;YAClG,IAAI,CAACuB,YAAY,OAAO;gBAAE6G,SAASvJ,gBAAgB,KAAK,wBAAwB;YAAqC;YACrH,IAAI,CAACnB,kCAAkCgD,QAAQ,CAACa,WAAWG,MAAM,GAAG;gBAClE,OAAO;oBACL0G,SAASvJ,gBAAgB,KAAK,sBAAsB;gBACtD;YACF;YACA,sEAAsE;YACtE,iDAAiD;YACjD,IAAIM,YAAYoC,WAAWpC,SAAS,IAAKoC,WAAWoH,SAAS,IAAIpH,WAAWoH,SAAS,CAACxJ,SAAS,KAAKA,WAAY;gBAC9G,OAAO;oBAAEiJ,SAASvJ,gBAAgB,KAAK,sBAAsB;gBAA8C;YAC7G;YACA6J,YAAYE,MAAM,CAACb,YAAY1G,GAAG,CAACH,eAAe;gBAChD,CAAC,CAAC,cAAc,EAAE/B,WAAW,CAAC,EAAE+I,mBAAAA,WAAY7K,WAAWwL,MAAM;gBAC7DC,aAAaP;YACf;YACA,MAAMQ,gBAAgB,cAAMxH,4BAAAA,WAAWwH,aAAa,YAAxBxH,4BAA4B,CAAC;YACzD,IAAI2G,UAAUa,aAAa,CAACC,OAAO7J,WAAW,GAAG+I;iBAC5C,OAAOa,aAAa,CAACC,OAAO7J,WAAW;YAC5C,iEAAiE;YACjE,4BAA4B;YAC5B,MAAM0C,OAA2B,aAAKN;gBAAYuH,aAAaP;;YAC/D,OAAO1G,KAAKkH,aAAa;YACzB,IAAIvE,OAAOyE,IAAI,CAACF,eAAe5I,MAAM,EAAE0B,KAAKkH,aAAa,GAAGA;YAC5D,OAAO;gBAAExH,YAAYM;YAAK;QAC5B;QACA,IAAI,aAAa2G,SAAS,OAAOA,QAAQJ,OAAO;QAChD,MAAM,EAAE7G,UAAU,EAAE,GAAGiH;QAEvB,IAAIN,UAAU;YACZ,mEAAmE;YACnE,uDAAuD;YACvD,MAAMT,QAAQhK,qBAAqB;gBACjCyD;gBACA/B;gBACAwF,QAAQuD,SAASvD,MAAM;gBACvBuE,QAAQhB,SAASgB,MAAM,KAAK;gBAC5BvD,YAAY,MAAMA,WAAW5E,WAAWC;gBACxCmI,MAAMZ;YACR;YACA,MAAMtK,iBAAiB+H,MAAM;gBAC3B5E,OAAOJ,OAAOI,KAAK;gBACnB6C,QAAQ1C,WAAW0C,MAAM;gBACzBmF,MAAMlL,uBAAuBqD;gBAC7B8H,WAAW5B,MAAM4B,SAAS;gBAC1BpI,MAAMwG,MAAMxG,IAAI;gBAChBkI,MAAMZ;YACR;QACF;QACA,MAAMvC,KAAKsD,cAAc,CACvBtI,OAAOI,KAAK,EACZ;YAAEyE,KAAK7E,OAAO6E,GAAG;YAAEC,OAAO9E,OAAO8E,KAAK;QAAC,GACvC5G,wBAAwBC,WAAWC,UACnC;YAAEmK,MAAM9K;YAAmC4D,IAAI/C,SAAS+C,EAAE;YAAE8B,MAAM7E,SAAS6E,IAAI;QAAC;QAElF,OAAOvF,WAAW;YAAEyI,IAAI;YAAM9F;QAAW;IAC3C;IAEA,OAAO;QAAE0E;QAAO4B;IAAK;AACvB"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/curate-routes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { readContainerIds, readContactContainerIds } from '@aglyn/aglyn/app-utils/container-membership'\nimport { consentGroupForHost } from '@aglyn/aglyn/app-utils/consent-groups'\nimport { readContactFacet } from '@aglyn/aglyn/app-utils/contacts'\nimport { CRM_COLLECTIONS } from '@aglyn/aglyn/app-utils/crm'\nimport { normalizeCrmEmailTemplate } from '@aglyn/aglyn/app-utils/crm-email-templates'\nimport type { PluginTextGenerator } from '@aglyn/aglyn/plugin-manager/plugin-text-generation'\nimport type { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport { readOrgContainers } from '@aglyn/tenant-data-admin/server/org-containers'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport {\n OUTREACH_CURATION_SYSTEM,\n outreachCurationPrompt,\n parseOutreachCurationAnswer,\n type OutreachCurationFacts,\n type OutreachCurationStep,\n} from '../engine/curation-prompt'\nimport { outreachCuratedEntry } from '../engine/enrollment-activity'\nimport { OUTREACH_OPEN_ENROLLMENT_STATUSES } from '../engine/gates'\nimport {\n hasOutreachValidationErrors,\n isInThreadEmailStep,\n validateOutreachCuratedStep,\n} from '../engine/sequence-validation'\nimport { readOutreachEnrollCandidates, type OutreachEnrollCandidate } from '../enrollment/enroll-people'\nimport { readOutreachStepOverrideRequest } from '../enrollment/step-overrides'\nimport type {\n OutreachCurateDraft,\n OutreachCurateDraftResponse,\n OutreachCurateSaveResponse,\n OutreachPersonRef,\n} from '../model/outreach-api'\nimport {\n OUTREACH_MAX_EMAIL_STEPS,\n type OutreachEnrollment,\n type OutreachSequence,\n type OutreachStepOverride,\n} from '../model/outreach.types'\nimport { fileOutreachNote } from '../runtime/timeline'\nimport {\n outreachEnrollmentLink,\n outreachEnrollmentPerson,\n outreachOrgCollection,\n readStoredOutreachEnrollment,\n readStoredOutreachMailbox,\n readStoredOutreachSequence,\n} from '../storage/outreach-records'\nimport { OUTREACH_READS_PEOPLE, type OutreachEnrollRouteDeps } from './enroll-routes'\nimport { OUTREACH_SEQUENCE_ACTIVITY_TARGET } from './route-deps'\nimport { outreachRouteGate, type OutreachRouteCaller } from './route-gate'\nimport {\n outreachMethodNotAllowed,\n outreachOk,\n outreachRefusal,\n readOutreachDocumentId,\n readOutreachJsonBody,\n} from './route-http'\n\n/**\n * CURATING ONE PERSON'S EMAILS (AGL-3324): `outreach/curate/draft` and\n * `outreach/curate/save`.\n *\n * A sequence's step is one template for everyone plus the personal line;\n * what an agent-run outbound tool does instead is write each send for each\n * prospect from what it knows about them, and let the seller approve or\n * edit. These two routes are that, with the member's confirmation kept as\n * the one thing that stores anything.\n *\n * DRAFT reads the person the way enrolling reads them — the contact, or the\n * lead as the contact it would be, with the company filed beside them — and\n * asks the workspace's text generator, on the core's seam, for this\n * person's copy of the email steps named: every email step for a person\n * about to be enrolled, the next one for an enrollment. The answer is\n * validated against the playbook's rules and returned with each draft's\n * issues, the prompt and the model. NOTHING IS STORED: the member reads it\n * in the dialog, edits it, ticks the confirmation, and then either enrolls\n * (the enroll route stores the copies with the enrollment) or saves.\n *\n * SAVE stores one confirmed copy on an open enrollment, for a step that\n * has not gone out, validated again — the request is not trusted to have\n * shown the member what it sends — and files \"Curated step N\" on the\n * person's record with who wrote the words. `override: null` clears a copy,\n * so the step goes back to the sequence's own words.\n *\n * Both read the CRM, so both ask for `data.manage` beside `outreach.use`;\n * the generator applies its own rules — the generation permission, the\n * plan, the switches, the allotments — to the member named, and meters the\n * draft on the member's month and the org's. Outreach never reaches the AI\n * plugin: the seam is the platform's, and a workspace where no plugin\n * generates text is told so in the route's words.\n */\n\n/** The purpose the generator's ledger files a draft under. */\nexport const OUTREACH_CURATION_PURPOSE = 'outreach-curate'\n\n/** The most tokens one drafting answer may run to: four short emails, with room. */\nconst CURATION_MAX_TOKENS = 2_400\n\n/** The org activity line a saved copy writes. */\nexport function outreachCuratedActivity(stepIndex: number, cleared: boolean): string {\n return cleared\n ? `Cleared the curated copy of step ${stepIndex + 1} of a sequence`\n : `Curated step ${stepIndex + 1} of a sequence for one person`\n}\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** The person's copies of steps a draft covers, and the enrollment they belong to when one exists. */\ninterface DraftSubject {\n sequence: OutreachSequence\n person: OutreachPersonRef\n personalLine: string\n enrollment: OutreachEnrollment | null\n /** The steps to draft, email steps only, unique, in order. */\n stepIndexes: number[]\n}\n\n/** Every email step's index, in order. */\nfunction emailStepIndexes(sequence: Pick<OutreachSequence, 'steps'>): number[] {\n return sequence.steps.map((step, index) => (step.kind === 'email' ? index : -1)).filter((index) => index >= 0)\n}\n\n/** The step indexes a request names, or the default for its subject; a refusal for anything else. */\nfunction readStepIndexes(\n raw: unknown,\n sequence: OutreachSequence,\n fallback: number[],\n): number[] | Response {\n if (raw === undefined) return fallback\n if (!Array.isArray(raw) || !raw.length) {\n return outreachRefusal(400, 'invalid-request', 'Name the email steps to draft.')\n }\n const wanted = [...new Set(raw.map((value) => Number(value)))]\n const emails = emailStepIndexes(sequence)\n if (wanted.some((index) => !emails.includes(index))) {\n return outreachRefusal(400, 'invalid-request', 'A curated step is one of the sequence’s email steps.')\n }\n if (wanted.length > OUTREACH_MAX_EMAIL_STEPS) {\n return outreachRefusal(400, 'invalid-request', `Draft at most ${OUTREACH_MAX_EMAIL_STEPS} emails at a time.`)\n }\n return wanted.sort((a, b) => a - b)\n}\n\n/** What the request is about: a person to enroll, or an enrollment's next step. */\nasync function draftSubject(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n body: Record<string, unknown>,\n): Promise<DraftSubject | Response> {\n const enrollmentId = body['enrollmentId'] === undefined ? null : readOutreachDocumentId(body['enrollmentId'])\n if (body['enrollmentId'] !== undefined && !enrollmentId) {\n return outreachRefusal(400, 'invalid-request', 'Name the enrollment to curate.')\n }\n if (enrollmentId) {\n const snapshot = await outreachOrgCollection(firestore, caller.orgId, 'enrollments').doc(enrollmentId).get()\n const enrollment = readStoredOutreachEnrollment(enrollmentId, snapshot.exists ? snapshot.data() : undefined)\n if (!enrollment) return outreachRefusal(404, 'enrollment-not-found', 'That enrollment no longer exists.')\n if (!OUTREACH_OPEN_ENROLLMENT_STATUSES.includes(enrollment.status)) {\n return outreachRefusal(409, 'transition-refused', 'This enrollment has ended, so nothing more is sent to them.')\n }\n const sequenceSnapshot = await outreachOrgCollection(firestore, caller.orgId, 'sequences')\n .doc(enrollment.sequenceId)\n .get()\n const sequence = readStoredOutreachSequence(\n enrollment.sequenceId,\n sequenceSnapshot.exists ? sequenceSnapshot.data() : undefined,\n )\n if (!sequence) return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n // The next email step that has not gone out: the one the enrollment is\n // on when it is an email, else the first email after it.\n const next = emailStepIndexes(sequence).find((index) => index >= enrollment.stepIndex)\n const stepIndexes = readStepIndexes(body['stepIndexes'], sequence, next === undefined ? [] : [next])\n if (stepIndexes instanceof Response) return stepIndexes\n if (stepIndexes.some((index) => index < enrollment.stepIndex)) {\n return outreachRefusal(409, 'transition-refused', 'That step already went out to this person.')\n }\n if (!stepIndexes.length) {\n return outreachRefusal(409, 'transition-refused', 'Every email in this sequence has gone out to this person.')\n }\n return {\n sequence,\n person: outreachEnrollmentPerson(enrollment),\n personalLine: enrollment.personalLine,\n enrollment,\n stepIndexes,\n }\n }\n const sequenceId = readOutreachDocumentId(body['sequenceId'])\n if (!sequenceId) return outreachRefusal(400, 'invalid-request', 'Name the sequence to draft for.')\n const contactId = body['contactId'] === undefined ? null : readOutreachDocumentId(body['contactId'])\n const leadId = body['leadId'] === undefined ? null : readOutreachDocumentId(body['leadId'])\n const person: OutreachPersonRef | null = contactId\n ? { kind: 'contact', id: contactId }\n : leadId\n ? { kind: 'lead', id: leadId }\n : null\n if (!person) return outreachRefusal(400, 'invalid-request', 'Name the contact or the lead to draft for.')\n const snapshot = await outreachOrgCollection(firestore, caller.orgId, 'sequences').doc(sequenceId).get()\n const sequence = readStoredOutreachSequence(sequenceId, snapshot.exists ? snapshot.data() : undefined)\n if (!sequence) return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n const stepIndexes = readStepIndexes(body['stepIndexes'], sequence, emailStepIndexes(sequence))\n if (stepIndexes instanceof Response) return stepIndexes\n if (!stepIndexes.length) {\n return outreachRefusal(400, 'invalid-request', 'This sequence sends no email, so there is nothing to draft.')\n }\n return {\n sequence,\n person,\n personalLine: typeof body['personalLine'] === 'string' ? body['personalLine'] : '',\n enrollment: null,\n stepIndexes,\n }\n}\n\nconst text = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\nconst texts = (value: unknown): string[] =>\n Array.isArray(value) ? value.map(text).filter(Boolean) : []\n\n/**\n * The names of the campaigns the person is filed under — the org's\n * containers of the `campaign` kind. A container that is gone names nothing;\n * a read that fails names none, and the draft goes without.\n */\nasync function campaignNames(firestore: Firestore, orgId: string, campaignIds: readonly string[]): Promise<string[]> {\n if (!campaignIds.length) return []\n try {\n const found = await readOrgContainers(firestore, 'campaign', orgId, campaignIds)\n return found.map((container) => container.name).filter(Boolean)\n } catch (error) {\n console.error('[outreach] the person’s campaigns could not be named for the draft', error)\n return []\n }\n}\n\n/** What the CRM holds about the person, for the prompt. */\nasync function curationFacts(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n subject: DraftSubject,\n candidate: OutreachEnrollCandidate,\n contactGroupId: string,\n): Promise<OutreachCurationFacts> {\n const contact = candidate.contact ?? {}\n const facet = readContactFacet(contact, contactGroupId) as unknown as Record<string, unknown>\n const lead = candidate.lead ?? {}\n const company = candidate.company ?? {}\n const campaignIds =\n candidate.target === 'lead' ? readContainerIds(lead, 'campaign') : readContactContainerIds(contact, contactGroupId, 'campaign')\n const [mailboxSnapshot, host, campaigns] = await Promise.all([\n subject.sequence.mailboxId\n ? outreachOrgCollection(firestore, caller.orgId, 'mailboxes').doc(subject.sequence.mailboxId).get()\n : null,\n subject.sequence.hostId ? firestore.collection('hosts').doc(subject.sequence.hostId).get() : null,\n campaignNames(firestore, caller.orgId, campaignIds),\n ])\n const mailbox = readStoredOutreachMailbox(\n subject.sequence.mailboxId,\n mailboxSnapshot?.exists ? mailboxSnapshot.data() : undefined,\n )\n return {\n name: candidate.name || text(contact['name']),\n company: text(facet['companyName']) || text(lead['company']) || text(company['name']),\n title: text(facet['jobTitle']) || text(lead['jobTitle']),\n website: text(company['website']) || text(company['domain']) || text(lead['website']) || text(lead['domain']),\n tags: [...new Set([...texts(facet['tags']), ...texts(company['tags'])])],\n sources: Object.entries((facet['sources'] ?? {}) as Record<string, unknown>)\n .filter(([, on]) => on === true)\n .map(([source]) => source)\n .sort(),\n campaigns,\n notes: candidate.target === 'lead' ? text(lead['notes']) : '',\n record: candidate.target,\n siteName: host?.exists ? text(host.get('name')) : '',\n senderName: mailbox?.displayName || '',\n personalLine: subject.personalLine,\n }\n}\n\n/** The email steps to draft, as the prompt states them: the step's words, or its template's. */\nasync function curationSteps(\n firestore: Firestore,\n orgId: string,\n sequence: OutreachSequence,\n stepIndexes: readonly number[],\n): Promise<OutreachCurationStep[]> {\n const emails = emailStepIndexes(sequence)\n const templates = firestore.collection('orgs').doc(orgId).collection(CRM_COLLECTIONS.emailTemplates)\n return Promise.all(\n stepIndexes.map(async (stepIndex) => {\n const step = sequence.steps[stepIndex]\n if (step?.kind !== 'email') throw new Error(`step ${stepIndex} is not an email`)\n let body = step.body\n if (step.templateId) {\n const template = await templates.doc(step.templateId).get()\n body = template.exists ? normalizeCrmEmailTemplate(template.data() as Record<string, unknown>).body : ''\n }\n return {\n stepIndex,\n position: emails.indexOf(stepIndex) + 1,\n count: emails.length,\n subject: step.subject,\n body,\n startsThread: !isInThreadEmailStep(sequence.steps, stepIndex),\n }\n }),\n )\n}\n\n/** The member's name for the record's entry: as the roster lists them, else their address. */\nasync function memberName(firestore: Firestore, caller: OutreachRouteCaller): Promise<string | null> {\n try {\n const member = await firestore.collection('orgs').doc(caller.orgId).collection('members').doc(caller.uid).get()\n return (member.exists ? text(member.get('displayName')) || text(member.get('email')) : '') || caller.email\n } catch {\n return caller.email\n }\n}\n\nexport interface OutreachCurateRoutes {\n draft: PluginWebApiHandler\n save: PluginWebApiHandler\n}\n\nexport function createOutreachCurateRoutes(deps: OutreachEnrollRouteDeps): OutreachCurateRoutes {\n const draft: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate, [OUTREACH_READS_PEOPLE])\n if (caller instanceof Response) return caller\n const generator = deps.textGenerator()\n if (!generator) {\n return outreachRefusal(503, 'curation-unavailable', 'AI drafting is not available in this workspace.')\n }\n const firestore = deps.firestore()\n const subject = await draftSubject(firestore, caller, body)\n if (subject instanceof Response) return subject\n const contactGroupId = consentGroupForHost(caller.org, subject.sequence.hostId).groupId\n const [candidate] = await readOutreachEnrollCandidates(firestore, {\n orgId: caller.orgId,\n hostId: subject.sequence.hostId,\n contactGroupId,\n people: [subject.person],\n })\n if (!candidate?.contact || !candidate.visible) {\n return outreachRefusal(404, 'contact-not-found', \"That person isn't in this sequence's site's CRM.\")\n }\n const [facts, steps] = await Promise.all([\n curationFacts(firestore, caller, subject, candidate, contactGroupId),\n curationSteps(firestore, caller.orgId, subject.sequence, subject.stepIndexes),\n ])\n const prompt = outreachCurationPrompt(facts, steps)\n const answer = await generator.generate({\n orgId: caller.orgId,\n hostId: subject.sequence.hostId || null,\n uid: caller.uid,\n staff: caller.staff,\n org: caller.org,\n purpose: OUTREACH_CURATION_PURPOSE,\n system: OUTREACH_CURATION_SYSTEM,\n prompt,\n maxTokens: CURATION_MAX_TOKENS,\n now: new Date(deps.now()),\n })\n // Compared to `false` rather than negated: the union narrows on the\n // literal, and this tree compiles without strict null checks.\n if (answer.ok === false) {\n return outreachRefusal(\n answer.status,\n answer.reason === 'unavailable' ? 'curation-unavailable' : 'curation-refused',\n answer.error,\n )\n }\n const parsed = parseOutreachCurationAnswer(answer.text, steps)\n if (!parsed) {\n return outreachRefusal(\n 502,\n 'curation-refused',\n 'The draft came back in a shape that can’t be read. Try again.',\n )\n }\n const drafts: OutreachCurateDraft[] = parsed.map((entry) => {\n const step = steps.find((candidateStep) => candidateStep.stepIndex === entry.stepIndex)\n return {\n ...entry,\n issues: validateOutreachCuratedStep(\n { subject: entry.subject, body: entry.body, startsThread: step?.startsThread === true },\n `stepOverrides.${entry.stepIndex}`,\n ),\n }\n })\n return outreachOk({ ok: true, drafts, prompt, model: answer.model } satisfies OutreachCurateDraftResponse)\n }\n\n const save: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate)\n if (caller instanceof Response) return caller\n const enrollmentId = readOutreachDocumentId(body['enrollmentId'])\n const stepIndex = Number(body['stepIndex'])\n if (!enrollmentId || !Number.isInteger(stepIndex) || stepIndex < 0) {\n return outreachRefusal(400, 'invalid-request', 'Name the enrollment and the step to curate.')\n }\n const firestore = deps.firestore()\n const enrollments = outreachOrgCollection(firestore, caller.orgId, 'enrollments')\n const current = await enrollments.doc(enrollmentId).get()\n const stored = readStoredOutreachEnrollment(enrollmentId, current.exists ? current.data() : undefined)\n if (!stored) return outreachRefusal(404, 'enrollment-not-found', 'That enrollment no longer exists.')\n const sequenceSnapshot = await outreachOrgCollection(firestore, caller.orgId, 'sequences').doc(stored.sequenceId).get()\n const sequence = readStoredOutreachSequence(stored.sequenceId, sequenceSnapshot.exists ? sequenceSnapshot.data() : undefined)\n if (!sequence) return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n if (sequence.steps[stepIndex]?.kind !== 'email') {\n return outreachRefusal(400, 'invalid-request', 'A curated step is one of the sequence’s email steps.')\n }\n\n const cleared = body['override'] === null\n let override: OutreachStepOverride | null = null\n if (!cleared) {\n const read = readOutreachStepOverrideRequest({ ...(body['override'] as object), stepIndex }, sequence.steps)\n if ('refusal' in read) return outreachRefusal(400, 'invalid-override', read.refusal)\n if (hasOutreachValidationErrors(read.issues)) {\n return outreachRefusal(400, 'invalid-override', 'This draft breaks a rule every sequence email keeps.', {\n issues: read.issues,\n })\n }\n override = { ...read.override, draftedAtMs: deps.now(), draftedByUid: caller.uid }\n }\n\n const nowMs = deps.now()\n const written = await firestore.runTransaction(async (transaction) => {\n const snapshot = await transaction.get(enrollments.doc(enrollmentId))\n const enrollment = readStoredOutreachEnrollment(enrollmentId, snapshot.exists ? snapshot.data() : undefined)\n if (!enrollment) return { refusal: outreachRefusal(404, 'enrollment-not-found', 'That enrollment no longer exists.') }\n if (!OUTREACH_OPEN_ENROLLMENT_STATUSES.includes(enrollment.status)) {\n return {\n refusal: outreachRefusal(409, 'transition-refused', 'This enrollment has ended, so nothing more is sent to them.'),\n }\n }\n // A step that already went out, or is going out now, is not rewritten\n // under the run: what was sent is what was sent.\n if (stepIndex < enrollment.stepIndex || (enrollment.sendClaim && enrollment.sendClaim.stepIndex === stepIndex)) {\n return { refusal: outreachRefusal(409, 'transition-refused', 'That step already went out to this person.') }\n }\n transaction.update(enrollments.doc(enrollmentId), {\n [`stepOverrides.${stepIndex}`]: override ?? FieldValue.delete(),\n updatedAtMs: nowMs,\n })\n const stepOverrides = { ...(enrollment.stepOverrides ?? {}) }\n if (override) stepOverrides[String(stepIndex)] = override\n else delete stepOverrides[String(stepIndex)]\n // Absent rather than empty once the last copy is cleared, as the\n // stored reader answers it.\n const next: OutreachEnrollment = { ...enrollment, updatedAtMs: nowMs }\n delete next.stepOverrides\n if (Object.keys(stepOverrides).length) next.stepOverrides = stepOverrides\n return { enrollment: next }\n })\n if ('refusal' in written) return written.refusal\n const { enrollment } = written\n\n if (override) {\n // The person's record says so (AGL-3274, AGL-3324): who wrote this\n // person's copy of the step, under the send it shapes.\n const entry = outreachCuratedEntry({\n enrollmentId,\n stepIndex,\n source: override.source,\n edited: override.edited === true,\n memberName: await memberName(firestore, caller),\n atMs: nowMs,\n })\n await fileOutreachNote(deps, {\n orgId: caller.orgId,\n hostId: enrollment.hostId,\n link: outreachEnrollmentLink(enrollment),\n dedupeKey: entry.dedupeKey,\n body: entry.body,\n atMs: nowMs,\n })\n }\n await deps.logOrgActivity(\n caller.orgId,\n { uid: caller.uid, email: caller.email },\n outreachCuratedActivity(stepIndex, cleared),\n { type: OUTREACH_SEQUENCE_ACTIVITY_TARGET, id: sequence.id, name: sequence.name },\n )\n return outreachOk({ ok: true, enrollment } satisfies OutreachCurateSaveResponse)\n }\n\n return { draft, save }\n}\n\n/** The text generator on the core's seam, for the default route deps; `null` when no plugin generates. */\nexport type OutreachTextGeneratorReader = () => PluginTextGenerator | null\n"],"names":["readContainerIds","readContactContainerIds","consentGroupForHost","readContactFacet","CRM_COLLECTIONS","normalizeCrmEmailTemplate","readOrgContainers","FieldValue","OUTREACH_CURATION_SYSTEM","outreachCurationPrompt","parseOutreachCurationAnswer","outreachCuratedEntry","OUTREACH_OPEN_ENROLLMENT_STATUSES","hasOutreachValidationErrors","isInThreadEmailStep","validateOutreachCuratedStep","readOutreachEnrollCandidates","readOutreachStepOverrideRequest","OUTREACH_MAX_EMAIL_STEPS","fileOutreachNote","outreachEnrollmentLink","outreachEnrollmentPerson","outreachOrgCollection","readStoredOutreachEnrollment","readStoredOutreachMailbox","readStoredOutreachSequence","OUTREACH_READS_PEOPLE","OUTREACH_SEQUENCE_ACTIVITY_TARGET","outreachRouteGate","outreachMethodNotAllowed","outreachOk","outreachRefusal","readOutreachDocumentId","readOutreachJsonBody","OUTREACH_CURATION_PURPOSE","CURATION_MAX_TOKENS","outreachCuratedActivity","stepIndex","cleared","emailStepIndexes","sequence","steps","map","step","index","kind","filter","readStepIndexes","raw","fallback","undefined","Array","isArray","length","wanted","Set","value","Number","emails","some","includes","sort","a","b","draftSubject","firestore","caller","body","enrollmentId","snapshot","orgId","doc","get","enrollment","exists","data","status","sequenceSnapshot","sequenceId","next","find","stepIndexes","Response","person","personalLine","contactId","leadId","id","text","trim","texts","Boolean","campaignNames","campaignIds","found","container","name","error","console","curationFacts","subject","candidate","contactGroupId","facet","contact","lead","company","target","mailboxSnapshot","host","campaigns","Promise","all","mailboxId","hostId","collection","mailbox","title","website","tags","sources","Object","entries","on","source","notes","record","siteName","senderName","displayName","curationSteps","templates","emailTemplates","Error","templateId","template","position","indexOf","count","startsThread","memberName","member","uid","email","createOutreachCurateRoutes","deps","draft","request","method","gate","generator","textGenerator","org","groupId","people","visible","facts","prompt","answer","generate","staff","purpose","system","maxTokens","now","Date","ok","reason","parsed","drafts","entry","candidateStep","issues","model","save","isInteger","enrollments","current","stored","override","read","refusal","draftedAtMs","draftedByUid","nowMs","written","runTransaction","transaction","sendClaim","update","delete","updatedAtMs","stepOverrides","String","keys","edited","atMs","link","dedupeKey","logOrgActivity","type"],"mappings":"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"}
@@ -14,12 +14,13 @@ 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 { CAMPAIGN_MEMBERSHIP_FIELD, contactCampaignFieldPath, normalizeCampaignIds } from "@aglyn/aglyn/app-utils/campaign-membership";
17
+ */ import { containerMembershipField, contactContainerFieldPath, normalizeContainerIds } from "@aglyn/aglyn/app-utils/container-membership";
18
18
  import { consentGroupForHost } from "@aglyn/aglyn/app-utils/consent-groups";
19
19
  import { scopeTokensForHost, seenOnlyThroughGrant } from "@aglyn/aglyn/app-utils/scope-tokens";
20
20
  import { CRM_COLLECTIONS, crmLeadStatus, crmViewIsListed, isCrmLeadOpen, normalizeCrmViewFilters } from "@aglyn/aglyn/app-utils/crm";
21
21
  import { dynamicListDimensionsForCrmView } from "@aglyn/aglyn/app-utils/dynamic-list-rule";
22
22
  import { findContactByEmail } from "@aglyn/tenant-data-admin/server/contact-email-index";
23
+ import { readOrgContainers } from "@aglyn/tenant-data-admin/server/org-containers";
23
24
  import { restampCrmListFieldsAt } from "@aglyn/tenant-data-admin/server/crm-records";
24
25
  import { FieldValue } from "firebase-admin/firestore";
25
26
  import { outreachCuratedEntry, outreachEnrolledEntry } from "../engine/enrollment-activity.js";
@@ -228,12 +229,12 @@ export function createOutreachEnrollRoutes(deps) {
228
229
  * enrollment, which already exists; a membership that could not be
229
230
  * stamped is one the list's bulk bar can add by hand.
230
231
  */ async function joinSequenceCampaigns(firestore, orgId, sequence, contactGroupId, candidate, nowMs) {
231
- const campaignIds = normalizeCampaignIds(sequence.campaignIds);
232
+ const campaignIds = normalizeContainerIds(sequence.campaignIds);
232
233
  if (!campaignIds.length) return;
233
234
  try {
234
235
  const ref = candidate.target === 'lead' && candidate.leadId ? firestore.collection('orgs').doc(orgId).collection('leads').doc(candidate.leadId) : candidate.contactId ? firestore.collection('orgs').doc(orgId).collection('contacts').doc(candidate.contactId) : null;
235
236
  if (ref) {
236
- const field = candidate.target === 'lead' ? CAMPAIGN_MEMBERSHIP_FIELD : contactCampaignFieldPath(contactGroupId);
237
+ const field = candidate.target === 'lead' ? containerMembershipField('campaign') : contactContainerFieldPath(contactGroupId, 'campaign');
237
238
  await ref.update({
238
239
  [field]: FieldValue.arrayUnion(...campaignIds),
239
240
  updatedAt: FieldValue.serverTimestamp()
@@ -259,19 +260,15 @@ export function createOutreachEnrollRoutes(deps) {
259
260
  }
260
261
  /**
261
262
  * The names of the sequence's campaigns as they stand, for the entry the
262
- * enroll files on the person's record (AGL-3274) — the org's containers,
263
- * `orgs/{orgId}/emailCampaigns`. Read once per request, not per person; a
264
- * container that is gone answers nothing and the entry names the rest.
263
+ * enroll files on the person's record (AGL-3274) — the org's containers of
264
+ * the `campaign` kind. Read once per request, not per person; a container
265
+ * that is gone answers nothing and the entry names the rest.
265
266
  */ async function sequenceCampaignNames(firestore, orgId, sequence) {
266
- const campaignIds = normalizeCampaignIds(sequence.campaignIds);
267
+ const campaignIds = normalizeContainerIds(sequence.campaignIds);
267
268
  if (!campaignIds.length || !orgId) return [];
268
269
  try {
269
- const containers = firestore.collection('orgs').doc(orgId).collection('emailCampaigns');
270
- const found = await firestore.getAll(...campaignIds.map((id)=>containers.doc(id)));
271
- return found.map((snapshot)=>{
272
- var _snapshot_get;
273
- return String((_snapshot_get = snapshot.get('name')) != null ? _snapshot_get : '').trim();
274
- }).filter(Boolean);
270
+ const found = await readOrgContainers(firestore, 'campaign', orgId, campaignIds);
271
+ return found.map((container)=>container.name).filter(Boolean);
275
272
  } catch (error) {
276
273
  console.error('[outreach] the sequence’s campaigns could not be named for the record', error);
277
274
  return [];