@aglyn/plugins-outreach 1.0.0-beta.156 → 1.0.0-beta.157
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +13 -12
- package/src/lib/components/enrollments-table.d.ts +7 -0
- package/src/lib/components/enrollments-table.js +46 -2
- package/src/lib/components/enrollments-table.js.map +1 -1
- package/src/lib/components/sequence-detail.js +14 -4
- package/src/lib/components/sequence-detail.js.map +1 -1
- package/src/lib/components/sequence-editor.js +22 -0
- package/src/lib/components/sequence-editor.js.map +1 -1
- package/src/lib/components/sequence-report-card.d.ts +23 -0
- package/src/lib/components/sequence-report-card.js +191 -0
- package/src/lib/components/sequence-report-card.js.map +1 -0
- package/src/lib/components/use-outreach-data.d.ts +11 -0
- package/src/lib/components/use-outreach-data.js +24 -1
- package/src/lib/components/use-outreach-data.js.map +1 -1
- package/src/lib/constants/api-routes.d.ts +9 -0
- package/src/lib/constants/api-routes.js +9 -1
- package/src/lib/constants/api-routes.js.map +1 -1
- package/src/lib/engine/click-tracking.d.ts +98 -0
- package/src/lib/engine/click-tracking.js +252 -0
- package/src/lib/engine/click-tracking.js.map +1 -0
- package/src/lib/engine/compose.d.ts +24 -0
- package/src/lib/engine/compose.js +15 -2
- package/src/lib/engine/compose.js.map +1 -1
- package/src/lib/engine/sequence-validation.js +4 -1
- package/src/lib/engine/sequence-validation.js.map +1 -1
- package/src/lib/model/enrollment-engagement.d.ts +31 -0
- package/src/lib/model/enrollment-engagement.js +44 -0
- package/src/lib/model/enrollment-engagement.js.map +1 -0
- package/src/lib/model/outreach.types.d.ts +89 -0
- package/src/lib/model/outreach.types.js +25 -0
- package/src/lib/model/outreach.types.js.map +1 -1
- package/src/lib/model/sequence-draft.js +5 -1
- package/src/lib/model/sequence-draft.js.map +1 -1
- package/src/lib/model/sequence-report.d.ts +68 -0
- package/src/lib/model/sequence-report.js +118 -0
- package/src/lib/model/sequence-report.js.map +1 -0
- package/src/lib/model/stored-records.d.ts +11 -1
- package/src/lib/model/stored-records.js +34 -1
- package/src/lib/model/stored-records.js.map +1 -1
- package/src/lib/routes/sequence-routes.js +4 -1
- package/src/lib/routes/sequence-routes.js.map +1 -1
- package/src/lib/runtime/click-events.d.ts +67 -0
- package/src/lib/runtime/click-events.js +232 -0
- package/src/lib/runtime/click-events.js.map +1 -0
- package/src/lib/runtime/click-link.d.ts +94 -0
- package/src/lib/runtime/click-link.js +158 -0
- package/src/lib/runtime/click-link.js.map +1 -0
- package/src/lib/runtime/click-route.d.ts +19 -0
- package/src/lib/runtime/click-route.js +108 -0
- package/src/lib/runtime/click-route.js.map +1 -0
- package/src/lib/runtime/platform-runtime-deps.d.ts +2 -0
- package/src/lib/runtime/platform-runtime-deps.js +9 -0
- package/src/lib/runtime/platform-runtime-deps.js.map +1 -1
- package/src/lib/runtime/runtime-deps.d.ts +11 -0
- package/src/lib/runtime/runtime-deps.js.map +1 -1
- package/src/lib/runtime/send-job.js +65 -5
- package/src/lib/runtime/send-job.js.map +1 -1
- package/src/lib/server.js +8 -0
- package/src/lib/server.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/model/sequence-draft.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*==========================================\n * A SEQUENCE AS THE EDITOR HOLDS IT (AGL-2980).\n *\n * The editor builds a draft, the save route reads the draft it was sent\n * through the same reader, and the engine's validator judges both — so the\n * issues the editor shows beside a field are the ones the route would\n * refuse the save with. Client-safe.\n *\n * ## Once someone is enrolled, the steps they stand on stay put\n *\n * An enrollment points at its next step by POSITION (`stepIndex`), and it\n * was enrolled into a site's CRM from one mailbox. So after the first\n * enrollment, a save may edit any step's wording and delay and may add\n * steps at the end, but may not remove, reorder or change the kind of a\n * step that already exists, and may not move the sequence to another site\n * or mailbox: each of those would send an enrolled person a step they were\n * never going to get, from an address they never heard from. A new\n * sequence is the way to change them.\n *==========================================*/\n\nimport {\n readOutreachSequenceSettings,\n type OutreachValidationIssue,\n type OutreachValidationCode,\n} from '../engine/sequence-validation'\nimport {\n type OutreachEmailStep,\n type OutreachMailbox,\n type OutreachSendWindow,\n type OutreachSequence,\n type OutreachSequenceSettings,\n type OutreachSequenceStep,\n type OutreachTaskKind,\n type OutreachTaskStep,\n} from './outreach.types'\n\n/** A sequence as the editor holds it and the save route receives it. */\nexport interface OutreachSequenceDraft {\n name: string\n /** The site whose CRM the enrolled people are records of. */\n hostId: string\n /** `''` until a mailbox is chosen. */\n mailboxId: string\n steps: OutreachSequenceStep[]\n settings: OutreachSequenceSettings\n}\n\n/** The most steps a draft is read with; the validator refuses past the real limit. */\nconst STEPS_READ_MAX = 20\n\n/** The delay a new email after the first starts with, in business days. */\nexport const OUTREACH_DEFAULT_FOLLOW_UP_DAYS = 3\n\nconst text = (value: unknown): string => (typeof value === 'string' ? value : '')\n\n/** A whole number, or `NaN` for anything else, so the validator names it. */\nconst wholeNumber = (value: unknown): number => {\n const number = typeof value === 'number' ? value : Number(text(value) || NaN)\n return Number.isInteger(number) ? number : Number.NaN\n}\n\nfunction readWindow(value: unknown): OutreachSendWindow | null {\n if (!value || typeof value !== 'object') return null\n const raw = value as Record<string, unknown>\n return {\n days: Array.isArray(raw['days']) ? raw['days'].map((day) => wholeNumber(day)) : [],\n startMinute: wholeNumber(raw['startMinute']),\n endMinute: wholeNumber(raw['endMinute']),\n }\n}\n\nfunction readStep(value: unknown): OutreachSequenceStep {\n const raw = (value && typeof value === 'object' ? value : {}) as Record<string, unknown>\n const id = text(raw['id']).trim().slice(0, 64)\n const delayBusinessDays = wholeNumber(raw['delayBusinessDays'])\n if (raw['kind'] === 'email') {\n const templateId = text(raw['templateId']).trim()\n const step: OutreachEmailStep = {\n id,\n kind: 'email',\n delayBusinessDays,\n subject: text(raw['subject']),\n replyInThread: raw['replyInThread'] !== false,\n body: text(raw['body']),\n templateId: templateId || null,\n }\n return step\n }\n if (raw['kind'] === 'task') {\n const step: OutreachTaskStep = {\n id,\n kind: 'task',\n taskKind: text(raw['taskKind']) as OutreachTaskKind,\n title: text(raw['title']),\n delayBusinessDays,\n }\n return step\n }\n // Neither kind: kept, so the validator can say \"a step is an email or a\n // task\" beside it rather than the step silently disappearing.\n return { id, kind: text(raw['kind']) } as unknown as OutreachSequenceStep\n}\n\n/**\n * A draft read out of anything — a request body, a stored sequence — with\n * every field the model does not name left behind and every value coerced\n * to its type. Nothing is judged here; that is the validator's.\n */\nexport function readOutreachSequenceDraft(input: unknown): OutreachSequenceDraft {\n const raw = (input && typeof input === 'object' ? input : {}) as Record<string, unknown>\n const settings = readOutreachSequenceSettings(\n (raw['settings'] && typeof raw['settings'] === 'object'\n ? raw['settings']\n : {}) as Record<string, unknown>,\n )\n return {\n name: text(raw['name']).replace(/\\s+/g, ' ').trim(),\n hostId: text(raw['hostId']).trim(),\n mailboxId: text(raw['mailboxId']).trim(),\n steps: (Array.isArray(raw['steps']) ? raw['steps'] : []).slice(0, STEPS_READ_MAX).map(readStep),\n settings: {\n ...settings,\n window: readWindow((raw['settings'] as Record<string, unknown> | undefined)?.['window']),\n },\n }\n}\n\n/** The draft a stored sequence opens as in the editor. */\nexport function outreachSequenceDraftOf(\n sequence: Pick<OutreachSequence, 'name' | 'hostId' | 'mailboxId' | 'steps' | 'settings'>,\n): OutreachSequenceDraft {\n return readOutreachSequenceDraft(sequence)\n}\n\n/** A step id nothing else in the sequence carries. */\nexport function newOutreachStepId(\n steps: readonly Pick<OutreachSequenceStep, 'id'>[] = [],\n random: () => number = Math.random,\n): string {\n const taken = new Set(steps.map((step) => step.id))\n for (let attempt = 0; ; attempt += 1) {\n const drawn = `step-${Math.floor(random() * 36 ** 6).toString(36).padStart(6, '0')}`\n // A source that keeps answering the same number still ends: the attempt\n // count makes the id unique once the draws stop being.\n const id = attempt < 4 ? drawn : `${drawn}-${attempt}`\n if (!taken.has(id)) return id\n }\n}\n\n/**\n * A new email step for the end of `steps`: the first email waits for the\n * next opening; a later one waits a few business days and replies in the\n * thread the first started.\n */\nexport function newOutreachEmailStep(\n steps: readonly OutreachSequenceStep[],\n random?: () => number,\n): OutreachEmailStep {\n const follows = steps.some((step) => step.kind === 'email')\n return {\n id: newOutreachStepId(steps, random),\n kind: 'email',\n delayBusinessDays: follows ? OUTREACH_DEFAULT_FOLLOW_UP_DAYS : 0,\n subject: '',\n replyInThread: true,\n body: '',\n templateId: null,\n }\n}\n\n/** A new task step for the end of `steps`, titled for its kind. */\nexport function newOutreachTaskStep(\n steps: readonly OutreachSequenceStep[],\n taskKind: OutreachTaskKind,\n random?: () => number,\n): OutreachTaskStep {\n const titles: Record<OutreachTaskKind, string> = {\n linkedin: 'Connect on LinkedIn',\n call: 'Call',\n todo: '',\n }\n return {\n id: newOutreachStepId(steps, random),\n kind: 'task',\n taskKind,\n title: titles[taskKind] ?? '',\n delayBusinessDays: 1,\n }\n}\n\n/** A new sequence: one email, for `hostId`, sending to the countries given. */\nexport function emptyOutreachSequenceDraft(input: {\n hostId: string\n mailboxId?: string\n allowedCountries: readonly string[]\n random?: () => number\n}): OutreachSequenceDraft {\n return {\n name: '',\n hostId: input.hostId,\n mailboxId: input.mailboxId ?? '',\n steps: [newOutreachEmailStep([], input.random)],\n settings: {\n window: null,\n allowedCountries: [...input.allowedCountries],\n allowCustomers: false,\n },\n }\n}\n\n/** The issues a save can be refused with beyond the engine's own. */\nexport type OutreachSequenceIssueCode =\n | OutreachValidationCode\n | 'steps_locked'\n | 'host_locked'\n | 'mailbox_locked'\n | 'host_unknown'\n | 'mailbox_unknown'\n | 'mailbox_not_yours'\n | 'mailbox_not_sending'\n | 'country_not_in_org'\n\nexport interface OutreachSequenceIssue extends Omit<OutreachValidationIssue, 'code'> {\n code: OutreachSequenceIssueCode\n}\n\n/**\n * What a save may not change once someone is enrolled — see the module\n * note. `previous` is the stored sequence; `next` the draft being saved.\n */\nexport function outreachEnrolledSequenceIssues(\n previous: Pick<OutreachSequenceDraft, 'hostId' | 'mailboxId' | 'steps'>,\n next: Pick<OutreachSequenceDraft, 'hostId' | 'mailboxId' | 'steps'>,\n): OutreachSequenceIssue[] {\n const issues: OutreachSequenceIssue[] = []\n const locked = (path: string, code: OutreachSequenceIssueCode, message: string) =>\n issues.push({ path, code, message, severity: 'error' })\n const kept = previous.steps.every(\n (step, index) => next.steps[index]?.id === step.id && next.steps[index]?.kind === step.kind,\n )\n if (!kept) {\n locked(\n 'steps',\n 'steps_locked',\n \"People are enrolled in this sequence, so its steps can't be removed, reordered or changed to another kind. Edit their wording, add steps at the end, or start a new sequence.\",\n )\n }\n if (previous.hostId !== next.hostId) {\n locked('hostId', 'host_locked', \"People are enrolled from this site's CRM, so the site can't change.\")\n }\n if (previous.mailboxId !== next.mailboxId) {\n locked(\n 'mailboxId',\n 'mailbox_locked',\n \"People are enrolled from this mailbox, so it can't change. Start a new sequence to send from another.\",\n )\n }\n return issues\n}\n\n/**\n * Whether a sequence's mailbox can send right now, as activation asks it:\n * `none` when no mailbox is chosen, `gone` when it was disconnected or is\n * not this organization's, `paused` or `reconnect_required` while it is\n * connected but not sending, and `sending` when it is connected.\n */\nexport type OutreachSequenceMailboxState =\n | 'none'\n | 'gone'\n | 'paused'\n | 'reconnect_required'\n | 'sending'\n\nexport function outreachSequenceMailboxState(\n mailboxId: string,\n mailbox: Pick<OutreachMailbox, 'status'> | null | undefined,\n): OutreachSequenceMailboxState {\n if (!mailboxId) return 'none'\n if (!mailbox || mailbox.status === 'disconnected') return 'gone'\n if (mailbox.status === 'paused' || mailbox.status === 'reconnect_required') {\n return mailbox.status\n }\n return 'sending'\n}\n\n/** What activation refuses with for each mailbox that cannot send. */\nconst MAILBOX_ACTIVATION_ISSUES: Record<\n Exclude<OutreachSequenceMailboxState, 'sending'>,\n { code: OutreachSequenceIssueCode; message: string }\n> = {\n none: {\n code: 'mailbox_required',\n message: 'Choose the mailbox this sequence sends from before activating it.',\n },\n gone: {\n code: 'mailbox_unknown',\n message: \"This sequence's mailbox is no longer connected. Choose another before activating it.\",\n },\n paused: {\n code: 'mailbox_not_sending',\n message: \"This sequence's mailbox is paused. Resume it in Mailboxes, then activate the sequence.\",\n },\n reconnect_required: {\n code: 'mailbox_not_sending',\n message:\n \"Google stopped accepting this sequence's mailbox. Reconnect it in Mailboxes, then activate the sequence.\",\n },\n}\n\n/**\n * Why a sequence cannot be activated on its mailbox, or `null` when it can\n * (AGL-2980). A sequence is activated only onto a mailbox that is sending:\n * connected, not paused, and not waiting for its member to reconnect it —\n * whether or not its steps include an email, because every step's due time\n * is read in the mailbox's timezone and sending hours.\n *\n * The status route refuses with it, and the sequence page shows it beside\n * a disabled Activate button, so the page says why before anyone clicks.\n */\nexport function outreachMailboxActivationIssue(\n mailboxId: string,\n mailbox: Pick<OutreachMailbox, 'status'> | null | undefined,\n): OutreachSequenceIssue | null {\n const state = outreachSequenceMailboxState(mailboxId, mailbox)\n if (state === 'sending') return null\n const { code, message } = MAILBOX_ACTIVATION_ISSUES[state]\n return { path: 'mailboxId', code, message, severity: 'error' }\n}\n"],"names":["readOutreachSequenceSettings","STEPS_READ_MAX","OUTREACH_DEFAULT_FOLLOW_UP_DAYS","text","value","wholeNumber","number","Number","NaN","isInteger","readWindow","raw","days","Array","isArray","map","day","startMinute","endMinute","readStep","id","trim","slice","delayBusinessDays","templateId","step","kind","subject","replyInThread","body","taskKind","title","readOutreachSequenceDraft","input","settings","name","replace","hostId","mailboxId","steps","window","outreachSequenceDraftOf","sequence","newOutreachStepId","random","Math","taken","Set","attempt","drawn","floor","toString","padStart","has","newOutreachEmailStep","follows","some","newOutreachTaskStep","titles","linkedin","call","todo","emptyOutreachSequenceDraft","allowedCountries","allowCustomers","outreachEnrolledSequenceIssues","previous","next","issues","locked","path","code","message","push","severity","kept","every","index","outreachSequenceMailboxState","mailbox","status","MAILBOX_ACTIVATION_ISSUES","none","gone","paused","reconnect_required","outreachMailboxActivationIssue","state"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;4CAkB4C,GAE5C,SACEA,4BAA4B,QAGvB,mCAA+B;AAuBtC,oFAAoF,GACpF,MAAMC,iBAAiB;AAEvB,yEAAyE,GACzE,OAAO,MAAMC,kCAAkC,EAAC;AAEhD,MAAMC,OAAO,CAACC,QAA4B,OAAOA,UAAU,WAAWA,QAAQ;AAE9E,2EAA2E,GAC3E,MAAMC,cAAc,CAACD;IACnB,MAAME,SAAS,OAAOF,UAAU,WAAWA,QAAQG,OAAOJ,KAAKC,UAAUI;IACzE,OAAOD,OAAOE,SAAS,CAACH,UAAUA,SAASC,OAAOC,GAAG;AACvD;AAEA,SAASE,WAAWN,KAAc;IAChC,IAAI,CAACA,SAAS,OAAOA,UAAU,UAAU,OAAO;IAChD,MAAMO,MAAMP;IACZ,OAAO;QACLQ,MAAMC,MAAMC,OAAO,CAACH,GAAG,CAAC,OAAO,IAAIA,GAAG,CAAC,OAAO,CAACI,GAAG,CAAC,CAACC,MAAQX,YAAYW,QAAQ,EAAE;QAClFC,aAAaZ,YAAYM,GAAG,CAAC,cAAc;QAC3CO,WAAWb,YAAYM,GAAG,CAAC,YAAY;IACzC;AACF;AAEA,SAASQ,SAASf,KAAc;IAC9B,MAAMO,MAAOP,SAAS,OAAOA,UAAU,WAAWA,QAAQ,CAAC;IAC3D,MAAMgB,KAAKjB,KAAKQ,GAAG,CAAC,KAAK,EAAEU,IAAI,GAAGC,KAAK,CAAC,GAAG;IAC3C,MAAMC,oBAAoBlB,YAAYM,GAAG,CAAC,oBAAoB;IAC9D,IAAIA,GAAG,CAAC,OAAO,KAAK,SAAS;QAC3B,MAAMa,aAAarB,KAAKQ,GAAG,CAAC,aAAa,EAAEU,IAAI;QAC/C,MAAMI,OAA0B;YAC9BL;YACAM,MAAM;YACNH;YACAI,SAASxB,KAAKQ,GAAG,CAAC,UAAU;YAC5BiB,eAAejB,GAAG,CAAC,gBAAgB,KAAK;YACxCkB,MAAM1B,KAAKQ,GAAG,CAAC,OAAO;YACtBa,YAAYA,cAAc;QAC5B;QACA,OAAOC;IACT;IACA,IAAId,GAAG,CAAC,OAAO,KAAK,QAAQ;QAC1B,MAAMc,OAAyB;YAC7BL;YACAM,MAAM;YACNI,UAAU3B,KAAKQ,GAAG,CAAC,WAAW;YAC9BoB,OAAO5B,KAAKQ,GAAG,CAAC,QAAQ;YACxBY;QACF;QACA,OAAOE;IACT;IACA,wEAAwE;IACxE,8DAA8D;IAC9D,OAAO;QAAEL;QAAIM,MAAMvB,KAAKQ,GAAG,CAAC,OAAO;IAAE;AACvC;AAEA;;;;CAIC,GACD,OAAO,SAASqB,0BAA0BC,KAAc;QAc9BtB;IAbxB,MAAMA,MAAOsB,SAAS,OAAOA,UAAU,WAAWA,QAAQ,CAAC;IAC3D,MAAMC,WAAWlC,6BACdW,GAAG,CAAC,WAAW,IAAI,OAAOA,GAAG,CAAC,WAAW,KAAK,WAC3CA,GAAG,CAAC,WAAW,GACf,CAAC;IAEP,OAAO;QACLwB,MAAMhC,KAAKQ,GAAG,CAAC,OAAO,EAAEyB,OAAO,CAAC,QAAQ,KAAKf,IAAI;QACjDgB,QAAQlC,KAAKQ,GAAG,CAAC,SAAS,EAAEU,IAAI;QAChCiB,WAAWnC,KAAKQ,GAAG,CAAC,YAAY,EAAEU,IAAI;QACtCkB,OAAO,AAAC1B,CAAAA,MAAMC,OAAO,CAACH,GAAG,CAAC,QAAQ,IAAIA,GAAG,CAAC,QAAQ,GAAG,EAAE,AAAD,EAAGW,KAAK,CAAC,GAAGrB,gBAAgBc,GAAG,CAACI;QACtFe,UAAU,aACLA;YACHM,QAAQ9B,YAAYC,gBAAAA,GAAG,CAAC,WAAW,qBAAhB,AAACA,aAAyD,CAAC,SAAS;;IAE3F;AACF;AAEA,wDAAwD,GACxD,OAAO,SAAS8B,wBACdC,QAAwF;IAExF,OAAOV,0BAA0BU;AACnC;AAEA,oDAAoD,GACpD,OAAO,SAASC,kBACdJ,QAAqD,EAAE,EACvDK,SAAuBC,KAAKD,MAAM;IAElC,MAAME,QAAQ,IAAIC,IAAIR,MAAMxB,GAAG,CAAC,CAACU,OAASA,KAAKL,EAAE;IACjD,IAAK,IAAI4B,UAAU,IAAKA,WAAW,EAAG;QACpC,MAAMC,QAAQ,CAAC,KAAK,EAAEJ,KAAKK,KAAK,CAACN,WAAW,MAAM,GAAGO,QAAQ,CAAC,IAAIC,QAAQ,CAAC,GAAG,MAAM;QACpF,wEAAwE;QACxE,uDAAuD;QACvD,MAAMhC,KAAK4B,UAAU,IAAIC,QAAQ,GAAGA,MAAM,CAAC,EAAED,SAAS;QACtD,IAAI,CAACF,MAAMO,GAAG,CAACjC,KAAK,OAAOA;IAC7B;AACF;AAEA;;;;CAIC,GACD,OAAO,SAASkC,qBACdf,KAAsC,EACtCK,MAAqB;IAErB,MAAMW,UAAUhB,MAAMiB,IAAI,CAAC,CAAC/B,OAASA,KAAKC,IAAI,KAAK;IACnD,OAAO;QACLN,IAAIuB,kBAAkBJ,OAAOK;QAC7BlB,MAAM;QACNH,mBAAmBgC,UAAUrD,kCAAkC;QAC/DyB,SAAS;QACTC,eAAe;QACfC,MAAM;QACNL,YAAY;IACd;AACF;AAEA,iEAAiE,GACjE,OAAO,SAASiC,oBACdlB,KAAsC,EACtCT,QAA0B,EAC1Bc,MAAqB;QAWZc;IATT,MAAMA,SAA2C;QAC/CC,UAAU;QACVC,MAAM;QACNC,MAAM;IACR;IACA,OAAO;QACLzC,IAAIuB,kBAAkBJ,OAAOK;QAC7BlB,MAAM;QACNI;QACAC,KAAK,GAAE2B,mBAAAA,MAAM,CAAC5B,SAAS,YAAhB4B,mBAAoB;QAC3BnC,mBAAmB;IACrB;AACF;AAEA,6EAA6E,GAC7E,OAAO,SAASuC,2BAA2B7B,KAK1C;QAIcA;IAHb,OAAO;QACLE,MAAM;QACNE,QAAQJ,MAAMI,MAAM;QACpBC,SAAS,GAAEL,mBAAAA,MAAMK,SAAS,YAAfL,mBAAmB;QAC9BM,OAAO;YAACe,qBAAqB,EAAE,EAAErB,MAAMW,MAAM;SAAE;QAC/CV,UAAU;YACRM,QAAQ;YACRuB,kBAAkB;mBAAI9B,MAAM8B,gBAAgB;aAAC;YAC7CC,gBAAgB;QAClB;IACF;AACF;AAkBA;;;CAGC,GACD,OAAO,SAASC,+BACdC,QAAuE,EACvEC,IAAmE;IAEnE,MAAMC,SAAkC,EAAE;IAC1C,MAAMC,SAAS,CAACC,MAAcC,MAAiCC,UAC7DJ,OAAOK,IAAI,CAAC;YAAEH;YAAMC;YAAMC;YAASE,UAAU;QAAQ;IACvD,MAAMC,OAAOT,SAAS3B,KAAK,CAACqC,KAAK,CAC/B,CAACnD,MAAMoD;YAAUV,mBAAqCA;eAArCA,EAAAA,oBAAAA,KAAK5B,KAAK,CAACsC,MAAM,qBAAjBV,kBAAmB/C,EAAE,MAAKK,KAAKL,EAAE,IAAI+C,EAAAA,qBAAAA,KAAK5B,KAAK,CAACsC,MAAM,qBAAjBV,mBAAmBzC,IAAI,MAAKD,KAAKC,IAAI;;IAE7F,IAAI,CAACiD,MAAM;QACTN,OACE,SACA,gBACA;IAEJ;IACA,IAAIH,SAAS7B,MAAM,KAAK8B,KAAK9B,MAAM,EAAE;QACnCgC,OAAO,UAAU,eAAe;IAClC;IACA,IAAIH,SAAS5B,SAAS,KAAK6B,KAAK7B,SAAS,EAAE;QACzC+B,OACE,aACA,kBACA;IAEJ;IACA,OAAOD;AACT;AAeA,OAAO,SAASU,6BACdxC,SAAiB,EACjByC,OAA2D;IAE3D,IAAI,CAACzC,WAAW,OAAO;IACvB,IAAI,CAACyC,WAAWA,QAAQC,MAAM,KAAK,gBAAgB,OAAO;IAC1D,IAAID,QAAQC,MAAM,KAAK,YAAYD,QAAQC,MAAM,KAAK,sBAAsB;QAC1E,OAAOD,QAAQC,MAAM;IACvB;IACA,OAAO;AACT;AAEA,oEAAoE,GACpE,MAAMC,4BAGF;IACFC,MAAM;QACJX,MAAM;QACNC,SAAS;IACX;IACAW,MAAM;QACJZ,MAAM;QACNC,SAAS;IACX;IACAY,QAAQ;QACNb,MAAM;QACNC,SAAS;IACX;IACAa,oBAAoB;QAClBd,MAAM;QACNC,SACE;IACJ;AACF;AAEA;;;;;;;;;CASC,GACD,OAAO,SAASc,+BACdhD,SAAiB,EACjByC,OAA2D;IAE3D,MAAMQ,QAAQT,6BAA6BxC,WAAWyC;IACtD,IAAIQ,UAAU,WAAW,OAAO;IAChC,MAAM,EAAEhB,IAAI,EAAEC,OAAO,EAAE,GAAGS,yBAAyB,CAACM,MAAM;IAC1D,OAAO;QAAEjB,MAAM;QAAaC;QAAMC;QAASE,UAAU;IAAQ;AAC/D"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/model/sequence-draft.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*==========================================\n * A SEQUENCE AS THE EDITOR HOLDS IT (AGL-2980).\n *\n * The editor builds a draft, the save route reads the draft it was sent\n * through the same reader, and the engine's validator judges both — so the\n * issues the editor shows beside a field are the ones the route would\n * refuse the save with. Client-safe.\n *\n * ## Once someone is enrolled, the steps they stand on stay put\n *\n * An enrollment points at its next step by POSITION (`stepIndex`), and it\n * was enrolled into a site's CRM from one mailbox. So after the first\n * enrollment, a save may edit any step's wording and delay and may add\n * steps at the end, but may not remove, reorder or change the kind of a\n * step that already exists, and may not move the sequence to another site\n * or mailbox: each of those would send an enrolled person a step they were\n * never going to get, from an address they never heard from. A new\n * sequence is the way to change them.\n *==========================================*/\n\nimport {\n readOutreachSequenceSettings,\n type OutreachValidationIssue,\n type OutreachValidationCode,\n} from '../engine/sequence-validation'\nimport {\n type OutreachEmailStep,\n type OutreachMailbox,\n type OutreachSendWindow,\n type OutreachSequence,\n type OutreachSequenceSettings,\n type OutreachSequenceStep,\n type OutreachTaskKind,\n type OutreachTaskStep,\n} from './outreach.types'\n\n/** A sequence as the editor holds it and the save route receives it. */\nexport interface OutreachSequenceDraft {\n name: string\n /** The site whose CRM the enrolled people are records of. */\n hostId: string\n /** `''` until a mailbox is chosen. */\n mailboxId: string\n steps: OutreachSequenceStep[]\n settings: OutreachSequenceSettings\n}\n\n/** The most steps a draft is read with; the validator refuses past the real limit. */\nconst STEPS_READ_MAX = 20\n\n/** The delay a new email after the first starts with, in business days. */\nexport const OUTREACH_DEFAULT_FOLLOW_UP_DAYS = 3\n\nconst text = (value: unknown): string => (typeof value === 'string' ? value : '')\n\n/** A whole number, or `NaN` for anything else, so the validator names it. */\nconst wholeNumber = (value: unknown): number => {\n const number = typeof value === 'number' ? value : Number(text(value) || NaN)\n return Number.isInteger(number) ? number : Number.NaN\n}\n\nfunction readWindow(value: unknown): OutreachSendWindow | null {\n if (!value || typeof value !== 'object') return null\n const raw = value as Record<string, unknown>\n return {\n days: Array.isArray(raw['days']) ? raw['days'].map((day) => wholeNumber(day)) : [],\n startMinute: wholeNumber(raw['startMinute']),\n endMinute: wholeNumber(raw['endMinute']),\n }\n}\n\nfunction readStep(value: unknown): OutreachSequenceStep {\n const raw = (value && typeof value === 'object' ? value : {}) as Record<string, unknown>\n const id = text(raw['id']).trim().slice(0, 64)\n const delayBusinessDays = wholeNumber(raw['delayBusinessDays'])\n if (raw['kind'] === 'email') {\n const templateId = text(raw['templateId']).trim()\n const step: OutreachEmailStep = {\n id,\n kind: 'email',\n delayBusinessDays,\n subject: text(raw['subject']),\n replyInThread: raw['replyInThread'] !== false,\n body: text(raw['body']),\n templateId: templateId || null,\n }\n return step\n }\n if (raw['kind'] === 'task') {\n const step: OutreachTaskStep = {\n id,\n kind: 'task',\n taskKind: text(raw['taskKind']) as OutreachTaskKind,\n title: text(raw['title']),\n delayBusinessDays,\n }\n return step\n }\n // Neither kind: kept, so the validator can say \"a step is an email or a\n // task\" beside it rather than the step silently disappearing.\n return { id, kind: text(raw['kind']) } as unknown as OutreachSequenceStep\n}\n\n/**\n * A draft read out of anything — a request body, a stored sequence — with\n * every field the model does not name left behind and every value coerced\n * to its type. Nothing is judged here; that is the validator's.\n */\nexport function readOutreachSequenceDraft(input: unknown): OutreachSequenceDraft {\n const raw = (input && typeof input === 'object' ? input : {}) as Record<string, unknown>\n const settings = readOutreachSequenceSettings(\n (raw['settings'] && typeof raw['settings'] === 'object'\n ? raw['settings']\n : {}) as Record<string, unknown>,\n )\n return {\n name: text(raw['name']).replace(/\\s+/g, ' ').trim(),\n hostId: text(raw['hostId']).trim(),\n mailboxId: text(raw['mailboxId']).trim(),\n steps: (Array.isArray(raw['steps']) ? raw['steps'] : []).slice(0, STEPS_READ_MAX).map(readStep),\n settings: {\n ...settings,\n window: readWindow((raw['settings'] as Record<string, unknown> | undefined)?.['window']),\n },\n }\n}\n\n/** The draft a stored sequence opens as in the editor. */\nexport function outreachSequenceDraftOf(\n sequence: Pick<OutreachSequence, 'name' | 'hostId' | 'mailboxId' | 'steps' | 'settings'>,\n): OutreachSequenceDraft {\n return readOutreachSequenceDraft(sequence)\n}\n\n/** A step id nothing else in the sequence carries. */\nexport function newOutreachStepId(\n steps: readonly Pick<OutreachSequenceStep, 'id'>[] = [],\n random: () => number = Math.random,\n): string {\n const taken = new Set(steps.map((step) => step.id))\n for (let attempt = 0; ; attempt += 1) {\n const drawn = `step-${Math.floor(random() * 36 ** 6).toString(36).padStart(6, '0')}`\n // A source that keeps answering the same number still ends: the attempt\n // count makes the id unique once the draws stop being.\n const id = attempt < 4 ? drawn : `${drawn}-${attempt}`\n if (!taken.has(id)) return id\n }\n}\n\n/**\n * A new email step for the end of `steps`: the first email waits for the\n * next opening; a later one waits a few business days and replies in the\n * thread the first started.\n */\nexport function newOutreachEmailStep(\n steps: readonly OutreachSequenceStep[],\n random?: () => number,\n): OutreachEmailStep {\n const follows = steps.some((step) => step.kind === 'email')\n return {\n id: newOutreachStepId(steps, random),\n kind: 'email',\n delayBusinessDays: follows ? OUTREACH_DEFAULT_FOLLOW_UP_DAYS : 0,\n subject: '',\n replyInThread: true,\n body: '',\n templateId: null,\n }\n}\n\n/** A new task step for the end of `steps`, titled for its kind. */\nexport function newOutreachTaskStep(\n steps: readonly OutreachSequenceStep[],\n taskKind: OutreachTaskKind,\n random?: () => number,\n): OutreachTaskStep {\n const titles: Record<OutreachTaskKind, string> = {\n linkedin: 'Connect on LinkedIn',\n call: 'Call',\n todo: '',\n }\n return {\n id: newOutreachStepId(steps, random),\n kind: 'task',\n taskKind,\n title: titles[taskKind] ?? '',\n delayBusinessDays: 1,\n }\n}\n\n/** A new sequence: one email, for `hostId`, sending to the countries given. */\nexport function emptyOutreachSequenceDraft(input: {\n hostId: string\n mailboxId?: string\n allowedCountries: readonly string[]\n random?: () => number\n}): OutreachSequenceDraft {\n return {\n name: '',\n hostId: input.hostId,\n mailboxId: input.mailboxId ?? '',\n steps: [newOutreachEmailStep([], input.random)],\n settings: {\n window: null,\n allowedCountries: [...input.allowedCountries],\n allowCustomers: false,\n // Off for a NEW sequence too (AGL-3239), not only for the ones that\n // predate it: a tracked link is visible in the body a cold recipient\n // reads, so it is turned on deliberately or not at all.\n trackClicks: false,\n },\n }\n}\n\n/** The issues a save can be refused with beyond the engine's own. */\nexport type OutreachSequenceIssueCode =\n | OutreachValidationCode\n | 'steps_locked'\n | 'host_locked'\n | 'mailbox_locked'\n | 'host_unknown'\n | 'mailbox_unknown'\n | 'mailbox_not_yours'\n | 'mailbox_not_sending'\n | 'country_not_in_org'\n\nexport interface OutreachSequenceIssue extends Omit<OutreachValidationIssue, 'code'> {\n code: OutreachSequenceIssueCode\n}\n\n/**\n * What a save may not change once someone is enrolled — see the module\n * note. `previous` is the stored sequence; `next` the draft being saved.\n */\nexport function outreachEnrolledSequenceIssues(\n previous: Pick<OutreachSequenceDraft, 'hostId' | 'mailboxId' | 'steps'>,\n next: Pick<OutreachSequenceDraft, 'hostId' | 'mailboxId' | 'steps'>,\n): OutreachSequenceIssue[] {\n const issues: OutreachSequenceIssue[] = []\n const locked = (path: string, code: OutreachSequenceIssueCode, message: string) =>\n issues.push({ path, code, message, severity: 'error' })\n const kept = previous.steps.every(\n (step, index) => next.steps[index]?.id === step.id && next.steps[index]?.kind === step.kind,\n )\n if (!kept) {\n locked(\n 'steps',\n 'steps_locked',\n \"People are enrolled in this sequence, so its steps can't be removed, reordered or changed to another kind. Edit their wording, add steps at the end, or start a new sequence.\",\n )\n }\n if (previous.hostId !== next.hostId) {\n locked('hostId', 'host_locked', \"People are enrolled from this site's CRM, so the site can't change.\")\n }\n if (previous.mailboxId !== next.mailboxId) {\n locked(\n 'mailboxId',\n 'mailbox_locked',\n \"People are enrolled from this mailbox, so it can't change. Start a new sequence to send from another.\",\n )\n }\n return issues\n}\n\n/**\n * Whether a sequence's mailbox can send right now, as activation asks it:\n * `none` when no mailbox is chosen, `gone` when it was disconnected or is\n * not this organization's, `paused` or `reconnect_required` while it is\n * connected but not sending, and `sending` when it is connected.\n */\nexport type OutreachSequenceMailboxState =\n | 'none'\n | 'gone'\n | 'paused'\n | 'reconnect_required'\n | 'sending'\n\nexport function outreachSequenceMailboxState(\n mailboxId: string,\n mailbox: Pick<OutreachMailbox, 'status'> | null | undefined,\n): OutreachSequenceMailboxState {\n if (!mailboxId) return 'none'\n if (!mailbox || mailbox.status === 'disconnected') return 'gone'\n if (mailbox.status === 'paused' || mailbox.status === 'reconnect_required') {\n return mailbox.status\n }\n return 'sending'\n}\n\n/** What activation refuses with for each mailbox that cannot send. */\nconst MAILBOX_ACTIVATION_ISSUES: Record<\n Exclude<OutreachSequenceMailboxState, 'sending'>,\n { code: OutreachSequenceIssueCode; message: string }\n> = {\n none: {\n code: 'mailbox_required',\n message: 'Choose the mailbox this sequence sends from before activating it.',\n },\n gone: {\n code: 'mailbox_unknown',\n message: \"This sequence's mailbox is no longer connected. Choose another before activating it.\",\n },\n paused: {\n code: 'mailbox_not_sending',\n message: \"This sequence's mailbox is paused. Resume it in Mailboxes, then activate the sequence.\",\n },\n reconnect_required: {\n code: 'mailbox_not_sending',\n message:\n \"Google stopped accepting this sequence's mailbox. Reconnect it in Mailboxes, then activate the sequence.\",\n },\n}\n\n/**\n * Why a sequence cannot be activated on its mailbox, or `null` when it can\n * (AGL-2980). A sequence is activated only onto a mailbox that is sending:\n * connected, not paused, and not waiting for its member to reconnect it —\n * whether or not its steps include an email, because every step's due time\n * is read in the mailbox's timezone and sending hours.\n *\n * The status route refuses with it, and the sequence page shows it beside\n * a disabled Activate button, so the page says why before anyone clicks.\n */\nexport function outreachMailboxActivationIssue(\n mailboxId: string,\n mailbox: Pick<OutreachMailbox, 'status'> | null | undefined,\n): OutreachSequenceIssue | null {\n const state = outreachSequenceMailboxState(mailboxId, mailbox)\n if (state === 'sending') return null\n const { code, message } = MAILBOX_ACTIVATION_ISSUES[state]\n return { path: 'mailboxId', code, message, severity: 'error' }\n}\n"],"names":["readOutreachSequenceSettings","STEPS_READ_MAX","OUTREACH_DEFAULT_FOLLOW_UP_DAYS","text","value","wholeNumber","number","Number","NaN","isInteger","readWindow","raw","days","Array","isArray","map","day","startMinute","endMinute","readStep","id","trim","slice","delayBusinessDays","templateId","step","kind","subject","replyInThread","body","taskKind","title","readOutreachSequenceDraft","input","settings","name","replace","hostId","mailboxId","steps","window","outreachSequenceDraftOf","sequence","newOutreachStepId","random","Math","taken","Set","attempt","drawn","floor","toString","padStart","has","newOutreachEmailStep","follows","some","newOutreachTaskStep","titles","linkedin","call","todo","emptyOutreachSequenceDraft","allowedCountries","allowCustomers","trackClicks","outreachEnrolledSequenceIssues","previous","next","issues","locked","path","code","message","push","severity","kept","every","index","outreachSequenceMailboxState","mailbox","status","MAILBOX_ACTIVATION_ISSUES","none","gone","paused","reconnect_required","outreachMailboxActivationIssue","state"],"mappings":"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"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { type CampaignLinkReport, type CampaignLinkRollup, type CampaignRate } from '@aglyn/shared-ui-email-campaigns/model';
|
|
18
|
+
import type { OutreachSequenceStats } from './outreach.types';
|
|
19
|
+
/** Why a number a reader expects is missing, or must not be read the obvious way. */
|
|
20
|
+
export interface OutreachReportCaveat {
|
|
21
|
+
/** Stable id, so a spec asserts on the caveat and not on its prose. */
|
|
22
|
+
id: 'opens-not-measured' | 'clicks-not-tracked' | 'clicks-unrecorded' | 'machine-clicks-excluded';
|
|
23
|
+
message: string;
|
|
24
|
+
}
|
|
25
|
+
/** Everything the sequence's report card renders. */
|
|
26
|
+
export interface OutreachSequenceReport {
|
|
27
|
+
/** Email steps that left. One person getting four emails counts four. */
|
|
28
|
+
sent: number;
|
|
29
|
+
/** Distinct people who have had at least one, or `null` when unrecorded. */
|
|
30
|
+
people: number | null;
|
|
31
|
+
/** Click events judged a person's. */
|
|
32
|
+
clicks: number;
|
|
33
|
+
/** Distinct people who clicked — the rate's numerator. */
|
|
34
|
+
uniqueClicks: number;
|
|
35
|
+
/** Clicks a scanner made, counted apart and never in the rate. */
|
|
36
|
+
machineClicks: number;
|
|
37
|
+
/** When a person last followed a link. */
|
|
38
|
+
lastClickAtMs: number | null;
|
|
39
|
+
/** Whether any email of this sequence went out with its links rewritten. */
|
|
40
|
+
clickTracked: boolean;
|
|
41
|
+
rates: {
|
|
42
|
+
/**
|
|
43
|
+
* Distinct people who clicked, over the people emailed.
|
|
44
|
+
*
|
|
45
|
+
* `null` when the sequence never sent a tracked link, when nobody has
|
|
46
|
+
* been emailed yet, and when the counters predate the feature.
|
|
47
|
+
*/
|
|
48
|
+
click: CampaignRate | null;
|
|
49
|
+
};
|
|
50
|
+
caveats: OutreachReportCaveat[];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Turns a sequence's stored counters into its report.
|
|
54
|
+
*
|
|
55
|
+
* @param stats The sequence's `stats`, or `undefined` for one that has none.
|
|
56
|
+
* @param trackClicks Whether the sequence is SET to track clicks now — which
|
|
57
|
+
* is a different question from whether it ever has, and the two together
|
|
58
|
+
* are what separate "nothing to report yet" from "this will never report".
|
|
59
|
+
*/
|
|
60
|
+
export declare function outreachSequenceReport(stats: OutreachSequenceStats | undefined, trackClicks: boolean): OutreachSequenceReport;
|
|
61
|
+
/**
|
|
62
|
+
* The sequence's link table, on the campaign rollup's own reader.
|
|
63
|
+
*
|
|
64
|
+
* Re-exported rather than wrapped: the stored shape is the campaign one, the
|
|
65
|
+
* key derivation is the campaign one, and the table a rep reads should be
|
|
66
|
+
* the same table a marketer reads.
|
|
67
|
+
*/
|
|
68
|
+
export declare function outreachSequenceLinkReport(rollup: CampaignLinkRollup | undefined): CampaignLinkReport;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ /*==========================================
|
|
17
|
+
* WHAT A SEQUENCE MEASURED, AND WHAT IT DID NOT (AGL-3239).
|
|
18
|
+
*
|
|
19
|
+
* The rules are `campaign-report.ts`'s, applied to a channel that can
|
|
20
|
+
* measure less, and the sameness is deliberate: a rate called "click rate"
|
|
21
|
+
* on the Sequences screen and a rate called "click rate" on a campaign's
|
|
22
|
+
* report are taken over comparably named denominators, and each says which
|
|
23
|
+
* one on screen.
|
|
24
|
+
*
|
|
25
|
+
* Three refusals carry over unchanged:
|
|
26
|
+
*
|
|
27
|
+
* 1. **A rate over a zero or unknown denominator is `null`**, never 0%. The
|
|
28
|
+
* rate helper itself is `campaignRate`, imported rather than rewritten,
|
|
29
|
+
* so the two reports cannot drift into two definitions of a percentage.
|
|
30
|
+
* 2. **An absent counter is "not recorded", not nought.** A sequence that
|
|
31
|
+
* ran before the counters existed reports nothing rather than zeroes.
|
|
32
|
+
* 3. **A structural zero is withheld with its reason named.** A sequence
|
|
33
|
+
* that never sent a tracked link could only ever have counted zero
|
|
34
|
+
* clicks, so its click rate measures our sending code and is not shown.
|
|
35
|
+
*
|
|
36
|
+
* And one is this channel's own:
|
|
37
|
+
*
|
|
38
|
+
* 4. **Opens are not measured at all.** Not zero, not withheld pending a
|
|
39
|
+
* webhook — impossible, because a sequence email is plain text by
|
|
40
|
+
* decision and a pixel needs an HTML part. The report SAYS that, in the
|
|
41
|
+
* place a reader looks for an open rate, so nobody concludes from a
|
|
42
|
+
* missing number that nobody read the mail.
|
|
43
|
+
*=========================================*/ import { campaignLinkReport, campaignRate } from "@aglyn/shared-ui-email-campaigns/model";
|
|
44
|
+
/** The sentence each caveat is shown as. */ const CAVEATS = {
|
|
45
|
+
'opens-not-measured': 'Opens aren’t measured. A sequence email is plain text, the way a one-to-one email is, and counting an open needs a tracking image in an HTML email. Clicks are measured instead.',
|
|
46
|
+
'clicks-not-tracked': 'Clicks aren’t being counted for this sequence. Turn on link tracking in its settings, and the emails sent after that have their links counted.',
|
|
47
|
+
'clicks-unrecorded': 'No click has been counted for this sequence yet: either its emails carry no links, or they went out before link tracking was turned on. Emails sent from now on are counted.',
|
|
48
|
+
'machine-clicks-excluded': 'Some clicks came from security scanners that open every link in an email before the recipient sees it. They’re counted separately and left out of the click rate.'
|
|
49
|
+
};
|
|
50
|
+
const caveat = (id)=>({
|
|
51
|
+
id,
|
|
52
|
+
message: CAVEATS[id]
|
|
53
|
+
});
|
|
54
|
+
/**
|
|
55
|
+
* Turns a sequence's stored counters into its report.
|
|
56
|
+
*
|
|
57
|
+
* @param stats The sequence's `stats`, or `undefined` for one that has none.
|
|
58
|
+
* @param trackClicks Whether the sequence is SET to track clicks now — which
|
|
59
|
+
* is a different question from whether it ever has, and the two together
|
|
60
|
+
* are what separate "nothing to report yet" from "this will never report".
|
|
61
|
+
*/ export function outreachSequenceReport(stats, trackClicks) {
|
|
62
|
+
const source = stats != null ? stats : {};
|
|
63
|
+
const count = (value)=>{
|
|
64
|
+
const parsed = Number(value != null ? value : 0);
|
|
65
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
|
66
|
+
};
|
|
67
|
+
const sent = count(source.sent);
|
|
68
|
+
/*
|
|
69
|
+
* ABSENT, not zero — the distinction `campaign-report.ts` makes about
|
|
70
|
+
* `delivered` and for the same reason. `people` is the denominator of the
|
|
71
|
+
* click rate, and `?? 0` here would turn "we never counted" into "nobody
|
|
72
|
+
* was emailed" and render that beside a non-zero send count.
|
|
73
|
+
*/ const people = source.people === undefined ? null : count(source.people);
|
|
74
|
+
const clicks = count(source.clicks);
|
|
75
|
+
const uniqueClicks = count(source.uniqueClicks);
|
|
76
|
+
const machineClicks = count(source.machineClicks);
|
|
77
|
+
const clickTracked = source.clickTracked === true;
|
|
78
|
+
const caveats = [
|
|
79
|
+
caveat('opens-not-measured')
|
|
80
|
+
];
|
|
81
|
+
if (!clickTracked) {
|
|
82
|
+
/*
|
|
83
|
+
* Two ways to have no click figures, and they are not the same
|
|
84
|
+
* situation: one is a setting nobody turned on, the other is a sequence
|
|
85
|
+
* that finished its sending before the counters existed. The first has
|
|
86
|
+
* something to do about it and the second does not, so they are named
|
|
87
|
+
* apart — and a sequence that has sent NOTHING yet gets neither, because
|
|
88
|
+
* there is nothing missing about a report for a sequence that has not
|
|
89
|
+
* run.
|
|
90
|
+
*/ if (sent > 0) caveats.push(caveat(trackClicks ? 'clicks-unrecorded' : 'clicks-not-tracked'));
|
|
91
|
+
else if (!trackClicks) caveats.push(caveat('clicks-not-tracked'));
|
|
92
|
+
}
|
|
93
|
+
if (machineClicks > 0) caveats.push(caveat('machine-clicks-excluded'));
|
|
94
|
+
return {
|
|
95
|
+
sent,
|
|
96
|
+
people,
|
|
97
|
+
clicks,
|
|
98
|
+
uniqueClicks,
|
|
99
|
+
machineClicks,
|
|
100
|
+
lastClickAtMs: typeof source.lastClickAtMs === 'number' && Number.isFinite(source.lastClickAtMs) ? source.lastClickAtMs : null,
|
|
101
|
+
clickTracked,
|
|
102
|
+
rates: {
|
|
103
|
+
click: clickTracked ? campaignRate(uniqueClicks, people != null ? people : undefined, 'people emailed') : null
|
|
104
|
+
},
|
|
105
|
+
caveats
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The sequence's link table, on the campaign rollup's own reader.
|
|
110
|
+
*
|
|
111
|
+
* Re-exported rather than wrapped: the stored shape is the campaign one, the
|
|
112
|
+
* key derivation is the campaign one, and the table a rep reads should be
|
|
113
|
+
* the same table a marketer reads.
|
|
114
|
+
*/ export function outreachSequenceLinkReport(rollup) {
|
|
115
|
+
return campaignLinkReport(rollup);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
//# sourceMappingURL=sequence-report.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/model/sequence-report.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*==========================================\n * WHAT A SEQUENCE MEASURED, AND WHAT IT DID NOT (AGL-3239).\n *\n * The rules are `campaign-report.ts`'s, applied to a channel that can\n * measure less, and the sameness is deliberate: a rate called \"click rate\"\n * on the Sequences screen and a rate called \"click rate\" on a campaign's\n * report are taken over comparably named denominators, and each says which\n * one on screen.\n *\n * Three refusals carry over unchanged:\n *\n * 1. **A rate over a zero or unknown denominator is `null`**, never 0%. The\n * rate helper itself is `campaignRate`, imported rather than rewritten,\n * so the two reports cannot drift into two definitions of a percentage.\n * 2. **An absent counter is \"not recorded\", not nought.** A sequence that\n * ran before the counters existed reports nothing rather than zeroes.\n * 3. **A structural zero is withheld with its reason named.** A sequence\n * that never sent a tracked link could only ever have counted zero\n * clicks, so its click rate measures our sending code and is not shown.\n *\n * And one is this channel's own:\n *\n * 4. **Opens are not measured at all.** Not zero, not withheld pending a\n * webhook — impossible, because a sequence email is plain text by\n * decision and a pixel needs an HTML part. The report SAYS that, in the\n * place a reader looks for an open rate, so nobody concludes from a\n * missing number that nobody read the mail.\n *=========================================*/\n\nimport {\n campaignLinkReport,\n campaignRate,\n type CampaignLinkReport,\n type CampaignLinkRollup,\n type CampaignRate,\n} from '@aglyn/shared-ui-email-campaigns/model'\nimport type { OutreachSequenceStats } from './outreach.types'\n\n/** Why a number a reader expects is missing, or must not be read the obvious way. */\nexport interface OutreachReportCaveat {\n /** Stable id, so a spec asserts on the caveat and not on its prose. */\n id:\n | 'opens-not-measured'\n | 'clicks-not-tracked'\n | 'clicks-unrecorded'\n | 'machine-clicks-excluded'\n message: string\n}\n\n/** Everything the sequence's report card renders. */\nexport interface OutreachSequenceReport {\n /** Email steps that left. One person getting four emails counts four. */\n sent: number\n /** Distinct people who have had at least one, or `null` when unrecorded. */\n people: number | null\n /** Click events judged a person's. */\n clicks: number\n /** Distinct people who clicked — the rate's numerator. */\n uniqueClicks: number\n /** Clicks a scanner made, counted apart and never in the rate. */\n machineClicks: number\n /** When a person last followed a link. */\n lastClickAtMs: number | null\n /** Whether any email of this sequence went out with its links rewritten. */\n clickTracked: boolean\n rates: {\n /**\n * Distinct people who clicked, over the people emailed.\n *\n * `null` when the sequence never sent a tracked link, when nobody has\n * been emailed yet, and when the counters predate the feature.\n */\n click: CampaignRate | null\n }\n caveats: OutreachReportCaveat[]\n}\n\n/** The sentence each caveat is shown as. */\nconst CAVEATS: Record<OutreachReportCaveat['id'], string> = {\n 'opens-not-measured':\n 'Opens aren’t measured. A sequence email is plain text, the way a one-to-one email is, and counting an open needs a tracking image in an HTML email. Clicks are measured instead.',\n 'clicks-not-tracked':\n 'Clicks aren’t being counted for this sequence. Turn on link tracking in its settings, and the emails sent after that have their links counted.',\n 'clicks-unrecorded':\n 'No click has been counted for this sequence yet: either its emails carry no links, or they went out before link tracking was turned on. Emails sent from now on are counted.',\n 'machine-clicks-excluded':\n 'Some clicks came from security scanners that open every link in an email before the recipient sees it. They’re counted separately and left out of the click rate.',\n}\n\nconst caveat = (id: OutreachReportCaveat['id']): OutreachReportCaveat => ({\n id,\n message: CAVEATS[id],\n})\n\n/**\n * Turns a sequence's stored counters into its report.\n *\n * @param stats The sequence's `stats`, or `undefined` for one that has none.\n * @param trackClicks Whether the sequence is SET to track clicks now — which\n * is a different question from whether it ever has, and the two together\n * are what separate \"nothing to report yet\" from \"this will never report\".\n */\nexport function outreachSequenceReport(\n stats: OutreachSequenceStats | undefined,\n trackClicks: boolean,\n): OutreachSequenceReport {\n const source = stats ?? {}\n const count = (value: unknown): number => {\n const parsed = Number(value ?? 0)\n return Number.isFinite(parsed) && parsed > 0 ? parsed : 0\n }\n const sent = count(source.sent)\n /*\n * ABSENT, not zero — the distinction `campaign-report.ts` makes about\n * `delivered` and for the same reason. `people` is the denominator of the\n * click rate, and `?? 0` here would turn \"we never counted\" into \"nobody\n * was emailed\" and render that beside a non-zero send count.\n */\n const people = source.people === undefined ? null : count(source.people)\n const clicks = count(source.clicks)\n const uniqueClicks = count(source.uniqueClicks)\n const machineClicks = count(source.machineClicks)\n const clickTracked = source.clickTracked === true\n\n const caveats: OutreachReportCaveat[] = [caveat('opens-not-measured')]\n if (!clickTracked) {\n /*\n * Two ways to have no click figures, and they are not the same\n * situation: one is a setting nobody turned on, the other is a sequence\n * that finished its sending before the counters existed. The first has\n * something to do about it and the second does not, so they are named\n * apart — and a sequence that has sent NOTHING yet gets neither, because\n * there is nothing missing about a report for a sequence that has not\n * run.\n */\n if (sent > 0) caveats.push(caveat(trackClicks ? 'clicks-unrecorded' : 'clicks-not-tracked'))\n else if (!trackClicks) caveats.push(caveat('clicks-not-tracked'))\n }\n if (machineClicks > 0) caveats.push(caveat('machine-clicks-excluded'))\n\n return {\n sent,\n people,\n clicks,\n uniqueClicks,\n machineClicks,\n lastClickAtMs:\n typeof source.lastClickAtMs === 'number' && Number.isFinite(source.lastClickAtMs)\n ? source.lastClickAtMs\n : null,\n clickTracked,\n rates: {\n click: clickTracked\n ? campaignRate(uniqueClicks, people ?? undefined, 'people emailed')\n : null,\n },\n caveats,\n }\n}\n\n/**\n * The sequence's link table, on the campaign rollup's own reader.\n *\n * Re-exported rather than wrapped: the stored shape is the campaign one, the\n * key derivation is the campaign one, and the table a rep reads should be\n * the same table a marketer reads.\n */\nexport function outreachSequenceLinkReport(\n rollup: CampaignLinkRollup | undefined,\n): CampaignLinkReport {\n return campaignLinkReport(rollup)\n}\n"],"names":["campaignLinkReport","campaignRate","CAVEATS","caveat","id","message","outreachSequenceReport","stats","trackClicks","source","count","value","parsed","Number","isFinite","sent","people","undefined","clicks","uniqueClicks","machineClicks","clickTracked","caveats","push","lastClickAtMs","rates","click","outreachSequenceLinkReport","rollup"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;2CA2B2C,GAE3C,SACEA,kBAAkB,EAClBC,YAAY,QAIP,yCAAwC;AA0C/C,0CAA0C,GAC1C,MAAMC,UAAsD;IAC1D,sBACE;IACF,sBACE;IACF,qBACE;IACF,2BACE;AACJ;AAEA,MAAMC,SAAS,CAACC,KAA0D,CAAA;QACxEA;QACAC,SAASH,OAAO,CAACE,GAAG;IACtB,CAAA;AAEA;;;;;;;CAOC,GACD,OAAO,SAASE,uBACdC,KAAwC,EACxCC,WAAoB;IAEpB,MAAMC,SAASF,gBAAAA,QAAS,CAAC;IACzB,MAAMG,QAAQ,CAACC;QACb,MAAMC,SAASC,OAAOF,gBAAAA,QAAS;QAC/B,OAAOE,OAAOC,QAAQ,CAACF,WAAWA,SAAS,IAAIA,SAAS;IAC1D;IACA,MAAMG,OAAOL,MAAMD,OAAOM,IAAI;IAC9B;;;;;GAKC,GACD,MAAMC,SAASP,OAAOO,MAAM,KAAKC,YAAY,OAAOP,MAAMD,OAAOO,MAAM;IACvE,MAAME,SAASR,MAAMD,OAAOS,MAAM;IAClC,MAAMC,eAAeT,MAAMD,OAAOU,YAAY;IAC9C,MAAMC,gBAAgBV,MAAMD,OAAOW,aAAa;IAChD,MAAMC,eAAeZ,OAAOY,YAAY,KAAK;IAE7C,MAAMC,UAAkC;QAACnB,OAAO;KAAsB;IACtE,IAAI,CAACkB,cAAc;QACjB;;;;;;;;KAQC,GACD,IAAIN,OAAO,GAAGO,QAAQC,IAAI,CAACpB,OAAOK,cAAc,sBAAsB;aACjE,IAAI,CAACA,aAAac,QAAQC,IAAI,CAACpB,OAAO;IAC7C;IACA,IAAIiB,gBAAgB,GAAGE,QAAQC,IAAI,CAACpB,OAAO;IAE3C,OAAO;QACLY;QACAC;QACAE;QACAC;QACAC;QACAI,eACE,OAAOf,OAAOe,aAAa,KAAK,YAAYX,OAAOC,QAAQ,CAACL,OAAOe,aAAa,IAC5Ef,OAAOe,aAAa,GACpB;QACNH;QACAI,OAAO;YACLC,OAAOL,eACHpB,aAAakB,cAAcH,iBAAAA,SAAUC,WAAW,oBAChD;QACN;QACAK;IACF;AACF;AAEA;;;;;;CAMC,GACD,OAAO,SAASK,2BACdC,MAAsC;IAEtC,OAAO5B,mBAAmB4B;AAC5B"}
|
|
@@ -14,9 +14,19 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
-
import { type OutreachEnrollment, type OutreachMailbox, type OutreachSequence } from './outreach.types';
|
|
17
|
+
import { type OutreachEnrollment, type OutreachMailbox, type OutreachSequence, type OutreachSequenceStats } from './outreach.types';
|
|
18
18
|
/** A stored sequence in its model shape, or `null` for no document. */
|
|
19
19
|
export declare function readStoredOutreachSequence(id: string, data: Record<string, unknown> | undefined): OutreachSequence | null;
|
|
20
|
+
/**
|
|
21
|
+
* A sequence's stored counters (AGL-3239), or `null` when it has none.
|
|
22
|
+
*
|
|
23
|
+
* Every field is carried through only when it is READABLE, so an absence
|
|
24
|
+
* survives the round trip as an absence: the whole of the report's honesty
|
|
25
|
+
* is that a counter nobody wrote is not a zero, and a normalizer that
|
|
26
|
+
* defaulted these to `0` would destroy the distinction here, before any
|
|
27
|
+
* reader could make it.
|
|
28
|
+
*/
|
|
29
|
+
export declare function readOutreachSequenceStats(raw: unknown): OutreachSequenceStats | null;
|
|
20
30
|
/** A stored enrollment in its model shape, or `null` for no document. */
|
|
21
31
|
export declare function readStoredOutreachEnrollment(id: string, data: Record<string, unknown> | undefined): OutreachEnrollment | null;
|
|
22
32
|
/** A stored mailbox with its id, or `null` for no document. */
|
|
@@ -31,14 +31,47 @@ const texts = (value)=>Array.isArray(value) ? value.filter((entry)=>typeof entry
|
|
|
31
31
|
if (!data) return null;
|
|
32
32
|
const draft = readOutreachSequenceDraft(data);
|
|
33
33
|
const status = data['status'];
|
|
34
|
+
const stats = readOutreachSequenceStats(data['stats']);
|
|
34
35
|
return _extends({
|
|
35
36
|
id
|
|
36
37
|
}, draft, {
|
|
37
|
-
status: OUTREACH_SEQUENCE_STATUSES.includes(status) ? status : 'draft'
|
|
38
|
+
status: OUTREACH_SEQUENCE_STATUSES.includes(status) ? status : 'draft'
|
|
39
|
+
}, stats ? {
|
|
40
|
+
stats
|
|
41
|
+
} : {}, {
|
|
38
42
|
createdAtMs: (_ms = ms(data['createdAtMs'])) != null ? _ms : 0,
|
|
39
43
|
updatedAtMs: (_ms1 = ms(data['updatedAtMs'])) != null ? _ms1 : 0
|
|
40
44
|
});
|
|
41
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* A sequence's stored counters (AGL-3239), or `null` when it has none.
|
|
48
|
+
*
|
|
49
|
+
* Every field is carried through only when it is READABLE, so an absence
|
|
50
|
+
* survives the round trip as an absence: the whole of the report's honesty
|
|
51
|
+
* is that a counter nobody wrote is not a zero, and a normalizer that
|
|
52
|
+
* defaulted these to `0` would destroy the distinction here, before any
|
|
53
|
+
* reader could make it.
|
|
54
|
+
*/ export function readOutreachSequenceStats(raw) {
|
|
55
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
56
|
+
const data = raw;
|
|
57
|
+
const stats = {};
|
|
58
|
+
for (const key of [
|
|
59
|
+
'sent',
|
|
60
|
+
'people',
|
|
61
|
+
'clicks',
|
|
62
|
+
'uniqueClicks',
|
|
63
|
+
'machineClicks'
|
|
64
|
+
]){
|
|
65
|
+
const value = data[key];
|
|
66
|
+
if (typeof value === 'number' && Number.isFinite(value) && value >= 0) {
|
|
67
|
+
stats[key] = Math.floor(value);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (data['clickTracked'] === true) stats.clickTracked = true;
|
|
71
|
+
const last = data['lastClickAtMs'];
|
|
72
|
+
if (typeof last === 'number' && Number.isFinite(last)) stats.lastClickAtMs = last;
|
|
73
|
+
return Object.keys(stats).length ? stats : null;
|
|
74
|
+
}
|
|
42
75
|
/** A stored enrollment in its model shape, or `null` for no document. */ export function readStoredOutreachEnrollment(id, data) {
|
|
43
76
|
var _ms, _ms1;
|
|
44
77
|
if (!data) return null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/model/stored-records.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * A stored Outreach document read back into its model shape (AGL-2980),\n * for the server's routes and the console's listeners alike. Client-safe.\n *\n * Read DEFENSIVELY, a field at a time, because the readers act on what they\n * read: a sequence's steps are what gets sent, and an enrollment's status is\n * what decides whether it is sent at all.\n */\n\nimport { readOutreachSequenceDraft } from './sequence-draft'\nimport {\n OUTREACH_ENROLLMENT_STATUSES,\n OUTREACH_SEQUENCE_STATUSES,\n OUTREACH_STOP_REASONS,\n type OutreachEnrollment,\n type OutreachEnrollmentStatus,\n type OutreachEnrollmentTarget,\n type OutreachMailbox,\n type OutreachSequence,\n type OutreachSequenceStatus,\n type OutreachStopReason,\n} from './outreach.types'\n\nconst text = (value: unknown): string =>\n typeof value === 'string' ? value : ''\nconst ms = (value: unknown): number | null =>\n typeof value === 'number' && Number.isFinite(value) ? value : null\nconst texts = (value: unknown): string[] =>\n Array.isArray(value)\n ? value.filter((entry): entry is string => typeof entry === 'string')\n : []\n\n/** A stored sequence in its model shape, or `null` for no document. */\nexport function readStoredOutreachSequence(\n id: string,\n data: Record<string, unknown> | undefined,\n): OutreachSequence | null {\n if (!data) return null\n const draft = readOutreachSequenceDraft(data)\n const status = data['status']\n return {\n id,\n ...draft,\n status: (OUTREACH_SEQUENCE_STATUSES as readonly unknown[]).includes(status)\n ? (status as OutreachSequenceStatus)\n : 'draft',\n createdAtMs: ms(data['createdAtMs']) ?? 0,\n updatedAtMs: ms(data['updatedAtMs']) ?? 0,\n }\n}\n\n/** A stored enrollment in its model shape, or `null` for no document. */\nexport function readStoredOutreachEnrollment(\n id: string,\n data: Record<string, unknown> | undefined,\n): OutreachEnrollment | null {\n if (!data) return null\n const status = data['status']\n const stopReason = data['stopReason']\n const stepIndex = Number(data['stepIndex'])\n // An enrollment written before leads could be sequenced (AGL-3234) names\n // no target and is a contact's; one that names a lead and no target is a\n // lead's, whatever else it says.\n const leadId = text(data['leadId']) || null\n const target: OutreachEnrollmentTarget =\n data['target'] === 'lead' || (data['target'] === undefined && leadId && !text(data['contactId']))\n ? 'lead'\n : 'contact'\n return {\n ...(data as unknown as OutreachEnrollment),\n id,\n sequenceId: text(data['sequenceId']),\n target,\n contactId: text(data['contactId']),\n leadId,\n contactName: text(data['contactName']),\n email: text(data['email']),\n hostId: text(data['hostId']),\n mailboxId: text(data['mailboxId']),\n stepIndex: Number.isInteger(stepIndex) && stepIndex >= 0 ? stepIndex : 0,\n nextDueAtMs: ms(data['nextDueAtMs']),\n // A status the model does not name is read as stopped: nothing sends\n // from a document nobody can say the state of.\n status: (OUTREACH_ENROLLMENT_STATUSES as readonly unknown[]).includes(\n status,\n )\n ? (status as OutreachEnrollmentStatus)\n : 'stopped',\n stopReason: (OUTREACH_STOP_REASONS as readonly unknown[]).includes(\n stopReason,\n )\n ? (stopReason as OutreachStopReason)\n : null,\n stopDetail:\n typeof data['stopDetail'] === 'string' ? data['stopDetail'] : null,\n stoppedAtMs: ms(data['stoppedAtMs']),\n stoppedByUid:\n typeof data['stoppedByUid'] === 'string' ? data['stoppedByUid'] : null,\n personalLine: text(data['personalLine']),\n cold: data['cold'] === true,\n enrolledByUid: text(data['enrolledByUid']),\n gmailThreadId:\n typeof data['gmailThreadId'] === 'string' ? data['gmailThreadId'] : null,\n gmailThreadIds: texts(data['gmailThreadIds']),\n threadSubject:\n typeof data['threadSubject'] === 'string' ? data['threadSubject'] : null,\n messageIds: texts(data['messageIds']),\n lastSentAtMs: ms(data['lastSentAtMs']),\n createdAtMs: ms(data['createdAtMs']) ?? 0,\n updatedAtMs: ms(data['updatedAtMs']) ?? 0,\n }\n}\n\n/** A stored mailbox with its id, or `null` for no document. */\nexport function readStoredOutreachMailbox(\n id: string,\n data: Record<string, unknown> | undefined,\n): OutreachMailbox | null {\n if (!data) return null\n return { ...(data as unknown as OutreachMailbox), id }\n}\n"],"names":["readOutreachSequenceDraft","OUTREACH_ENROLLMENT_STATUSES","OUTREACH_SEQUENCE_STATUSES","OUTREACH_STOP_REASONS","text","value","ms","Number","isFinite","texts","Array","isArray","filter","entry","readStoredOutreachSequence","id","data","draft","status","includes","createdAtMs","updatedAtMs","readStoredOutreachEnrollment","stopReason","stepIndex","leadId","target","undefined","sequenceId","contactId","contactName","email","hostId","mailboxId","isInteger","nextDueAtMs","stopDetail","stoppedAtMs","stoppedByUid","personalLine","cold","enrolledByUid","gmailThreadId","gmailThreadIds","threadSubject","messageIds","lastSentAtMs","readStoredOutreachMailbox"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/model/stored-records.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * A stored Outreach document read back into its model shape (AGL-2980),\n * for the server's routes and the console's listeners alike. Client-safe.\n *\n * Read DEFENSIVELY, a field at a time, because the readers act on what they\n * read: a sequence's steps are what gets sent, and an enrollment's status is\n * what decides whether it is sent at all.\n */\n\nimport { readOutreachSequenceDraft } from './sequence-draft'\nimport {\n OUTREACH_ENROLLMENT_STATUSES,\n OUTREACH_SEQUENCE_STATUSES,\n OUTREACH_STOP_REASONS,\n type OutreachEnrollment,\n type OutreachEnrollmentStatus,\n type OutreachEnrollmentTarget,\n type OutreachMailbox,\n type OutreachSequence,\n type OutreachSequenceStats,\n type OutreachSequenceStatus,\n type OutreachStopReason,\n} from './outreach.types'\n\nconst text = (value: unknown): string =>\n typeof value === 'string' ? value : ''\nconst ms = (value: unknown): number | null =>\n typeof value === 'number' && Number.isFinite(value) ? value : null\nconst texts = (value: unknown): string[] =>\n Array.isArray(value)\n ? value.filter((entry): entry is string => typeof entry === 'string')\n : []\n\n/** A stored sequence in its model shape, or `null` for no document. */\nexport function readStoredOutreachSequence(\n id: string,\n data: Record<string, unknown> | undefined,\n): OutreachSequence | null {\n if (!data) return null\n const draft = readOutreachSequenceDraft(data)\n const status = data['status']\n const stats = readOutreachSequenceStats(data['stats'])\n return {\n id,\n ...draft,\n status: (OUTREACH_SEQUENCE_STATUSES as readonly unknown[]).includes(status)\n ? (status as OutreachSequenceStatus)\n : 'draft',\n // Absent rather than an empty object for a sequence that has none\n // (AGL-3239): the report reads an absent counter as \"not recorded\" and\n // a present one as a number, and `{}` would be neither.\n ...(stats ? { stats } : {}),\n createdAtMs: ms(data['createdAtMs']) ?? 0,\n updatedAtMs: ms(data['updatedAtMs']) ?? 0,\n }\n}\n\n/**\n * A sequence's stored counters (AGL-3239), or `null` when it has none.\n *\n * Every field is carried through only when it is READABLE, so an absence\n * survives the round trip as an absence: the whole of the report's honesty\n * is that a counter nobody wrote is not a zero, and a normalizer that\n * defaulted these to `0` would destroy the distinction here, before any\n * reader could make it.\n */\nexport function readOutreachSequenceStats(\n raw: unknown,\n): OutreachSequenceStats | null {\n if (!raw || typeof raw !== 'object') return null\n const data = raw as Record<string, unknown>\n const stats: OutreachSequenceStats = {}\n for (const key of ['sent', 'people', 'clicks', 'uniqueClicks', 'machineClicks'] as const) {\n const value = data[key]\n if (typeof value === 'number' && Number.isFinite(value) && value >= 0) {\n stats[key] = Math.floor(value)\n }\n }\n if (data['clickTracked'] === true) stats.clickTracked = true\n const last = data['lastClickAtMs']\n if (typeof last === 'number' && Number.isFinite(last)) stats.lastClickAtMs = last\n return Object.keys(stats).length ? stats : null\n}\n\n/** A stored enrollment in its model shape, or `null` for no document. */\nexport function readStoredOutreachEnrollment(\n id: string,\n data: Record<string, unknown> | undefined,\n): OutreachEnrollment | null {\n if (!data) return null\n const status = data['status']\n const stopReason = data['stopReason']\n const stepIndex = Number(data['stepIndex'])\n // An enrollment written before leads could be sequenced (AGL-3234) names\n // no target and is a contact's; one that names a lead and no target is a\n // lead's, whatever else it says.\n const leadId = text(data['leadId']) || null\n const target: OutreachEnrollmentTarget =\n data['target'] === 'lead' || (data['target'] === undefined && leadId && !text(data['contactId']))\n ? 'lead'\n : 'contact'\n return {\n ...(data as unknown as OutreachEnrollment),\n id,\n sequenceId: text(data['sequenceId']),\n target,\n contactId: text(data['contactId']),\n leadId,\n contactName: text(data['contactName']),\n email: text(data['email']),\n hostId: text(data['hostId']),\n mailboxId: text(data['mailboxId']),\n stepIndex: Number.isInteger(stepIndex) && stepIndex >= 0 ? stepIndex : 0,\n nextDueAtMs: ms(data['nextDueAtMs']),\n // A status the model does not name is read as stopped: nothing sends\n // from a document nobody can say the state of.\n status: (OUTREACH_ENROLLMENT_STATUSES as readonly unknown[]).includes(\n status,\n )\n ? (status as OutreachEnrollmentStatus)\n : 'stopped',\n stopReason: (OUTREACH_STOP_REASONS as readonly unknown[]).includes(\n stopReason,\n )\n ? (stopReason as OutreachStopReason)\n : null,\n stopDetail:\n typeof data['stopDetail'] === 'string' ? data['stopDetail'] : null,\n stoppedAtMs: ms(data['stoppedAtMs']),\n stoppedByUid:\n typeof data['stoppedByUid'] === 'string' ? data['stoppedByUid'] : null,\n personalLine: text(data['personalLine']),\n cold: data['cold'] === true,\n enrolledByUid: text(data['enrolledByUid']),\n gmailThreadId:\n typeof data['gmailThreadId'] === 'string' ? data['gmailThreadId'] : null,\n gmailThreadIds: texts(data['gmailThreadIds']),\n threadSubject:\n typeof data['threadSubject'] === 'string' ? data['threadSubject'] : null,\n messageIds: texts(data['messageIds']),\n lastSentAtMs: ms(data['lastSentAtMs']),\n createdAtMs: ms(data['createdAtMs']) ?? 0,\n updatedAtMs: ms(data['updatedAtMs']) ?? 0,\n }\n}\n\n/** A stored mailbox with its id, or `null` for no document. */\nexport function readStoredOutreachMailbox(\n id: string,\n data: Record<string, unknown> | undefined,\n): OutreachMailbox | null {\n if (!data) return null\n return { ...(data as unknown as OutreachMailbox), id }\n}\n"],"names":["readOutreachSequenceDraft","OUTREACH_ENROLLMENT_STATUSES","OUTREACH_SEQUENCE_STATUSES","OUTREACH_STOP_REASONS","text","value","ms","Number","isFinite","texts","Array","isArray","filter","entry","readStoredOutreachSequence","id","data","draft","status","stats","readOutreachSequenceStats","includes","createdAtMs","updatedAtMs","raw","key","Math","floor","clickTracked","last","lastClickAtMs","Object","keys","length","readStoredOutreachEnrollment","stopReason","stepIndex","leadId","target","undefined","sequenceId","contactId","contactName","email","hostId","mailboxId","isInteger","nextDueAtMs","stopDetail","stoppedAtMs","stoppedByUid","personalLine","cold","enrolledByUid","gmailThreadId","gmailThreadIds","threadSubject","messageIds","lastSentAtMs","readStoredOutreachMailbox"],"mappings":"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"}
|
|
@@ -223,7 +223,10 @@ export function createOutreachSequenceRoutes(deps) {
|
|
|
223
223
|
const sequence = _extends({
|
|
224
224
|
id: ref.id
|
|
225
225
|
}, draft, {
|
|
226
|
-
status: (_ref = existing == null ? void 0 : existing.status) != null ? _ref : 'draft'
|
|
226
|
+
status: (_ref = existing == null ? void 0 : existing.status) != null ? _ref : 'draft'
|
|
227
|
+
}, (existing == null ? void 0 : existing.stats) ? {
|
|
228
|
+
stats: existing.stats
|
|
229
|
+
} : {}, {
|
|
227
230
|
createdAtMs: (existing == null ? void 0 : existing.createdAtMs) || nowMs,
|
|
228
231
|
updatedAtMs: nowMs
|
|
229
232
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/sequence-routes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport type { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport { applyOutreachEnrollmentEvent } from '../engine/enrollment-state'\nimport {\n validateOutreachSequence,\n validateOutreachSequenceActivation,\n} from '../engine/sequence-validation'\nimport type {\n OutreachSequenceAction,\n OutreachSequenceSaveResponse,\n OutreachSequenceStatusResponse,\n} from '../model/outreach-api'\nimport { OUTREACH_OPEN_ENROLLMENT_STATUSES } from '../engine/gates'\nimport type { OutreachMailbox, OutreachSequence, OutreachSequenceStatus } from '../model/outreach.types'\nimport {\n outreachEnrolledSequenceIssues,\n outreachMailboxActivationIssue,\n readOutreachSequenceDraft,\n type OutreachSequenceDraft,\n type OutreachSequenceIssue,\n} from '../model/sequence-draft'\nimport { readOutreachComplianceSettingsDoc } from '../storage/compliance-settings-store'\nimport {\n outreachOrgCollection,\n readStoredOutreachEnrollment,\n readStoredOutreachMailbox,\n readStoredOutreachSequence,\n} from '../storage/outreach-records'\nimport { OUTREACH_SEQUENCE_ACTIVITY_TARGET, type OutreachRouteDeps } from './route-deps'\nimport { outreachRouteGate, type OutreachRouteCaller } from './route-gate'\nimport {\n outreachMethodNotAllowed,\n outreachOk,\n outreachRefusal,\n readOutreachDocumentId,\n readOutreachJsonBody,\n} from './route-http'\n\n/**\n * THE SEQUENCE ROUTES (AGL-2980): save, activate, pause, archive, delete.\n *\n * ## Who may change a sequence\n *\n * A sequence sends as the member whose mailbox it names, so it is theirs to\n * change: the member who connected the mailbox, or an organization owner or\n * admin, may save it, activate it and send from it — the same two the\n * mailbox's own routes let manage the mailbox. Anyone with `outreach.use`\n * may pause or archive any sequence, because stopping mail is never the act\n * that needs guarding.\n *\n * ## Saving\n *\n * The draft is read with the editor's reader and judged by the engine's\n * validator, so the save refuses exactly what the editor showed beside a\n * field. On top of that, the site must be one of the organization's, the\n * mailbox one of its connected mailboxes, the countries ones the\n * organization allows, and — once anyone is enrolled — the steps, site and\n * mailbox fixed (`outreachEnrolledSequenceIssues`). A new sequence is a\n * draft; an edit keeps the status it had, and an active sequence stays\n * active only because the edit passed the same validator activation does.\n *\n * ## Activating\n *\n * `validateOutreachSequenceActivation`: everything a save refuses on, and an\n * organization whose footer cannot be written — no legal name, or no postal\n * address. And its mailbox must be sending (`outreachMailboxActivationIssue`):\n * chosen, still connected, not paused, and not waiting to be reconnected —\n * the same words the sequence page shows beside its disabled Activate.\n *\n * ## Archiving\n *\n * Final. Every person still active or paused in the sequence is stopped\n * with `sequence_archived`, in batches, so no archived sequence goes on\n * sending.\n */\n\n/** A batch holds 500 writes; a page of enrollments stops this many. */\nconst ARCHIVE_PAGE = 400\n\n/** The activity line each lifecycle act writes. */\nexport const OUTREACH_SEQUENCE_ACTIVITY = {\n create: 'Created a sequence',\n activate: 'Activated a sequence',\n pause: 'Paused a sequence',\n archive: 'Archived a sequence',\n delete: 'Deleted a sequence',\n} as const\n\n/** Where each action may start, and where it ends. */\nconst TRANSITIONS: Record<\n OutreachSequenceAction,\n { from: readonly OutreachSequenceStatus[]; to: OutreachSequenceStatus }\n> = {\n activate: { from: ['draft', 'paused'], to: 'active' },\n pause: { from: ['active'], to: 'paused' },\n archive: { from: ['draft', 'active', 'paused'], to: 'archived' },\n}\n\nconst ACTION_REFUSALS: Record<OutreachSequenceAction, string> = {\n activate: 'Only a draft or paused sequence can be activated.',\n pause: 'Only an active sequence can be paused.',\n archive: 'This sequence is already archived.',\n}\n\nexport interface OutreachSequenceRoutes {\n save: PluginWebApiHandler\n status: PluginWebApiHandler\n remove: PluginWebApiHandler\n}\n\ntype Firestore = FirebaseFirestore.Firestore\n\nasync function loadSequence(\n firestore: Firestore,\n orgId: string,\n sequenceId: string,\n): Promise<OutreachSequence | null> {\n const snapshot = await outreachOrgCollection(firestore, orgId, 'sequences').doc(sequenceId).get()\n return readStoredOutreachSequence(sequenceId, snapshot.exists ? snapshot.data() : undefined)\n}\n\n/** Whether anyone was ever enrolled in the sequence. */\nasync function hasEnrollments(firestore: Firestore, orgId: string, sequenceId: string): Promise<boolean> {\n const found = await outreachOrgCollection(firestore, orgId, 'enrollments')\n .where('sequenceId', '==', sequenceId)\n .limit(1)\n .get()\n return !found.empty\n}\n\nconst issue = (\n path: string,\n code: OutreachSequenceIssue['code'],\n message: string,\n): OutreachSequenceIssue => ({ path, code, message, severity: 'error' })\n\n/**\n * The organization-level facts a draft has to agree with: its site is the\n * organization's, its mailbox is connected and the caller may send from it,\n * and its countries are ones the organization allows. `activating` adds\n * that the mailbox is sending now — chosen, and neither paused nor waiting\n * to be reconnected.\n */\nasync function draftPlacementIssues(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n draft: OutreachSequenceDraft,\n options: { activating?: boolean } = {},\n): Promise<OutreachSequenceIssue[]> {\n const issues: OutreachSequenceIssue[] = []\n if (draft.hostId) {\n const host = readOutreachDocumentId(draft.hostId)\n ? await firestore.collection('hosts').doc(draft.hostId).get()\n : null\n if (!host?.exists || host.get('orgId') !== caller.orgId) {\n issues.push(issue('hostId', 'host_unknown', \"Choose one of this organization's sites.\"))\n }\n } else {\n issues.push(issue('hostId', 'host_unknown', \"Choose the site whose contacts this sequence emails.\"))\n }\n const mailbox = draft.mailboxId ? await loadMailbox(firestore, caller.orgId, draft.mailboxId) : null\n const activation = options.activating ? outreachMailboxActivationIssue(draft.mailboxId, mailbox) : null\n if (draft.mailboxId) {\n const placement = mailboxIssues(caller, mailbox)\n // A mailbox that is gone is refused once, in activation's words when\n // activating: they say what to do next.\n issues.push(\n ...(activation?.code === 'mailbox_unknown'\n ? placement.filter((entry) => entry.code !== 'mailbox_unknown')\n : placement),\n )\n }\n if (activation && !issues.some((entry) => entry.code === 'mailbox_not_yours')) issues.push(activation)\n const settings = await readOutreachComplianceSettingsDoc(firestore, caller.orgId)\n const outside = draft.settings.allowedCountries.filter(\n (code) => !settings.allowedCountries.includes(code),\n )\n if (outside.length) {\n issues.push(\n issue(\n 'settings.allowedCountries',\n 'country_not_in_org',\n `${outside.join(', ')} ${outside.length === 1 ? \"isn't\" : \"aren't\"} among the countries your organization allows. Add ${outside.length === 1 ? 'it' : 'them'} in Sequences compliance settings first.`,\n ),\n )\n }\n return issues\n}\n\n/** A mailbox of this organization's, as stored; `null` when there is none by that id. */\nasync function loadMailbox(firestore: Firestore, orgId: string, mailboxId: string): Promise<OutreachMailbox | null> {\n const snapshot = readOutreachDocumentId(mailboxId)\n ? await outreachOrgCollection(firestore, orgId, 'mailboxes').doc(mailboxId).get()\n : null\n return readStoredOutreachMailbox(mailboxId, snapshot?.exists ? snapshot.data() : undefined)\n}\n\n/** Whether the mailbox is connected here, and the caller may send from it. */\nfunction mailboxIssues(caller: OutreachRouteCaller, mailbox: OutreachMailbox | null): OutreachSequenceIssue[] {\n if (!mailbox || mailbox.status === 'disconnected') {\n return [issue('mailboxId', 'mailbox_unknown', 'That mailbox is not connected to this organization.')]\n }\n if (mailbox.connectedByUid !== caller.uid && !caller.isOrgAdmin) {\n return [\n issue(\n 'mailboxId',\n 'mailbox_not_yours',\n 'Only the member who connected this mailbox, or an organization owner or admin, can send from it.',\n ),\n ]\n }\n return []\n}\n\n/** One issue per field and code: activation and the engine can both ask for a mailbox. */\nfunction distinctIssues(issues: readonly OutreachSequenceIssue[]): OutreachSequenceIssue[] {\n const seen = new Set<string>()\n return issues.filter((entry) => {\n const key = `${entry.path}|${entry.code}`\n if (seen.has(key)) return false\n seen.add(key)\n return true\n })\n}\n\nexport function createOutreachSequenceRoutes(deps: OutreachRouteDeps): OutreachSequenceRoutes {\n const activity = (caller: OutreachRouteCaller, action: string, sequence: Pick<OutreachSequence, 'id' | 'name'>) =>\n deps.logOrgActivity(caller.orgId, { uid: caller.uid, email: caller.email }, action, {\n type: OUTREACH_SEQUENCE_ACTIVITY_TARGET,\n id: sequence.id,\n name: sequence.name,\n })\n\n const save: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate)\n if (caller instanceof Response) return caller\n const firestore = deps.firestore()\n\n const rawId = body['sequenceId']\n const sequenceId = rawId === undefined || rawId === null || rawId === '' ? null : readOutreachDocumentId(rawId)\n if (rawId && !sequenceId) return outreachRefusal(400, 'invalid-request', 'Name the sequence to save.')\n const existing = sequenceId ? await loadSequence(firestore, caller.orgId, sequenceId) : null\n if (sequenceId && !existing) {\n return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n }\n if (existing?.status === 'archived') {\n return outreachRefusal(409, 'sequence-archived', \"An archived sequence can't be edited.\")\n }\n\n const draft = readOutreachSequenceDraft(body['sequence'])\n // A sequence sends as its mailbox's member, so it is theirs (or an\n // admin's) to change — the stored mailbox as well as a new one: moving\n // a colleague's sequence onto your own mailbox is still changing theirs.\n if (existing?.mailboxId && existing.mailboxId !== draft.mailboxId) {\n const stored = mailboxIssues(caller, await loadMailbox(firestore, caller.orgId, existing.mailboxId))\n const refused = stored.find((entry) => entry.code === 'mailbox_not_yours')\n if (refused) return outreachRefusal(403, 'permission', refused.message)\n }\n const judged: OutreachSequenceIssue[] = [\n ...validateOutreachSequence(draft),\n ...(await draftPlacementIssues(firestore, caller, draft)),\n ]\n const ownership = judged.find((entry) => entry.code === 'mailbox_not_yours')\n if (ownership) return outreachRefusal(403, 'permission', ownership.message)\n if (existing && (await hasEnrollments(firestore, caller.orgId, existing.id))) {\n judged.push(...outreachEnrolledSequenceIssues(existing, draft))\n }\n const errors = judged.filter((entry) => entry.severity === 'error')\n if (errors.length) {\n return outreachRefusal(400, 'invalid-sequence', errors[0].message, { issues: judged })\n }\n\n const nowMs = deps.now()\n const ref = existing\n ? outreachOrgCollection(firestore, caller.orgId, 'sequences').doc(existing.id)\n : outreachOrgCollection(firestore, caller.orgId, 'sequences').doc(createResourceUid())\n const sequence: OutreachSequence = {\n id: ref.id,\n ...draft,\n status: existing?.status ?? 'draft',\n createdAtMs: existing?.createdAtMs || nowMs,\n updatedAtMs: nowMs,\n }\n await ref.set(sequence)\n if (!existing) await activity(caller, OUTREACH_SEQUENCE_ACTIVITY.create, sequence)\n return outreachOk({\n ok: true,\n sequence,\n created: !existing,\n warnings: judged.filter((entry) => entry.severity === 'warning'),\n } satisfies OutreachSequenceSaveResponse)\n }\n\n const status: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate)\n if (caller instanceof Response) return caller\n const action = body['action'] as OutreachSequenceAction\n const sequenceId = readOutreachDocumentId(body['sequenceId'])\n if (!sequenceId || !Object.prototype.hasOwnProperty.call(TRANSITIONS, action)) {\n return outreachRefusal(400, 'invalid-request', 'Name the sequence, and activate, pause or archive.')\n }\n const firestore = deps.firestore()\n const sequence = await loadSequence(firestore, caller.orgId, sequenceId)\n if (!sequence) return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n const transition = TRANSITIONS[action]\n if (sequence.status === transition.to) {\n return outreachOk({ ok: true, sequence, stoppedEnrollments: 0 } satisfies OutreachSequenceStatusResponse)\n }\n if (!transition.from.includes(sequence.status)) {\n return outreachRefusal(409, 'transition-refused', ACTION_REFUSALS[action])\n }\n\n if (action === 'activate') {\n const orgSettings = await readOutreachComplianceSettingsDoc(firestore, caller.orgId)\n const judged: OutreachSequenceIssue[] = distinctIssues([\n ...validateOutreachSequenceActivation(sequence, orgSettings),\n ...(await draftPlacementIssues(firestore, caller, sequence, { activating: true })),\n ]).filter((entry) => entry.severity === 'error')\n if (judged.length) {\n const ownership = judged.find((entry) => entry.code === 'mailbox_not_yours')\n return ownership\n ? outreachRefusal(403, 'permission', ownership.message)\n : outreachRefusal(409, 'activation-refused', judged[0].message, { issues: judged })\n }\n }\n\n const nowMs = deps.now()\n const next: OutreachSequence = { ...sequence, status: transition.to, updatedAtMs: nowMs }\n await outreachOrgCollection(firestore, caller.orgId, 'sequences')\n .doc(sequence.id)\n .update({ status: next.status, updatedAtMs: nowMs })\n const stoppedEnrollments =\n action === 'archive' ? await stopOpenEnrollments(firestore, caller, sequence.id, nowMs) : 0\n await activity(caller, OUTREACH_SEQUENCE_ACTIVITY[action], sequence)\n return outreachOk({ ok: true, sequence: next, stoppedEnrollments } satisfies OutreachSequenceStatusResponse)\n }\n\n const remove: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate)\n if (caller instanceof Response) return caller\n const sequenceId = readOutreachDocumentId(body['sequenceId'])\n if (!sequenceId) return outreachRefusal(400, 'invalid-request', 'Name the sequence to delete.')\n const firestore = deps.firestore()\n const sequence = await loadSequence(firestore, caller.orgId, sequenceId)\n if (!sequence) return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n if (sequence.status !== 'draft' || (await hasEnrollments(firestore, caller.orgId, sequence.id))) {\n // What people were sent, and why they stopped, is the record of what\n // this organization did; only a draft nobody was enrolled in has none.\n return outreachRefusal(\n 409,\n 'delete-refused',\n 'Only a draft nobody was enrolled in can be deleted. Archive this sequence instead.',\n )\n }\n await outreachOrgCollection(firestore, caller.orgId, 'sequences').doc(sequence.id).delete()\n await activity(caller, OUTREACH_SEQUENCE_ACTIVITY.delete, sequence)\n return outreachOk({ ok: true })\n }\n\n return { save, status, remove }\n}\n\n/**\n * Stops everyone still active or paused in an archived sequence, a page at\n * a time, through the engine's own transition. Returns how many it stopped.\n */\nasync function stopOpenEnrollments(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n sequenceId: string,\n nowMs: number,\n): Promise<number> {\n const enrollments = outreachOrgCollection(firestore, caller.orgId, 'enrollments')\n let stopped = 0\n let after: FirebaseFirestore.QueryDocumentSnapshot | null = null\n for (;;) {\n let page = enrollments.where('sequenceId', '==', sequenceId).orderBy('__name__').limit(ARCHIVE_PAGE)\n if (after) page = page.startAfter(after)\n const snapshot = await page.get()\n if (snapshot.empty) break\n const batch = firestore.batch()\n let writes = 0\n for (const doc of snapshot.docs) {\n const enrollment = readStoredOutreachEnrollment(doc.id, doc.data())\n if (!enrollment || !OUTREACH_OPEN_ENROLLMENT_STATUSES.includes(enrollment.status)) continue\n const { patch } = applyOutreachEnrollmentEvent(enrollment, {\n type: 'stop',\n reason: 'sequence_archived',\n atMs: nowMs,\n byUid: caller.uid,\n })\n if (!patch) continue\n batch.update(doc.ref, { ...patch, updatedAtMs: nowMs })\n writes += 1\n }\n if (writes) await batch.commit()\n stopped += writes\n after = snapshot.docs[snapshot.docs.length - 1]\n if (snapshot.size < ARCHIVE_PAGE) break\n }\n return stopped\n}\n"],"names":["createResourceUid","applyOutreachEnrollmentEvent","validateOutreachSequence","validateOutreachSequenceActivation","OUTREACH_OPEN_ENROLLMENT_STATUSES","outreachEnrolledSequenceIssues","outreachMailboxActivationIssue","readOutreachSequenceDraft","readOutreachComplianceSettingsDoc","outreachOrgCollection","readStoredOutreachEnrollment","readStoredOutreachMailbox","readStoredOutreachSequence","OUTREACH_SEQUENCE_ACTIVITY_TARGET","outreachRouteGate","outreachMethodNotAllowed","outreachOk","outreachRefusal","readOutreachDocumentId","readOutreachJsonBody","ARCHIVE_PAGE","OUTREACH_SEQUENCE_ACTIVITY","create","activate","pause","archive","delete","TRANSITIONS","from","to","ACTION_REFUSALS","loadSequence","firestore","orgId","sequenceId","snapshot","doc","get","exists","data","undefined","hasEnrollments","found","where","limit","empty","issue","path","code","message","severity","draftPlacementIssues","caller","draft","options","issues","hostId","host","collection","push","mailbox","mailboxId","loadMailbox","activation","activating","placement","mailboxIssues","filter","entry","some","settings","outside","allowedCountries","includes","length","join","status","connectedByUid","uid","isOrgAdmin","distinctIssues","seen","Set","key","has","add","createOutreachSequenceRoutes","deps","activity","action","sequence","logOrgActivity","email","type","id","name","save","request","method","body","gate","Response","rawId","existing","stored","refused","find","judged","ownership","errors","nowMs","now","ref","createdAtMs","updatedAtMs","set","ok","created","warnings","Object","prototype","hasOwnProperty","call","transition","stoppedEnrollments","orgSettings","next","update","stopOpenEnrollments","remove","enrollments","stopped","after","page","orderBy","startAfter","batch","writes","docs","enrollment","patch","reason","atMs","byUid","commit","size"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/sequence-routes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport type { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport { applyOutreachEnrollmentEvent } from '../engine/enrollment-state'\nimport {\n validateOutreachSequence,\n validateOutreachSequenceActivation,\n} from '../engine/sequence-validation'\nimport type {\n OutreachSequenceAction,\n OutreachSequenceSaveResponse,\n OutreachSequenceStatusResponse,\n} from '../model/outreach-api'\nimport { OUTREACH_OPEN_ENROLLMENT_STATUSES } from '../engine/gates'\nimport type { OutreachMailbox, OutreachSequence, OutreachSequenceStatus } from '../model/outreach.types'\nimport {\n outreachEnrolledSequenceIssues,\n outreachMailboxActivationIssue,\n readOutreachSequenceDraft,\n type OutreachSequenceDraft,\n type OutreachSequenceIssue,\n} from '../model/sequence-draft'\nimport { readOutreachComplianceSettingsDoc } from '../storage/compliance-settings-store'\nimport {\n outreachOrgCollection,\n readStoredOutreachEnrollment,\n readStoredOutreachMailbox,\n readStoredOutreachSequence,\n} from '../storage/outreach-records'\nimport { OUTREACH_SEQUENCE_ACTIVITY_TARGET, type OutreachRouteDeps } from './route-deps'\nimport { outreachRouteGate, type OutreachRouteCaller } from './route-gate'\nimport {\n outreachMethodNotAllowed,\n outreachOk,\n outreachRefusal,\n readOutreachDocumentId,\n readOutreachJsonBody,\n} from './route-http'\n\n/**\n * THE SEQUENCE ROUTES (AGL-2980): save, activate, pause, archive, delete.\n *\n * ## Who may change a sequence\n *\n * A sequence sends as the member whose mailbox it names, so it is theirs to\n * change: the member who connected the mailbox, or an organization owner or\n * admin, may save it, activate it and send from it — the same two the\n * mailbox's own routes let manage the mailbox. Anyone with `outreach.use`\n * may pause or archive any sequence, because stopping mail is never the act\n * that needs guarding.\n *\n * ## Saving\n *\n * The draft is read with the editor's reader and judged by the engine's\n * validator, so the save refuses exactly what the editor showed beside a\n * field. On top of that, the site must be one of the organization's, the\n * mailbox one of its connected mailboxes, the countries ones the\n * organization allows, and — once anyone is enrolled — the steps, site and\n * mailbox fixed (`outreachEnrolledSequenceIssues`). A new sequence is a\n * draft; an edit keeps the status it had, and an active sequence stays\n * active only because the edit passed the same validator activation does.\n *\n * ## Activating\n *\n * `validateOutreachSequenceActivation`: everything a save refuses on, and an\n * organization whose footer cannot be written — no legal name, or no postal\n * address. And its mailbox must be sending (`outreachMailboxActivationIssue`):\n * chosen, still connected, not paused, and not waiting to be reconnected —\n * the same words the sequence page shows beside its disabled Activate.\n *\n * ## Archiving\n *\n * Final. Every person still active or paused in the sequence is stopped\n * with `sequence_archived`, in batches, so no archived sequence goes on\n * sending.\n */\n\n/** A batch holds 500 writes; a page of enrollments stops this many. */\nconst ARCHIVE_PAGE = 400\n\n/** The activity line each lifecycle act writes. */\nexport const OUTREACH_SEQUENCE_ACTIVITY = {\n create: 'Created a sequence',\n activate: 'Activated a sequence',\n pause: 'Paused a sequence',\n archive: 'Archived a sequence',\n delete: 'Deleted a sequence',\n} as const\n\n/** Where each action may start, and where it ends. */\nconst TRANSITIONS: Record<\n OutreachSequenceAction,\n { from: readonly OutreachSequenceStatus[]; to: OutreachSequenceStatus }\n> = {\n activate: { from: ['draft', 'paused'], to: 'active' },\n pause: { from: ['active'], to: 'paused' },\n archive: { from: ['draft', 'active', 'paused'], to: 'archived' },\n}\n\nconst ACTION_REFUSALS: Record<OutreachSequenceAction, string> = {\n activate: 'Only a draft or paused sequence can be activated.',\n pause: 'Only an active sequence can be paused.',\n archive: 'This sequence is already archived.',\n}\n\nexport interface OutreachSequenceRoutes {\n save: PluginWebApiHandler\n status: PluginWebApiHandler\n remove: PluginWebApiHandler\n}\n\ntype Firestore = FirebaseFirestore.Firestore\n\nasync function loadSequence(\n firestore: Firestore,\n orgId: string,\n sequenceId: string,\n): Promise<OutreachSequence | null> {\n const snapshot = await outreachOrgCollection(firestore, orgId, 'sequences').doc(sequenceId).get()\n return readStoredOutreachSequence(sequenceId, snapshot.exists ? snapshot.data() : undefined)\n}\n\n/** Whether anyone was ever enrolled in the sequence. */\nasync function hasEnrollments(firestore: Firestore, orgId: string, sequenceId: string): Promise<boolean> {\n const found = await outreachOrgCollection(firestore, orgId, 'enrollments')\n .where('sequenceId', '==', sequenceId)\n .limit(1)\n .get()\n return !found.empty\n}\n\nconst issue = (\n path: string,\n code: OutreachSequenceIssue['code'],\n message: string,\n): OutreachSequenceIssue => ({ path, code, message, severity: 'error' })\n\n/**\n * The organization-level facts a draft has to agree with: its site is the\n * organization's, its mailbox is connected and the caller may send from it,\n * and its countries are ones the organization allows. `activating` adds\n * that the mailbox is sending now — chosen, and neither paused nor waiting\n * to be reconnected.\n */\nasync function draftPlacementIssues(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n draft: OutreachSequenceDraft,\n options: { activating?: boolean } = {},\n): Promise<OutreachSequenceIssue[]> {\n const issues: OutreachSequenceIssue[] = []\n if (draft.hostId) {\n const host = readOutreachDocumentId(draft.hostId)\n ? await firestore.collection('hosts').doc(draft.hostId).get()\n : null\n if (!host?.exists || host.get('orgId') !== caller.orgId) {\n issues.push(issue('hostId', 'host_unknown', \"Choose one of this organization's sites.\"))\n }\n } else {\n issues.push(issue('hostId', 'host_unknown', \"Choose the site whose contacts this sequence emails.\"))\n }\n const mailbox = draft.mailboxId ? await loadMailbox(firestore, caller.orgId, draft.mailboxId) : null\n const activation = options.activating ? outreachMailboxActivationIssue(draft.mailboxId, mailbox) : null\n if (draft.mailboxId) {\n const placement = mailboxIssues(caller, mailbox)\n // A mailbox that is gone is refused once, in activation's words when\n // activating: they say what to do next.\n issues.push(\n ...(activation?.code === 'mailbox_unknown'\n ? placement.filter((entry) => entry.code !== 'mailbox_unknown')\n : placement),\n )\n }\n if (activation && !issues.some((entry) => entry.code === 'mailbox_not_yours')) issues.push(activation)\n const settings = await readOutreachComplianceSettingsDoc(firestore, caller.orgId)\n const outside = draft.settings.allowedCountries.filter(\n (code) => !settings.allowedCountries.includes(code),\n )\n if (outside.length) {\n issues.push(\n issue(\n 'settings.allowedCountries',\n 'country_not_in_org',\n `${outside.join(', ')} ${outside.length === 1 ? \"isn't\" : \"aren't\"} among the countries your organization allows. Add ${outside.length === 1 ? 'it' : 'them'} in Sequences compliance settings first.`,\n ),\n )\n }\n return issues\n}\n\n/** A mailbox of this organization's, as stored; `null` when there is none by that id. */\nasync function loadMailbox(firestore: Firestore, orgId: string, mailboxId: string): Promise<OutreachMailbox | null> {\n const snapshot = readOutreachDocumentId(mailboxId)\n ? await outreachOrgCollection(firestore, orgId, 'mailboxes').doc(mailboxId).get()\n : null\n return readStoredOutreachMailbox(mailboxId, snapshot?.exists ? snapshot.data() : undefined)\n}\n\n/** Whether the mailbox is connected here, and the caller may send from it. */\nfunction mailboxIssues(caller: OutreachRouteCaller, mailbox: OutreachMailbox | null): OutreachSequenceIssue[] {\n if (!mailbox || mailbox.status === 'disconnected') {\n return [issue('mailboxId', 'mailbox_unknown', 'That mailbox is not connected to this organization.')]\n }\n if (mailbox.connectedByUid !== caller.uid && !caller.isOrgAdmin) {\n return [\n issue(\n 'mailboxId',\n 'mailbox_not_yours',\n 'Only the member who connected this mailbox, or an organization owner or admin, can send from it.',\n ),\n ]\n }\n return []\n}\n\n/** One issue per field and code: activation and the engine can both ask for a mailbox. */\nfunction distinctIssues(issues: readonly OutreachSequenceIssue[]): OutreachSequenceIssue[] {\n const seen = new Set<string>()\n return issues.filter((entry) => {\n const key = `${entry.path}|${entry.code}`\n if (seen.has(key)) return false\n seen.add(key)\n return true\n })\n}\n\nexport function createOutreachSequenceRoutes(deps: OutreachRouteDeps): OutreachSequenceRoutes {\n const activity = (caller: OutreachRouteCaller, action: string, sequence: Pick<OutreachSequence, 'id' | 'name'>) =>\n deps.logOrgActivity(caller.orgId, { uid: caller.uid, email: caller.email }, action, {\n type: OUTREACH_SEQUENCE_ACTIVITY_TARGET,\n id: sequence.id,\n name: sequence.name,\n })\n\n const save: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate)\n if (caller instanceof Response) return caller\n const firestore = deps.firestore()\n\n const rawId = body['sequenceId']\n const sequenceId = rawId === undefined || rawId === null || rawId === '' ? null : readOutreachDocumentId(rawId)\n if (rawId && !sequenceId) return outreachRefusal(400, 'invalid-request', 'Name the sequence to save.')\n const existing = sequenceId ? await loadSequence(firestore, caller.orgId, sequenceId) : null\n if (sequenceId && !existing) {\n return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n }\n if (existing?.status === 'archived') {\n return outreachRefusal(409, 'sequence-archived', \"An archived sequence can't be edited.\")\n }\n\n const draft = readOutreachSequenceDraft(body['sequence'])\n // A sequence sends as its mailbox's member, so it is theirs (or an\n // admin's) to change — the stored mailbox as well as a new one: moving\n // a colleague's sequence onto your own mailbox is still changing theirs.\n if (existing?.mailboxId && existing.mailboxId !== draft.mailboxId) {\n const stored = mailboxIssues(caller, await loadMailbox(firestore, caller.orgId, existing.mailboxId))\n const refused = stored.find((entry) => entry.code === 'mailbox_not_yours')\n if (refused) return outreachRefusal(403, 'permission', refused.message)\n }\n const judged: OutreachSequenceIssue[] = [\n ...validateOutreachSequence(draft),\n ...(await draftPlacementIssues(firestore, caller, draft)),\n ]\n const ownership = judged.find((entry) => entry.code === 'mailbox_not_yours')\n if (ownership) return outreachRefusal(403, 'permission', ownership.message)\n if (existing && (await hasEnrollments(firestore, caller.orgId, existing.id))) {\n judged.push(...outreachEnrolledSequenceIssues(existing, draft))\n }\n const errors = judged.filter((entry) => entry.severity === 'error')\n if (errors.length) {\n return outreachRefusal(400, 'invalid-sequence', errors[0].message, { issues: judged })\n }\n\n const nowMs = deps.now()\n const ref = existing\n ? outreachOrgCollection(firestore, caller.orgId, 'sequences').doc(existing.id)\n : outreachOrgCollection(firestore, caller.orgId, 'sequences').doc(createResourceUid())\n const sequence: OutreachSequence = {\n id: ref.id,\n ...draft,\n status: existing?.status ?? 'draft',\n /*\n * Carried through explicitly, because the write below REPLACES the\n * document (AGL-3239). The counters are the runtime's and the click\n * route's, never the editor's, and a member renaming a sequence must\n * not reset what it measured.\n */\n ...(existing?.stats ? { stats: existing.stats } : {}),\n createdAtMs: existing?.createdAtMs || nowMs,\n updatedAtMs: nowMs,\n }\n await ref.set(sequence)\n if (!existing) await activity(caller, OUTREACH_SEQUENCE_ACTIVITY.create, sequence)\n return outreachOk({\n ok: true,\n sequence,\n created: !existing,\n warnings: judged.filter((entry) => entry.severity === 'warning'),\n } satisfies OutreachSequenceSaveResponse)\n }\n\n const status: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate)\n if (caller instanceof Response) return caller\n const action = body['action'] as OutreachSequenceAction\n const sequenceId = readOutreachDocumentId(body['sequenceId'])\n if (!sequenceId || !Object.prototype.hasOwnProperty.call(TRANSITIONS, action)) {\n return outreachRefusal(400, 'invalid-request', 'Name the sequence, and activate, pause or archive.')\n }\n const firestore = deps.firestore()\n const sequence = await loadSequence(firestore, caller.orgId, sequenceId)\n if (!sequence) return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n const transition = TRANSITIONS[action]\n if (sequence.status === transition.to) {\n return outreachOk({ ok: true, sequence, stoppedEnrollments: 0 } satisfies OutreachSequenceStatusResponse)\n }\n if (!transition.from.includes(sequence.status)) {\n return outreachRefusal(409, 'transition-refused', ACTION_REFUSALS[action])\n }\n\n if (action === 'activate') {\n const orgSettings = await readOutreachComplianceSettingsDoc(firestore, caller.orgId)\n const judged: OutreachSequenceIssue[] = distinctIssues([\n ...validateOutreachSequenceActivation(sequence, orgSettings),\n ...(await draftPlacementIssues(firestore, caller, sequence, { activating: true })),\n ]).filter((entry) => entry.severity === 'error')\n if (judged.length) {\n const ownership = judged.find((entry) => entry.code === 'mailbox_not_yours')\n return ownership\n ? outreachRefusal(403, 'permission', ownership.message)\n : outreachRefusal(409, 'activation-refused', judged[0].message, { issues: judged })\n }\n }\n\n const nowMs = deps.now()\n const next: OutreachSequence = { ...sequence, status: transition.to, updatedAtMs: nowMs }\n await outreachOrgCollection(firestore, caller.orgId, 'sequences')\n .doc(sequence.id)\n .update({ status: next.status, updatedAtMs: nowMs })\n const stoppedEnrollments =\n action === 'archive' ? await stopOpenEnrollments(firestore, caller, sequence.id, nowMs) : 0\n await activity(caller, OUTREACH_SEQUENCE_ACTIVITY[action], sequence)\n return outreachOk({ ok: true, sequence: next, stoppedEnrollments } satisfies OutreachSequenceStatusResponse)\n }\n\n const remove: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate)\n if (caller instanceof Response) return caller\n const sequenceId = readOutreachDocumentId(body['sequenceId'])\n if (!sequenceId) return outreachRefusal(400, 'invalid-request', 'Name the sequence to delete.')\n const firestore = deps.firestore()\n const sequence = await loadSequence(firestore, caller.orgId, sequenceId)\n if (!sequence) return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n if (sequence.status !== 'draft' || (await hasEnrollments(firestore, caller.orgId, sequence.id))) {\n // What people were sent, and why they stopped, is the record of what\n // this organization did; only a draft nobody was enrolled in has none.\n return outreachRefusal(\n 409,\n 'delete-refused',\n 'Only a draft nobody was enrolled in can be deleted. Archive this sequence instead.',\n )\n }\n await outreachOrgCollection(firestore, caller.orgId, 'sequences').doc(sequence.id).delete()\n await activity(caller, OUTREACH_SEQUENCE_ACTIVITY.delete, sequence)\n return outreachOk({ ok: true })\n }\n\n return { save, status, remove }\n}\n\n/**\n * Stops everyone still active or paused in an archived sequence, a page at\n * a time, through the engine's own transition. Returns how many it stopped.\n */\nasync function stopOpenEnrollments(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n sequenceId: string,\n nowMs: number,\n): Promise<number> {\n const enrollments = outreachOrgCollection(firestore, caller.orgId, 'enrollments')\n let stopped = 0\n let after: FirebaseFirestore.QueryDocumentSnapshot | null = null\n for (;;) {\n let page = enrollments.where('sequenceId', '==', sequenceId).orderBy('__name__').limit(ARCHIVE_PAGE)\n if (after) page = page.startAfter(after)\n const snapshot = await page.get()\n if (snapshot.empty) break\n const batch = firestore.batch()\n let writes = 0\n for (const doc of snapshot.docs) {\n const enrollment = readStoredOutreachEnrollment(doc.id, doc.data())\n if (!enrollment || !OUTREACH_OPEN_ENROLLMENT_STATUSES.includes(enrollment.status)) continue\n const { patch } = applyOutreachEnrollmentEvent(enrollment, {\n type: 'stop',\n reason: 'sequence_archived',\n atMs: nowMs,\n byUid: caller.uid,\n })\n if (!patch) continue\n batch.update(doc.ref, { ...patch, updatedAtMs: nowMs })\n writes += 1\n }\n if (writes) await batch.commit()\n stopped += writes\n after = snapshot.docs[snapshot.docs.length - 1]\n if (snapshot.size < ARCHIVE_PAGE) break\n }\n return stopped\n}\n"],"names":["createResourceUid","applyOutreachEnrollmentEvent","validateOutreachSequence","validateOutreachSequenceActivation","OUTREACH_OPEN_ENROLLMENT_STATUSES","outreachEnrolledSequenceIssues","outreachMailboxActivationIssue","readOutreachSequenceDraft","readOutreachComplianceSettingsDoc","outreachOrgCollection","readStoredOutreachEnrollment","readStoredOutreachMailbox","readStoredOutreachSequence","OUTREACH_SEQUENCE_ACTIVITY_TARGET","outreachRouteGate","outreachMethodNotAllowed","outreachOk","outreachRefusal","readOutreachDocumentId","readOutreachJsonBody","ARCHIVE_PAGE","OUTREACH_SEQUENCE_ACTIVITY","create","activate","pause","archive","delete","TRANSITIONS","from","to","ACTION_REFUSALS","loadSequence","firestore","orgId","sequenceId","snapshot","doc","get","exists","data","undefined","hasEnrollments","found","where","limit","empty","issue","path","code","message","severity","draftPlacementIssues","caller","draft","options","issues","hostId","host","collection","push","mailbox","mailboxId","loadMailbox","activation","activating","placement","mailboxIssues","filter","entry","some","settings","outside","allowedCountries","includes","length","join","status","connectedByUid","uid","isOrgAdmin","distinctIssues","seen","Set","key","has","add","createOutreachSequenceRoutes","deps","activity","action","sequence","logOrgActivity","email","type","id","name","save","request","method","body","gate","Response","rawId","existing","stored","refused","find","judged","ownership","errors","nowMs","now","ref","stats","createdAtMs","updatedAtMs","set","ok","created","warnings","Object","prototype","hasOwnProperty","call","transition","stoppedEnrollments","orgSettings","next","update","stopOpenEnrollments","remove","enrollments","stopped","after","page","orderBy","startAfter","batch","writes","docs","enrollment","patch","reason","atMs","byUid","commit","size"],"mappings":"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"}
|