@aglyn/plugins-outreach 1.0.0-beta.217 → 1.0.0-beta.219
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/compliance-section.js +72 -34
- package/src/lib/components/compliance-section.js.map +1 -1
- package/src/lib/components/enroll-dialog.d.ts +12 -2
- package/src/lib/components/enroll-dialog.js +72 -18
- package/src/lib/components/enroll-dialog.js.map +1 -1
- package/src/lib/components/enrollment-detail.d.ts +5 -1
- package/src/lib/components/enrollment-detail.js +12 -6
- package/src/lib/components/enrollment-detail.js.map +1 -1
- package/src/lib/components/outreach-console-sections.d.ts +1 -1
- package/src/lib/components/outreach-console-sections.js +1 -1
- package/src/lib/components/outreach-console-sections.js.map +1 -1
- package/src/lib/components/sequence-editor.js +85 -40
- package/src/lib/components/sequence-editor.js.map +1 -1
- package/src/lib/components/use-outreach-api.d.ts +2 -1
- package/src/lib/components/use-outreach-api.js +5 -3
- package/src/lib/components/use-outreach-api.js.map +1 -1
- package/src/lib/engine/compose.d.ts +1 -1
- package/src/lib/engine/compose.js +8 -4
- package/src/lib/engine/compose.js.map +1 -1
- package/src/lib/engine/enrollment-activity.d.ts +9 -1
- package/src/lib/engine/enrollment-activity.js +15 -2
- package/src/lib/engine/enrollment-activity.js.map +1 -1
- package/src/lib/engine/enrollment-state.d.ts +33 -2
- package/src/lib/engine/enrollment-state.js +66 -7
- package/src/lib/engine/enrollment-state.js.map +1 -1
- package/src/lib/engine/sending-capacity.d.ts +1 -1
- package/src/lib/engine/sending-capacity.js +1 -1
- package/src/lib/engine/sending-capacity.js.map +1 -1
- package/src/lib/engine/sequence-validation.d.ts +20 -3
- package/src/lib/engine/sequence-validation.js +30 -4
- package/src/lib/engine/sequence-validation.js.map +1 -1
- package/src/lib/enrollment/enroll-people.js +6 -4
- package/src/lib/enrollment/enroll-people.js.map +1 -1
- package/src/lib/enrollment/step-overrides.d.ts +4 -2
- package/src/lib/enrollment/step-overrides.js +11 -4
- package/src/lib/enrollment/step-overrides.js.map +1 -1
- package/src/lib/mailboxes/mailbox-api.d.ts +5 -1
- package/src/lib/mailboxes/mailbox-api.js.map +1 -1
- package/src/lib/mailboxes/mailbox-routes.d.ts +5 -2
- package/src/lib/mailboxes/mailbox-routes.js +10 -7
- package/src/lib/mailboxes/mailbox-routes.js.map +1 -1
- package/src/lib/mailboxes/mailbox-settings.d.ts +2 -2
- package/src/lib/mailboxes/mailbox-settings.js +1 -1
- package/src/lib/mailboxes/mailbox-settings.js.map +1 -1
- package/src/lib/model/enrollment-timeline.d.ts +1 -1
- package/src/lib/model/enrollment-timeline.js +22 -3
- package/src/lib/model/enrollment-timeline.js.map +1 -1
- package/src/lib/model/outreach-api.d.ts +19 -1
- package/src/lib/model/outreach-api.js.map +1 -1
- package/src/lib/model/outreach.types.d.ts +25 -0
- package/src/lib/model/outreach.types.js.map +1 -1
- package/src/lib/model/step-preview.d.ts +2 -0
- package/src/lib/model/step-preview.js +11 -7
- package/src/lib/model/step-preview.js.map +1 -1
- package/src/lib/model/stored-records.js +26 -0
- package/src/lib/model/stored-records.js.map +1 -1
- package/src/lib/plugin.js +1 -1
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/routes/enroll-routes.d.ts +6 -0
- package/src/lib/routes/enroll-routes.js +21 -6
- package/src/lib/routes/enroll-routes.js.map +1 -1
- package/src/lib/routes/preview-routes.js +14 -5
- package/src/lib/routes/preview-routes.js.map +1 -1
- package/src/lib/routes/step-test-routes.js +3 -3
- package/src/lib/routes/step-test-routes.js.map +1 -1
- package/src/lib/testing/stand-in-crm-record-routes.d.ts +25 -0
- package/src/lib/testing/stand-in-crm-record-routes.js +46 -0
- package/src/lib/testing/stand-in-crm-record-routes.js.map +1 -0
- package/src/lib/transport/gmail-errors.d.ts +27 -0
- package/src/lib/transport/gmail-errors.js +56 -0
- package/src/lib/transport/gmail-errors.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/model/enrollment-timeline.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 * EVERYTHING THAT HAPPENED TO ONE PERSON IN ONE SEQUENCE (AGL-3332).\n *\n * The enrollment's detail view is one list, newest first, and this builds\n * it from what is stored — the enrollment itself, and its history rows —\n * without inventing anything that is not:\n *\n * - **Each send** is a step record: the step, the time, the subject as\n * sent, and whether it went out as the person's curated copy.\n * - **Each click** is a history row: the destination, the step whose email\n * carried it, and a person's or a scanner's. Clicks from before the\n * history began exist only as totals, and are ONE row saying so — never\n * a row per click, which would be a fabrication with a timestamp on it.\n * - **Each member's act** — pause, resume, stop, do-not-contact — is a\n * history row with who and why. An enrollment holds only its current\n * stop, so the row is the only record of a pause that was resumed.\n * - **The current stop** — a reply, a bounce, an opt-out, a hold — comes\n * from the enrollment's own fields, with its full reason, unless a\n * history row already says it.\n * - **The gateway hold** (AGL-3326), and its release.\n * - **The enrollment**, with who enrolled them, and each curated step.\n *\n * Client-safe and free of React: the view renders these entries, and the\n * spec reads them.\n *=========================================*/\n\nimport type { OutreachClickMachineReason } from '../engine/click-tracking'\nimport type { OutreachOpenMachineReason } from '../engine/open-tracking'\nimport { OUTREACH_MAIL_GATEWAY_LABELS, type OutreachMailGateway } from '../engine/mail-gateway'\nimport {\n outreachClickSummary,\n outreachLoggedEngagementRows,\n readOutreachEngagement,\n} from './enrollment-engagement'\nimport {\n OUTREACH_ENROLLMENT_HISTORY_MAX,\n OUTREACH_TASK_KIND_LABELS,\n type OutreachEnrollment,\n type OutreachEnrollmentHistoryEntry,\n type OutreachHistoryAction,\n type OutreachSequenceStep,\n type OutreachStepRecord,\n} from './outreach.types'\n\n/** What kind of thing an entry is, which the view draws an icon for. */\nexport type OutreachTimelineKind =\n | 'enrolled'\n | 'curated'\n | 'sent'\n | 'task'\n | 'click'\n | 'scanner'\n | 'earlier-clicks'\n | 'open'\n | 'machine-open'\n | 'paused'\n | 'resumed'\n | 'stopped'\n | 'do-not-contact'\n | 'replied'\n | 'bounced'\n | 'opted-out'\n | 'held'\n | 'released'\n | 'failed'\n | 'finished'\n\n/** How loudly the view draws an entry. */\nexport type OutreachTimelineTone = 'default' | 'success' | 'info' | 'warning' | 'error'\n\n/** One line of the timeline. */\nexport interface OutreachTimelineEntry {\n /** Stable across renders, for React and for a spec. */\n key: string\n kind: OutreachTimelineKind\n atMs: number\n /** What happened, in a few words. */\n title: string\n /** The one fact that matters most, shown in full: a subject, a reason, a diagnostic. */\n detail?: string | null\n /** The rest, each short: the step, who, a scanner's reason. */\n facts: string[]\n /** A destination the person followed, shown whole and linkable. */\n url?: string | null\n /** What `url` is, when it is not simply \"the link they followed\". */\n urlLabel?: string\n /** The Gmail thread this entry opened, for an \"Open in Gmail\" link. */\n gmailThreadId?: string | null\n tone: OutreachTimelineTone\n}\n\nexport interface OutreachTimelineInput {\n enrollment: OutreachEnrollment\n steps: readonly OutreachSequenceStep[]\n /** The history rows, newest or oldest first; `null` while unread or unreadable. */\n history: readonly OutreachEnrollmentHistoryEntry[] | null\n /** A member's name for a stored uid; the uid itself when the roster does not know it. */\n memberName: (uid: string) => string\n /** A moment as the reader reads it — in the mailbox's zone. */\n formatTime: (ms: number) => string\n /** The mail gateway in front of the person's domain, when it is known. */\n gateway?: OutreachMailGateway | null\n}\n\n/** \"Email 2 · step 3\" for an email step, \"Step 2 · Call\" for a task, \"Step 4\" for one the sequence no longer has. */\nexport function outreachStepName(steps: readonly OutreachSequenceStep[], stepIndex: number): string {\n const step = steps[stepIndex]\n if (step?.kind === 'email') {\n const position = steps.slice(0, stepIndex + 1).filter((entry) => entry.kind === 'email').length\n return `Email ${position} · step ${stepIndex + 1}`\n }\n if (step?.kind === 'task') return `Step ${stepIndex + 1} · ${OUTREACH_TASK_KIND_LABELS[step.taskKind]}`\n return `Step ${stepIndex + 1}`\n}\n\n/** Why a click was read as a scanner's, as the timeline says it. */\nexport const OUTREACH_MACHINE_REASON_LABELS: Record<OutreachClickMachineReason, string> = {\n agent: 'the request named itself as a scanner or a link preview',\n too_soon: 'it came within 30 seconds of the email arriving, faster than anyone reads',\n method: 'it checked the link without opening the page',\n}\n\n/** Why a fetch of the tracking image was not counted as an open (AGL-3395). */\nexport const OUTREACH_OPEN_MACHINE_REASON_LABELS: Record<OutreachOpenMachineReason, string> = {\n agent: 'the request named itself as a scanner or a preview',\n too_soon: 'it came within 30 seconds of the email arriving, faster than anyone reads',\n method: 'it checked the image without loading it',\n image_proxy: 'Yahoo’s image proxy fetched it, which it does whether or not the person reads it',\n privacy_proxy: 'Apple Mail’s privacy protection fetched it as the email arrived',\n}\n\nconst ACTION_TITLES: Record<OutreachHistoryAction, string> = {\n pause: 'Paused',\n resume: 'Resumed',\n stop: 'Stopped by a member',\n do_not_contact: 'Marked do-not-contact',\n}\nconst ACTION_KINDS: Record<OutreachHistoryAction, OutreachTimelineKind> = {\n pause: 'paused',\n resume: 'resumed',\n stop: 'stopped',\n do_not_contact: 'do-not-contact',\n}\n\n/** Which of two entries at the same moment reads first: the outcome above what led to it. */\nconst TIE_ORDER: Record<OutreachTimelineKind, number> = {\n finished: 0,\n replied: 1,\n bounced: 1,\n 'opted-out': 1,\n failed: 1,\n stopped: 2,\n 'do-not-contact': 2,\n paused: 2,\n held: 2,\n released: 3,\n resumed: 3,\n click: 4,\n scanner: 4,\n 'earlier-clicks': 4,\n open: 4,\n 'machine-open': 4,\n task: 5,\n sent: 5,\n curated: 6,\n enrolled: 7,\n}\n\nconst plural = (count: number, one: string, many: string) => `${count} ${count === 1 ? one : many}`\nconst byWhom = (uid: string | null | undefined, memberName: (uid: string) => string) =>\n uid ? `By ${memberName(uid)}` : null\n\n/** The step records that are emails, in the order they went. */\nfunction emailRecords(enrollment: OutreachEnrollment): OutreachStepRecord[] {\n return (enrollment.stepRecords ?? []).filter((record) => record?.kind === 'email')\n}\n\n/**\n * The timeline, newest first — see the module note.\n */\nexport function outreachEnrollmentTimeline(input: OutreachTimelineInput): OutreachTimelineEntry[] {\n const { enrollment, steps, memberName } = input\n const entries: OutreachTimelineEntry[] = []\n const history = input.history ?? []\n const gatewayName = input.gateway ? OUTREACH_MAIL_GATEWAY_LABELS[input.gateway] : null\n\n // ── The enrollment ──────────────────────────────────────────────────────\n if (enrollment.createdAtMs) {\n entries.push({\n key: 'enrolled',\n kind: 'enrolled',\n atMs: enrollment.createdAtMs,\n title: 'Enrolled',\n facts: [\n byWhom(enrollment.enrolledByUid, memberName),\n enrollment.target === 'lead' ? 'As a lead' : 'As a contact',\n enrollment.cold ? 'Cold — confirmed by the member who enrolled them' : null,\n ].filter((fact): fact is string => Boolean(fact)),\n tone: 'default',\n })\n }\n\n // ── Curated copies (AGL-3324) ───────────────────────────────────────────\n for (const [key, override] of Object.entries(enrollment.stepOverrides ?? {})) {\n if (!override.draftedAtMs) continue\n entries.push({\n key: `curated-${key}`,\n kind: 'curated',\n atMs: override.draftedAtMs,\n title: `Curated ${outreachStepName(steps, Number(key))}`,\n facts: [\n override.source === 'ai' ? (override.edited ? 'AI draft, edited' : 'AI draft') : 'Written by a member',\n byWhom(override.draftedByUid, memberName),\n ].filter((fact): fact is string => Boolean(fact)),\n tone: 'default',\n })\n }\n\n // ── Sends and tasks ─────────────────────────────────────────────────────\n let previousThread: string | null = null\n ;(enrollment.stepRecords ?? []).forEach((record, index) => {\n if (!record || typeof record.atMs !== 'number') return\n if (record.kind === 'task') {\n const step = steps.find((entry) => entry.id === record.stepId) ?? steps[record.stepIndex]\n entries.push({\n key: `task-${index}`,\n kind: 'task',\n atMs: record.atMs,\n title:\n step?.kind === 'task'\n ? `${OUTREACH_TASK_KIND_LABELS[step.taskKind]} task filed`\n : 'Task filed',\n detail: step?.kind === 'task' ? step.title : null,\n facts: [\n outreachStepName(steps, record.stepIndex),\n record.taskId === null ? 'The CRM could not file it' : null,\n ].filter((fact): fact is string => Boolean(fact)),\n tone: 'default',\n })\n return\n }\n const thread = record.gmailThreadId ?? null\n // The link to Gmail sits on the email that STARTED a thread, once.\n const startsThread = Boolean(thread) && thread !== previousThread\n previousThread = thread ?? previousThread\n entries.push({\n key: `sent-${index}`,\n kind: 'sent',\n atMs: record.atMs,\n title: `${outreachStepName(steps, record.stepIndex)} sent`,\n detail: record.subject ? `Subject: ${record.subject}` : null,\n facts: [\n record.curated === 'ai'\n ? 'Curated copy — AI draft'\n : record.curated === 'member'\n ? 'Curated copy — written by a member'\n : null,\n record.links?.length ? plural(record.links.length, 'tracked link', 'tracked links') : null,\n record.openTracked ? 'Tracking image' : null,\n ].filter((fact): fact is string => Boolean(fact)),\n gmailThreadId: startsThread ? thread : null,\n tone: 'default',\n })\n })\n\n // ── Clicks ──────────────────────────────────────────────────────────────\n for (const row of history) {\n if (row.kind !== 'click') continue\n entries.push(\n row.human\n ? {\n key: `history-${row.id}`,\n kind: 'click',\n atMs: row.atMs,\n title: 'Clicked a link',\n url: row.url,\n facts: [`From ${outreachStepName(steps, row.stepIndex)}`],\n tone: 'success',\n }\n : {\n key: `history-${row.id}`,\n kind: 'scanner',\n atMs: row.atMs,\n title: 'A scanner followed a link — not counted',\n url: row.url,\n facts: [\n `From ${outreachStepName(steps, row.stepIndex)}`,\n row.machineReason ? `Read as a scanner: ${OUTREACH_MACHINE_REASON_LABELS[row.machineReason]}` : null,\n ].filter((fact): fact is string => Boolean(fact)),\n tone: 'default',\n },\n )\n }\n\n // ── Opens (AGL-3395) ────────────────────────────────────────────────────\n for (const row of history) {\n if (row.kind !== 'open') continue\n entries.push(\n row.human\n ? {\n key: `history-${row.id}`,\n kind: 'open',\n atMs: row.atMs,\n title: 'Opened an email',\n facts: [`${outreachStepName(steps, row.stepIndex)}`],\n tone: 'info',\n }\n : {\n key: `history-${row.id}`,\n kind: 'machine-open',\n atMs: row.atMs,\n title: 'The tracking image was fetched by a machine — not counted',\n facts: [\n outreachStepName(steps, row.stepIndex),\n row.machineReason ? `Read as a machine: ${OUTREACH_OPEN_MACHINE_REASON_LABELS[row.machineReason]}` : null,\n ].filter((fact): fact is string => Boolean(fact)),\n tone: 'default',\n },\n )\n }\n\n /*\n * Clicks with no row of their own: counted before the per-click history\n * began, or past its limit. ONE entry, placed at the first of them when\n * that is known, saying what the totals hold and nothing more.\n */\n const clicks = outreachClickSummary(enrollment.engagement)\n const engagement = enrollment.engagement\n if (clicks.unloggedClicks > 0 || clicks.unloggedMachineClicks > 0) {\n const everyHumanUnlogged = clicks.unloggedClicks === clicks.clicks\n const loggedRows = outreachLoggedEngagementRows(readOutreachEngagement(enrollment.engagement))\n const facts: string[] = []\n const first = engagement?.firstClickAtMs ?? null\n const last = engagement?.lastClickAtMs ?? null\n if (clicks.unloggedClicks > 0 && first !== null && everyHumanUnlogged && last !== null) {\n facts.push(\n clicks.unloggedClicks === 1\n ? `At ${input.formatTime(first)}`\n : first === last\n ? `First and last at ${input.formatTime(first)}`\n : `First at ${input.formatTime(first)}, last at ${input.formatTime(last)}`,\n )\n } else if (clicks.unloggedClicks > 0 && first !== null) {\n // Later clicks have rows of their own, so only the first is one of these.\n facts.push(`First at ${input.formatTime(first)}`)\n }\n if (clicks.unloggedMachineClicks > 0) {\n facts.push(plural(clicks.unloggedMachineClicks, 'scanner click, not counted', 'scanner clicks, not counted'))\n }\n entries.push({\n key: 'earlier-clicks',\n kind: 'earlier-clicks',\n atMs:\n (clicks.unloggedClicks > 0 ? engagement?.firstClickAtMs : null) ??\n engagement?.lastClickAtMs ??\n emailRecords(enrollment)[0]?.atMs ??\n enrollment.createdAtMs,\n title:\n clicks.unloggedClicks > 0\n ? `${plural(clicks.unloggedClicks, 'earlier click', 'earlier clicks')}, kept as a total`\n : `${plural(clicks.unloggedMachineClicks, 'earlier scanner click', 'earlier scanner clicks')}, kept as a total`,\n detail:\n loggedRows >= OUTREACH_ENROLLMENT_HISTORY_MAX\n ? `Each click is listed on its own up to ${OUTREACH_ENROLLMENT_HISTORY_MAX}; these came before per-click history began, or after this person reached that limit.`\n : 'These came before each click was recorded on its own, so only their count and times were kept.',\n // The last destination is theirs only when every counted click was.\n url: clicks.unloggedClicks > 0 && everyHumanUnlogged ? (engagement?.lastClickUrl ?? null) : null,\n urlLabel: clicks.unloggedClicks === 1 ? 'Link' : 'Last link',\n facts,\n tone: clicks.unloggedClicks > 0 ? 'success' : 'default',\n })\n }\n\n // ── Members' acts ───────────────────────────────────────────────────────\n const actionTimes = new Set<number>()\n for (const row of history) {\n if (row.kind !== 'action') continue\n actionTimes.add(row.atMs)\n const released =\n row.action === 'resume' && enrollment.gatewayHold?.releasedAtMs === row.atMs\n if (released) continue\n entries.push({\n key: `history-${row.id}`,\n kind: ACTION_KINDS[row.action],\n atMs: row.atMs,\n title: ACTION_TITLES[row.action],\n detail: row.detail,\n facts: [byWhom(row.byUid, memberName)].filter((fact): fact is string => Boolean(fact)),\n tone: row.action === 'resume' ? 'default' : row.action === 'pause' ? 'warning' : 'default',\n })\n }\n\n // ── The gateway hold (AGL-3326) ─────────────────────────────────────────\n const hold = enrollment.gatewayHold\n const holdGateway = hold ? OUTREACH_MAIL_GATEWAY_LABELS[hold.gateway] : null\n if (hold?.heldAtMs) {\n entries.push({\n key: 'held',\n kind: 'held',\n atMs: hold.heldAtMs,\n title: `Held — ${holdGateway} refused this sender`,\n detail:\n enrollment.stopReason === 'gateway_blocked_here' && enrollment.stoppedAtMs === hold.heldAtMs\n ? enrollment.stopDetail\n : null,\n facts: ['Nothing was sent to them while it was held'],\n tone: 'warning',\n })\n }\n if (hold?.releasedAtMs) {\n entries.push({\n key: 'released',\n kind: 'released',\n atMs: hold.releasedAtMs,\n title: hold.heldAtMs\n ? `Resumed past the ${holdGateway} hold — sent anyway`\n : `Enrolled past the ${holdGateway} warning`,\n facts: [byWhom(hold.releasedByUid, memberName)].filter((fact): fact is string => Boolean(fact)),\n tone: 'default',\n })\n }\n\n // ── The current stop, from the enrollment ───────────────────────────────\n const stoppedAtMs = enrollment.stoppedAtMs\n const alreadySaid =\n stoppedAtMs !== null &&\n (actionTimes.has(stoppedAtMs) ||\n (enrollment.stopReason === 'gateway_blocked_here' && hold?.heldAtMs === stoppedAtMs))\n if (stoppedAtMs && !alreadySaid && enrollment.status !== 'active' && enrollment.status !== 'finished') {\n const stop = currentStop(enrollment, memberName, gatewayName)\n if (stop) entries.push({ key: 'stop', atMs: stoppedAtMs, ...stop })\n }\n\n // ── Finished ────────────────────────────────────────────────────────────\n if (enrollment.status === 'finished') {\n const last = (enrollment.stepRecords ?? []).at(-1)\n entries.push({\n key: 'finished',\n kind: 'finished',\n atMs: last?.atMs ?? enrollment.updatedAtMs,\n title: 'Finished — every step ran',\n facts: [],\n tone: 'success',\n })\n }\n\n return entries.sort((a, b) => b.atMs - a.atMs || TIE_ORDER[a.kind] - TIE_ORDER[b.kind])\n}\n\n/** The entry for the stop the enrollment is in now. */\nfunction currentStop(\n enrollment: OutreachEnrollment,\n memberName: (uid: string) => string,\n gatewayName: string | null,\n): Omit<OutreachTimelineEntry, 'key' | 'atMs'> | null {\n const detail = enrollment.stopDetail\n const by = byWhom(enrollment.stoppedByUid, memberName)\n const facts = (...values: Array<string | null>) => values.filter((fact): fact is string => Boolean(fact))\n switch (enrollment.stopReason) {\n case 'reply':\n return {\n kind: 'replied',\n title: 'Replied',\n detail,\n facts: ['The sequence stopped for them'],\n gmailThreadId: enrollment.gmailThreadId ?? enrollment.gmailThreadIds.at(-1) ?? null,\n tone: 'info',\n }\n case 'hard_bounce':\n return {\n kind: 'bounced',\n title: 'Bounced',\n detail,\n facts: facts(gatewayName ? `Their mail gateway: ${gatewayName}` : null, 'The address is on the do-not-contact list'),\n tone: 'error',\n }\n case 'opt_out_reply':\n return { kind: 'opted-out', title: 'Asked not to be emailed, in a reply', detail, facts: [], tone: 'warning' }\n case 'unsubscribe':\n return { kind: 'opted-out', title: 'Unsubscribed', detail, facts: [], tone: 'warning' }\n case 'do_not_contact':\n return { kind: 'do-not-contact', title: 'Put on the do-not-contact list', detail, facts: facts(by), tone: 'warning' }\n case 'manual':\n return enrollment.status === 'paused'\n ? { kind: 'paused', title: 'Paused', detail, facts: facts(by), tone: 'warning' }\n : { kind: 'stopped', title: 'Stopped by a member', detail, facts: facts(by), tone: 'default' }\n case 'gate':\n return { kind: 'stopped', title: 'Stopped — no longer eligible', detail, facts: [], tone: 'default' }\n case 'sequence_archived':\n return { kind: 'stopped', title: 'Stopped — the sequence was archived', detail, facts: [], tone: 'default' }\n case 'gateway_blocked_here':\n return { kind: 'held', title: 'Held — their mail gateway refused this sender', detail, facts: [], tone: 'warning' }\n case 'send_failed':\n return { kind: 'failed', title: 'An email couldn’t be sent', detail, facts: [], tone: 'error' }\n default:\n return null\n }\n}\n\n/** The five figures above the timeline. */\nexport interface OutreachEnrollmentFigures {\n emailsSent: number\n clicks: number\n /** Distinct destinations followed; `atLeast` when only a floor is known. */\n linksFollowed: { count: number; atLeast: boolean }\n scannerClicks: number\n /** Opens judged theirs, and the image fetches that were a machine's (AGL-3395). */\n opens: number\n machineOpens: number\n replies: number\n /** Whether anything has happened to them beyond being enrolled. */\n anything: boolean\n}\n\nexport function outreachEnrollmentFigures(enrollment: OutreachEnrollment): OutreachEnrollmentFigures {\n const clicks = outreachClickSummary(enrollment.engagement)\n const engagement = readOutreachEngagement(enrollment.engagement)\n const emailsSent = emailRecords(enrollment).length\n const replies =\n enrollment.status === 'replied' || enrollment.stopReason === 'reply' || enrollment.stopReason === 'opt_out_reply'\n ? 1\n : 0\n return {\n emailsSent,\n clicks: clicks.clicks,\n linksFollowed:\n clicks.linkCount === null\n ? { count: clicks.followed.length, atLeast: true }\n : { count: clicks.linkCount, atLeast: clicks.linksCapped },\n scannerClicks: clicks.machineClicks,\n opens: engagement.opens,\n machineOpens: engagement.machineOpens,\n replies,\n anything:\n emailsSent > 0 ||\n (enrollment.stepRecords ?? []).length > 0 ||\n clicks.clicks > 0 ||\n clicks.machineClicks > 0 ||\n engagement.opens > 0 ||\n engagement.machineOpens > 0 ||\n enrollment.stoppedAtMs !== null,\n }\n}\n"],"names":["OUTREACH_MAIL_GATEWAY_LABELS","outreachClickSummary","outreachLoggedEngagementRows","readOutreachEngagement","OUTREACH_ENROLLMENT_HISTORY_MAX","OUTREACH_TASK_KIND_LABELS","outreachStepName","steps","stepIndex","step","kind","position","slice","filter","entry","length","taskKind","OUTREACH_MACHINE_REASON_LABELS","agent","too_soon","method","OUTREACH_OPEN_MACHINE_REASON_LABELS","image_proxy","privacy_proxy","ACTION_TITLES","pause","resume","stop","do_not_contact","ACTION_KINDS","TIE_ORDER","finished","replied","bounced","failed","stopped","paused","held","released","resumed","click","scanner","open","task","sent","curated","enrolled","plural","count","one","many","byWhom","uid","memberName","emailRecords","enrollment","stepRecords","record","outreachEnrollmentTimeline","input","entries","history","gatewayName","gateway","createdAtMs","push","key","atMs","title","facts","enrolledByUid","target","cold","fact","Boolean","tone","override","Object","stepOverrides","draftedAtMs","Number","source","edited","draftedByUid","previousThread","forEach","index","find","id","stepId","detail","taskId","thread","gmailThreadId","startsThread","subject","links","openTracked","row","human","url","machineReason","clicks","engagement","unloggedClicks","unloggedMachineClicks","everyHumanUnlogged","loggedRows","first","firstClickAtMs","last","lastClickAtMs","formatTime","lastClickUrl","urlLabel","actionTimes","Set","add","action","gatewayHold","releasedAtMs","byUid","hold","holdGateway","heldAtMs","stopReason","stoppedAtMs","stopDetail","releasedByUid","alreadySaid","has","status","currentStop","at","updatedAtMs","sort","a","b","by","stoppedByUid","values","gmailThreadIds","outreachEnrollmentFigures","emailsSent","replies","linksFollowed","linkCount","followed","atLeast","linksCapped","scannerClicks","machineClicks","opens","machineOpens","anything"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/model/enrollment-timeline.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 * EVERYTHING THAT HAPPENED TO ONE PERSON IN ONE SEQUENCE (AGL-3332).\n *\n * The enrollment's detail view is one list, newest first, and this builds\n * it from what is stored — the enrollment itself, and its history rows —\n * without inventing anything that is not:\n *\n * - **Each send** is a step record: the step, the time, the subject as\n * sent, and whether it went out as the person's curated copy.\n * - **Each click** is a history row: the destination, the step whose email\n * carried it, and a person's or a scanner's. Clicks from before the\n * history began exist only as totals, and are ONE row saying so — never\n * a row per click, which would be a fabrication with a timestamp on it.\n * - **Each member's act** — pause, resume, stop, do-not-contact — is a\n * history row with who and why. An enrollment holds only its current\n * stop, so the row is the only record of a pause that was resumed.\n * - **The current stop** — a reply, a bounce, an opt-out, a hold — comes\n * from the enrollment's own fields, with its full reason, unless a\n * history row already says it.\n * - **The gateway hold** (AGL-3326), and its release.\n * - **The enrollment**, with who enrolled them, and each curated step.\n * - **The steps it skipped** (AGL-3228), when it began past the first: one\n * entry naming them, never a \"sent\" — nothing of ours went out for them.\n *\n * Client-safe and free of React: the view renders these entries, and the\n * spec reads them.\n *=========================================*/\n\nimport type { OutreachClickMachineReason } from '../engine/click-tracking'\nimport type { OutreachOpenMachineReason } from '../engine/open-tracking'\nimport { OUTREACH_MAIL_GATEWAY_LABELS, type OutreachMailGateway } from '../engine/mail-gateway'\nimport {\n outreachClickSummary,\n outreachLoggedEngagementRows,\n readOutreachEngagement,\n} from './enrollment-engagement'\nimport {\n OUTREACH_ENROLLMENT_HISTORY_MAX,\n OUTREACH_TASK_KIND_LABELS,\n type OutreachEnrollment,\n type OutreachEnrollmentHistoryEntry,\n type OutreachHistoryAction,\n type OutreachSequenceStep,\n type OutreachStepRecord,\n} from './outreach.types'\n\n/** What kind of thing an entry is, which the view draws an icon for. */\nexport type OutreachTimelineKind =\n | 'enrolled'\n | 'skipped'\n | 'curated'\n | 'sent'\n | 'task'\n | 'click'\n | 'scanner'\n | 'earlier-clicks'\n | 'open'\n | 'machine-open'\n | 'paused'\n | 'resumed'\n | 'stopped'\n | 'do-not-contact'\n | 'replied'\n | 'bounced'\n | 'opted-out'\n | 'held'\n | 'released'\n | 'failed'\n | 'finished'\n\n/** How loudly the view draws an entry. */\nexport type OutreachTimelineTone = 'default' | 'success' | 'info' | 'warning' | 'error'\n\n/** One line of the timeline. */\nexport interface OutreachTimelineEntry {\n /** Stable across renders, for React and for a spec. */\n key: string\n kind: OutreachTimelineKind\n atMs: number\n /** What happened, in a few words. */\n title: string\n /** The one fact that matters most, shown in full: a subject, a reason, a diagnostic. */\n detail?: string | null\n /** The rest, each short: the step, who, a scanner's reason. */\n facts: string[]\n /** A destination the person followed, shown whole and linkable. */\n url?: string | null\n /** What `url` is, when it is not simply \"the link they followed\". */\n urlLabel?: string\n /** The Gmail thread this entry opened, for an \"Open in Gmail\" link. */\n gmailThreadId?: string | null\n tone: OutreachTimelineTone\n}\n\nexport interface OutreachTimelineInput {\n enrollment: OutreachEnrollment\n steps: readonly OutreachSequenceStep[]\n /** The history rows, newest or oldest first; `null` while unread or unreadable. */\n history: readonly OutreachEnrollmentHistoryEntry[] | null\n /** A member's name for a stored uid; the uid itself when the roster does not know it. */\n memberName: (uid: string) => string\n /** A moment as the reader reads it — in the mailbox's zone. */\n formatTime: (ms: number) => string\n /** The mail gateway in front of the person's domain, when it is known. */\n gateway?: OutreachMailGateway | null\n}\n\n/** \"Email 2 · step 3\" for an email step, \"Step 2 · Call\" for a task, \"Step 4\" for one the sequence no longer has. */\nexport function outreachStepName(steps: readonly OutreachSequenceStep[], stepIndex: number): string {\n const step = steps[stepIndex]\n if (step?.kind === 'email') {\n const position = steps.slice(0, stepIndex + 1).filter((entry) => entry.kind === 'email').length\n return `Email ${position} · step ${stepIndex + 1}`\n }\n if (step?.kind === 'task') return `Step ${stepIndex + 1} · ${OUTREACH_TASK_KIND_LABELS[step.taskKind]}`\n return `Step ${stepIndex + 1}`\n}\n\n/** Why a click was read as a scanner's, as the timeline says it. */\nexport const OUTREACH_MACHINE_REASON_LABELS: Record<OutreachClickMachineReason, string> = {\n agent: 'the request named itself as a scanner or a link preview',\n too_soon: 'it came within 30 seconds of the email arriving, faster than anyone reads',\n method: 'it checked the link without opening the page',\n}\n\n/** Why a fetch of the tracking image was not counted as an open (AGL-3395). */\nexport const OUTREACH_OPEN_MACHINE_REASON_LABELS: Record<OutreachOpenMachineReason, string> = {\n agent: 'the request named itself as a scanner or a preview',\n too_soon: 'it came within 30 seconds of the email arriving, faster than anyone reads',\n method: 'it checked the image without loading it',\n image_proxy: 'Yahoo’s image proxy fetched it, which it does whether or not the person reads it',\n privacy_proxy: 'Apple Mail’s privacy protection fetched it as the email arrived',\n}\n\nconst ACTION_TITLES: Record<OutreachHistoryAction, string> = {\n pause: 'Paused',\n resume: 'Resumed',\n stop: 'Stopped by a member',\n do_not_contact: 'Marked do-not-contact',\n}\nconst ACTION_KINDS: Record<OutreachHistoryAction, OutreachTimelineKind> = {\n pause: 'paused',\n resume: 'resumed',\n stop: 'stopped',\n do_not_contact: 'do-not-contact',\n}\n\n/** Which of two entries at the same moment reads first: the outcome above what led to it. */\nconst TIE_ORDER: Record<OutreachTimelineKind, number> = {\n finished: 0,\n replied: 1,\n bounced: 1,\n 'opted-out': 1,\n failed: 1,\n stopped: 2,\n 'do-not-contact': 2,\n paused: 2,\n held: 2,\n released: 3,\n resumed: 3,\n click: 4,\n scanner: 4,\n 'earlier-clicks': 4,\n open: 4,\n 'machine-open': 4,\n task: 5,\n sent: 5,\n curated: 6,\n skipped: 7,\n enrolled: 8,\n}\n\nconst plural = (count: number, one: string, many: string) => `${count} ${count === 1 ? one : many}`\nconst byWhom = (uid: string | null | undefined, memberName: (uid: string) => string) =>\n uid ? `By ${memberName(uid)}` : null\n\n/** The step records that are emails, in the order they went. */\nfunction emailRecords(enrollment: OutreachEnrollment): OutreachStepRecord[] {\n return (enrollment.stepRecords ?? []).filter((record) => record?.kind === 'email')\n}\n\n/**\n * The timeline, newest first — see the module note.\n */\nexport function outreachEnrollmentTimeline(input: OutreachTimelineInput): OutreachTimelineEntry[] {\n const { enrollment, steps, memberName } = input\n const entries: OutreachTimelineEntry[] = []\n const history = input.history ?? []\n const gatewayName = input.gateway ? OUTREACH_MAIL_GATEWAY_LABELS[input.gateway] : null\n\n // ── The enrollment ──────────────────────────────────────────────────────\n if (enrollment.createdAtMs) {\n entries.push({\n key: 'enrolled',\n kind: 'enrolled',\n atMs: enrollment.createdAtMs,\n title: 'Enrolled',\n facts: [\n byWhom(enrollment.enrolledByUid, memberName),\n enrollment.target === 'lead' ? 'As a lead' : 'As a contact',\n enrollment.cold ? 'Cold — confirmed by the member who enrolled them' : null,\n ].filter((fact): fact is string => Boolean(fact)),\n tone: 'default',\n })\n }\n\n // ── Skipped at enrollment (AGL-3228) ───────────────────────────────────\n const skipped = (enrollment.skippedSteps ?? []).filter((entry) => Number.isInteger(entry?.stepIndex))\n if (skipped.length) {\n const start = enrollment.startStepIndex ?? skipped.length\n entries.push({\n key: 'skipped',\n kind: 'skipped',\n atMs: skipped[0].atMs || enrollment.createdAtMs,\n title:\n skipped.length === 1\n ? `Began at step ${start + 1} — step 1 skipped`\n : `Began at step ${start + 1} — steps 1–${skipped.length} skipped`,\n detail: 'Already done by hand before they were enrolled, so the sequence never sent them.',\n facts: skipped.map((entry) => outreachStepName(steps, entry.stepIndex)),\n tone: 'default',\n })\n }\n\n // ── Curated copies (AGL-3324) ───────────────────────────────────────────\n for (const [key, override] of Object.entries(enrollment.stepOverrides ?? {})) {\n if (!override.draftedAtMs) continue\n entries.push({\n key: `curated-${key}`,\n kind: 'curated',\n atMs: override.draftedAtMs,\n title: `Curated ${outreachStepName(steps, Number(key))}`,\n facts: [\n override.source === 'ai' ? (override.edited ? 'AI draft, edited' : 'AI draft') : 'Written by a member',\n byWhom(override.draftedByUid, memberName),\n ].filter((fact): fact is string => Boolean(fact)),\n tone: 'default',\n })\n }\n\n // ── Sends and tasks ─────────────────────────────────────────────────────\n let previousThread: string | null = null\n ;(enrollment.stepRecords ?? []).forEach((record, index) => {\n if (!record || typeof record.atMs !== 'number') return\n if (record.kind === 'task') {\n const step = steps.find((entry) => entry.id === record.stepId) ?? steps[record.stepIndex]\n entries.push({\n key: `task-${index}`,\n kind: 'task',\n atMs: record.atMs,\n title:\n step?.kind === 'task'\n ? `${OUTREACH_TASK_KIND_LABELS[step.taskKind]} task filed`\n : 'Task filed',\n detail: step?.kind === 'task' ? step.title : null,\n facts: [\n outreachStepName(steps, record.stepIndex),\n record.taskId === null ? 'The CRM could not file it' : null,\n ].filter((fact): fact is string => Boolean(fact)),\n tone: 'default',\n })\n return\n }\n const thread = record.gmailThreadId ?? null\n // The link to Gmail sits on the email that STARTED a thread, once.\n const startsThread = Boolean(thread) && thread !== previousThread\n previousThread = thread ?? previousThread\n entries.push({\n key: `sent-${index}`,\n kind: 'sent',\n atMs: record.atMs,\n title: `${outreachStepName(steps, record.stepIndex)} sent`,\n detail: record.subject ? `Subject: ${record.subject}` : null,\n facts: [\n record.curated === 'ai'\n ? 'Curated copy — AI draft'\n : record.curated === 'member'\n ? 'Curated copy — written by a member'\n : null,\n record.links?.length ? plural(record.links.length, 'tracked link', 'tracked links') : null,\n record.openTracked ? 'Tracking image' : null,\n ].filter((fact): fact is string => Boolean(fact)),\n gmailThreadId: startsThread ? thread : null,\n tone: 'default',\n })\n })\n\n // ── Clicks ──────────────────────────────────────────────────────────────\n for (const row of history) {\n if (row.kind !== 'click') continue\n entries.push(\n row.human\n ? {\n key: `history-${row.id}`,\n kind: 'click',\n atMs: row.atMs,\n title: 'Clicked a link',\n url: row.url,\n facts: [`From ${outreachStepName(steps, row.stepIndex)}`],\n tone: 'success',\n }\n : {\n key: `history-${row.id}`,\n kind: 'scanner',\n atMs: row.atMs,\n title: 'A scanner followed a link — not counted',\n url: row.url,\n facts: [\n `From ${outreachStepName(steps, row.stepIndex)}`,\n row.machineReason ? `Read as a scanner: ${OUTREACH_MACHINE_REASON_LABELS[row.machineReason]}` : null,\n ].filter((fact): fact is string => Boolean(fact)),\n tone: 'default',\n },\n )\n }\n\n // ── Opens (AGL-3395) ────────────────────────────────────────────────────\n for (const row of history) {\n if (row.kind !== 'open') continue\n entries.push(\n row.human\n ? {\n key: `history-${row.id}`,\n kind: 'open',\n atMs: row.atMs,\n title: 'Opened an email',\n facts: [`${outreachStepName(steps, row.stepIndex)}`],\n tone: 'info',\n }\n : {\n key: `history-${row.id}`,\n kind: 'machine-open',\n atMs: row.atMs,\n title: 'The tracking image was fetched by a machine — not counted',\n facts: [\n outreachStepName(steps, row.stepIndex),\n row.machineReason ? `Read as a machine: ${OUTREACH_OPEN_MACHINE_REASON_LABELS[row.machineReason]}` : null,\n ].filter((fact): fact is string => Boolean(fact)),\n tone: 'default',\n },\n )\n }\n\n /*\n * Clicks with no row of their own: counted before the per-click history\n * began, or past its limit. ONE entry, placed at the first of them when\n * that is known, saying what the totals hold and nothing more.\n */\n const clicks = outreachClickSummary(enrollment.engagement)\n const engagement = enrollment.engagement\n if (clicks.unloggedClicks > 0 || clicks.unloggedMachineClicks > 0) {\n const everyHumanUnlogged = clicks.unloggedClicks === clicks.clicks\n const loggedRows = outreachLoggedEngagementRows(readOutreachEngagement(enrollment.engagement))\n const facts: string[] = []\n const first = engagement?.firstClickAtMs ?? null\n const last = engagement?.lastClickAtMs ?? null\n if (clicks.unloggedClicks > 0 && first !== null && everyHumanUnlogged && last !== null) {\n facts.push(\n clicks.unloggedClicks === 1\n ? `At ${input.formatTime(first)}`\n : first === last\n ? `First and last at ${input.formatTime(first)}`\n : `First at ${input.formatTime(first)}, last at ${input.formatTime(last)}`,\n )\n } else if (clicks.unloggedClicks > 0 && first !== null) {\n // Later clicks have rows of their own, so only the first is one of these.\n facts.push(`First at ${input.formatTime(first)}`)\n }\n if (clicks.unloggedMachineClicks > 0) {\n facts.push(plural(clicks.unloggedMachineClicks, 'scanner click, not counted', 'scanner clicks, not counted'))\n }\n entries.push({\n key: 'earlier-clicks',\n kind: 'earlier-clicks',\n atMs:\n (clicks.unloggedClicks > 0 ? engagement?.firstClickAtMs : null) ??\n engagement?.lastClickAtMs ??\n emailRecords(enrollment)[0]?.atMs ??\n enrollment.createdAtMs,\n title:\n clicks.unloggedClicks > 0\n ? `${plural(clicks.unloggedClicks, 'earlier click', 'earlier clicks')}, kept as a total`\n : `${plural(clicks.unloggedMachineClicks, 'earlier scanner click', 'earlier scanner clicks')}, kept as a total`,\n detail:\n loggedRows >= OUTREACH_ENROLLMENT_HISTORY_MAX\n ? `Each click is listed on its own up to ${OUTREACH_ENROLLMENT_HISTORY_MAX}; these came before per-click history began, or after this person reached that limit.`\n : 'These came before each click was recorded on its own, so only their count and times were kept.',\n // The last destination is theirs only when every counted click was.\n url: clicks.unloggedClicks > 0 && everyHumanUnlogged ? (engagement?.lastClickUrl ?? null) : null,\n urlLabel: clicks.unloggedClicks === 1 ? 'Link' : 'Last link',\n facts,\n tone: clicks.unloggedClicks > 0 ? 'success' : 'default',\n })\n }\n\n // ── Members' acts ───────────────────────────────────────────────────────\n const actionTimes = new Set<number>()\n for (const row of history) {\n if (row.kind !== 'action') continue\n actionTimes.add(row.atMs)\n const released =\n row.action === 'resume' && enrollment.gatewayHold?.releasedAtMs === row.atMs\n if (released) continue\n entries.push({\n key: `history-${row.id}`,\n kind: ACTION_KINDS[row.action],\n atMs: row.atMs,\n title: ACTION_TITLES[row.action],\n detail: row.detail,\n facts: [byWhom(row.byUid, memberName)].filter((fact): fact is string => Boolean(fact)),\n tone: row.action === 'resume' ? 'default' : row.action === 'pause' ? 'warning' : 'default',\n })\n }\n\n // ── The gateway hold (AGL-3326) ─────────────────────────────────────────\n const hold = enrollment.gatewayHold\n const holdGateway = hold ? OUTREACH_MAIL_GATEWAY_LABELS[hold.gateway] : null\n if (hold?.heldAtMs) {\n entries.push({\n key: 'held',\n kind: 'held',\n atMs: hold.heldAtMs,\n title: `Held — ${holdGateway} refused this sender`,\n detail:\n enrollment.stopReason === 'gateway_blocked_here' && enrollment.stoppedAtMs === hold.heldAtMs\n ? enrollment.stopDetail\n : null,\n facts: ['Nothing was sent to them while it was held'],\n tone: 'warning',\n })\n }\n if (hold?.releasedAtMs) {\n entries.push({\n key: 'released',\n kind: 'released',\n atMs: hold.releasedAtMs,\n title: hold.heldAtMs\n ? `Resumed past the ${holdGateway} hold — sent anyway`\n : `Enrolled past the ${holdGateway} warning`,\n facts: [byWhom(hold.releasedByUid, memberName)].filter((fact): fact is string => Boolean(fact)),\n tone: 'default',\n })\n }\n\n // ── The current stop, from the enrollment ───────────────────────────────\n const stoppedAtMs = enrollment.stoppedAtMs\n const alreadySaid =\n stoppedAtMs !== null &&\n (actionTimes.has(stoppedAtMs) ||\n (enrollment.stopReason === 'gateway_blocked_here' && hold?.heldAtMs === stoppedAtMs))\n if (stoppedAtMs && !alreadySaid && enrollment.status !== 'active' && enrollment.status !== 'finished') {\n const stop = currentStop(enrollment, memberName, gatewayName)\n if (stop) entries.push({ key: 'stop', atMs: stoppedAtMs, ...stop })\n }\n\n // ── Finished ────────────────────────────────────────────────────────────\n if (enrollment.status === 'finished') {\n const last = (enrollment.stepRecords ?? []).at(-1)\n entries.push({\n key: 'finished',\n kind: 'finished',\n atMs: last?.atMs ?? enrollment.updatedAtMs,\n title: enrollment.skippedSteps?.length ? 'Finished — every remaining step ran' : 'Finished — every step ran',\n facts: [],\n tone: 'success',\n })\n }\n\n return entries.sort((a, b) => b.atMs - a.atMs || TIE_ORDER[a.kind] - TIE_ORDER[b.kind])\n}\n\n/** The entry for the stop the enrollment is in now. */\nfunction currentStop(\n enrollment: OutreachEnrollment,\n memberName: (uid: string) => string,\n gatewayName: string | null,\n): Omit<OutreachTimelineEntry, 'key' | 'atMs'> | null {\n const detail = enrollment.stopDetail\n const by = byWhom(enrollment.stoppedByUid, memberName)\n const facts = (...values: Array<string | null>) => values.filter((fact): fact is string => Boolean(fact))\n switch (enrollment.stopReason) {\n case 'reply':\n return {\n kind: 'replied',\n title: 'Replied',\n detail,\n facts: ['The sequence stopped for them'],\n gmailThreadId: enrollment.gmailThreadId ?? enrollment.gmailThreadIds.at(-1) ?? null,\n tone: 'info',\n }\n case 'hard_bounce':\n return {\n kind: 'bounced',\n title: 'Bounced',\n detail,\n facts: facts(gatewayName ? `Their mail gateway: ${gatewayName}` : null, 'The address is on the do-not-contact list'),\n tone: 'error',\n }\n case 'opt_out_reply':\n return { kind: 'opted-out', title: 'Asked not to be emailed, in a reply', detail, facts: [], tone: 'warning' }\n case 'unsubscribe':\n return { kind: 'opted-out', title: 'Unsubscribed', detail, facts: [], tone: 'warning' }\n case 'do_not_contact':\n return { kind: 'do-not-contact', title: 'Put on the do-not-contact list', detail, facts: facts(by), tone: 'warning' }\n case 'manual':\n return enrollment.status === 'paused'\n ? { kind: 'paused', title: 'Paused', detail, facts: facts(by), tone: 'warning' }\n : { kind: 'stopped', title: 'Stopped by a member', detail, facts: facts(by), tone: 'default' }\n case 'gate':\n return { kind: 'stopped', title: 'Stopped — no longer eligible', detail, facts: [], tone: 'default' }\n case 'sequence_archived':\n return { kind: 'stopped', title: 'Stopped — the sequence was archived', detail, facts: [], tone: 'default' }\n case 'gateway_blocked_here':\n return { kind: 'held', title: 'Held — their mail gateway refused this sender', detail, facts: [], tone: 'warning' }\n case 'send_failed':\n return { kind: 'failed', title: 'An email couldn’t be sent', detail, facts: [], tone: 'error' }\n default:\n return null\n }\n}\n\n/** The five figures above the timeline. */\nexport interface OutreachEnrollmentFigures {\n emailsSent: number\n clicks: number\n /** Distinct destinations followed; `atLeast` when only a floor is known. */\n linksFollowed: { count: number; atLeast: boolean }\n scannerClicks: number\n /** Opens judged theirs, and the image fetches that were a machine's (AGL-3395). */\n opens: number\n machineOpens: number\n replies: number\n /** Whether anything has happened to them beyond being enrolled. */\n anything: boolean\n}\n\nexport function outreachEnrollmentFigures(enrollment: OutreachEnrollment): OutreachEnrollmentFigures {\n const clicks = outreachClickSummary(enrollment.engagement)\n const engagement = readOutreachEngagement(enrollment.engagement)\n const emailsSent = emailRecords(enrollment).length\n const replies =\n enrollment.status === 'replied' || enrollment.stopReason === 'reply' || enrollment.stopReason === 'opt_out_reply'\n ? 1\n : 0\n return {\n emailsSent,\n clicks: clicks.clicks,\n linksFollowed:\n clicks.linkCount === null\n ? { count: clicks.followed.length, atLeast: true }\n : { count: clicks.linkCount, atLeast: clicks.linksCapped },\n scannerClicks: clicks.machineClicks,\n opens: engagement.opens,\n machineOpens: engagement.machineOpens,\n replies,\n anything:\n emailsSent > 0 ||\n (enrollment.stepRecords ?? []).length > 0 ||\n clicks.clicks > 0 ||\n clicks.machineClicks > 0 ||\n engagement.opens > 0 ||\n engagement.machineOpens > 0 ||\n enrollment.stoppedAtMs !== null,\n }\n}\n"],"names":["OUTREACH_MAIL_GATEWAY_LABELS","outreachClickSummary","outreachLoggedEngagementRows","readOutreachEngagement","OUTREACH_ENROLLMENT_HISTORY_MAX","OUTREACH_TASK_KIND_LABELS","outreachStepName","steps","stepIndex","step","kind","position","slice","filter","entry","length","taskKind","OUTREACH_MACHINE_REASON_LABELS","agent","too_soon","method","OUTREACH_OPEN_MACHINE_REASON_LABELS","image_proxy","privacy_proxy","ACTION_TITLES","pause","resume","stop","do_not_contact","ACTION_KINDS","TIE_ORDER","finished","replied","bounced","failed","stopped","paused","held","released","resumed","click","scanner","open","task","sent","curated","skipped","enrolled","plural","count","one","many","byWhom","uid","memberName","emailRecords","enrollment","stepRecords","record","outreachEnrollmentTimeline","input","entries","history","gatewayName","gateway","createdAtMs","push","key","atMs","title","facts","enrolledByUid","target","cold","fact","Boolean","tone","skippedSteps","Number","isInteger","start","startStepIndex","detail","map","override","Object","stepOverrides","draftedAtMs","source","edited","draftedByUid","previousThread","forEach","index","find","id","stepId","taskId","thread","gmailThreadId","startsThread","subject","links","openTracked","row","human","url","machineReason","clicks","engagement","unloggedClicks","unloggedMachineClicks","everyHumanUnlogged","loggedRows","first","firstClickAtMs","last","lastClickAtMs","formatTime","lastClickUrl","urlLabel","actionTimes","Set","add","action","gatewayHold","releasedAtMs","byUid","hold","holdGateway","heldAtMs","stopReason","stoppedAtMs","stopDetail","releasedByUid","alreadySaid","has","status","currentStop","at","updatedAtMs","sort","a","b","by","stoppedByUid","values","gmailThreadIds","outreachEnrollmentFigures","emailsSent","replies","linksFollowed","linkCount","followed","atLeast","linksCapped","scannerClicks","machineClicks","opens","machineOpens","anything"],"mappings":"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"}
|
|
@@ -30,7 +30,13 @@ import type { OutreachComplianceIssue } from './compliance-settings';
|
|
|
30
30
|
import type { OutreachAttestationKind, OutreachComplianceSettingsDocument, OutreachDoNotContactDomainEntry, OutreachEnrollment, OutreachSequence, OutreachStepOverrideSource } from './outreach.types';
|
|
31
31
|
import type { OutreachSequenceDraft, OutreachSequenceIssue } from './sequence-draft';
|
|
32
32
|
/** Every reason these routes refuse with, for a caller to branch on. */
|
|
33
|
-
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' | 'rate-limited'
|
|
33
|
+
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' | 'rate-limited'
|
|
34
|
+
/** The Google account has no Gmail service: off for it, or not yet provisioned. */
|
|
35
|
+
| 'mail-service-unavailable'
|
|
36
|
+
/** Google refused the account itself, such as an administrator barring Gmail API access. */
|
|
37
|
+
| 'google-refused' | 'google-unavailable' | 'curation-unavailable' | 'curation-refused' | 'invalid-override'
|
|
38
|
+
/** The step to begin enrolling at is not one the sequence has (AGL-3228). */
|
|
39
|
+
| 'invalid-start-step';
|
|
34
40
|
/** A refusal, in the one shape every route answers with. */
|
|
35
41
|
export interface OutreachRouteRefusal {
|
|
36
42
|
/** A sentence written for the person reading it. */
|
|
@@ -213,6 +219,13 @@ export interface OutreachEnrollRequest {
|
|
|
213
219
|
orgId: string;
|
|
214
220
|
sequenceId: string;
|
|
215
221
|
people: OutreachEnrollPersonRequest[];
|
|
222
|
+
/**
|
|
223
|
+
* The step everyone begins at (AGL-3228), zero-based; the first when
|
|
224
|
+
* absent. For people who already had the earlier steps by hand: those are
|
|
225
|
+
* marked skipped, and this step goes out after its own delay, counted from
|
|
226
|
+
* the enrollment. A step the sequence does not have refuses the request.
|
|
227
|
+
*/
|
|
228
|
+
startStepIndex?: number;
|
|
216
229
|
}
|
|
217
230
|
interface OutreachEnrollOutcomePerson {
|
|
218
231
|
personId: string;
|
|
@@ -332,6 +345,11 @@ export interface OutreachPreviewRequest {
|
|
|
332
345
|
stepIndex?: number;
|
|
333
346
|
/** The person's copies of steps as the dialog holds them (AGL-3324), so the preview shows the curated version. */
|
|
334
347
|
stepOverrides?: OutreachStepOverrideRequest[];
|
|
348
|
+
/**
|
|
349
|
+
* The step the enrollment would begin at (AGL-3228), zero-based: the
|
|
350
|
+
* first email written is the first one sent from there.
|
|
351
|
+
*/
|
|
352
|
+
startStepIndex?: number;
|
|
335
353
|
}
|
|
336
354
|
export interface OutreachPreviewResponse {
|
|
337
355
|
ok: true;
|
|
@@ -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 { OutreachGatewayStanding } from '../engine/mail-gateway'\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 | 'rate-limited'\n | 'google-unavailable'\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\n/**\n * The mail gateway in front of one person's domain (AGL-3326), and what it\n * did with this organization's mail this week — what the Check-people chip\n * shows — with `hold` set when the engine would hold the send into it: the\n * gateway refused the sender twice in the last thirty days and delivered\n * nothing. A held person is not blocked; they default to un-ticked, and\n * ticking them is the member's say-so.\n */\nexport interface OutreachEnrollGateway extends OutreachGatewayStanding {\n hold: boolean\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 /** The mail gateway in front of their domain (AGL-3326); `null` when it could not be read. */\n gateway: OutreachEnrollGateway | null\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 * A TEST OF ONE STEP (AGL-3325)\n *==========================================*/\n\n/**\n * `POST outreach/steps/test` — one step of a saved sequence, rendered\n * exactly as `outreach/preview` renders it (the same person, personal\n * line and step) and sent through the sequence's mailbox to the member.\n */\nexport interface OutreachStepTestRequest extends OutreachPreviewRequest {\n /** Where the test goes; the member's own address when absent or empty. */\n to?: string\n}\n\nexport interface OutreachStepTestResponse {\n ok: true\n stepIndex: number\n /** The address the test went to. */\n sentTo: string\n /** The subject as sent, `[Test]` prefix included. */\n subject: string\n sentAtMs: number\n /** How many tests the mailbox has sent on its local day, this one included. */\n testsToday: number\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\n/**\n * `POST outreach/enrollments/gateway` (AGL-3332): the mail gateway in front\n * of one enrolled person's domain, for their page's Details.\n *\n * Read on the server because both halves are: the domain's MX lives in the\n * platform's `mailDomains` cache (AGL-3328), which no member reads, and its\n * standing is the ledger of the sequence mailbox's sending domain.\n */\nexport interface OutreachEnrollmentGatewayResponse {\n ok: true\n /** The person's domain; `null` for an address without one. */\n domain: string | null\n /** The domain the enrollment's mailbox sends from, whose ledger `standing` reads. */\n sendingDomain: string | null\n /** The domain's MX, classified; `null` when it could not be looked up. */\n intel: {\n status: 'mx' | 'implicit_mx' | 'null_mx' | 'no_mx'\n mx: string[]\n gateway: OutreachGatewayStanding['gateway']\n resolvedAtMs: number\n } | null\n /** What that gateway did with mail from the sending domain; `null` when unread. */\n standing: OutreachGatewayStanding | null\n}\n"],"names":["OUTREACH_ENROLL_BATCH_MAX"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;CAQC,GA2HD;;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 { OutreachGatewayStanding } from '../engine/mail-gateway'\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 | 'rate-limited'\n /** The Google account has no Gmail service: off for it, or not yet provisioned. */\n | 'mail-service-unavailable'\n /** Google refused the account itself, such as an administrator barring Gmail API access. */\n | 'google-refused'\n | 'google-unavailable'\n | 'curation-unavailable'\n | 'curation-refused'\n | 'invalid-override'\n /** The step to begin enrolling at is not one the sequence has (AGL-3228). */\n | 'invalid-start-step'\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\n/**\n * The mail gateway in front of one person's domain (AGL-3326), and what it\n * did with this organization's mail this week — what the Check-people chip\n * shows — with `hold` set when the engine would hold the send into it: the\n * gateway refused the sender twice in the last thirty days and delivered\n * nothing. A held person is not blocked; they default to un-ticked, and\n * ticking them is the member's say-so.\n */\nexport interface OutreachEnrollGateway extends OutreachGatewayStanding {\n hold: boolean\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 /** The mail gateway in front of their domain (AGL-3326); `null` when it could not be read. */\n gateway: OutreachEnrollGateway | null\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 * The step everyone begins at (AGL-3228), zero-based; the first when\n * absent. For people who already had the earlier steps by hand: those are\n * marked skipped, and this step goes out after its own delay, counted from\n * the enrollment. A step the sequence does not have refuses the request.\n */\n startStepIndex?: number\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 * The step the enrollment would begin at (AGL-3228), zero-based: the\n * first email written is the first one sent from there.\n */\n startStepIndex?: 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\n/*==========================================\n * A TEST OF ONE STEP (AGL-3325)\n *==========================================*/\n\n/**\n * `POST outreach/steps/test` — one step of a saved sequence, rendered\n * exactly as `outreach/preview` renders it (the same person, personal\n * line and step) and sent through the sequence's mailbox to the member.\n */\nexport interface OutreachStepTestRequest extends OutreachPreviewRequest {\n /** Where the test goes; the member's own address when absent or empty. */\n to?: string\n}\n\nexport interface OutreachStepTestResponse {\n ok: true\n stepIndex: number\n /** The address the test went to. */\n sentTo: string\n /** The subject as sent, `[Test]` prefix included. */\n subject: string\n sentAtMs: number\n /** How many tests the mailbox has sent on its local day, this one included. */\n testsToday: number\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\n/**\n * `POST outreach/enrollments/gateway` (AGL-3332): the mail gateway in front\n * of one enrolled person's domain, for their page's Details.\n *\n * Read on the server because both halves are: the domain's MX lives in the\n * platform's `mailDomains` cache (AGL-3328), which no member reads, and its\n * standing is the ledger of the sequence mailbox's sending domain.\n */\nexport interface OutreachEnrollmentGatewayResponse {\n ok: true\n /** The person's domain; `null` for an address without one. */\n domain: string | null\n /** The domain the enrollment's mailbox sends from, whose ledger `standing` reads. */\n sendingDomain: string | null\n /** The domain's MX, classified; `null` when it could not be looked up. */\n intel: {\n status: 'mx' | 'implicit_mx' | 'null_mx' | 'no_mx'\n mx: string[]\n gateway: OutreachGatewayStanding['gateway']\n resolvedAtMs: number\n } | null\n /** What that gateway did with mail from the sending domain; `null` when unread. */\n standing: OutreachGatewayStanding | null\n}\n"],"names":["OUTREACH_ENROLL_BATCH_MAX"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;CAQC,GAiID;;4CAE4C,GAE5C,qDAAqD,GACrD,OAAO,MAAMA,4BAA4B,GAAE"}
|
|
@@ -735,6 +735,19 @@ export interface OutreachEnrollment extends OutreachTimestamps {
|
|
|
735
735
|
mailboxId: string;
|
|
736
736
|
/** The index into `steps` of the NEXT step to run. */
|
|
737
737
|
stepIndex: number;
|
|
738
|
+
/**
|
|
739
|
+
* The step the enrollment began at (AGL-3228), when it was not the first:
|
|
740
|
+
* the member enrolled someone who already had the earlier steps by hand.
|
|
741
|
+
* Absent on an enrollment that began at step 1. The first email sent from
|
|
742
|
+
* here starts the enrollment's thread, whatever `replyInThread` says —
|
|
743
|
+
* there is no earlier email of ours for it to answer.
|
|
744
|
+
*/
|
|
745
|
+
startStepIndex?: number;
|
|
746
|
+
/**
|
|
747
|
+
* The steps before {@link startStepIndex}, marked skipped at enrollment
|
|
748
|
+
* and never run (AGL-3228). Absent when none were.
|
|
749
|
+
*/
|
|
750
|
+
skippedSteps?: OutreachSkippedStep[];
|
|
738
751
|
/** When that step comes due, or `null` when nothing is waiting. */
|
|
739
752
|
nextDueAtMs: number | null;
|
|
740
753
|
status: OutreachEnrollmentStatus;
|
|
@@ -856,6 +869,18 @@ export interface OutreachSendClaim {
|
|
|
856
869
|
*/
|
|
857
870
|
messageId: string | null;
|
|
858
871
|
}
|
|
872
|
+
/**
|
|
873
|
+
* One step an enrollment skipped because it began past it (AGL-3228): the
|
|
874
|
+
* person already had it by hand, so the runtime never runs it. Kept apart
|
|
875
|
+
* from {@link OutreachStepRecord}, which every reader counts as a send.
|
|
876
|
+
*/
|
|
877
|
+
export interface OutreachSkippedStep {
|
|
878
|
+
stepIndex: number;
|
|
879
|
+
stepId: string;
|
|
880
|
+
kind: 'email' | 'task';
|
|
881
|
+
/** When it was marked skipped: the enrollment. */
|
|
882
|
+
atMs: number;
|
|
883
|
+
}
|
|
859
884
|
/** One step the runtime ran for an enrollment (AGL-2981). */
|
|
860
885
|
export interface OutreachStepRecord {
|
|
861
886
|
stepIndex: number;
|