@aglyn/plugins-outreach 1.0.0-beta.273 → 1.0.0-beta.275
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/README.md +7 -2
- package/package.json +14 -14
- package/src/lib/components/enrollment-actions.d.ts +11 -4
- package/src/lib/components/enrollment-actions.js +16 -0
- package/src/lib/components/enrollment-actions.js.map +1 -1
- package/src/lib/components/enrollment-detail.js +40 -13
- package/src/lib/components/enrollment-detail.js.map +1 -1
- package/src/lib/components/mailbox-card.js +13 -2
- package/src/lib/components/mailbox-card.js.map +1 -1
- package/src/lib/components/record-reply-button.d.ts +52 -0
- package/src/lib/components/record-reply-button.js +92 -0
- package/src/lib/components/record-reply-button.js.map +1 -0
- package/src/lib/components/reply-dialog.d.ts +66 -0
- package/src/lib/components/reply-dialog.js +353 -0
- package/src/lib/components/reply-dialog.js.map +1 -0
- package/src/lib/components/use-outreach-api.d.ts +5 -1
- package/src/lib/components/use-outreach-api.js +10 -0
- package/src/lib/components/use-outreach-api.js.map +1 -1
- package/src/lib/constants/api-routes.d.ts +8 -0
- package/src/lib/constants/api-routes.js +6 -0
- package/src/lib/constants/api-routes.js.map +1 -1
- package/src/lib/engine/index.d.ts +1 -0
- package/src/lib/engine/index.js +1 -0
- package/src/lib/engine/index.js.map +1 -1
- package/src/lib/engine/manual-reply.d.ts +108 -0
- package/src/lib/engine/manual-reply.js +301 -0
- package/src/lib/engine/manual-reply.js.map +1 -0
- package/src/lib/model/enrollment-timeline.d.ts +3 -1
- package/src/lib/model/enrollment-timeline.js +83 -7
- package/src/lib/model/enrollment-timeline.js.map +1 -1
- package/src/lib/model/outreach-api.d.ts +66 -2
- package/src/lib/model/outreach-api.js.map +1 -1
- package/src/lib/model/outreach.types.d.ts +85 -3
- package/src/lib/model/outreach.types.js +6 -0
- package/src/lib/model/outreach.types.js.map +1 -1
- package/src/lib/model/stored-records.d.ts +7 -1
- package/src/lib/model/stored-records.js +37 -1
- package/src/lib/model/stored-records.js.map +1 -1
- package/src/lib/plugin.d.ts +4 -0
- package/src/lib/plugin.js +17 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/routes/register-routes.d.ts +6 -0
- package/src/lib/routes/register-routes.js +18 -0
- package/src/lib/routes/register-routes.js.map +1 -1
- package/src/lib/routes/reply-routes.d.ts +76 -0
- package/src/lib/routes/reply-routes.js +563 -0
- package/src/lib/routes/reply-routes.js.map +1 -0
- package/src/lib/runtime/sync-job.js +14 -0
- package/src/lib/runtime/sync-job.js.map +1 -1
- package/src/lib/transport/send-message.d.ts +11 -0
- package/src/lib/transport/send-message.js +22 -0
- package/src/lib/transport/send-message.js.map +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/engine/manual-reply.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 * ANSWERING A PERSON WHO WROTE BACK (AGL-4020).\n *\n * A person who answers a sequence email is answered from the same mailbox,\n * in the same thread, by a member: the reply is a one-to-one email, never a\n * step of the sequence. This module is the engine's half of it — what the\n * person wrote, as the enrollment keeps it, and the answer, as the\n * transport sends it — and decides nothing about storage or delivery.\n *\n * ## What the person wrote\n *\n * {@link outreachInboundMessagesOf} keeps the messages of a thread the\n * classifier reads as theirs: a reply, an automatic answer, an opt-out. Each\n * carries its sender, its time, its `Message-ID` and `References` (what an\n * answer threads by), and its text with the quoted history trimmed, the way\n * the record's timeline keeps it (`emailExcerpt`).\n *\n * ## The answer\n *\n * {@link composeOutreachManualReply} writes it as every mail client writes a\n * reply: `Re:` the subject of the message it answers, `In-Reply-To` that\n * message's `Message-ID`, and `References` the whole chain — the sequence's\n * own sends, what the person's message already named, and the message\n * itself — so it lands in the same thread in the person's mailbox and in\n * ours. Plain text, the member's words first and the message answered quoted\n * beneath. No footer and no `List-Unsubscribe`: the person wrote first, and\n * this is the answer a person would type in their own mail client.\n *\n * ## What still refuses\n *\n * {@link outreachManualReplyRefusal}: an address or a domain on the\n * do-not-contact list, a person who opted out or whose address bounced, and\n * a mailbox that is paused, waiting to be reconnected, or gone. The cold\n * rules — the allowed countries, the company-domain rule, the personal line,\n * the daily cap — do not apply to someone who wrote first, and are not\n * asked.\n *==========================================*/\n\nimport { normalizeContactEmail } from '@aglyn/aglyn/app-utils/contacts'\nimport { CRM_EMAIL_SUBJECT_MAX } from '@aglyn/aglyn/app-utils/crm'\nimport { emailAddressOf, emailExcerpt, threadSubject } from '@aglyn/aglyn/app-utils/email-text'\nimport {\n OUTREACH_INBOUND_KINDS,\n OUTREACH_INBOUND_MESSAGES_MAX,\n type OutreachEnrollment,\n type OutreachInboundKind,\n type OutreachInboundMessage,\n type OutreachMailbox,\n} from '../model/outreach.types'\nimport { outreachMessageIdHeader } from './compose'\nimport { outreachEmailDomain } from './do-not-contact-domain'\nimport { classifyOutreachMessage, type OutreachMessageContext } from './thread-classification'\nimport { outreachHeader, type OutreachThreadMessage } from './thread-message'\n\n/** The most a member may write in one answer. */\nexport const OUTREACH_MANUAL_REPLY_BODY_MAX = 10_000\n\n/** The most of an answer the enrollment and the record keep: the timeline's excerpt length. */\nexport const OUTREACH_MANUAL_REPLY_STORED_MAX = 4_000\n\n/** The most `Message-ID`s one answer's `References` names: the first, and the latest. */\nconst REFERENCES_MAX = 20\n\n/** The display name in a `From` value — `\"Ada Lovelace\" <ada@example.com>` answers `Ada Lovelace` — or `''`. */\nexport function outreachDisplayNameOf(value: unknown): string {\n const raw = String(value ?? '').trim()\n const angled = raw.lastIndexOf('<')\n if (angled <= 0) return ''\n return raw\n .slice(0, angled)\n .trim()\n .replace(/^\"(.*)\"$/, '$1')\n .replace(/\\\\(.)/g, '$1')\n .trim()\n}\n\n/** Every `Message-ID` a `References` or `In-Reply-To` value names, in header form, in order, once each. */\nexport function outreachMessageIdList(value: unknown): string[] {\n const found: string[] = []\n for (const match of String(value ?? '').matchAll(/<[^<>\\s]+>/g)) {\n const id = outreachMessageIdHeader(match[0])\n if (id && !found.includes(id)) found.push(id)\n }\n return found\n}\n\n/** A message's text as the record keeps it: the text part, else the HTML's words, else the preview. */\nfunction bodyOf(message: OutreachThreadMessage): string {\n return emailExcerpt(message.textBody ?? '', message.htmlBody ?? '') || emailExcerpt(message.snippet ?? '')\n}\n\n/**\n * The person's own messages among a thread's — see the module note — oldest\n * first. A message with no sender address is left out: it could be neither\n * shown as theirs nor answered.\n */\nexport function outreachInboundMessagesOf(\n messages: readonly OutreachThreadMessage[],\n context: OutreachMessageContext,\n): OutreachInboundMessage[] {\n const kept: OutreachInboundMessage[] = []\n for (const message of messages) {\n if (!message?.id) continue\n const classification = classifyOutreachMessage(message, context)\n if (!(OUTREACH_INBOUND_KINDS as readonly string[]).includes(classification.kind)) continue\n const from = emailAddressOf(message.from)\n if (!from) continue\n const replyTo = emailAddressOf(outreachHeader(message, 'Reply-To'))\n kept.push({\n id: message.id,\n threadId: message.threadId ?? null,\n messageId: outreachMessageIdHeader(outreachHeader(message, 'Message-ID')),\n references: outreachMessageIdList(outreachHeader(message, 'References')),\n from,\n fromName: outreachDisplayNameOf(message.from),\n replyTo: replyTo && replyTo !== from ? replyTo : null,\n subject: String(message.subject ?? '').replace(/\\s+/g, ' ').trim(),\n atMs: classification.atMs,\n kind: classification.kind as OutreachInboundKind,\n body: bodyOf(message),\n })\n }\n return kept.sort((a, b) => a.atMs - b.atMs)\n}\n\n/**\n * What an enrollment keeps once more of the person's messages are read: one\n * entry per message — a re-read replaces the earlier copy — oldest first,\n * the last {@link OUTREACH_INBOUND_MESSAGES_MAX}.\n */\nexport function mergeOutreachInboundMessages(\n stored: readonly OutreachInboundMessage[] | null | undefined,\n found: readonly OutreachInboundMessage[],\n): OutreachInboundMessage[] {\n const byId = new Map<string, OutreachInboundMessage>()\n for (const message of [...(stored ?? []), ...found]) byId.set(message.id, message)\n return [...byId.values()].sort((a, b) => a.atMs - b.atMs).slice(-OUTREACH_INBOUND_MESSAGES_MAX)\n}\n\n/**\n * The message an answer replies to: the one named, when the enrollment\n * kept it, else the newest the person wrote. `null` when they wrote nothing\n * the enrollment kept.\n */\nexport function outreachMessageToAnswer(\n inbound: readonly OutreachInboundMessage[] | null | undefined,\n id?: string | null,\n): OutreachInboundMessage | null {\n const messages = [...(inbound ?? [])].sort((a, b) => a.atMs - b.atMs)\n if (id) return messages.find((message) => message.id === id) ?? null\n return messages.at(-1) ?? null\n}\n\n/** Whether a member may answer this enrollment at all: the person wrote something it kept, or a reply stopped it. */\nexport function outreachEnrollmentHasInbound(\n enrollment: Pick<OutreachEnrollment, 'inboundMessages' | 'stopReason' | 'status'>,\n): boolean {\n return Boolean(enrollment.inboundMessages?.length) || enrollment.stopReason === 'reply' || enrollment.status === 'replied'\n}\n\n/**\n * The answer's `References` — see the module note: the sequence's own sends,\n * then what the answered message named, then the message itself, each once.\n * A long chain keeps its first id and its latest, as RFC 5322 §3.6.4 allows.\n */\nexport function outreachReplyReferences(\n sentMessageIds: readonly string[],\n answering: Pick<OutreachInboundMessage, 'references' | 'messageId'>,\n): string[] {\n const chain: string[] = []\n for (const id of [...sentMessageIds, ...answering.references, answering.messageId]) {\n const header = outreachMessageIdHeader(id)\n if (header && !chain.includes(header)) chain.push(header)\n }\n return chain.length > REFERENCES_MAX ? [chain[0], ...chain.slice(-(REFERENCES_MAX - 1))] : chain\n}\n\n/** `Re:` the subject of the message answered, else of the thread the sequence started. */\nexport function outreachReplySubject(\n answering: Pick<OutreachInboundMessage, 'subject'>,\n enrollmentThreadSubject: string | null | undefined,\n): string {\n const base = threadSubject(answering.subject) || threadSubject(enrollmentThreadSubject)\n return (base ? `Re: ${base}` : 'Re:').slice(0, CRM_EMAIL_SUBJECT_MAX)\n}\n\n/** \"Tue, Oct 21, 2026, 8:02 AM\", in the mailbox's zone. */\nfunction quotedTime(atMs: number, timeZone: string | null | undefined): string {\n const options: Intl.DateTimeFormatOptions = {\n weekday: 'short',\n month: 'short',\n day: 'numeric',\n year: 'numeric',\n hour: 'numeric',\n minute: '2-digit',\n }\n // Plain spaces: newer ICU puts a narrow no-break space before \"AM\".\n const format = (zone: string) =>\n new Intl.DateTimeFormat('en-US', { ...options, timeZone: zone }).format(atMs).replace(/[\\u00a0\\u202f]/g, ' ')\n try {\n return format(timeZone || 'UTC')\n } catch {\n return format('UTC')\n }\n}\n\n/** The answered message beneath the answer, quoted the way mail clients quote: `On …, … wrote:` and `> ` lines. */\nexport function outreachReplyQuote(\n answering: Pick<OutreachInboundMessage, 'atMs' | 'from' | 'fromName' | 'body'>,\n timeZone: string | null | undefined,\n): string {\n const who = answering.fromName ? `${answering.fromName} <${answering.from}>` : answering.from\n const lines = answering.body.split('\\n').map((line) => (line ? `> ${line}` : '>'))\n return [`On ${quotedTime(answering.atMs, timeZone)}, ${who} wrote:`, ...lines].join('\\n')\n}\n\n/** An answer, written: what the transport sends and the records keep. */\nexport interface OutreachManualReply {\n to: string\n subject: string\n /** The whole text sent: the member's words, then the quote. */\n text: string\n /** The member's words alone, bounded, as the records keep them. */\n body: string\n /** The provider thread it is filed into; `null` lets the provider thread it by its headers. */\n threadId: string | null\n /** The answered message's `Message-ID`; `null` when it carried none. */\n inReplyTo: string | null\n references: string[]\n}\n\nexport type OutreachManualReplyErrorCode = 'empty' | 'too_long' | 'invalid_recipient'\n\nexport interface OutreachManualReplyCompose {\n reply: OutreachManualReply | null\n error: { code: OutreachManualReplyErrorCode; message: string } | null\n}\n\n/** Writes the answer — see the module note. */\nexport function composeOutreachManualReply(input: {\n enrollment: Pick<OutreachEnrollment, 'messageIds' | 'threadSubject'>\n answering: OutreachInboundMessage\n body: unknown\n /** The mailbox's zone, for the quote's time. */\n timeZone: string | null | undefined\n}): OutreachManualReplyCompose {\n const words = String(input.body ?? '')\n .replace(/\\r\\n?/g, '\\n')\n .replace(/[ \\t]+$/gm, '')\n .replace(/^\\n+|\\n+$/g, '')\n if (!words.trim()) return { reply: null, error: { code: 'empty', message: 'Write your answer before sending it.' } }\n if (words.length > OUTREACH_MANUAL_REPLY_BODY_MAX) {\n return {\n reply: null,\n error: {\n code: 'too_long',\n message: `An answer can be at most ${OUTREACH_MANUAL_REPLY_BODY_MAX.toLocaleString('en-US')} characters.`,\n },\n }\n }\n const to = normalizeContactEmail(input.answering.replyTo ?? input.answering.from)\n if (!to) {\n return { reply: null, error: { code: 'invalid_recipient', message: 'Their message names no address to answer.' } }\n }\n return {\n reply: {\n to,\n subject: outreachReplySubject(input.answering, input.enrollment.threadSubject),\n text: `${words}\\n\\n${outreachReplyQuote(input.answering, input.timeZone)}`,\n body: words.slice(0, OUTREACH_MANUAL_REPLY_STORED_MAX),\n threadId: input.answering.threadId,\n inReplyTo: input.answering.messageId,\n references: outreachReplyReferences(input.enrollment.messageIds ?? [], input.answering),\n },\n error: null,\n }\n}\n\nexport type OutreachManualReplyRefusalCode =\n | 'opted-out'\n | 'bounced'\n | 'do-not-contact'\n | 'unchecked'\n | 'mailbox-unavailable'\n | 'permission'\n\nexport interface OutreachManualReplyRefusal {\n code: OutreachManualReplyRefusalCode\n message: string\n}\n\n/** The address a mailbox sends as. */\nexport const outreachMailboxAddress = (mailbox: Pick<OutreachMailbox, 'sendAs' | 'email'>): string =>\n mailbox.sendAs || mailbox.email\n\n/**\n * Why an answer cannot go now, in the member's words, or `null` — see the\n * module note. The person's own state first, then the lists, then the\n * mailbox, then who may send from it.\n */\nexport function outreachManualReplyRefusal(input: {\n enrollment: Pick<OutreachEnrollment, 'status' | 'email'>\n /** Every address the answer would reach, each looked up. */\n doNotContact: boolean | null\n doNotContactDomain: boolean | null\n mailbox: Pick<OutreachMailbox, 'status' | 'sendAs' | 'email'> | null\n /** The member is the mailbox's own, or an organization owner or admin. */\n mayUseMailbox: boolean\n}): OutreachManualReplyRefusal | null {\n const { enrollment, mailbox } = input\n if (enrollment.status === 'opted_out') {\n return { code: 'opted-out', message: 'They asked not to be emailed, so no answer can be sent to them.' }\n }\n if (enrollment.status === 'bounced') {\n return { code: 'bounced', message: 'Their address bounced, so an answer would not reach them.' }\n }\n if (input.doNotContact === true) {\n return {\n code: 'do-not-contact',\n message: `${enrollment.email} is on your do-not-contact list. Take it off the list on the Compliance page to answer them.`,\n }\n }\n if (input.doNotContactDomain === true) {\n const domain = outreachEmailDomain(enrollment.email) ?? 'Their domain'\n return {\n code: 'do-not-contact',\n message: `${domain} is on your do-not-contact list. Take it off the list on the Compliance page to answer them.`,\n }\n }\n if (input.doNotContact === null || input.doNotContactDomain === null) {\n return { code: 'unchecked', message: 'The do-not-contact list could not be checked. Try again in a moment.' }\n }\n if (!mailbox) {\n return {\n code: 'mailbox-unavailable',\n message: 'The mailbox that wrote to them is no longer connected, so the answer cannot go from it.',\n }\n }\n const address = outreachMailboxAddress(mailbox)\n if (mailbox.status === 'paused') {\n return {\n code: 'mailbox-unavailable',\n message: `${address} is paused. Resume it on the Mailboxes page to answer from it.`,\n }\n }\n if (mailbox.status !== 'connected') {\n return {\n code: 'mailbox-unavailable',\n message: `${address} needs to be connected again. Reconnect it on the Mailboxes page to answer from it.`,\n }\n }\n if (!input.mayUseMailbox) {\n return {\n code: 'permission',\n message: `Only the member who connected ${address}, or an organization owner or admin, can answer from it.`,\n }\n }\n return null\n}\n"],"names":["normalizeContactEmail","CRM_EMAIL_SUBJECT_MAX","emailAddressOf","emailExcerpt","threadSubject","OUTREACH_INBOUND_KINDS","OUTREACH_INBOUND_MESSAGES_MAX","outreachMessageIdHeader","outreachEmailDomain","classifyOutreachMessage","outreachHeader","OUTREACH_MANUAL_REPLY_BODY_MAX","OUTREACH_MANUAL_REPLY_STORED_MAX","REFERENCES_MAX","outreachDisplayNameOf","value","raw","String","trim","angled","lastIndexOf","slice","replace","outreachMessageIdList","found","match","matchAll","id","includes","push","bodyOf","message","textBody","htmlBody","snippet","outreachInboundMessagesOf","messages","context","kept","classification","kind","from","replyTo","threadId","messageId","references","fromName","subject","atMs","body","sort","a","b","mergeOutreachInboundMessages","stored","byId","Map","set","values","outreachMessageToAnswer","inbound","find","at","outreachEnrollmentHasInbound","enrollment","Boolean","inboundMessages","length","stopReason","status","outreachReplyReferences","sentMessageIds","answering","chain","header","outreachReplySubject","enrollmentThreadSubject","base","quotedTime","timeZone","options","weekday","month","day","year","hour","minute","format","zone","Intl","DateTimeFormat","outreachReplyQuote","who","lines","split","map","line","join","composeOutreachManualReply","input","words","reply","error","code","toLocaleString","to","text","inReplyTo","messageIds","outreachMailboxAddress","mailbox","sendAs","email","outreachManualReplyRefusal","doNotContact","doNotContactDomain","domain","address","mayUseMailbox"],"mappings":"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"}
|
|
@@ -20,7 +20,7 @@ import type { OutreachOpenSource } from '../engine/open-source';
|
|
|
20
20
|
import { type OutreachMailGateway } from '../engine/mail-gateway';
|
|
21
21
|
import { type OutreachEnrollment, type OutreachEnrollmentHistoryEntry, type OutreachSequenceStep } from './outreach.types';
|
|
22
22
|
/** What kind of thing an entry is, which the view draws an icon for. */
|
|
23
|
-
export type OutreachTimelineKind = 'enrolled' | 'skipped' | 'curated' | 'sent' | 'task' | 'click' | 'scanner' | 'earlier-clicks' | 'open' | 'machine-open' | 'paused' | 'resumed' | 'stopped' | 'do-not-contact' | 'replied' | 'bounced' | 'opted-out' | 'held' | 'released' | 'failed' | 'finished';
|
|
23
|
+
export type OutreachTimelineKind = 'enrolled' | 'skipped' | 'curated' | 'sent' | 'task' | 'click' | 'scanner' | 'earlier-clicks' | 'open' | 'machine-open' | 'paused' | 'resumed' | 'stopped' | 'do-not-contact' | 'replied' | 'auto-reply' | 'answered' | 'bounced' | 'opted-out' | 'held' | 'released' | 'failed' | 'finished';
|
|
24
24
|
/** How loudly the view draws an entry. */
|
|
25
25
|
export type OutreachTimelineTone = 'default' | 'success' | 'info' | 'warning' | 'error';
|
|
26
26
|
/** One line of the timeline. */
|
|
@@ -81,6 +81,8 @@ export interface OutreachEnrollmentFigures {
|
|
|
81
81
|
opens: number;
|
|
82
82
|
machineOpens: number;
|
|
83
83
|
replies: number;
|
|
84
|
+
/** Answers members sent them in the thread (AGL-4020), never counted in `emailsSent`. */
|
|
85
|
+
answers: number;
|
|
84
86
|
/** Whether anything has happened to them beyond being enrolled. */
|
|
85
87
|
anything: boolean;
|
|
86
88
|
}
|
|
@@ -36,6 +36,11 @@
|
|
|
36
36
|
* - **The enrollment**, with who enrolled them, and each curated step.
|
|
37
37
|
* - **The steps it skipped** (AGL-3228), when it began past the first: one
|
|
38
38
|
* entry naming them, never a "sent" — nothing of ours went out for them.
|
|
39
|
+
* - **What the person wrote** (AGL-4020): each reply, automatic answer and
|
|
40
|
+
* opt-out the enrollment kept, with their words. The one that stopped the
|
|
41
|
+
* sequence carries the stop, in place of a second entry saying it.
|
|
42
|
+
* - **Each answer a member sent** in the thread (AGL-4020), with their
|
|
43
|
+
* words and who sent it — never counted as one of the sequence's sends.
|
|
39
44
|
*
|
|
40
45
|
* Client-safe and free of React: the view renders these entries, and the
|
|
41
46
|
* spec reads them.
|
|
@@ -88,6 +93,8 @@ const ACTION_KINDS = {
|
|
|
88
93
|
/** Which of two entries at the same moment reads first: the outcome above what led to it. */ const TIE_ORDER = {
|
|
89
94
|
finished: 0,
|
|
90
95
|
replied: 1,
|
|
96
|
+
'auto-reply': 4,
|
|
97
|
+
answered: 5,
|
|
91
98
|
bounced: 1,
|
|
92
99
|
'opted-out': 1,
|
|
93
100
|
failed: 1,
|
|
@@ -114,10 +121,45 @@ const byWhom = (uid, memberName)=>uid ? `By ${memberName(uid)}` : null;
|
|
|
114
121
|
var _enrollment_stepRecords;
|
|
115
122
|
return ((_enrollment_stepRecords = enrollment.stepRecords) != null ? _enrollment_stepRecords : []).filter((record)=>(record == null ? void 0 : record.kind) === 'email');
|
|
116
123
|
}
|
|
124
|
+
/** Who wrote a message, as the timeline names them: their name, else their address. */ const senderOf = (message)=>message.fromName || message.from;
|
|
125
|
+
/** The entry for one message the person wrote (AGL-4020). */ function inboundEntry(message, index, stopped) {
|
|
126
|
+
const facts = [
|
|
127
|
+
message.fromName ? `From ${message.from}` : null,
|
|
128
|
+
message.subject ? `Subject: ${message.subject}` : null,
|
|
129
|
+
stopped ? 'The sequence stopped for them' : null
|
|
130
|
+
].filter((fact)=>Boolean(fact));
|
|
131
|
+
const base = {
|
|
132
|
+
key: `inbound-${index}`,
|
|
133
|
+
atMs: message.atMs,
|
|
134
|
+
detail: message.body || null,
|
|
135
|
+
facts,
|
|
136
|
+
gmailThreadId: message.threadId
|
|
137
|
+
};
|
|
138
|
+
switch(message.kind){
|
|
139
|
+
case 'auto_reply':
|
|
140
|
+
return _extends({}, base, {
|
|
141
|
+
kind: 'auto-reply',
|
|
142
|
+
title: `Automatic reply from ${senderOf(message)}`,
|
|
143
|
+
tone: 'default'
|
|
144
|
+
});
|
|
145
|
+
case 'opt_out':
|
|
146
|
+
return _extends({}, base, {
|
|
147
|
+
kind: 'opted-out',
|
|
148
|
+
title: `${senderOf(message)} asked not to be emailed`,
|
|
149
|
+
tone: 'warning'
|
|
150
|
+
});
|
|
151
|
+
default:
|
|
152
|
+
return _extends({}, base, {
|
|
153
|
+
kind: 'replied',
|
|
154
|
+
title: `Reply from ${senderOf(message)}`,
|
|
155
|
+
tone: 'info'
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
117
159
|
/**
|
|
118
160
|
* The timeline, newest first — see the module note.
|
|
119
161
|
*/ export function outreachEnrollmentTimeline(input) {
|
|
120
|
-
var _input_history, _enrollment_skippedSteps, _enrollment_stepOverrides, _enrollment_stepRecords;
|
|
162
|
+
var _input_history, _enrollment_skippedSteps, _enrollment_stepOverrides, _enrollment_stepRecords, _enrollment_inboundMessages, _enrollment_inboundMessages1;
|
|
121
163
|
const { enrollment, steps, memberName } = input;
|
|
122
164
|
const entries = [];
|
|
123
165
|
const history = (_input_history = input.history) != null ? _input_history : [];
|
|
@@ -173,6 +215,25 @@ const byWhom = (uid, memberName)=>uid ? `By ${memberName(uid)}` : null;
|
|
|
173
215
|
var _record_gmailThreadId;
|
|
174
216
|
var _record_links;
|
|
175
217
|
if (!record || typeof record.atMs !== 'number') return;
|
|
218
|
+
// A member's answer in the thread (AGL-4020): their words, never a step.
|
|
219
|
+
if (record.kind === 'manual_reply') {
|
|
220
|
+
var _record_gmailThreadId1;
|
|
221
|
+
entries.push({
|
|
222
|
+
key: `answered-${index}`,
|
|
223
|
+
kind: 'answered',
|
|
224
|
+
atMs: record.atMs,
|
|
225
|
+
title: 'Answered in the thread',
|
|
226
|
+
detail: record.body || null,
|
|
227
|
+
facts: [
|
|
228
|
+
byWhom(record.byUid, memberName),
|
|
229
|
+
record.from ? `From ${record.from}` : null,
|
|
230
|
+
record.subject ? `Subject: ${record.subject}` : null
|
|
231
|
+
].filter((fact)=>Boolean(fact)),
|
|
232
|
+
gmailThreadId: (_record_gmailThreadId1 = record.gmailThreadId) != null ? _record_gmailThreadId1 : null,
|
|
233
|
+
tone: 'default'
|
|
234
|
+
});
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
176
237
|
if (record.kind === 'task') {
|
|
177
238
|
var _steps_find;
|
|
178
239
|
const step = (_steps_find = steps.find((entry)=>entry.id === record.stepId)) != null ? _steps_find : steps[record.stepIndex];
|
|
@@ -345,9 +406,16 @@ const byWhom = (uid, memberName)=>uid ? `By ${memberName(uid)}` : null;
|
|
|
345
406
|
tone: 'default'
|
|
346
407
|
});
|
|
347
408
|
}
|
|
348
|
-
// ──
|
|
409
|
+
// ── What the person wrote (AGL-4020) ────────────────────────────────────
|
|
349
410
|
const stoppedAtMs = enrollment.stoppedAtMs;
|
|
350
|
-
|
|
411
|
+
// The message that stopped the sequence carries the stop, so the person's
|
|
412
|
+
// words and what they did to the sequence read as one entry.
|
|
413
|
+
const stoppedByMessage = stoppedAtMs !== null && (enrollment.stopReason === 'reply' || enrollment.stopReason === 'opt_out_reply') && ((_enrollment_inboundMessages = enrollment.inboundMessages) != null ? _enrollment_inboundMessages : []).some((message)=>message.atMs === stoppedAtMs);
|
|
414
|
+
((_enrollment_inboundMessages1 = enrollment.inboundMessages) != null ? _enrollment_inboundMessages1 : []).forEach((message, index)=>{
|
|
415
|
+
entries.push(inboundEntry(message, index, stoppedByMessage && message.atMs === stoppedAtMs));
|
|
416
|
+
});
|
|
417
|
+
// ── The current stop, from the enrollment ───────────────────────────────
|
|
418
|
+
const alreadySaid = stoppedByMessage || stoppedAtMs !== null && (actionTimes.has(stoppedAtMs) || enrollment.stopReason === 'gateway_blocked_here' && (hold == null ? void 0 : hold.heldAtMs) === stoppedAtMs);
|
|
351
419
|
if (stoppedAtMs && !alreadySaid && enrollment.status !== 'active' && enrollment.status !== 'finished') {
|
|
352
420
|
const stop = currentStop(enrollment, memberName, gatewayName);
|
|
353
421
|
if (stop) entries.push(_extends({
|
|
@@ -359,7 +427,8 @@ const byWhom = (uid, memberName)=>uid ? `By ${memberName(uid)}` : null;
|
|
|
359
427
|
if (enrollment.status === 'finished') {
|
|
360
428
|
var _enrollment_stepRecords1, _ref6;
|
|
361
429
|
var _enrollment_skippedSteps1;
|
|
362
|
-
|
|
430
|
+
// The last step the sequence ran: a member's answer after it is no step.
|
|
431
|
+
const last = ((_enrollment_stepRecords1 = enrollment.stepRecords) != null ? _enrollment_stepRecords1 : []).filter((record)=>(record == null ? void 0 : record.kind) !== 'manual_reply').at(-1);
|
|
363
432
|
entries.push({
|
|
364
433
|
key: 'finished',
|
|
365
434
|
kind: 'finished',
|
|
@@ -471,11 +540,17 @@ const byWhom = (uid, memberName)=>uid ? `By ${memberName(uid)}` : null;
|
|
|
471
540
|
}
|
|
472
541
|
}
|
|
473
542
|
export function outreachEnrollmentFigures(enrollment) {
|
|
474
|
-
var _enrollment_stepRecords;
|
|
543
|
+
var _enrollment_inboundMessages, _enrollment_stepRecords, _enrollment_stepRecords1;
|
|
544
|
+
var _enrollment_inboundMessages1;
|
|
475
545
|
const clicks = outreachClickSummary(enrollment.engagement);
|
|
476
546
|
const engagement = readOutreachEngagement(enrollment.engagement);
|
|
477
547
|
const emailsSent = emailRecords(enrollment).length;
|
|
478
|
-
const
|
|
548
|
+
const stoppedByReply = enrollment.status === 'replied' || enrollment.stopReason === 'reply' || enrollment.stopReason === 'opt_out_reply';
|
|
549
|
+
// Every reply they wrote that the enrollment kept (AGL-4020); one for a
|
|
550
|
+
// reply that stopped it before replies were kept.
|
|
551
|
+
const kept = ((_enrollment_inboundMessages = enrollment.inboundMessages) != null ? _enrollment_inboundMessages : []).filter((message)=>message.kind !== 'auto_reply').length;
|
|
552
|
+
const replies = Math.max(kept, stoppedByReply ? 1 : 0);
|
|
553
|
+
const answers = ((_enrollment_stepRecords = enrollment.stepRecords) != null ? _enrollment_stepRecords : []).filter((record)=>(record == null ? void 0 : record.kind) === 'manual_reply').length;
|
|
479
554
|
return {
|
|
480
555
|
emailsSent,
|
|
481
556
|
clicks: clicks.clicks,
|
|
@@ -490,7 +565,8 @@ export function outreachEnrollmentFigures(enrollment) {
|
|
|
490
565
|
opens: engagement.opens,
|
|
491
566
|
machineOpens: engagement.machineOpens,
|
|
492
567
|
replies,
|
|
493
|
-
|
|
568
|
+
answers,
|
|
569
|
+
anything: emailsSent > 0 || ((_enrollment_stepRecords1 = enrollment.stepRecords) != null ? _enrollment_stepRecords1 : []).length > 0 || clicks.clicks > 0 || clicks.machineClicks > 0 || engagement.opens > 0 || engagement.machineOpens > 0 || Boolean((_enrollment_inboundMessages1 = enrollment.inboundMessages) == null ? void 0 : _enrollment_inboundMessages1.length) || enrollment.stoppedAtMs !== null
|
|
494
570
|
};
|
|
495
571
|
}
|
|
496
572
|
|
|
@@ -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 * - **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 type { OutreachOpenSource } from '../engine/open-source'\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 scanner: 'a mail provider’s or a security product’s scanner fetched it, not the person',\n}\n\n/** Where a fetch of the tracking image came from (AGL-3488), as the timeline says it. */\nexport const OUTREACH_OPEN_SOURCE_LABELS: Record<OutreachOpenSource, string> = {\n google: 'From Google’s network',\n microsoft_filter: 'From Microsoft’s mail filter',\n microsoft: 'From Microsoft’s mail servers',\n apple: 'From Apple’s privacy relay',\n yahoo: 'From Yahoo’s mail proxy',\n other: 'From an address outside the mail providers’ networks',\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: [\n outreachStepName(steps, row.stepIndex),\n row.source ? OUTREACH_OPEN_SOURCE_LABELS[row.source] : null,\n ].filter((fact): fact is string => Boolean(fact)),\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 row.source ? OUTREACH_OPEN_SOURCE_LABELS[row.source] : 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","scanner","OUTREACH_OPEN_SOURCE_LABELS","google","microsoft_filter","microsoft","apple","yahoo","other","ACTION_TITLES","pause","resume","stop","do_not_contact","ACTION_KINDS","TIE_ORDER","finished","replied","bounced","failed","stopped","paused","held","released","resumed","click","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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"}
|
|
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 * - **What the person wrote** (AGL-4020): each reply, automatic answer and\n * opt-out the enrollment kept, with their words. The one that stopped the\n * sequence carries the stop, in place of a second entry saying it.\n * - **Each answer a member sent** in the thread (AGL-4020), with their\n * words and who sent it — never counted as one of the sequence's sends.\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 type { OutreachOpenSource } from '../engine/open-source'\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 OutreachInboundMessage,\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 | 'auto-reply'\n | 'answered'\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 scanner: 'a mail provider’s or a security product’s scanner fetched it, not the person',\n}\n\n/** Where a fetch of the tracking image came from (AGL-3488), as the timeline says it. */\nexport const OUTREACH_OPEN_SOURCE_LABELS: Record<OutreachOpenSource, string> = {\n google: 'From Google’s network',\n microsoft_filter: 'From Microsoft’s mail filter',\n microsoft: 'From Microsoft’s mail servers',\n apple: 'From Apple’s privacy relay',\n yahoo: 'From Yahoo’s mail proxy',\n other: 'From an address outside the mail providers’ networks',\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 'auto-reply': 4,\n answered: 5,\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/** Who wrote a message, as the timeline names them: their name, else their address. */\nconst senderOf = (message: Pick<OutreachInboundMessage, 'fromName' | 'from'>) => message.fromName || message.from\n\n/** The entry for one message the person wrote (AGL-4020). */\nfunction inboundEntry(message: OutreachInboundMessage, index: number, stopped: boolean): OutreachTimelineEntry {\n const facts = [\n message.fromName ? `From ${message.from}` : null,\n message.subject ? `Subject: ${message.subject}` : null,\n stopped ? 'The sequence stopped for them' : null,\n ].filter((fact): fact is string => Boolean(fact))\n const base = { key: `inbound-${index}`, atMs: message.atMs, detail: message.body || null, facts, gmailThreadId: message.threadId }\n switch (message.kind) {\n case 'auto_reply':\n return { ...base, kind: 'auto-reply', title: `Automatic reply from ${senderOf(message)}`, tone: 'default' }\n case 'opt_out':\n return { ...base, kind: 'opted-out', title: `${senderOf(message)} asked not to be emailed`, tone: 'warning' }\n default:\n return { ...base, kind: 'replied', title: `Reply from ${senderOf(message)}`, tone: 'info' }\n }\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 // A member's answer in the thread (AGL-4020): their words, never a step.\n if (record.kind === 'manual_reply') {\n entries.push({\n key: `answered-${index}`,\n kind: 'answered',\n atMs: record.atMs,\n title: 'Answered in the thread',\n detail: record.body || null,\n facts: [\n byWhom(record.byUid, memberName),\n record.from ? `From ${record.from}` : null,\n record.subject ? `Subject: ${record.subject}` : null,\n ].filter((fact): fact is string => Boolean(fact)),\n gmailThreadId: record.gmailThreadId ?? null,\n tone: 'default',\n })\n return\n }\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: [\n outreachStepName(steps, row.stepIndex),\n row.source ? OUTREACH_OPEN_SOURCE_LABELS[row.source] : null,\n ].filter((fact): fact is string => Boolean(fact)),\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 row.source ? OUTREACH_OPEN_SOURCE_LABELS[row.source] : 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 // ── What the person wrote (AGL-4020) ────────────────────────────────────\n const stoppedAtMs = enrollment.stoppedAtMs\n // The message that stopped the sequence carries the stop, so the person's\n // words and what they did to the sequence read as one entry.\n const stoppedByMessage =\n stoppedAtMs !== null &&\n (enrollment.stopReason === 'reply' || enrollment.stopReason === 'opt_out_reply') &&\n (enrollment.inboundMessages ?? []).some((message) => message.atMs === stoppedAtMs)\n ;(enrollment.inboundMessages ?? []).forEach((message, index) => {\n entries.push(inboundEntry(message, index, stoppedByMessage && message.atMs === stoppedAtMs))\n })\n\n // ── The current stop, from the enrollment ───────────────────────────────\n const alreadySaid =\n stoppedByMessage ||\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 // The last step the sequence ran: a member's answer after it is no step.\n const last = (enrollment.stepRecords ?? []).filter((record) => record?.kind !== 'manual_reply').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 /** Answers members sent them in the thread (AGL-4020), never counted in `emailsSent`. */\n answers: 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 stoppedByReply =\n enrollment.status === 'replied' || enrollment.stopReason === 'reply' || enrollment.stopReason === 'opt_out_reply'\n // Every reply they wrote that the enrollment kept (AGL-4020); one for a\n // reply that stopped it before replies were kept.\n const kept = (enrollment.inboundMessages ?? []).filter((message) => message.kind !== 'auto_reply').length\n const replies = Math.max(kept, stoppedByReply ? 1 : 0)\n const answers = (enrollment.stepRecords ?? []).filter((record) => record?.kind === 'manual_reply').length\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 answers,\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 Boolean(enrollment.inboundMessages?.length) ||\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","scanner","OUTREACH_OPEN_SOURCE_LABELS","google","microsoft_filter","microsoft","apple","yahoo","other","ACTION_TITLES","pause","resume","stop","do_not_contact","ACTION_KINDS","TIE_ORDER","finished","replied","answered","bounced","failed","stopped","paused","held","released","resumed","click","open","task","sent","curated","skipped","enrolled","plural","count","one","many","byWhom","uid","memberName","emailRecords","enrollment","stepRecords","record","senderOf","message","fromName","from","inboundEntry","index","facts","subject","fact","Boolean","base","key","atMs","detail","body","gmailThreadId","threadId","title","tone","outreachEnrollmentTimeline","input","entries","history","gatewayName","gateway","createdAtMs","push","enrolledByUid","target","cold","skippedSteps","Number","isInteger","start","startStepIndex","map","override","Object","stepOverrides","draftedAtMs","source","edited","draftedByUid","previousThread","forEach","byUid","find","id","stepId","taskId","thread","startsThread","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","hold","holdGateway","heldAtMs","stopReason","stoppedAtMs","stopDetail","releasedByUid","stoppedByMessage","inboundMessages","some","alreadySaid","has","status","currentStop","at","updatedAtMs","sort","a","b","by","stoppedByUid","values","gmailThreadIds","outreachEnrollmentFigures","emailsSent","stoppedByReply","kept","replies","Math","max","answers","linksFollowed","linkCount","followed","atLeast","linksCapped","scannerClicks","machineClicks","opens","machineOpens","anything"],"mappings":"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"}
|
|
@@ -25,9 +25,10 @@
|
|
|
25
25
|
*/
|
|
26
26
|
import type { OutreachComposeError } from '../engine/compose';
|
|
27
27
|
import type { OutreachGatewayStanding } from '../engine/mail-gateway';
|
|
28
|
+
import type { OutreachManualReplyRefusalCode } from '../engine/manual-reply';
|
|
28
29
|
import type { OutreachValidationIssue } from '../engine/sequence-validation';
|
|
29
30
|
import type { OutreachComplianceIssue } from './compliance-settings';
|
|
30
|
-
import type { OutreachAttestationKind, OutreachComplianceSettingsDocument, OutreachDoNotContactDomainEntry, OutreachEnrollment, OutreachSequence, OutreachStepOverrideSource } from './outreach.types';
|
|
31
|
+
import type { OutreachAttestationKind, OutreachComplianceSettingsDocument, OutreachDoNotContactDomainEntry, OutreachEnrollment, OutreachInboundMessage, OutreachMailbox, OutreachSequence, OutreachStepOverrideSource, OutreachStepRecord } from './outreach.types';
|
|
31
32
|
import type { OutreachSequenceDraft, OutreachSequenceIssue } from './sequence-draft';
|
|
32
33
|
/** Every reason these routes refuse with, for a caller to branch on. */
|
|
33
34
|
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'
|
|
@@ -36,7 +37,13 @@ export type OutreachRouteRefusalReason = 'unauthenticated' | 'email-unverified'
|
|
|
36
37
|
/** Google refused the account itself, such as an administrator barring Gmail API access. */
|
|
37
38
|
| 'google-refused' | 'google-unavailable' | 'curation-unavailable' | 'curation-refused' | 'invalid-override'
|
|
38
39
|
/** The step to begin enrolling at is not one the sequence has (AGL-3228). */
|
|
39
|
-
| 'invalid-start-step'
|
|
40
|
+
| 'invalid-start-step'
|
|
41
|
+
/** An answer to a person who wrote back cannot go now: a list, their state, the mailbox (AGL-4020). */
|
|
42
|
+
| 'reply-refused'
|
|
43
|
+
/** The enrollment kept nothing the person wrote, and the mailbox held none either (AGL-4020). */
|
|
44
|
+
| 'nothing-to-answer'
|
|
45
|
+
/** An answer to them is already being sent (AGL-4020). */
|
|
46
|
+
| 'reply-in-flight';
|
|
40
47
|
/** A refusal, in the one shape every route answers with. */
|
|
41
48
|
export interface OutreachRouteRefusal {
|
|
42
49
|
/** A sentence written for the person reading it. */
|
|
@@ -264,6 +271,63 @@ export interface OutreachEnrollmentActionResponse {
|
|
|
264
271
|
/** Other open enrollments of the same address a do-not-contact stopped. */
|
|
265
272
|
stoppedOthers: number;
|
|
266
273
|
}
|
|
274
|
+
/** `POST outreach/enrollments/thread` — what the reply composer opens with. */
|
|
275
|
+
export interface OutreachEnrollmentThreadRequest {
|
|
276
|
+
orgId: string;
|
|
277
|
+
enrollmentId: string;
|
|
278
|
+
}
|
|
279
|
+
/** Where an answer would go, from what, under what subject. */
|
|
280
|
+
export interface OutreachReplyDraft {
|
|
281
|
+
/** The provider id of the message it answers: the newest the person wrote. */
|
|
282
|
+
answerId: string;
|
|
283
|
+
to: string;
|
|
284
|
+
subject: string;
|
|
285
|
+
from: {
|
|
286
|
+
address: string;
|
|
287
|
+
name: string;
|
|
288
|
+
provider: OutreachMailbox['provider'];
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
export interface OutreachEnrollmentThreadResponse {
|
|
292
|
+
ok: true;
|
|
293
|
+
/** The enrollment as stored, with anything the mailbox read added to it. */
|
|
294
|
+
enrollment: OutreachEnrollment;
|
|
295
|
+
/** What the person wrote, oldest first. */
|
|
296
|
+
inboundMessages: OutreachInboundMessage[];
|
|
297
|
+
/** `null` when they wrote nothing that could be read. */
|
|
298
|
+
draft: OutreachReplyDraft | null;
|
|
299
|
+
/** Why an answer cannot go now, in a sentence; `null` when it can. */
|
|
300
|
+
refusal: {
|
|
301
|
+
code: OutreachManualReplyRefusalCode;
|
|
302
|
+
message: string;
|
|
303
|
+
} | null;
|
|
304
|
+
/** Whether the mailbox was read for messages the enrollment had not kept. */
|
|
305
|
+
readMailbox: boolean;
|
|
306
|
+
}
|
|
307
|
+
/** `POST outreach/enrollments/reply` — sends a member's answer in the thread. */
|
|
308
|
+
export interface OutreachEnrollmentReplyRequest {
|
|
309
|
+
orgId: string;
|
|
310
|
+
enrollmentId: string;
|
|
311
|
+
/** What the member wrote: plain text. */
|
|
312
|
+
body: string;
|
|
313
|
+
/** The message it answers, by its provider id; the newest the person wrote when omitted. */
|
|
314
|
+
answerId?: string;
|
|
315
|
+
/**
|
|
316
|
+
* A key the composer mints once per answer, which becomes the answer's
|
|
317
|
+
* `Message-ID`: a retry of the same answer is recognised, never sent twice.
|
|
318
|
+
*/
|
|
319
|
+
replyKey?: string;
|
|
320
|
+
}
|
|
321
|
+
export interface OutreachEnrollmentReplyResponse {
|
|
322
|
+
ok: true;
|
|
323
|
+
enrollment: OutreachEnrollment;
|
|
324
|
+
/** The `manual_reply` record the answer was kept as. */
|
|
325
|
+
record: OutreachStepRecord;
|
|
326
|
+
sentTo: string;
|
|
327
|
+
subject: string;
|
|
328
|
+
/** The same answer had already been sent: nothing went out again. */
|
|
329
|
+
alreadySent: boolean;
|
|
330
|
+
}
|
|
267
331
|
/** `GET outreach/do-not-contact/domains?orgId` — and the answer to a change. */
|
|
268
332
|
export interface OutreachDoNotContactDomainsResponse {
|
|
269
333
|
ok: true;
|