@aglyn/plugins-outreach 1.0.0-beta.194 → 1.0.0-beta.196
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +14 -14
- package/src/lib/components/curate-step-dialog.d.ts +50 -0
- package/src/lib/components/curate-step-dialog.js +235 -0
- package/src/lib/components/curate-step-dialog.js.map +1 -0
- package/src/lib/components/curated-step-editor.d.ts +80 -0
- package/src/lib/components/curated-step-editor.js +255 -0
- package/src/lib/components/curated-step-editor.js.map +1 -0
- package/src/lib/components/enroll-dialog.d.ts +15 -0
- package/src/lib/components/enroll-dialog.js +154 -13
- package/src/lib/components/enroll-dialog.js.map +1 -1
- package/src/lib/components/enrollments-table.d.ts +2 -0
- package/src/lib/components/enrollments-table.js +62 -8
- package/src/lib/components/enrollments-table.js.map +1 -1
- package/src/lib/components/use-outreach-api.d.ts +8 -1
- package/src/lib/components/use-outreach-api.js +12 -0
- package/src/lib/components/use-outreach-api.js.map +1 -1
- package/src/lib/constants/api-routes.d.ts +8 -0
- package/src/lib/constants/api-routes.js +6 -0
- package/src/lib/constants/api-routes.js.map +1 -1
- package/src/lib/engine/compose.d.ts +12 -2
- package/src/lib/engine/compose.js +32 -6
- package/src/lib/engine/compose.js.map +1 -1
- package/src/lib/engine/curation-prompt.d.ts +73 -0
- package/src/lib/engine/curation-prompt.js +151 -0
- package/src/lib/engine/curation-prompt.js.map +1 -0
- package/src/lib/engine/enrollment-activity.d.ts +24 -0
- package/src/lib/engine/enrollment-activity.js +25 -0
- package/src/lib/engine/enrollment-activity.js.map +1 -1
- package/src/lib/engine/sequence-validation.d.ts +10 -1
- package/src/lib/engine/sequence-validation.js +54 -0
- package/src/lib/engine/sequence-validation.js.map +1 -1
- package/src/lib/enrollment/step-overrides.d.ts +53 -0
- package/src/lib/enrollment/step-overrides.js +140 -0
- package/src/lib/enrollment/step-overrides.js.map +1 -0
- package/src/lib/model/outreach-api.d.ts +69 -2
- package/src/lib/model/outreach-api.js.map +1 -1
- package/src/lib/model/outreach.types.d.ts +36 -0
- package/src/lib/model/outreach.types.js +18 -0
- package/src/lib/model/outreach.types.js.map +1 -1
- package/src/lib/model/step-preview.d.ts +3 -1
- package/src/lib/model/step-preview.js +6 -3
- package/src/lib/model/step-preview.js.map +1 -1
- package/src/lib/model/stored-records.d.ts +9 -1
- package/src/lib/model/stored-records.js +47 -2
- package/src/lib/model/stored-records.js.map +1 -1
- package/src/lib/routes/curate-routes.d.ts +63 -0
- package/src/lib/routes/curate-routes.js +424 -0
- package/src/lib/routes/curate-routes.js.map +1 -0
- package/src/lib/routes/enroll-routes.d.ts +7 -0
- package/src/lib/routes/enroll-routes.js +58 -4
- package/src/lib/routes/enroll-routes.js.map +1 -1
- package/src/lib/routes/preview-routes.js +15 -1
- package/src/lib/routes/preview-routes.js.map +1 -1
- package/src/lib/routes/register-routes.js +17 -0
- package/src/lib/routes/register-routes.js.map +1 -1
- package/src/lib/runtime/send-job.js +2 -0
- package/src/lib/runtime/send-job.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/engine/sequence-validation.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*==========================================\n * WHAT A SEQUENCE MAY BE (AGL-2979).\n *\n * One validator for the three readers that need it: the editor, which shows\n * every issue beside the field it belongs to; the route that saves, which\n * refuses a sequence with an error; and activation, which also refuses while\n * the organization's footer cannot be written. Each issue names its field by\n * a dotted path (`steps.2.subject`) and says what to do in a sentence.\n *\n * ERRORS AND WARNINGS. An error is a sequence the engine cannot send or the\n * rules forbid: a fifth email, a first email with no subject, a `Re:` on a\n * message that answers nothing. A warning is a sequence that sends and\n * probably is not what its author meant — a merge field nobody fills, a\n * first email that never uses the personal line the rep writes at\n * enrollment.\n *==========================================*/\n\nimport { CRM_EMAIL_BODY_MAX, CRM_EMAIL_SUBJECT_MAX } from '@aglyn/aglyn/app-utils/crm'\nimport { readListUnsubscribeSetting } from '@aglyn/shared-util-email/list-unsubscribe'\nimport {\n CRM_MERGE_FIELDS,\n type CrmMergeFieldGroup,\n crmMergeFieldsIn,\n} from '@aglyn/aglyn/app-utils/crm-email-templates'\nimport { crmThreadSubject } from '@aglyn/aglyn/app-utils/crm-inbound'\nimport {\n OUTREACH_DEFAULT_ALLOWED_COUNTRIES,\n OUTREACH_MAX_EMAIL_STEPS,\n OUTREACH_MAX_STEP_DELAY_BUSINESS_DAYS,\n OUTREACH_MAX_STEPS,\n OUTREACH_MIN_EMAIL_FOLLOW_UP_BUSINESS_DAYS,\n OUTREACH_TASK_KINDS,\n type OutreachEmailStep,\n type OutreachOrgSettings,\n type OutreachSendWindow,\n type OutreachSequence,\n type OutreachSequenceSettings,\n type OutreachSequenceStep,\n} from '../model/outreach.types'\n\n/** The merge field the rep's signal sentence is written into a step with. */\nexport const OUTREACH_PERSONAL_LINE_FIELD = 'enrollment.personalLine'\n\n/** A merge field as the Outreach editor's picker offers it. */\nexport interface OutreachMergeFieldDefinition {\n key: string\n label: string\n group: CrmMergeFieldGroup | 'Enrollment'\n}\n\n/**\n * Every field an Outreach step may use: the CRM's, which the composer fills\n * through the CRM's own resolver, and the enrollment's.\n */\nexport const OUTREACH_MERGE_FIELDS: readonly OutreachMergeFieldDefinition[] = [\n ...CRM_MERGE_FIELDS,\n { key: OUTREACH_PERSONAL_LINE_FIELD, label: 'Personal line', group: 'Enrollment' },\n]\n\n/** The longest name a sequence carries. */\nexport const OUTREACH_SEQUENCE_NAME_MAX = 120\n\n/** The longest title a task step carries. */\nexport const OUTREACH_TASK_TITLE_MAX = 200\n\nexport type OutreachValidationSeverity = 'error' | 'warning'\n\nexport type OutreachValidationCode =\n | 'name_required'\n | 'name_too_long'\n | 'mailbox_required'\n | 'steps_required'\n | 'too_many_steps'\n | 'too_many_email_steps'\n | 'step_id_required'\n | 'duplicate_step_id'\n | 'unknown_step_kind'\n | 'delay_invalid'\n | 'subject_required'\n | 'subject_too_long'\n | 'subject_reply_prefix'\n | 'body_required'\n | 'body_and_template'\n | 'body_too_long'\n | 'unknown_merge_field'\n | 'personal_line_unused'\n | 'task_kind_invalid'\n | 'task_title_required'\n | 'task_title_too_long'\n | 'window_days_invalid'\n | 'window_hours_invalid'\n | 'countries_required'\n | 'country_invalid'\n | 'legal_name_required'\n | 'postal_address_required'\n\nexport interface OutreachValidationIssue {\n /** The field, dotted: `name`, `steps.2.subject`, `settings.window`, `orgSettings.postalAddress`. */\n path: string\n code: OutreachValidationCode\n message: string\n severity: OutreachValidationSeverity\n}\n\n/** Whether any issue in the list stops a save or an activation. */\nexport function hasOutreachValidationErrors(\n issues: readonly OutreachValidationIssue[],\n): boolean {\n return issues.some((issue) => issue.severity === 'error')\n}\n\nconst error = (\n path: string,\n code: OutreachValidationCode,\n message: string,\n): OutreachValidationIssue => ({ path, code, message, severity: 'error' })\n\nconst warning = (\n path: string,\n code: OutreachValidationCode,\n message: string,\n): OutreachValidationIssue => ({ path, code, message, severity: 'warning' })\n\nconst asText = (value: unknown): string => (typeof value === 'string' ? value : '')\n\nconst isEmailStep = (step: OutreachSequenceStep | null | undefined): step is OutreachEmailStep =>\n step?.kind === 'email'\n\n/** The index of the first email step, or `-1` for a sequence with none. */\nexport function firstEmailStepIndex(\n steps: readonly OutreachSequenceStep[] | null | undefined,\n): number {\n return (steps ?? []).findIndex(isEmailStep)\n}\n\n/**\n * Whether the email at `index` is sent as a reply in the thread an earlier\n * email started: every email after the first unless it says otherwise. The\n * first email starts the thread whatever its `replyInThread` says.\n */\nexport function isInThreadEmailStep(\n steps: readonly OutreachSequenceStep[] | null | undefined,\n index: number,\n): boolean {\n const step = steps?.[index]\n if (!isEmailStep(step)) return false\n const first = firstEmailStepIndex(steps)\n return index > first && step.replyInThread !== false\n}\n\n/**\n * Whether a subject opens with a reply or forward prefix — `Re:`, `Fwd:`,\n * `AW:` — which on an email that answers nothing tells the recipient a\n * conversation exists that does not. The prefixes are the CRM thread\n * reader's, so the two agree on what one looks like.\n */\nexport function outreachSubjectHasReplyPrefix(subject: unknown): boolean {\n const collapsed = asText(subject).replace(/\\s+/g, ' ').trim()\n return collapsed !== '' && crmThreadSubject(collapsed) !== collapsed.slice(0, CRM_EMAIL_SUBJECT_MAX)\n}\n\nconst WEEKDAY_NAMES = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']\n\n/** A sending window's shape: at least one weekday, and an opening before a close. */\nexport function validateOutreachSendWindow(\n window: OutreachSendWindow | null | undefined,\n path = 'window',\n): OutreachValidationIssue[] {\n const issues: OutreachValidationIssue[] = []\n const days = Array.isArray(window?.days) ? window.days : []\n const daysValid =\n days.length > 0 &&\n days.every((day) => Number.isInteger(day) && day >= 0 && day < WEEKDAY_NAMES.length) &&\n new Set(days).size === days.length\n if (!daysValid) {\n issues.push(\n error(`${path}.days`, 'window_days_invalid', 'Pick at least one day of the week to send on.'),\n )\n }\n const start = window?.startMinute\n const end = window?.endMinute\n const hoursValid =\n Number.isInteger(start) &&\n Number.isInteger(end) &&\n start >= 0 &&\n end <= 24 * 60 &&\n start < end\n if (!hoursValid) {\n issues.push(\n error(\n path,\n 'window_hours_invalid',\n 'The sending hours need a start time before the end time, within one day.',\n ),\n )\n }\n return issues\n}\n\n/**\n * The allowed countries, as stored or typed, reduced to unique uppercase\n * alpha-2 codes in the order given. Anything that is not two letters is\n * dropped, so a caller that needs to report it checks the raw value.\n */\nexport function normalizeOutreachAllowedCountries(value: unknown): string[] {\n if (!Array.isArray(value)) return []\n const codes: string[] = []\n for (const entry of value) {\n const code = asText(entry).trim().toUpperCase()\n if (/^[A-Z]{2}$/.test(code) && !codes.includes(code)) codes.push(code)\n }\n return codes\n}\n\n/**\n * A stored settings map with the defaults applied: the United States alone,\n * no customers, the mailbox's own window. A field a writer never set reads\n * as its default rather than as a permission nobody gave.\n */\nexport function readOutreachSequenceSettings(\n raw: Partial<OutreachSequenceSettings> | Record<string, unknown> | null | undefined,\n): OutreachSequenceSettings {\n const data = (raw ?? {}) as Record<string, unknown>\n const countries = normalizeOutreachAllowedCountries(data['allowedCountries'])\n const window = data['window'] as OutreachSendWindow | null | undefined\n return {\n window: window && typeof window === 'object' ? window : null,\n allowedCountries: Array.isArray(data['allowedCountries'])\n ? countries\n : [...OUTREACH_DEFAULT_ALLOWED_COUNTRIES],\n allowCustomers: data['allowCustomers'] === true,\n // Absent is off (AGL-3239). Every sequence written before the setting\n // existed reads as untracked, which is what it was.\n trackClicks: data['trackClicks'] === true,\n // Absent is off (AGL-3296): a sequence is one-to-one mail, and every\n // sequence written before the setting existed reads as headerless.\n // Read by the shared reader campaigns use too (AGL-3307), with the\n // sequence's own default.\n listUnsubscribe: readListUnsubscribeSetting(data['listUnsubscribe'], false),\n }\n}\n\nexport function validateOutreachSequenceSettings(\n settings: OutreachSequenceSettings | null | undefined,\n path = 'settings',\n): OutreachValidationIssue[] {\n const issues: OutreachValidationIssue[] = []\n if (settings?.window) {\n issues.push(...validateOutreachSendWindow(settings.window, `${path}.window`))\n }\n const raw = Array.isArray(settings?.allowedCountries) ? settings.allowedCountries : []\n if (!raw.length) {\n issues.push(\n error(\n `${path}.allowedCountries`,\n 'countries_required',\n 'Choose at least one country this sequence may send to.',\n ),\n )\n }\n raw.forEach((entry, index) => {\n if (!/^[A-Z]{2}$/.test(asText(entry))) {\n issues.push(\n error(\n `${path}.allowedCountries.${index}`,\n 'country_invalid',\n `\"${asText(entry)}\" isn't a two-letter country code, like US.`,\n ),\n )\n }\n })\n return issues\n}\n\nconst KNOWN_MERGE_FIELDS = new Set(OUTREACH_MERGE_FIELDS.map((field) => field.key))\n\nfunction mergeFieldWarnings(text: string, path: string): OutreachValidationIssue[] {\n return crmMergeFieldsIn(text)\n .filter((key) => !KNOWN_MERGE_FIELDS.has(key))\n .map((key) =>\n warning(\n path,\n 'unknown_merge_field',\n `{{${key}}} isn't a merge field, so it will be sent empty.`,\n ),\n )\n}\n\nfunction delayIssues(\n steps: readonly OutreachSequenceStep[],\n step: OutreachSequenceStep,\n index: number,\n path: string,\n): OutreachValidationIssue[] {\n const delay = step.delayBusinessDays\n const followsAnEmail = isEmailStep(step) && index > firstEmailStepIndex(steps)\n const min = followsAnEmail ? OUTREACH_MIN_EMAIL_FOLLOW_UP_BUSINESS_DAYS : 0\n if (Number.isInteger(delay) && delay >= min && delay <= OUTREACH_MAX_STEP_DELAY_BUSINESS_DAYS) {\n return []\n }\n const message =\n min > 0\n ? `Wait ${min}–${OUTREACH_MAX_STEP_DELAY_BUSINESS_DAYS} business days before this email.`\n : `Wait 0–${OUTREACH_MAX_STEP_DELAY_BUSINESS_DAYS} business days before this step.`\n return [error(`${path}.delayBusinessDays`, 'delay_invalid', message)]\n}\n\nfunction emailStepIssues(\n steps: readonly OutreachSequenceStep[],\n step: OutreachEmailStep,\n index: number,\n path: string,\n): OutreachValidationIssue[] {\n const issues: OutreachValidationIssue[] = []\n const first = firstEmailStepIndex(steps) === index\n const startsThread = !isInThreadEmailStep(steps, index)\n const subject = asText(step.subject)\n if (startsThread) {\n if (!subject.trim()) {\n issues.push(\n error(\n `${path}.subject`,\n 'subject_required',\n first\n ? 'The first email needs a subject.'\n : 'An email that starts a new thread needs a subject.',\n ),\n )\n } else if (outreachSubjectHasReplyPrefix(subject)) {\n issues.push(\n error(\n `${path}.subject`,\n 'subject_reply_prefix',\n 'Remove the \"Re:\" or \"Fwd:\" — this email starts a conversation, it doesn\\'t answer one.',\n ),\n )\n }\n if (subject.length > CRM_EMAIL_SUBJECT_MAX) {\n issues.push(\n error(\n `${path}.subject`,\n 'subject_too_long',\n `Keep the subject under ${CRM_EMAIL_SUBJECT_MAX} characters.`,\n ),\n )\n }\n issues.push(...mergeFieldWarnings(subject, `${path}.subject`))\n }\n const body = asText(step.body)\n const templateId = asText(step.templateId).trim()\n if (body.trim() && templateId) {\n issues.push(\n error(\n `${path}.body`,\n 'body_and_template',\n 'Write the email here or pick a template, not both.',\n ),\n )\n } else if (!body.trim() && !templateId) {\n issues.push(error(`${path}.body`, 'body_required', 'Write the email, or pick a template.'))\n }\n if (body.length > CRM_EMAIL_BODY_MAX) {\n issues.push(\n error(\n `${path}.body`,\n 'body_too_long',\n `Keep the email under ${CRM_EMAIL_BODY_MAX.toLocaleString('en-US')} characters.`,\n ),\n )\n }\n if (body.trim()) {\n issues.push(...mergeFieldWarnings(body, `${path}.body`))\n if (first && !crmMergeFieldsIn(body).includes(OUTREACH_PERSONAL_LINE_FIELD)) {\n issues.push(\n warning(\n `${path}.body`,\n 'personal_line_unused',\n `The first email doesn't use {{${OUTREACH_PERSONAL_LINE_FIELD}}}, so the line written for each person won't appear.`,\n ),\n )\n }\n }\n return issues\n}\n\n/** The steps of a sequence: their count, their ids, and each one's own fields. */\nexport function validateOutreachSteps(\n steps: readonly OutreachSequenceStep[] | null | undefined,\n path = 'steps',\n): OutreachValidationIssue[] {\n const list = Array.isArray(steps) ? steps : []\n const issues: OutreachValidationIssue[] = []\n if (!list.length) {\n issues.push(error(path, 'steps_required', 'Add at least one step.'))\n return issues\n }\n if (list.length > OUTREACH_MAX_STEPS) {\n issues.push(\n error(path, 'too_many_steps', `A sequence holds at most ${OUTREACH_MAX_STEPS} steps.`),\n )\n }\n const emailCount = list.filter(isEmailStep).length\n if (emailCount > OUTREACH_MAX_EMAIL_STEPS) {\n issues.push(\n error(\n path,\n 'too_many_email_steps',\n `A sequence sends at most ${OUTREACH_MAX_EMAIL_STEPS} emails to one person.`,\n ),\n )\n }\n const seen = new Set<string>()\n list.forEach((step, index) => {\n const stepPath = `${path}.${index}`\n const id = asText(step?.id).trim()\n if (!id) {\n issues.push(error(`${stepPath}.id`, 'step_id_required', 'Every step needs an id.'))\n } else if (seen.has(id)) {\n issues.push(error(`${stepPath}.id`, 'duplicate_step_id', 'Two steps share an id.'))\n }\n seen.add(id)\n if (isEmailStep(step)) {\n issues.push(...delayIssues(list, step, index, stepPath))\n issues.push(...emailStepIssues(list, step, index, stepPath))\n } else if (step?.kind === 'task') {\n issues.push(...delayIssues(list, step, index, stepPath))\n if (!(OUTREACH_TASK_KINDS as readonly string[]).includes(step.taskKind)) {\n issues.push(\n error(`${stepPath}.taskKind`, 'task_kind_invalid', 'Pick LinkedIn, Call or To-do.'),\n )\n }\n const title = asText(step.title)\n if (!title.trim()) {\n issues.push(error(`${stepPath}.title`, 'task_title_required', 'Give the task a title.'))\n } else if (title.length > OUTREACH_TASK_TITLE_MAX) {\n issues.push(\n error(\n `${stepPath}.title`,\n 'task_title_too_long',\n `Keep the title under ${OUTREACH_TASK_TITLE_MAX} characters.`,\n ),\n )\n }\n } else {\n issues.push(\n error(stepPath, 'unknown_step_kind', 'A step is an email or a task.'),\n )\n }\n })\n return issues\n}\n\n/** Everything a stored sequence has to be before it may be saved. */\nexport function validateOutreachSequence(\n sequence: Pick<OutreachSequence, 'name' | 'mailboxId' | 'steps' | 'settings'> | null | undefined,\n): OutreachValidationIssue[] {\n const issues: OutreachValidationIssue[] = []\n const name = asText(sequence?.name)\n if (!name.trim()) {\n issues.push(error('name', 'name_required', 'Name the sequence.'))\n } else if (name.length > OUTREACH_SEQUENCE_NAME_MAX) {\n issues.push(\n error('name', 'name_too_long', `Keep the name under ${OUTREACH_SEQUENCE_NAME_MAX} characters.`),\n )\n }\n const steps = sequence?.steps ?? []\n if (steps.some(isEmailStep) && !asText(sequence?.mailboxId).trim()) {\n issues.push(\n error('mailboxId', 'mailbox_required', 'Choose the mailbox this sequence sends from.'),\n )\n }\n issues.push(...validateOutreachSteps(steps))\n issues.push(...validateOutreachSequenceSettings(sequence?.settings))\n return issues\n}\n\n/**\n * The organization's footer fields: a legal name and a postal address, the\n * two things the law requires every commercial email to carry.\n */\nexport function validateOutreachOrgSettings(\n settings: Pick<OutreachOrgSettings, 'legalName' | 'postalAddress'> | null | undefined,\n path = 'orgSettings',\n): OutreachValidationIssue[] {\n const issues: OutreachValidationIssue[] = []\n if (!asText(settings?.legalName).trim()) {\n issues.push(\n error(\n `${path}.legalName`,\n 'legal_name_required',\n \"Add your organization's legal name in Sequences settings. It goes in every email's footer.\",\n ),\n )\n }\n if (!asText(settings?.postalAddress).trim()) {\n issues.push(\n error(\n `${path}.postalAddress`,\n 'postal_address_required',\n \"Add your organization's postal address in Sequences settings. The law requires one in every sales email.\",\n ),\n )\n }\n return issues\n}\n\n/**\n * What activating a sequence refuses on: every issue saving refuses on, and\n * an organization whose footer cannot be written yet.\n */\nexport function validateOutreachSequenceActivation(\n sequence: Pick<OutreachSequence, 'name' | 'mailboxId' | 'steps' | 'settings'> | null | undefined,\n orgSettings: Pick<OutreachOrgSettings, 'legalName' | 'postalAddress'> | null | undefined,\n): OutreachValidationIssue[] {\n return [...validateOutreachSequence(sequence), ...validateOutreachOrgSettings(orgSettings)]\n}\n"],"names":["CRM_EMAIL_BODY_MAX","CRM_EMAIL_SUBJECT_MAX","readListUnsubscribeSetting","CRM_MERGE_FIELDS","crmMergeFieldsIn","crmThreadSubject","OUTREACH_DEFAULT_ALLOWED_COUNTRIES","OUTREACH_MAX_EMAIL_STEPS","OUTREACH_MAX_STEP_DELAY_BUSINESS_DAYS","OUTREACH_MAX_STEPS","OUTREACH_MIN_EMAIL_FOLLOW_UP_BUSINESS_DAYS","OUTREACH_TASK_KINDS","OUTREACH_PERSONAL_LINE_FIELD","OUTREACH_MERGE_FIELDS","key","label","group","OUTREACH_SEQUENCE_NAME_MAX","OUTREACH_TASK_TITLE_MAX","hasOutreachValidationErrors","issues","some","issue","severity","error","path","code","message","warning","asText","value","isEmailStep","step","kind","firstEmailStepIndex","steps","findIndex","isInThreadEmailStep","index","first","replyInThread","outreachSubjectHasReplyPrefix","subject","collapsed","replace","trim","slice","WEEKDAY_NAMES","validateOutreachSendWindow","window","days","Array","isArray","daysValid","length","every","day","Number","isInteger","Set","size","push","start","startMinute","end","endMinute","hoursValid","normalizeOutreachAllowedCountries","codes","entry","toUpperCase","test","includes","readOutreachSequenceSettings","raw","data","countries","allowedCountries","allowCustomers","trackClicks","listUnsubscribe","validateOutreachSequenceSettings","settings","forEach","KNOWN_MERGE_FIELDS","map","field","mergeFieldWarnings","text","filter","has","delayIssues","delay","delayBusinessDays","followsAnEmail","min","emailStepIssues","startsThread","body","templateId","toLocaleString","validateOutreachSteps","list","emailCount","seen","stepPath","id","add","taskKind","title","validateOutreachSequence","sequence","name","mailboxId","validateOutreachOrgSettings","legalName","postalAddress","validateOutreachSequenceActivation","orgSettings"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/engine/sequence-validation.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*==========================================\n * WHAT A SEQUENCE MAY BE (AGL-2979).\n *\n * One validator for the three readers that need it: the editor, which shows\n * every issue beside the field it belongs to; the route that saves, which\n * refuses a sequence with an error; and activation, which also refuses while\n * the organization's footer cannot be written. Each issue names its field by\n * a dotted path (`steps.2.subject`) and says what to do in a sentence.\n *\n * ERRORS AND WARNINGS. An error is a sequence the engine cannot send or the\n * rules forbid: a fifth email, a first email with no subject, a `Re:` on a\n * message that answers nothing. A warning is a sequence that sends and\n * probably is not what its author meant — a merge field nobody fills, a\n * first email that never uses the personal line the rep writes at\n * enrollment.\n *==========================================*/\n\nimport { CRM_EMAIL_BODY_MAX, CRM_EMAIL_SUBJECT_MAX } from '@aglyn/aglyn/app-utils/crm'\nimport { readListUnsubscribeSetting } from '@aglyn/shared-util-email/list-unsubscribe'\nimport {\n CRM_MERGE_FIELDS,\n type CrmMergeFieldGroup,\n crmMergeFieldsIn,\n} from '@aglyn/aglyn/app-utils/crm-email-templates'\nimport { crmThreadSubject } from '@aglyn/aglyn/app-utils/crm-inbound'\nimport {\n OUTREACH_DEFAULT_ALLOWED_COUNTRIES,\n OUTREACH_MAX_EMAIL_STEPS,\n OUTREACH_MAX_STEP_DELAY_BUSINESS_DAYS,\n OUTREACH_MAX_STEPS,\n OUTREACH_MIN_EMAIL_FOLLOW_UP_BUSINESS_DAYS,\n OUTREACH_TASK_KINDS,\n type OutreachEmailStep,\n type OutreachOrgSettings,\n type OutreachSendWindow,\n type OutreachSequence,\n type OutreachSequenceSettings,\n type OutreachSequenceStep,\n} from '../model/outreach.types'\nimport { outreachBodyLinks } from './click-tracking'\n\n/** The merge field the rep's signal sentence is written into a step with. */\nexport const OUTREACH_PERSONAL_LINE_FIELD = 'enrollment.personalLine'\n\n/** A merge field as the Outreach editor's picker offers it. */\nexport interface OutreachMergeFieldDefinition {\n key: string\n label: string\n group: CrmMergeFieldGroup | 'Enrollment'\n}\n\n/**\n * Every field an Outreach step may use: the CRM's, which the composer fills\n * through the CRM's own resolver, and the enrollment's.\n */\nexport const OUTREACH_MERGE_FIELDS: readonly OutreachMergeFieldDefinition[] = [\n ...CRM_MERGE_FIELDS,\n { key: OUTREACH_PERSONAL_LINE_FIELD, label: 'Personal line', group: 'Enrollment' },\n]\n\n/** The longest name a sequence carries. */\nexport const OUTREACH_SEQUENCE_NAME_MAX = 120\n\n/** The longest title a task step carries. */\nexport const OUTREACH_TASK_TITLE_MAX = 200\n\nexport type OutreachValidationSeverity = 'error' | 'warning'\n\nexport type OutreachValidationCode =\n | 'name_required'\n | 'name_too_long'\n | 'mailbox_required'\n | 'steps_required'\n | 'too_many_steps'\n | 'too_many_email_steps'\n | 'step_id_required'\n | 'duplicate_step_id'\n | 'unknown_step_kind'\n | 'delay_invalid'\n | 'subject_required'\n | 'subject_too_long'\n | 'subject_reply_prefix'\n | 'body_required'\n | 'body_and_template'\n | 'body_too_long'\n | 'unknown_merge_field'\n | 'personal_line_unused'\n | 'task_kind_invalid'\n | 'task_title_required'\n | 'task_title_too_long'\n | 'window_days_invalid'\n | 'window_hours_invalid'\n | 'countries_required'\n | 'country_invalid'\n | 'legal_name_required'\n | 'postal_address_required'\n | 'too_many_links'\n | 'discount_language'\n | 'not_plain_text'\n\nexport interface OutreachValidationIssue {\n /** The field, dotted: `name`, `steps.2.subject`, `settings.window`, `orgSettings.postalAddress`. */\n path: string\n code: OutreachValidationCode\n message: string\n severity: OutreachValidationSeverity\n}\n\n/** Whether any issue in the list stops a save or an activation. */\nexport function hasOutreachValidationErrors(\n issues: readonly OutreachValidationIssue[],\n): boolean {\n return issues.some((issue) => issue.severity === 'error')\n}\n\nconst error = (\n path: string,\n code: OutreachValidationCode,\n message: string,\n): OutreachValidationIssue => ({ path, code, message, severity: 'error' })\n\nconst warning = (\n path: string,\n code: OutreachValidationCode,\n message: string,\n): OutreachValidationIssue => ({ path, code, message, severity: 'warning' })\n\nconst asText = (value: unknown): string => (typeof value === 'string' ? value : '')\n\nconst isEmailStep = (step: OutreachSequenceStep | null | undefined): step is OutreachEmailStep =>\n step?.kind === 'email'\n\n/** The index of the first email step, or `-1` for a sequence with none. */\nexport function firstEmailStepIndex(\n steps: readonly OutreachSequenceStep[] | null | undefined,\n): number {\n return (steps ?? []).findIndex(isEmailStep)\n}\n\n/**\n * Whether the email at `index` is sent as a reply in the thread an earlier\n * email started: every email after the first unless it says otherwise. The\n * first email starts the thread whatever its `replyInThread` says.\n */\nexport function isInThreadEmailStep(\n steps: readonly OutreachSequenceStep[] | null | undefined,\n index: number,\n): boolean {\n const step = steps?.[index]\n if (!isEmailStep(step)) return false\n const first = firstEmailStepIndex(steps)\n return index > first && step.replyInThread !== false\n}\n\n/**\n * Whether a subject opens with a reply or forward prefix — `Re:`, `Fwd:`,\n * `AW:` — which on an email that answers nothing tells the recipient a\n * conversation exists that does not. The prefixes are the CRM thread\n * reader's, so the two agree on what one looks like.\n */\nexport function outreachSubjectHasReplyPrefix(subject: unknown): boolean {\n const collapsed = asText(subject).replace(/\\s+/g, ' ').trim()\n return collapsed !== '' && crmThreadSubject(collapsed) !== collapsed.slice(0, CRM_EMAIL_SUBJECT_MAX)\n}\n\nconst WEEKDAY_NAMES = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']\n\n/** A sending window's shape: at least one weekday, and an opening before a close. */\nexport function validateOutreachSendWindow(\n window: OutreachSendWindow | null | undefined,\n path = 'window',\n): OutreachValidationIssue[] {\n const issues: OutreachValidationIssue[] = []\n const days = Array.isArray(window?.days) ? window.days : []\n const daysValid =\n days.length > 0 &&\n days.every((day) => Number.isInteger(day) && day >= 0 && day < WEEKDAY_NAMES.length) &&\n new Set(days).size === days.length\n if (!daysValid) {\n issues.push(\n error(`${path}.days`, 'window_days_invalid', 'Pick at least one day of the week to send on.'),\n )\n }\n const start = window?.startMinute\n const end = window?.endMinute\n const hoursValid =\n Number.isInteger(start) &&\n Number.isInteger(end) &&\n start >= 0 &&\n end <= 24 * 60 &&\n start < end\n if (!hoursValid) {\n issues.push(\n error(\n path,\n 'window_hours_invalid',\n 'The sending hours need a start time before the end time, within one day.',\n ),\n )\n }\n return issues\n}\n\n/**\n * The allowed countries, as stored or typed, reduced to unique uppercase\n * alpha-2 codes in the order given. Anything that is not two letters is\n * dropped, so a caller that needs to report it checks the raw value.\n */\nexport function normalizeOutreachAllowedCountries(value: unknown): string[] {\n if (!Array.isArray(value)) return []\n const codes: string[] = []\n for (const entry of value) {\n const code = asText(entry).trim().toUpperCase()\n if (/^[A-Z]{2}$/.test(code) && !codes.includes(code)) codes.push(code)\n }\n return codes\n}\n\n/**\n * A stored settings map with the defaults applied: the United States alone,\n * no customers, the mailbox's own window. A field a writer never set reads\n * as its default rather than as a permission nobody gave.\n */\nexport function readOutreachSequenceSettings(\n raw: Partial<OutreachSequenceSettings> | Record<string, unknown> | null | undefined,\n): OutreachSequenceSettings {\n const data = (raw ?? {}) as Record<string, unknown>\n const countries = normalizeOutreachAllowedCountries(data['allowedCountries'])\n const window = data['window'] as OutreachSendWindow | null | undefined\n return {\n window: window && typeof window === 'object' ? window : null,\n allowedCountries: Array.isArray(data['allowedCountries'])\n ? countries\n : [...OUTREACH_DEFAULT_ALLOWED_COUNTRIES],\n allowCustomers: data['allowCustomers'] === true,\n // Absent is off (AGL-3239). Every sequence written before the setting\n // existed reads as untracked, which is what it was.\n trackClicks: data['trackClicks'] === true,\n // Absent is off (AGL-3296): a sequence is one-to-one mail, and every\n // sequence written before the setting existed reads as headerless.\n // Read by the shared reader campaigns use too (AGL-3307), with the\n // sequence's own default.\n listUnsubscribe: readListUnsubscribeSetting(data['listUnsubscribe'], false),\n }\n}\n\nexport function validateOutreachSequenceSettings(\n settings: OutreachSequenceSettings | null | undefined,\n path = 'settings',\n): OutreachValidationIssue[] {\n const issues: OutreachValidationIssue[] = []\n if (settings?.window) {\n issues.push(...validateOutreachSendWindow(settings.window, `${path}.window`))\n }\n const raw = Array.isArray(settings?.allowedCountries) ? settings.allowedCountries : []\n if (!raw.length) {\n issues.push(\n error(\n `${path}.allowedCountries`,\n 'countries_required',\n 'Choose at least one country this sequence may send to.',\n ),\n )\n }\n raw.forEach((entry, index) => {\n if (!/^[A-Z]{2}$/.test(asText(entry))) {\n issues.push(\n error(\n `${path}.allowedCountries.${index}`,\n 'country_invalid',\n `\"${asText(entry)}\" isn't a two-letter country code, like US.`,\n ),\n )\n }\n })\n return issues\n}\n\nconst KNOWN_MERGE_FIELDS = new Set(OUTREACH_MERGE_FIELDS.map((field) => field.key))\n\nfunction mergeFieldWarnings(text: string, path: string): OutreachValidationIssue[] {\n return crmMergeFieldsIn(text)\n .filter((key) => !KNOWN_MERGE_FIELDS.has(key))\n .map((key) =>\n warning(\n path,\n 'unknown_merge_field',\n `{{${key}}} isn't a merge field, so it will be sent empty.`,\n ),\n )\n}\n\nfunction delayIssues(\n steps: readonly OutreachSequenceStep[],\n step: OutreachSequenceStep,\n index: number,\n path: string,\n): OutreachValidationIssue[] {\n const delay = step.delayBusinessDays\n const followsAnEmail = isEmailStep(step) && index > firstEmailStepIndex(steps)\n const min = followsAnEmail ? OUTREACH_MIN_EMAIL_FOLLOW_UP_BUSINESS_DAYS : 0\n if (Number.isInteger(delay) && delay >= min && delay <= OUTREACH_MAX_STEP_DELAY_BUSINESS_DAYS) {\n return []\n }\n const message =\n min > 0\n ? `Wait ${min}–${OUTREACH_MAX_STEP_DELAY_BUSINESS_DAYS} business days before this email.`\n : `Wait 0–${OUTREACH_MAX_STEP_DELAY_BUSINESS_DAYS} business days before this step.`\n return [error(`${path}.delayBusinessDays`, 'delay_invalid', message)]\n}\n\nfunction emailStepIssues(\n steps: readonly OutreachSequenceStep[],\n step: OutreachEmailStep,\n index: number,\n path: string,\n): OutreachValidationIssue[] {\n const issues: OutreachValidationIssue[] = []\n const first = firstEmailStepIndex(steps) === index\n const startsThread = !isInThreadEmailStep(steps, index)\n const subject = asText(step.subject)\n if (startsThread) {\n if (!subject.trim()) {\n issues.push(\n error(\n `${path}.subject`,\n 'subject_required',\n first\n ? 'The first email needs a subject.'\n : 'An email that starts a new thread needs a subject.',\n ),\n )\n } else if (outreachSubjectHasReplyPrefix(subject)) {\n issues.push(\n error(\n `${path}.subject`,\n 'subject_reply_prefix',\n 'Remove the \"Re:\" or \"Fwd:\" — this email starts a conversation, it doesn\\'t answer one.',\n ),\n )\n }\n if (subject.length > CRM_EMAIL_SUBJECT_MAX) {\n issues.push(\n error(\n `${path}.subject`,\n 'subject_too_long',\n `Keep the subject under ${CRM_EMAIL_SUBJECT_MAX} characters.`,\n ),\n )\n }\n issues.push(...mergeFieldWarnings(subject, `${path}.subject`))\n }\n const body = asText(step.body)\n const templateId = asText(step.templateId).trim()\n if (body.trim() && templateId) {\n issues.push(\n error(\n `${path}.body`,\n 'body_and_template',\n 'Write the email here or pick a template, not both.',\n ),\n )\n } else if (!body.trim() && !templateId) {\n issues.push(error(`${path}.body`, 'body_required', 'Write the email, or pick a template.'))\n }\n if (body.length > CRM_EMAIL_BODY_MAX) {\n issues.push(\n error(\n `${path}.body`,\n 'body_too_long',\n `Keep the email under ${CRM_EMAIL_BODY_MAX.toLocaleString('en-US')} characters.`,\n ),\n )\n }\n if (body.trim()) {\n issues.push(...mergeFieldWarnings(body, `${path}.body`))\n if (first && !crmMergeFieldsIn(body).includes(OUTREACH_PERSONAL_LINE_FIELD)) {\n issues.push(\n warning(\n `${path}.body`,\n 'personal_line_unused',\n `The first email doesn't use {{${OUTREACH_PERSONAL_LINE_FIELD}}}, so the line written for each person won't appear.`,\n ),\n )\n }\n }\n return issues\n}\n\n/** The steps of a sequence: their count, their ids, and each one's own fields. */\nexport function validateOutreachSteps(\n steps: readonly OutreachSequenceStep[] | null | undefined,\n path = 'steps',\n): OutreachValidationIssue[] {\n const list = Array.isArray(steps) ? steps : []\n const issues: OutreachValidationIssue[] = []\n if (!list.length) {\n issues.push(error(path, 'steps_required', 'Add at least one step.'))\n return issues\n }\n if (list.length > OUTREACH_MAX_STEPS) {\n issues.push(\n error(path, 'too_many_steps', `A sequence holds at most ${OUTREACH_MAX_STEPS} steps.`),\n )\n }\n const emailCount = list.filter(isEmailStep).length\n if (emailCount > OUTREACH_MAX_EMAIL_STEPS) {\n issues.push(\n error(\n path,\n 'too_many_email_steps',\n `A sequence sends at most ${OUTREACH_MAX_EMAIL_STEPS} emails to one person.`,\n ),\n )\n }\n const seen = new Set<string>()\n list.forEach((step, index) => {\n const stepPath = `${path}.${index}`\n const id = asText(step?.id).trim()\n if (!id) {\n issues.push(error(`${stepPath}.id`, 'step_id_required', 'Every step needs an id.'))\n } else if (seen.has(id)) {\n issues.push(error(`${stepPath}.id`, 'duplicate_step_id', 'Two steps share an id.'))\n }\n seen.add(id)\n if (isEmailStep(step)) {\n issues.push(...delayIssues(list, step, index, stepPath))\n issues.push(...emailStepIssues(list, step, index, stepPath))\n } else if (step?.kind === 'task') {\n issues.push(...delayIssues(list, step, index, stepPath))\n if (!(OUTREACH_TASK_KINDS as readonly string[]).includes(step.taskKind)) {\n issues.push(\n error(`${stepPath}.taskKind`, 'task_kind_invalid', 'Pick LinkedIn, Call or To-do.'),\n )\n }\n const title = asText(step.title)\n if (!title.trim()) {\n issues.push(error(`${stepPath}.title`, 'task_title_required', 'Give the task a title.'))\n } else if (title.length > OUTREACH_TASK_TITLE_MAX) {\n issues.push(\n error(\n `${stepPath}.title`,\n 'task_title_too_long',\n `Keep the title under ${OUTREACH_TASK_TITLE_MAX} characters.`,\n ),\n )\n }\n } else {\n issues.push(\n error(stepPath, 'unknown_step_kind', 'A step is an email or a task.'),\n )\n }\n })\n return issues\n}\n\n/** Everything a stored sequence has to be before it may be saved. */\nexport function validateOutreachSequence(\n sequence: Pick<OutreachSequence, 'name' | 'mailboxId' | 'steps' | 'settings'> | null | undefined,\n): OutreachValidationIssue[] {\n const issues: OutreachValidationIssue[] = []\n const name = asText(sequence?.name)\n if (!name.trim()) {\n issues.push(error('name', 'name_required', 'Name the sequence.'))\n } else if (name.length > OUTREACH_SEQUENCE_NAME_MAX) {\n issues.push(\n error('name', 'name_too_long', `Keep the name under ${OUTREACH_SEQUENCE_NAME_MAX} characters.`),\n )\n }\n const steps = sequence?.steps ?? []\n if (steps.some(isEmailStep) && !asText(sequence?.mailboxId).trim()) {\n issues.push(\n error('mailboxId', 'mailbox_required', 'Choose the mailbox this sequence sends from.'),\n )\n }\n issues.push(...validateOutreachSteps(steps))\n issues.push(...validateOutreachSequenceSettings(sequence?.settings))\n return issues\n}\n\n/*==========================================\n * A CURATED STEP (AGL-3324).\n *\n * One person's own copy of a step is held to everything a step is held to —\n * a subject where the email starts a thread, a body, the lengths, the merge\n * fields — and to the outbound playbook's rules the sequence editor trusts\n * its author with but an AI draft cannot be trusted with: one link at most,\n * list price only, plain text. Each is an ERROR: a draft that breaks one is\n * refused rather than stored, whoever wrote it.\n *==========================================*/\n\n/**\n * Words the playbook keeps out of every message: a discount, a coupon, a\n * cohort, a slot count, a deadline. Matched on word boundaries, so \"count\"\n * and \"expired domain\" pass and \"coupon\" and \"20% off\" do not.\n */\nconst DISCOUNT_LANGUAGE =\n /\\b(discounts?|discounted|coupons?|promo(?:tional)?\\s+codes?|promo\\b|vouchers?|\\d+\\s*%\\s*off|percent\\s+off|money\\s+off|special\\s+offer|limited[-\\s]time|expires?\\s+(?:on|at|in|soon|today|tomorrow)|deadline|(?:only|last|first)\\s+\\d+\\s+(?:spots?|slots?|seats?|places?)|cohorts?)\\b/i\n\n/** Markup a plain-text email would show as typed: bold stars, a markdown link, a heading line. */\nconst MARKDOWN = /\\*\\*[^*\\n]+\\*\\*|\\[[^\\]\\n]+\\]\\([^)\\s]+\\)|^#{1,6}\\s|<\\/?[a-z][^>]*>/im\n\nexport interface OutreachCuratedStepInput {\n /** The subject, when the copy carries one. Read only where the email starts a thread. */\n subject?: string | null\n body?: string | null\n /** Whether the email this copy is for starts a thread — `!isInThreadEmailStep`. */\n startsThread: boolean\n}\n\n/** Everything one person's copy of a step has to be before it may be stored — see the section note. */\nexport function validateOutreachCuratedStep(\n input: OutreachCuratedStepInput,\n path = 'override',\n): OutreachValidationIssue[] {\n const issues: OutreachValidationIssue[] = []\n const subject = asText(input.subject)\n if (input.startsThread && input.subject !== undefined && input.subject !== null) {\n if (!subject.trim()) {\n issues.push(error(`${path}.subject`, 'subject_required', 'This email starts a thread, so it needs a subject.'))\n } else if (outreachSubjectHasReplyPrefix(subject)) {\n issues.push(\n error(\n `${path}.subject`,\n 'subject_reply_prefix',\n 'Remove the \"Re:\" or \"Fwd:\" — this email starts a conversation, it doesn\\'t answer one.',\n ),\n )\n }\n if (subject.length > CRM_EMAIL_SUBJECT_MAX) {\n issues.push(\n error(`${path}.subject`, 'subject_too_long', `Keep the subject under ${CRM_EMAIL_SUBJECT_MAX} characters.`),\n )\n }\n if (DISCOUNT_LANGUAGE.test(subject)) {\n issues.push(\n error(`${path}.subject`, 'discount_language', 'The subject offers a discount or a deadline. List price only.'),\n )\n }\n issues.push(...mergeFieldWarnings(subject, `${path}.subject`))\n }\n const body = asText(input.body)\n if (!body.trim()) {\n issues.push(error(`${path}.body`, 'body_required', 'Write the email.'))\n return issues\n }\n if (body.length > CRM_EMAIL_BODY_MAX) {\n issues.push(\n error(\n `${path}.body`,\n 'body_too_long',\n `Keep the email under ${CRM_EMAIL_BODY_MAX.toLocaleString('en-US')} characters.`,\n ),\n )\n }\n const links = outreachBodyLinks(body)\n if (links.length > 1) {\n issues.push(\n error(\n `${path}.body`,\n 'too_many_links',\n `This email carries ${links.length} links. A sequence email carries one at most.`,\n ),\n )\n }\n if (DISCOUNT_LANGUAGE.test(body)) {\n issues.push(\n error(\n `${path}.body`,\n 'discount_language',\n 'This email offers a discount, a coupon, a cohort, a slot count or a deadline. List price only.',\n ),\n )\n }\n if (MARKDOWN.test(body)) {\n issues.push(\n error(\n `${path}.body`,\n 'not_plain_text',\n 'This email carries markup — bold stars, a markdown link or a heading — which a plain-text email shows as typed.',\n ),\n )\n }\n issues.push(...mergeFieldWarnings(body, `${path}.body`))\n return issues\n}\n\n/**\n * The organization's footer fields: a legal name and a postal address, the\n * two things the law requires every commercial email to carry.\n */\nexport function validateOutreachOrgSettings(\n settings: Pick<OutreachOrgSettings, 'legalName' | 'postalAddress'> | null | undefined,\n path = 'orgSettings',\n): OutreachValidationIssue[] {\n const issues: OutreachValidationIssue[] = []\n if (!asText(settings?.legalName).trim()) {\n issues.push(\n error(\n `${path}.legalName`,\n 'legal_name_required',\n \"Add your organization's legal name in Sequences settings. It goes in every email's footer.\",\n ),\n )\n }\n if (!asText(settings?.postalAddress).trim()) {\n issues.push(\n error(\n `${path}.postalAddress`,\n 'postal_address_required',\n \"Add your organization's postal address in Sequences settings. The law requires one in every sales email.\",\n ),\n )\n }\n return issues\n}\n\n/**\n * What activating a sequence refuses on: every issue saving refuses on, and\n * an organization whose footer cannot be written yet.\n */\nexport function validateOutreachSequenceActivation(\n sequence: Pick<OutreachSequence, 'name' | 'mailboxId' | 'steps' | 'settings'> | null | undefined,\n orgSettings: Pick<OutreachOrgSettings, 'legalName' | 'postalAddress'> | null | undefined,\n): OutreachValidationIssue[] {\n return [...validateOutreachSequence(sequence), ...validateOutreachOrgSettings(orgSettings)]\n}\n"],"names":["CRM_EMAIL_BODY_MAX","CRM_EMAIL_SUBJECT_MAX","readListUnsubscribeSetting","CRM_MERGE_FIELDS","crmMergeFieldsIn","crmThreadSubject","OUTREACH_DEFAULT_ALLOWED_COUNTRIES","OUTREACH_MAX_EMAIL_STEPS","OUTREACH_MAX_STEP_DELAY_BUSINESS_DAYS","OUTREACH_MAX_STEPS","OUTREACH_MIN_EMAIL_FOLLOW_UP_BUSINESS_DAYS","OUTREACH_TASK_KINDS","outreachBodyLinks","OUTREACH_PERSONAL_LINE_FIELD","OUTREACH_MERGE_FIELDS","key","label","group","OUTREACH_SEQUENCE_NAME_MAX","OUTREACH_TASK_TITLE_MAX","hasOutreachValidationErrors","issues","some","issue","severity","error","path","code","message","warning","asText","value","isEmailStep","step","kind","firstEmailStepIndex","steps","findIndex","isInThreadEmailStep","index","first","replyInThread","outreachSubjectHasReplyPrefix","subject","collapsed","replace","trim","slice","WEEKDAY_NAMES","validateOutreachSendWindow","window","days","Array","isArray","daysValid","length","every","day","Number","isInteger","Set","size","push","start","startMinute","end","endMinute","hoursValid","normalizeOutreachAllowedCountries","codes","entry","toUpperCase","test","includes","readOutreachSequenceSettings","raw","data","countries","allowedCountries","allowCustomers","trackClicks","listUnsubscribe","validateOutreachSequenceSettings","settings","forEach","KNOWN_MERGE_FIELDS","map","field","mergeFieldWarnings","text","filter","has","delayIssues","delay","delayBusinessDays","followsAnEmail","min","emailStepIssues","startsThread","body","templateId","toLocaleString","validateOutreachSteps","list","emailCount","seen","stepPath","id","add","taskKind","title","validateOutreachSequence","sequence","name","mailboxId","DISCOUNT_LANGUAGE","MARKDOWN","validateOutreachCuratedStep","input","undefined","links","validateOutreachOrgSettings","legalName","postalAddress","validateOutreachSequenceActivation","orgSettings"],"mappings":"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"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { type OutreachValidationIssue } from '../engine/sequence-validation';
|
|
18
|
+
import type { OutreachStepOverrideRequest } from '../model/outreach-api';
|
|
19
|
+
import { type OutreachSequenceStep, type OutreachStepOverride, type OutreachStepOverrides } from '../model/outreach.types';
|
|
20
|
+
/** The most of a prompt an override keeps for the audit log. */
|
|
21
|
+
export declare const OUTREACH_OVERRIDE_PROMPT_MAX = 12000;
|
|
22
|
+
export interface OutreachStepOverrideRead {
|
|
23
|
+
/** The overrides as they would be stored, keyed by step index, stamped by the caller. */
|
|
24
|
+
overrides: OutreachStepOverrides;
|
|
25
|
+
/** Every issue, each path prefixed `stepOverrides.<index>`. */
|
|
26
|
+
issues: OutreachValidationIssue[];
|
|
27
|
+
/** Why the request could not be read at all, in a sentence; `null` when it could. */
|
|
28
|
+
refusal: string | null;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* One override request as a stored override, before the caller's stamp.
|
|
32
|
+
* `null` when the request is not one.
|
|
33
|
+
*/
|
|
34
|
+
export declare function readOutreachStepOverrideRequest(raw: unknown, steps: readonly OutreachSequenceStep[]): {
|
|
35
|
+
stepIndex: number;
|
|
36
|
+
override: Omit<OutreachStepOverride, 'draftedAtMs' | 'draftedByUid'>;
|
|
37
|
+
issues: OutreachValidationIssue[];
|
|
38
|
+
} | {
|
|
39
|
+
refusal: string;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Every override a request carries, read and validated, and stamped with
|
|
43
|
+
* who confirmed them and when. An empty or absent list reads as no
|
|
44
|
+
* overrides and no issues.
|
|
45
|
+
*/
|
|
46
|
+
export declare function readOutreachStepOverrideRequests(raw: unknown, steps: readonly OutreachSequenceStep[], stamp: {
|
|
47
|
+
uid: string;
|
|
48
|
+
nowMs: number;
|
|
49
|
+
}): OutreachStepOverrideRead;
|
|
50
|
+
/** Whether a read carries an error that keeps its overrides from being stored. */
|
|
51
|
+
export declare function outreachStepOverridesRefused(read: OutreachStepOverrideRead): boolean;
|
|
52
|
+
/** The requests' shape, for a caller that builds one from stored overrides. */
|
|
53
|
+
export declare function outreachStepOverrideRequests(overrides: OutreachStepOverrides | null | undefined): OutreachStepOverrideRequest[];
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ /**
|
|
18
|
+
* WHAT A REQUEST MAY STORE AS ONE PERSON'S COPY OF A STEP (AGL-3324).
|
|
19
|
+
*
|
|
20
|
+
* The enroll route, the preview route and the curate-save route all take
|
|
21
|
+
* the console's `OutreachStepOverrideRequest`, and all three read it the
|
|
22
|
+
* same way: a step index that names an EMAIL step of the sequence, a body,
|
|
23
|
+
* a subject only where the email starts a thread, and the validator's
|
|
24
|
+
* verdict on each. A request that names a task step, a step the sequence
|
|
25
|
+
* does not have, or the same step twice is refused whole — nothing here is
|
|
26
|
+
* guessed, because what is read here is what gets sent.
|
|
27
|
+
*
|
|
28
|
+
* The prompt and the model ride through untouched, for the audit log; who
|
|
29
|
+
* confirmed and when are the route's to stamp, never the request's to
|
|
30
|
+
* claim.
|
|
31
|
+
*/ import { CRM_EMAIL_BODY_MAX } from "@aglyn/aglyn/app-utils/crm";
|
|
32
|
+
import { hasOutreachValidationErrors, isInThreadEmailStep, validateOutreachCuratedStep } from "../engine/sequence-validation.js";
|
|
33
|
+
import { OUTREACH_STEP_OVERRIDE_SOURCES } from "../model/outreach.types.js";
|
|
34
|
+
/** The most of a prompt an override keeps for the audit log. */ export const OUTREACH_OVERRIDE_PROMPT_MAX = 12000;
|
|
35
|
+
/**
|
|
36
|
+
* One override request as a stored override, before the caller's stamp.
|
|
37
|
+
* `null` when the request is not one.
|
|
38
|
+
*/ export function readOutreachStepOverrideRequest(raw, steps) {
|
|
39
|
+
if (!raw || typeof raw !== 'object') return {
|
|
40
|
+
refusal: 'A curated step is an object.'
|
|
41
|
+
};
|
|
42
|
+
const entry = raw;
|
|
43
|
+
const stepIndex = Number(entry['stepIndex']);
|
|
44
|
+
const step = Number.isInteger(stepIndex) && stepIndex >= 0 ? steps[stepIndex] : undefined;
|
|
45
|
+
if ((step == null ? void 0 : step.kind) !== 'email') {
|
|
46
|
+
return {
|
|
47
|
+
refusal: 'A curated step names one of the sequence’s email steps.'
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const startsThread = !isInThreadEmailStep(steps, stepIndex);
|
|
51
|
+
const body = typeof entry['body'] === 'string' ? entry['body'].replace(/\r\n?/g, '\n') : '';
|
|
52
|
+
const subject = startsThread && typeof entry['subject'] === 'string' ? entry['subject'] : undefined;
|
|
53
|
+
const source = entry['source'];
|
|
54
|
+
if (!OUTREACH_STEP_OVERRIDE_SOURCES.includes(source)) {
|
|
55
|
+
return {
|
|
56
|
+
refusal: 'A curated step says who wrote it: the AI, or the member.'
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const issues = validateOutreachCuratedStep({
|
|
60
|
+
subject,
|
|
61
|
+
body,
|
|
62
|
+
startsThread
|
|
63
|
+
}, `stepOverrides.${stepIndex}`);
|
|
64
|
+
const prompt = typeof entry['prompt'] === 'string' ? entry['prompt'].slice(0, OUTREACH_OVERRIDE_PROMPT_MAX) : '';
|
|
65
|
+
const model = typeof entry['model'] === 'string' ? entry['model'].trim().slice(0, 200) : '';
|
|
66
|
+
return {
|
|
67
|
+
stepIndex,
|
|
68
|
+
issues,
|
|
69
|
+
override: _extends({}, subject !== undefined ? {
|
|
70
|
+
subject: subject.replace(/\s+/g, ' ').trim()
|
|
71
|
+
} : {}, {
|
|
72
|
+
body: body.slice(0, CRM_EMAIL_BODY_MAX),
|
|
73
|
+
source: source
|
|
74
|
+
}, source === 'ai' && entry['edited'] === true ? {
|
|
75
|
+
edited: true
|
|
76
|
+
} : {}, source === 'ai' && prompt ? {
|
|
77
|
+
prompt
|
|
78
|
+
} : {}, source === 'ai' && model ? {
|
|
79
|
+
model
|
|
80
|
+
} : {})
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Every override a request carries, read and validated, and stamped with
|
|
85
|
+
* who confirmed them and when. An empty or absent list reads as no
|
|
86
|
+
* overrides and no issues.
|
|
87
|
+
*/ export function readOutreachStepOverrideRequests(raw, steps, stamp) {
|
|
88
|
+
const list = Array.isArray(raw) ? raw : [];
|
|
89
|
+
const overrides = {};
|
|
90
|
+
const issues = [];
|
|
91
|
+
for (const entry of list){
|
|
92
|
+
const read = readOutreachStepOverrideRequest(entry, steps);
|
|
93
|
+
if ('refusal' in read) return {
|
|
94
|
+
overrides: {},
|
|
95
|
+
issues: [],
|
|
96
|
+
refusal: read.refusal
|
|
97
|
+
};
|
|
98
|
+
if (overrides[String(read.stepIndex)]) {
|
|
99
|
+
return {
|
|
100
|
+
overrides: {},
|
|
101
|
+
issues: [],
|
|
102
|
+
refusal: 'A step is curated once per person.'
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
issues.push(...read.issues);
|
|
106
|
+
overrides[String(read.stepIndex)] = _extends({}, read.override, {
|
|
107
|
+
draftedAtMs: stamp.nowMs,
|
|
108
|
+
draftedByUid: stamp.uid
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
overrides,
|
|
113
|
+
issues,
|
|
114
|
+
refusal: null
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/** Whether a read carries an error that keeps its overrides from being stored. */ export function outreachStepOverridesRefused(read) {
|
|
118
|
+
return read.refusal !== null || hasOutreachValidationErrors(read.issues);
|
|
119
|
+
}
|
|
120
|
+
/** The requests' shape, for a caller that builds one from stored overrides. */ export function outreachStepOverrideRequests(overrides) {
|
|
121
|
+
return Object.entries(overrides != null ? overrides : {}).map(([key, override])=>{
|
|
122
|
+
var _override_body;
|
|
123
|
+
return _extends({
|
|
124
|
+
stepIndex: Number(key)
|
|
125
|
+
}, typeof override.subject === 'string' ? {
|
|
126
|
+
subject: override.subject
|
|
127
|
+
} : {}, {
|
|
128
|
+
body: (_override_body = override.body) != null ? _override_body : '',
|
|
129
|
+
source: override.source
|
|
130
|
+
}, override.edited ? {
|
|
131
|
+
edited: true
|
|
132
|
+
} : {}, override.prompt ? {
|
|
133
|
+
prompt: override.prompt
|
|
134
|
+
} : {}, override.model ? {
|
|
135
|
+
model: override.model
|
|
136
|
+
} : {});
|
|
137
|
+
}).sort((a, b)=>a.stepIndex - b.stepIndex);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
//# sourceMappingURL=step-overrides.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/enrollment/step-overrides.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * WHAT A REQUEST MAY STORE AS ONE PERSON'S COPY OF A STEP (AGL-3324).\n *\n * The enroll route, the preview route and the curate-save route all take\n * the console's `OutreachStepOverrideRequest`, and all three read it the\n * same way: a step index that names an EMAIL step of the sequence, a body,\n * a subject only where the email starts a thread, and the validator's\n * verdict on each. A request that names a task step, a step the sequence\n * does not have, or the same step twice is refused whole — nothing here is\n * guessed, because what is read here is what gets sent.\n *\n * The prompt and the model ride through untouched, for the audit log; who\n * confirmed and when are the route's to stamp, never the request's to\n * claim.\n */\n\nimport { CRM_EMAIL_BODY_MAX } from '@aglyn/aglyn/app-utils/crm'\nimport {\n hasOutreachValidationErrors,\n isInThreadEmailStep,\n validateOutreachCuratedStep,\n type OutreachValidationIssue,\n} from '../engine/sequence-validation'\nimport type { OutreachStepOverrideRequest } from '../model/outreach-api'\nimport {\n OUTREACH_STEP_OVERRIDE_SOURCES,\n type OutreachSequenceStep,\n type OutreachStepOverride,\n type OutreachStepOverrides,\n} from '../model/outreach.types'\n\n/** The most of a prompt an override keeps for the audit log. */\nexport const OUTREACH_OVERRIDE_PROMPT_MAX = 12_000\n\nexport interface OutreachStepOverrideRead {\n /** The overrides as they would be stored, keyed by step index, stamped by the caller. */\n overrides: OutreachStepOverrides\n /** Every issue, each path prefixed `stepOverrides.<index>`. */\n issues: OutreachValidationIssue[]\n /** Why the request could not be read at all, in a sentence; `null` when it could. */\n refusal: string | null\n}\n\n/**\n * One override request as a stored override, before the caller's stamp.\n * `null` when the request is not one.\n */\nexport function readOutreachStepOverrideRequest(\n raw: unknown,\n steps: readonly OutreachSequenceStep[],\n): { stepIndex: number; override: Omit<OutreachStepOverride, 'draftedAtMs' | 'draftedByUid'>; issues: OutreachValidationIssue[] } | { refusal: string } {\n if (!raw || typeof raw !== 'object') return { refusal: 'A curated step is an object.' }\n const entry = raw as Record<string, unknown>\n const stepIndex = Number(entry['stepIndex'])\n const step = Number.isInteger(stepIndex) && stepIndex >= 0 ? steps[stepIndex] : undefined\n if (step?.kind !== 'email') {\n return { refusal: 'A curated step names one of the sequence’s email steps.' }\n }\n const startsThread = !isInThreadEmailStep(steps, stepIndex)\n const body = typeof entry['body'] === 'string' ? entry['body'].replace(/\\r\\n?/g, '\\n') : ''\n const subject = startsThread && typeof entry['subject'] === 'string' ? entry['subject'] : undefined\n const source = entry['source']\n if (!(OUTREACH_STEP_OVERRIDE_SOURCES as readonly unknown[]).includes(source)) {\n return { refusal: 'A curated step says who wrote it: the AI, or the member.' }\n }\n const issues = validateOutreachCuratedStep({ subject, body, startsThread }, `stepOverrides.${stepIndex}`)\n const prompt = typeof entry['prompt'] === 'string' ? entry['prompt'].slice(0, OUTREACH_OVERRIDE_PROMPT_MAX) : ''\n const model = typeof entry['model'] === 'string' ? entry['model'].trim().slice(0, 200) : ''\n return {\n stepIndex,\n issues,\n override: {\n ...(subject !== undefined ? { subject: subject.replace(/\\s+/g, ' ').trim() } : {}),\n body: body.slice(0, CRM_EMAIL_BODY_MAX),\n source: source as OutreachStepOverride['source'],\n ...(source === 'ai' && entry['edited'] === true ? { edited: true } : {}),\n ...(source === 'ai' && prompt ? { prompt } : {}),\n ...(source === 'ai' && model ? { model } : {}),\n },\n }\n}\n\n/**\n * Every override a request carries, read and validated, and stamped with\n * who confirmed them and when. An empty or absent list reads as no\n * overrides and no issues.\n */\nexport function readOutreachStepOverrideRequests(\n raw: unknown,\n steps: readonly OutreachSequenceStep[],\n stamp: { uid: string; nowMs: number },\n): OutreachStepOverrideRead {\n const list = Array.isArray(raw) ? (raw as unknown[]) : []\n const overrides: OutreachStepOverrides = {}\n const issues: OutreachValidationIssue[] = []\n for (const entry of list) {\n const read = readOutreachStepOverrideRequest(entry, steps)\n if ('refusal' in read) return { overrides: {}, issues: [], refusal: read.refusal }\n if (overrides[String(read.stepIndex)]) {\n return { overrides: {}, issues: [], refusal: 'A step is curated once per person.' }\n }\n issues.push(...read.issues)\n overrides[String(read.stepIndex)] = {\n ...read.override,\n draftedAtMs: stamp.nowMs,\n draftedByUid: stamp.uid,\n }\n }\n return { overrides, issues, refusal: null }\n}\n\n/** Whether a read carries an error that keeps its overrides from being stored. */\nexport function outreachStepOverridesRefused(read: OutreachStepOverrideRead): boolean {\n return read.refusal !== null || hasOutreachValidationErrors(read.issues)\n}\n\n/** The requests' shape, for a caller that builds one from stored overrides. */\nexport function outreachStepOverrideRequests(\n overrides: OutreachStepOverrides | null | undefined,\n): OutreachStepOverrideRequest[] {\n return Object.entries(overrides ?? {})\n .map(([key, override]) => ({\n stepIndex: Number(key),\n ...(typeof override.subject === 'string' ? { subject: override.subject } : {}),\n body: override.body ?? '',\n source: override.source,\n ...(override.edited ? { edited: true } : {}),\n ...(override.prompt ? { prompt: override.prompt } : {}),\n ...(override.model ? { model: override.model } : {}),\n }))\n .sort((a, b) => a.stepIndex - b.stepIndex)\n}\n"],"names":["CRM_EMAIL_BODY_MAX","hasOutreachValidationErrors","isInThreadEmailStep","validateOutreachCuratedStep","OUTREACH_STEP_OVERRIDE_SOURCES","OUTREACH_OVERRIDE_PROMPT_MAX","readOutreachStepOverrideRequest","raw","steps","refusal","entry","stepIndex","Number","step","isInteger","undefined","kind","startsThread","body","replace","subject","source","includes","issues","prompt","slice","model","trim","override","edited","readOutreachStepOverrideRequests","stamp","list","Array","isArray","overrides","read","String","push","draftedAtMs","nowMs","draftedByUid","uid","outreachStepOverridesRefused","outreachStepOverrideRequests","Object","entries","map","key","sort","a","b"],"mappings":"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"}
|
|
@@ -24,11 +24,12 @@
|
|
|
24
24
|
* surface that names no site.
|
|
25
25
|
*/
|
|
26
26
|
import type { OutreachComposeError } from '../engine/compose';
|
|
27
|
+
import type { OutreachValidationIssue } from '../engine/sequence-validation';
|
|
27
28
|
import type { OutreachComplianceIssue } from './compliance-settings';
|
|
28
|
-
import type { OutreachAttestationKind, OutreachComplianceSettingsDocument, OutreachDoNotContactDomainEntry, OutreachEnrollment, OutreachSequence } from './outreach.types';
|
|
29
|
+
import type { OutreachAttestationKind, OutreachComplianceSettingsDocument, OutreachDoNotContactDomainEntry, OutreachEnrollment, OutreachSequence, OutreachStepOverrideSource } from './outreach.types';
|
|
29
30
|
import type { OutreachSequenceDraft, OutreachSequenceIssue } from './sequence-draft';
|
|
30
31
|
/** Every reason these routes refuse with, for a caller to branch on. */
|
|
31
|
-
export type OutreachRouteRefusalReason = 'unauthenticated' | 'email-unverified' | 'org-required' | 'not-a-member' | 'not-org-wide' | 'permission' | 'entitlement' | 'method-not-allowed' | 'invalid-request' | 'invalid-domain' | 'invalid-settings' | 'invalid-sequence' | 'sequence-not-found' | 'sequence-archived' | 'sequence-not-active' | 'activation-refused' | 'delete-refused' | 'mailbox-unavailable' | 'enrollment-not-found' | 'transition-refused' | 'view-not-found' | 'view-unsupported' | 'contact-not-found' | 'link-domain-refused';
|
|
32
|
+
export type OutreachRouteRefusalReason = 'unauthenticated' | 'email-unverified' | 'org-required' | 'not-a-member' | 'not-org-wide' | 'permission' | 'entitlement' | 'method-not-allowed' | 'invalid-request' | 'invalid-domain' | 'invalid-settings' | 'invalid-sequence' | 'sequence-not-found' | 'sequence-archived' | 'sequence-not-active' | 'activation-refused' | 'delete-refused' | 'mailbox-unavailable' | 'enrollment-not-found' | 'transition-refused' | 'view-not-found' | 'view-unsupported' | 'contact-not-found' | 'link-domain-refused' | 'curation-unavailable' | 'curation-refused' | 'invalid-override';
|
|
32
33
|
/** A refusal, in the one shape every route answers with. */
|
|
33
34
|
export interface OutreachRouteRefusal {
|
|
34
35
|
/** A sentence written for the person reading it. */
|
|
@@ -166,12 +167,32 @@ export interface OutreachEnrollPreviewResponse {
|
|
|
166
167
|
/** True when the source named more people than one enrollment reads. */
|
|
167
168
|
truncated: boolean;
|
|
168
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* One person's confirmed copy of one step, as the console sends it
|
|
172
|
+
* (AGL-3324): what the member read and ticked the confirmation for. The
|
|
173
|
+
* route validates it again and stamps who and when.
|
|
174
|
+
*/
|
|
175
|
+
export interface OutreachStepOverrideRequest {
|
|
176
|
+
stepIndex: number;
|
|
177
|
+
/** Read only where the email starts a thread. */
|
|
178
|
+
subject?: string;
|
|
179
|
+
body: string;
|
|
180
|
+
source: OutreachStepOverrideSource;
|
|
181
|
+
/** An AI draft the member changed before confirming it. */
|
|
182
|
+
edited?: boolean;
|
|
183
|
+
/** The prompt the AI was given, returned by the draft route, for the audit log. */
|
|
184
|
+
prompt?: string;
|
|
185
|
+
/** The model that drafted it, returned by the draft route. */
|
|
186
|
+
model?: string;
|
|
187
|
+
}
|
|
169
188
|
/** One person to enroll, with what the rep supplied for them: a contact by id, or a lead by key. */
|
|
170
189
|
export interface OutreachEnrollPersonRequest {
|
|
171
190
|
contactId?: string;
|
|
172
191
|
leadId?: string;
|
|
173
192
|
personalLine?: string;
|
|
174
193
|
attestations?: OutreachAttestationKind[];
|
|
194
|
+
/** The steps the member curated for this person before enrolling them (AGL-3324). */
|
|
195
|
+
stepOverrides?: OutreachStepOverrideRequest[];
|
|
175
196
|
}
|
|
176
197
|
/** `POST outreach/enroll` — the gates are checked again, here, before anything is written. */
|
|
177
198
|
export interface OutreachEnrollRequest {
|
|
@@ -295,6 +316,8 @@ export interface OutreachPreviewRequest {
|
|
|
295
316
|
personalLine?: string;
|
|
296
317
|
/** Which step; the first email when absent. */
|
|
297
318
|
stepIndex?: number;
|
|
319
|
+
/** The person's copies of steps as the dialog holds them (AGL-3324), so the preview shows the curated version. */
|
|
320
|
+
stepOverrides?: OutreachStepOverrideRequest[];
|
|
298
321
|
}
|
|
299
322
|
export interface OutreachPreviewResponse {
|
|
300
323
|
ok: true;
|
|
@@ -306,4 +329,48 @@ export interface OutreachPreviewResponse {
|
|
|
306
329
|
/** Why it could not be written, when it could not. */
|
|
307
330
|
error: OutreachComposeError | null;
|
|
308
331
|
}
|
|
332
|
+
/**
|
|
333
|
+
* `POST outreach/curate/draft` — this person's own copies of the sequence's
|
|
334
|
+
* email steps, drafted by the workspace's AI. Names EITHER a person about to
|
|
335
|
+
* be enrolled (the sequence, a contact or a lead, the personal line the rep
|
|
336
|
+
* is writing) OR an enrollment, whose next email step is drafted. Nothing is
|
|
337
|
+
* stored: the answer is what the member reads before confirming.
|
|
338
|
+
*/
|
|
339
|
+
export interface OutreachCurateDraftRequest {
|
|
340
|
+
orgId: string;
|
|
341
|
+
sequenceId?: string;
|
|
342
|
+
contactId?: string;
|
|
343
|
+
leadId?: string;
|
|
344
|
+
personalLine?: string;
|
|
345
|
+
enrollmentId?: string;
|
|
346
|
+
/** Which email steps; every email step for a person to enroll, the next one for an enrollment, when absent. */
|
|
347
|
+
stepIndexes?: number[];
|
|
348
|
+
}
|
|
349
|
+
/** One drafted step, with what the validator said about it. */
|
|
350
|
+
export interface OutreachCurateDraft {
|
|
351
|
+
stepIndex: number;
|
|
352
|
+
/** `null` for an in-thread email, which is sent as `Re:` the thread's subject. */
|
|
353
|
+
subject: string | null;
|
|
354
|
+
body: string;
|
|
355
|
+
/** Errors and warnings; a draft with an error cannot be confirmed as it stands. */
|
|
356
|
+
issues: OutreachValidationIssue[];
|
|
357
|
+
}
|
|
358
|
+
export interface OutreachCurateDraftResponse {
|
|
359
|
+
ok: true;
|
|
360
|
+
drafts: OutreachCurateDraft[];
|
|
361
|
+
/** The prompt the AI was given and the model that answered, for the override's audit record. */
|
|
362
|
+
prompt: string;
|
|
363
|
+
model: string;
|
|
364
|
+
}
|
|
365
|
+
/** `POST outreach/curate/save` — stores a confirmed copy of one step on an enrollment, or clears it with `override: null`. */
|
|
366
|
+
export interface OutreachCurateSaveRequest {
|
|
367
|
+
orgId: string;
|
|
368
|
+
enrollmentId: string;
|
|
369
|
+
stepIndex: number;
|
|
370
|
+
override: Omit<OutreachStepOverrideRequest, 'stepIndex'> | null;
|
|
371
|
+
}
|
|
372
|
+
export interface OutreachCurateSaveResponse {
|
|
373
|
+
ok: true;
|
|
374
|
+
enrollment: OutreachEnrollment;
|
|
375
|
+
}
|
|
309
376
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/model/outreach-api.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 * THE CONTRACT OF OUTREACH'S SETTINGS, SEQUENCE AND ENROLLMENT ROUTES\n * (AGL-2980): what the console sends, what the routes answer, and every\n * reason one refuses. Client-safe — types and the batch limit only.\n *\n * Every request names its organization (`orgId`, in the query of a `GET`\n * and the JSON body of a `POST`): the routes serve an organization-level\n * surface that names no site.\n */\n\nimport type { OutreachComposeError } from '../engine/compose'\nimport type { OutreachComplianceIssue } from './compliance-settings'\nimport type {\n OutreachAttestationKind,\n OutreachComplianceSettingsDocument,\n OutreachDoNotContactDomainEntry,\n OutreachEnrollment,\n OutreachSequence,\n} from './outreach.types'\nimport type { OutreachSequenceDraft, OutreachSequenceIssue } from './sequence-draft'\n\n/** Every reason these routes refuse with, for a caller to branch on. */\nexport type OutreachRouteRefusalReason =\n | 'unauthenticated'\n | 'email-unverified'\n | 'org-required'\n | 'not-a-member'\n | 'not-org-wide'\n | 'permission'\n | 'entitlement'\n | 'method-not-allowed'\n | 'invalid-request'\n | 'invalid-domain'\n | 'invalid-settings'\n | 'invalid-sequence'\n | 'sequence-not-found'\n | 'sequence-archived'\n | 'sequence-not-active'\n | 'activation-refused'\n | 'delete-refused'\n | 'mailbox-unavailable'\n | 'enrollment-not-found'\n | 'transition-refused'\n | 'view-not-found'\n | 'view-unsupported'\n | 'contact-not-found'\n | 'link-domain-refused'\n\n/** A refusal, in the one shape every route answers with. */\nexport interface OutreachRouteRefusal {\n /** A sentence written for the person reading it. */\n error: string\n reason: OutreachRouteRefusalReason\n /** The field-level reasons behind an `invalid-*` or `activation-refused` refusal. */\n issues?: ReadonlyArray<OutreachComplianceIssue | OutreachSequenceIssue>\n}\n\n/*==========================================\n * COMPLIANCE SETTINGS\n *==========================================*/\n\n/** `GET outreach/settings?orgId` — and the answer to a save. */\nexport interface OutreachSettingsResponse {\n ok: true\n settings: OutreachComplianceSettingsDocument\n}\n\n/** `POST outreach/settings` */\nexport interface OutreachSettingsSaveRequest {\n orgId: string\n legalName: string\n brandName: string\n postalAddress: string\n allowedCountries: string[]\n}\n\n/** The answer to a save: the settings as stored, and whether anything changed. */\nexport interface OutreachSettingsSaveResponse extends OutreachSettingsResponse {\n changed: boolean\n}\n\n/*==========================================\n * SEQUENCES\n *==========================================*/\n\n/** `POST outreach/sequences/save` — a new sequence without `sequenceId`, an edit with one. */\nexport interface OutreachSequenceSaveRequest {\n orgId: string\n sequenceId?: string\n sequence: OutreachSequenceDraft\n}\n\nexport interface OutreachSequenceSaveResponse {\n ok: true\n sequence: OutreachSequence\n created: boolean\n /** What the save allowed but probably isn't what was meant. */\n warnings: OutreachSequenceIssue[]\n}\n\n/** What `outreach/sequences/status` does to a sequence. */\nexport type OutreachSequenceAction = 'activate' | 'pause' | 'archive'\n\n/** `POST outreach/sequences/status` */\nexport interface OutreachSequenceStatusRequest {\n orgId: string\n sequenceId: string\n action: OutreachSequenceAction\n}\n\nexport interface OutreachSequenceStatusResponse {\n ok: true\n sequence: OutreachSequence\n /** The open enrollments an archive stopped; `0` for anything else. */\n stoppedEnrollments: number\n}\n\n/** `POST outreach/sequences/delete` — a draft nobody was ever enrolled in. */\nexport interface OutreachSequenceDeleteRequest {\n orgId: string\n sequenceId: string\n}\n\n/*==========================================\n * ENROLLING\n *==========================================*/\n\n/** The most people one preview or one enroll reads. */\nexport const OUTREACH_ENROLL_BATCH_MAX = 50\n\n/**\n * Which CRM record a person to enroll is (AGL-3234): a contact by its\n * document id, or a lead the sequence's site holds by its person key.\n */\nexport type OutreachPersonRef = { kind: 'contact'; id: string } | { kind: 'lead'; id: string }\n\n/**\n * Where the people to enroll come from: a saved Contacts or Leads view,\n * contacts picked by search, or leads picked from the sequence's site.\n */\nexport type OutreachEnrollSource =\n | { kind: 'view'; viewId: string }\n | { kind: 'contacts'; contactIds: string[] }\n | { kind: 'leads'; leadIds: string[] }\n\n/** `POST outreach/enroll/preview` */\nexport interface OutreachEnrollPreviewRequest {\n orgId: string\n sequenceId: string\n source: OutreachEnrollSource\n}\n\n/**\n * Where one person stands:\n *\n * - `eligible` — enrolled on Confirm as they are;\n * - `needs_confirmation` — enrolled once the rep writes the personal line\n * and ticks the attestations `requires` names;\n * - `blocked` — refused whatever the rep confirms, for the `blocks` given.\n */\nexport type OutreachEnrollPreviewStatus = 'eligible' | 'needs_confirmation' | 'blocked'\n\n/** One reason a person can't be enrolled, in a sentence a rep can act on. */\nexport interface OutreachEnrollBlock {\n code: string\n reason: string\n}\n\nexport interface OutreachEnrollPreviewPerson {\n /** The key the dialog and the confirm name this person by: the record's own id. */\n personId: string\n /** The record the person is (AGL-3234). */\n target: 'contact' | 'lead'\n /** The contact's id; `''` for a lead. */\n contactId: string\n /** The lead's person key; `null` for a contact. */\n leadId: string | null\n name: string\n /** The address the steps would go to, or `null` when the contact has none. */\n email: string | null\n /** No inbound capture behind them — the heavier rules apply. */\n cold: boolean\n /** ISO-3166-1 alpha-2, when anything names one. */\n country: string | null\n status: OutreachEnrollPreviewStatus\n /** Why a `blocked` person is refused; empty otherwise. */\n blocks: OutreachEnrollBlock[]\n /** What the rep must supply before a `needs_confirmation` person is enrolled. */\n requires: {\n personalLine: boolean\n attestations: OutreachAttestationKind[]\n }\n}\n\nexport interface OutreachEnrollPreviewResponse {\n ok: true\n people: OutreachEnrollPreviewPerson[]\n /** Everyone the source named, before the batch limit. */\n total: number\n /** True when the source named more people than one enrollment reads. */\n truncated: boolean\n}\n\n/** One person to enroll, with what the rep supplied for them: a contact by id, or a lead by key. */\nexport interface OutreachEnrollPersonRequest {\n contactId?: string\n leadId?: string\n personalLine?: string\n attestations?: OutreachAttestationKind[]\n}\n\n/** `POST outreach/enroll` — the gates are checked again, here, before anything is written. */\nexport interface OutreachEnrollRequest {\n orgId: string\n sequenceId: string\n people: OutreachEnrollPersonRequest[]\n}\n\ninterface OutreachEnrollOutcomePerson {\n personId: string\n target: 'contact' | 'lead'\n contactId: string\n leadId: string | null\n email: string | null\n}\n\nexport type OutreachEnrollOutcome =\n | (OutreachEnrollOutcomePerson & { outcome: 'enrolled'; enrollmentId: string })\n | (OutreachEnrollOutcomePerson & { outcome: 'blocked'; blocks: OutreachEnrollBlock[] })\n\nexport interface OutreachEnrollResponse {\n ok: true\n results: OutreachEnrollOutcome[]\n enrolled: number\n}\n\n/*==========================================\n * ENROLLMENTS\n *==========================================*/\n\n/** What a member may do to one enrollment. */\nexport type OutreachEnrollmentAction = 'pause' | 'resume' | 'stop' | 'do_not_contact'\n\n/** `POST outreach/enrollments/action` */\nexport interface OutreachEnrollmentActionRequest {\n orgId: string\n enrollmentId: string\n action: OutreachEnrollmentAction\n /** Why, for a stop or a do-not-contact. */\n detail?: string\n}\n\nexport interface OutreachEnrollmentActionResponse {\n ok: true\n /** False when the enrollment was already where the action would put it. */\n changed: boolean\n enrollment: OutreachEnrollment\n /** Other open enrollments of the same address a do-not-contact stopped. */\n stoppedOthers: number\n}\n\n/*==========================================\n * DO NOT CONTACT: DOMAINS (AGL-3244)\n *==========================================*/\n\n/** `GET outreach/do-not-contact/domains?orgId` — and the answer to a change. */\nexport interface OutreachDoNotContactDomainsResponse {\n ok: true\n /** Every domain on the list, alphabetically. */\n domains: OutreachDoNotContactDomainEntry[]\n}\n\n/** `POST outreach/do-not-contact/domains` */\nexport interface OutreachDoNotContactDomainRequest {\n orgId: string\n action: 'add' | 'remove'\n /** The domain, or an address at it. */\n domain: string\n /** Why, for an add. */\n detail?: string\n}\n\n/** The answer to a change: the list as it stands, and whether anything changed. */\nexport interface OutreachDoNotContactDomainResponse extends OutreachDoNotContactDomainsResponse {\n changed: boolean\n /** The domain as the list spells it. */\n domain: string\n}\n\n/*==========================================\n * LINK DOMAINS (AGL-3306)\n *==========================================*/\n\n/** Where a mailbox domain's click-tracking host stands. */\nexport type OutreachLinkDomainStatus = 'not-set-up' | 'requested' | 'records-issued' | 'verified' | 'failed'\n\n/** One DNS record the member publishes, as the sending-domain cards print them. */\nexport interface OutreachLinkDomainRecord {\n type: string\n name: string\n value: string\n required: boolean\n note: string\n}\n\n/**\n * One sending domain of the organization's connected mailboxes, and its\n * `links.` host: what tracked links in mail from it read now, and what is\n * left to do for them to read `https://links.<domain>/<id>`.\n */\nexport interface OutreachLinkDomain {\n /** The mailboxes' sending domain. */\n domain: string\n /** `links.<domain>`. */\n host: string\n status: OutreachLinkDomainStatus\n /** The records to publish; empty until the host is set up. */\n records: OutreachLinkDomainRecord[]\n /** Why it is not verified, in a sentence; `null` when nothing is wrong. */\n detail: string | null\n /** How a tracked link in mail from this domain starts today. */\n linkPrefix: string | null\n checkedAtMs: number | null\n verifiedAtMs: number | null\n}\n\n/** `GET outreach/link-domains?orgId` */\nexport interface OutreachLinkDomainsResponse {\n ok: true\n domains: OutreachLinkDomain[]\n /** Whether the member may set a host up, check it, or remove it. */\n canManage: boolean\n}\n\n/** `POST outreach/link-domains` */\nexport interface OutreachLinkDomainRequest {\n orgId: string\n domain: string\n action: 'set-up' | 'check' | 'remove'\n}\n\nexport interface OutreachLinkDomainResponse extends OutreachLinkDomainsResponse {\n /** The domain the action was for. */\n domain: string\n}\n\n/*==========================================\n * PREVIEW\n *==========================================*/\n\n/** `POST outreach/preview` — one email of a saved sequence, as a person would get it. */\nexport interface OutreachPreviewRequest {\n orgId: string\n sequenceId: string\n /** The contact to write it to; a sample person when absent. */\n contactId?: string\n /** Or a lead the sequence's site holds (AGL-3234), written to as the contact it would be. */\n leadId?: string\n personalLine?: string\n /** Which step; the first email when absent. */\n stepIndex?: number\n}\n\nexport interface OutreachPreviewResponse {\n ok: true\n stepIndex: number\n subject: string\n /** The plain text, footer included. */\n text: string\n unresolvedFields: string[]\n /** Why it could not be written, when it could not. */\n error: OutreachComposeError | null\n}\n"],"names":["OUTREACH_ENROLL_BATCH_MAX"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;CAQC,GAmHD;;4CAE4C,GAE5C,qDAAqD,GACrD,OAAO,MAAMA,4BAA4B,GAAE"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/model/outreach-api.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 * THE CONTRACT OF OUTREACH'S SETTINGS, SEQUENCE AND ENROLLMENT ROUTES\n * (AGL-2980): what the console sends, what the routes answer, and every\n * reason one refuses. Client-safe — types and the batch limit only.\n *\n * Every request names its organization (`orgId`, in the query of a `GET`\n * and the JSON body of a `POST`): the routes serve an organization-level\n * surface that names no site.\n */\n\nimport type { OutreachComposeError } from '../engine/compose'\nimport type { OutreachValidationIssue } from '../engine/sequence-validation'\nimport type { OutreachComplianceIssue } from './compliance-settings'\nimport type {\n OutreachAttestationKind,\n OutreachComplianceSettingsDocument,\n OutreachDoNotContactDomainEntry,\n OutreachEnrollment,\n OutreachSequence,\n OutreachStepOverrideSource,\n} from './outreach.types'\nimport type { OutreachSequenceDraft, OutreachSequenceIssue } from './sequence-draft'\n\n/** Every reason these routes refuse with, for a caller to branch on. */\nexport type OutreachRouteRefusalReason =\n | 'unauthenticated'\n | 'email-unverified'\n | 'org-required'\n | 'not-a-member'\n | 'not-org-wide'\n | 'permission'\n | 'entitlement'\n | 'method-not-allowed'\n | 'invalid-request'\n | 'invalid-domain'\n | 'invalid-settings'\n | 'invalid-sequence'\n | 'sequence-not-found'\n | 'sequence-archived'\n | 'sequence-not-active'\n | 'activation-refused'\n | 'delete-refused'\n | 'mailbox-unavailable'\n | 'enrollment-not-found'\n | 'transition-refused'\n | 'view-not-found'\n | 'view-unsupported'\n | 'contact-not-found'\n | 'link-domain-refused'\n | 'curation-unavailable'\n | 'curation-refused'\n | 'invalid-override'\n\n/** A refusal, in the one shape every route answers with. */\nexport interface OutreachRouteRefusal {\n /** A sentence written for the person reading it. */\n error: string\n reason: OutreachRouteRefusalReason\n /** The field-level reasons behind an `invalid-*` or `activation-refused` refusal. */\n issues?: ReadonlyArray<OutreachComplianceIssue | OutreachSequenceIssue>\n}\n\n/*==========================================\n * COMPLIANCE SETTINGS\n *==========================================*/\n\n/** `GET outreach/settings?orgId` — and the answer to a save. */\nexport interface OutreachSettingsResponse {\n ok: true\n settings: OutreachComplianceSettingsDocument\n}\n\n/** `POST outreach/settings` */\nexport interface OutreachSettingsSaveRequest {\n orgId: string\n legalName: string\n brandName: string\n postalAddress: string\n allowedCountries: string[]\n}\n\n/** The answer to a save: the settings as stored, and whether anything changed. */\nexport interface OutreachSettingsSaveResponse extends OutreachSettingsResponse {\n changed: boolean\n}\n\n/*==========================================\n * SEQUENCES\n *==========================================*/\n\n/** `POST outreach/sequences/save` — a new sequence without `sequenceId`, an edit with one. */\nexport interface OutreachSequenceSaveRequest {\n orgId: string\n sequenceId?: string\n sequence: OutreachSequenceDraft\n}\n\nexport interface OutreachSequenceSaveResponse {\n ok: true\n sequence: OutreachSequence\n created: boolean\n /** What the save allowed but probably isn't what was meant. */\n warnings: OutreachSequenceIssue[]\n}\n\n/** What `outreach/sequences/status` does to a sequence. */\nexport type OutreachSequenceAction = 'activate' | 'pause' | 'archive'\n\n/** `POST outreach/sequences/status` */\nexport interface OutreachSequenceStatusRequest {\n orgId: string\n sequenceId: string\n action: OutreachSequenceAction\n}\n\nexport interface OutreachSequenceStatusResponse {\n ok: true\n sequence: OutreachSequence\n /** The open enrollments an archive stopped; `0` for anything else. */\n stoppedEnrollments: number\n}\n\n/** `POST outreach/sequences/delete` — a draft nobody was ever enrolled in. */\nexport interface OutreachSequenceDeleteRequest {\n orgId: string\n sequenceId: string\n}\n\n/*==========================================\n * ENROLLING\n *==========================================*/\n\n/** The most people one preview or one enroll reads. */\nexport const OUTREACH_ENROLL_BATCH_MAX = 50\n\n/**\n * Which CRM record a person to enroll is (AGL-3234): a contact by its\n * document id, or a lead the sequence's site holds by its person key.\n */\nexport type OutreachPersonRef = { kind: 'contact'; id: string } | { kind: 'lead'; id: string }\n\n/**\n * Where the people to enroll come from: a saved Contacts or Leads view,\n * contacts picked by search, or leads picked from the sequence's site.\n */\nexport type OutreachEnrollSource =\n | { kind: 'view'; viewId: string }\n | { kind: 'contacts'; contactIds: string[] }\n | { kind: 'leads'; leadIds: string[] }\n\n/** `POST outreach/enroll/preview` */\nexport interface OutreachEnrollPreviewRequest {\n orgId: string\n sequenceId: string\n source: OutreachEnrollSource\n}\n\n/**\n * Where one person stands:\n *\n * - `eligible` — enrolled on Confirm as they are;\n * - `needs_confirmation` — enrolled once the rep writes the personal line\n * and ticks the attestations `requires` names;\n * - `blocked` — refused whatever the rep confirms, for the `blocks` given.\n */\nexport type OutreachEnrollPreviewStatus = 'eligible' | 'needs_confirmation' | 'blocked'\n\n/** One reason a person can't be enrolled, in a sentence a rep can act on. */\nexport interface OutreachEnrollBlock {\n code: string\n reason: string\n}\n\nexport interface OutreachEnrollPreviewPerson {\n /** The key the dialog and the confirm name this person by: the record's own id. */\n personId: string\n /** The record the person is (AGL-3234). */\n target: 'contact' | 'lead'\n /** The contact's id; `''` for a lead. */\n contactId: string\n /** The lead's person key; `null` for a contact. */\n leadId: string | null\n name: string\n /** The address the steps would go to, or `null` when the contact has none. */\n email: string | null\n /** No inbound capture behind them — the heavier rules apply. */\n cold: boolean\n /** ISO-3166-1 alpha-2, when anything names one. */\n country: string | null\n status: OutreachEnrollPreviewStatus\n /** Why a `blocked` person is refused; empty otherwise. */\n blocks: OutreachEnrollBlock[]\n /** What the rep must supply before a `needs_confirmation` person is enrolled. */\n requires: {\n personalLine: boolean\n attestations: OutreachAttestationKind[]\n }\n}\n\nexport interface OutreachEnrollPreviewResponse {\n ok: true\n people: OutreachEnrollPreviewPerson[]\n /** Everyone the source named, before the batch limit. */\n total: number\n /** True when the source named more people than one enrollment reads. */\n truncated: boolean\n}\n\n/**\n * One person's confirmed copy of one step, as the console sends it\n * (AGL-3324): what the member read and ticked the confirmation for. The\n * route validates it again and stamps who and when.\n */\nexport interface OutreachStepOverrideRequest {\n stepIndex: number\n /** Read only where the email starts a thread. */\n subject?: string\n body: string\n source: OutreachStepOverrideSource\n /** An AI draft the member changed before confirming it. */\n edited?: boolean\n /** The prompt the AI was given, returned by the draft route, for the audit log. */\n prompt?: string\n /** The model that drafted it, returned by the draft route. */\n model?: string\n}\n\n/** One person to enroll, with what the rep supplied for them: a contact by id, or a lead by key. */\nexport interface OutreachEnrollPersonRequest {\n contactId?: string\n leadId?: string\n personalLine?: string\n attestations?: OutreachAttestationKind[]\n /** The steps the member curated for this person before enrolling them (AGL-3324). */\n stepOverrides?: OutreachStepOverrideRequest[]\n}\n\n/** `POST outreach/enroll` — the gates are checked again, here, before anything is written. */\nexport interface OutreachEnrollRequest {\n orgId: string\n sequenceId: string\n people: OutreachEnrollPersonRequest[]\n}\n\ninterface OutreachEnrollOutcomePerson {\n personId: string\n target: 'contact' | 'lead'\n contactId: string\n leadId: string | null\n email: string | null\n}\n\nexport type OutreachEnrollOutcome =\n | (OutreachEnrollOutcomePerson & { outcome: 'enrolled'; enrollmentId: string })\n | (OutreachEnrollOutcomePerson & { outcome: 'blocked'; blocks: OutreachEnrollBlock[] })\n\nexport interface OutreachEnrollResponse {\n ok: true\n results: OutreachEnrollOutcome[]\n enrolled: number\n}\n\n/*==========================================\n * ENROLLMENTS\n *==========================================*/\n\n/** What a member may do to one enrollment. */\nexport type OutreachEnrollmentAction = 'pause' | 'resume' | 'stop' | 'do_not_contact'\n\n/** `POST outreach/enrollments/action` */\nexport interface OutreachEnrollmentActionRequest {\n orgId: string\n enrollmentId: string\n action: OutreachEnrollmentAction\n /** Why, for a stop or a do-not-contact. */\n detail?: string\n}\n\nexport interface OutreachEnrollmentActionResponse {\n ok: true\n /** False when the enrollment was already where the action would put it. */\n changed: boolean\n enrollment: OutreachEnrollment\n /** Other open enrollments of the same address a do-not-contact stopped. */\n stoppedOthers: number\n}\n\n/*==========================================\n * DO NOT CONTACT: DOMAINS (AGL-3244)\n *==========================================*/\n\n/** `GET outreach/do-not-contact/domains?orgId` — and the answer to a change. */\nexport interface OutreachDoNotContactDomainsResponse {\n ok: true\n /** Every domain on the list, alphabetically. */\n domains: OutreachDoNotContactDomainEntry[]\n}\n\n/** `POST outreach/do-not-contact/domains` */\nexport interface OutreachDoNotContactDomainRequest {\n orgId: string\n action: 'add' | 'remove'\n /** The domain, or an address at it. */\n domain: string\n /** Why, for an add. */\n detail?: string\n}\n\n/** The answer to a change: the list as it stands, and whether anything changed. */\nexport interface OutreachDoNotContactDomainResponse extends OutreachDoNotContactDomainsResponse {\n changed: boolean\n /** The domain as the list spells it. */\n domain: string\n}\n\n/*==========================================\n * LINK DOMAINS (AGL-3306)\n *==========================================*/\n\n/** Where a mailbox domain's click-tracking host stands. */\nexport type OutreachLinkDomainStatus = 'not-set-up' | 'requested' | 'records-issued' | 'verified' | 'failed'\n\n/** One DNS record the member publishes, as the sending-domain cards print them. */\nexport interface OutreachLinkDomainRecord {\n type: string\n name: string\n value: string\n required: boolean\n note: string\n}\n\n/**\n * One sending domain of the organization's connected mailboxes, and its\n * `links.` host: what tracked links in mail from it read now, and what is\n * left to do for them to read `https://links.<domain>/<id>`.\n */\nexport interface OutreachLinkDomain {\n /** The mailboxes' sending domain. */\n domain: string\n /** `links.<domain>`. */\n host: string\n status: OutreachLinkDomainStatus\n /** The records to publish; empty until the host is set up. */\n records: OutreachLinkDomainRecord[]\n /** Why it is not verified, in a sentence; `null` when nothing is wrong. */\n detail: string | null\n /** How a tracked link in mail from this domain starts today. */\n linkPrefix: string | null\n checkedAtMs: number | null\n verifiedAtMs: number | null\n}\n\n/** `GET outreach/link-domains?orgId` */\nexport interface OutreachLinkDomainsResponse {\n ok: true\n domains: OutreachLinkDomain[]\n /** Whether the member may set a host up, check it, or remove it. */\n canManage: boolean\n}\n\n/** `POST outreach/link-domains` */\nexport interface OutreachLinkDomainRequest {\n orgId: string\n domain: string\n action: 'set-up' | 'check' | 'remove'\n}\n\nexport interface OutreachLinkDomainResponse extends OutreachLinkDomainsResponse {\n /** The domain the action was for. */\n domain: string\n}\n\n/*==========================================\n * PREVIEW\n *==========================================*/\n\n/** `POST outreach/preview` — one email of a saved sequence, as a person would get it. */\nexport interface OutreachPreviewRequest {\n orgId: string\n sequenceId: string\n /** The contact to write it to; a sample person when absent. */\n contactId?: string\n /** Or a lead the sequence's site holds (AGL-3234), written to as the contact it would be. */\n leadId?: string\n personalLine?: string\n /** Which step; the first email when absent. */\n stepIndex?: number\n /** The person's copies of steps as the dialog holds them (AGL-3324), so the preview shows the curated version. */\n stepOverrides?: OutreachStepOverrideRequest[]\n}\n\nexport interface OutreachPreviewResponse {\n ok: true\n stepIndex: number\n subject: string\n /** The plain text, footer included. */\n text: string\n unresolvedFields: string[]\n /** Why it could not be written, when it could not. */\n error: OutreachComposeError | null\n}\n\n/*==========================================\n * CURATING (AGL-3324)\n *==========================================*/\n\n/**\n * `POST outreach/curate/draft` — this person's own copies of the sequence's\n * email steps, drafted by the workspace's AI. Names EITHER a person about to\n * be enrolled (the sequence, a contact or a lead, the personal line the rep\n * is writing) OR an enrollment, whose next email step is drafted. Nothing is\n * stored: the answer is what the member reads before confirming.\n */\nexport interface OutreachCurateDraftRequest {\n orgId: string\n sequenceId?: string\n contactId?: string\n leadId?: string\n personalLine?: string\n enrollmentId?: string\n /** Which email steps; every email step for a person to enroll, the next one for an enrollment, when absent. */\n stepIndexes?: number[]\n}\n\n/** One drafted step, with what the validator said about it. */\nexport interface OutreachCurateDraft {\n stepIndex: number\n /** `null` for an in-thread email, which is sent as `Re:` the thread's subject. */\n subject: string | null\n body: string\n /** Errors and warnings; a draft with an error cannot be confirmed as it stands. */\n issues: OutreachValidationIssue[]\n}\n\nexport interface OutreachCurateDraftResponse {\n ok: true\n drafts: OutreachCurateDraft[]\n /** The prompt the AI was given and the model that answered, for the override's audit record. */\n prompt: string\n model: string\n}\n\n/** `POST outreach/curate/save` — stores a confirmed copy of one step on an enrollment, or clears it with `override: null`. */\nexport interface OutreachCurateSaveRequest {\n orgId: string\n enrollmentId: string\n stepIndex: number\n override: Omit<OutreachStepOverrideRequest, 'stepIndex'> | null\n}\n\nexport interface OutreachCurateSaveResponse {\n ok: true\n enrollment: OutreachEnrollment\n}\n"],"names":["OUTREACH_ENROLL_BATCH_MAX"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;CAQC,GAwHD;;4CAE4C,GAE5C,qDAAqD,GACrD,OAAO,MAAMA,4BAA4B,GAAE"}
|
|
@@ -512,6 +512,31 @@ export interface OutreachAttestation {
|
|
|
512
512
|
export type OutreachAttestations = Partial<Record<OutreachAttestationKind, OutreachAttestation>>;
|
|
513
513
|
/** The longest personal line an enrollment keeps: one sentence, not a letter. */
|
|
514
514
|
export declare const OUTREACH_PERSONAL_LINE_MAX = 300;
|
|
515
|
+
/** Who wrote the override: the AI, or the member by hand. */
|
|
516
|
+
export declare const OUTREACH_STEP_OVERRIDE_SOURCES: readonly ["ai", "member"];
|
|
517
|
+
export type OutreachStepOverrideSource = (typeof OUTREACH_STEP_OVERRIDE_SOURCES)[number];
|
|
518
|
+
/** One person's own copy of one step. */
|
|
519
|
+
export interface OutreachStepOverride {
|
|
520
|
+
/** The subject, merge fields allowed; absent keeps the step's own. Not read on an in-thread email. */
|
|
521
|
+
subject?: string;
|
|
522
|
+
/** The plain-text body, merge fields allowed; absent keeps the step's own (or its template's). */
|
|
523
|
+
body?: string;
|
|
524
|
+
source: OutreachStepOverrideSource;
|
|
525
|
+
/** When the member confirmed it — the moment it was stored, never before. */
|
|
526
|
+
draftedAtMs: number;
|
|
527
|
+
/** The member who confirmed it. */
|
|
528
|
+
draftedByUid?: string;
|
|
529
|
+
/** An AI draft the member changed before confirming it. */
|
|
530
|
+
edited?: boolean;
|
|
531
|
+
/** The prompt the AI was given, for the audit log; absent on a member's own words. */
|
|
532
|
+
prompt?: string;
|
|
533
|
+
/** The model that drafted it, for the audit log. */
|
|
534
|
+
model?: string;
|
|
535
|
+
}
|
|
536
|
+
/** The overrides an enrollment carries, keyed by the step's index as text. */
|
|
537
|
+
export type OutreachStepOverrides = Record<string, OutreachStepOverride>;
|
|
538
|
+
/** The sentence a member ticks before a curated draft is stored — a fourth attestation. */
|
|
539
|
+
export declare const OUTREACH_CURATION_CONFIRMATION_LABEL = "I've read this draft, and it goes out to this person as written";
|
|
515
540
|
/**
|
|
516
541
|
* Which CRM record an enrollment names (AGL-3234). A person is a LEAD until
|
|
517
542
|
* somebody qualifies them and a CONTACT after, and a sequence works either:
|
|
@@ -612,6 +637,12 @@ export interface OutreachEnrollment extends OutreachTimestamps {
|
|
|
612
637
|
* in it, and one it leaves keeps what it was credited with.
|
|
613
638
|
*/
|
|
614
639
|
campaignIds?: string[];
|
|
640
|
+
/**
|
|
641
|
+
* This person's own copies of steps (AGL-3324), by step index. Absent on
|
|
642
|
+
* an enrollment nobody curated, which sends the sequence's steps as
|
|
643
|
+
* written; a step without an entry sends the same.
|
|
644
|
+
*/
|
|
645
|
+
stepOverrides?: OutreachStepOverrides;
|
|
615
646
|
}
|
|
616
647
|
/** A sending run's claim on one enrollment's step (AGL-2981). */
|
|
617
648
|
export interface OutreachSendClaim {
|
|
@@ -652,6 +683,11 @@ export interface OutreachStepRecord {
|
|
|
652
683
|
* read afterwards for what it actually offered them.
|
|
653
684
|
*/
|
|
654
685
|
links?: string[];
|
|
686
|
+
/**
|
|
687
|
+
* The email went out as this person's curated copy (AGL-3324), and who
|
|
688
|
+
* wrote it. Absent on a step sent as the sequence wrote it.
|
|
689
|
+
*/
|
|
690
|
+
curated?: OutreachStepOverrideSource;
|
|
655
691
|
}
|
|
656
692
|
/**
|
|
657
693
|
* What every Outreach email's footer says about who sent it — the
|