@aglyn/plugins-outreach 1.0.0-beta.161 → 1.0.0-beta.163
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 -13
- package/src/lib/components/sequence-editor.js +24 -3
- package/src/lib/components/sequence-editor.js.map +1 -1
- package/src/lib/declarations.console-server.js +27 -3
- package/src/lib/declarations.console-server.js.map +1 -1
- package/src/lib/engine/enrollment-state.d.ts +1 -1
- package/src/lib/engine/enrollment-state.js +4 -0
- package/src/lib/engine/enrollment-state.js.map +1 -1
- package/src/lib/engine/gates.d.ts +1 -1
- package/src/lib/engine/gates.js +6 -1
- package/src/lib/engine/gates.js.map +1 -1
- package/src/lib/enrollment/enroll-people.js +6 -3
- package/src/lib/enrollment/enroll-people.js.map +1 -1
- package/src/lib/model/outreach.types.d.ts +15 -0
- package/src/lib/model/outreach.types.js.map +1 -1
- package/src/lib/model/sequence-draft.d.ts +8 -2
- package/src/lib/model/sequence-draft.js +6 -3
- package/src/lib/model/sequence-draft.js.map +1 -1
- package/src/lib/model/stored-records.js +6 -1
- package/src/lib/model/stored-records.js.map +1 -1
- package/src/lib/routes/enroll-routes.js +39 -1
- package/src/lib/routes/enroll-routes.js.map +1 -1
- package/src/lib/routes/enrollment-routes.js +13 -0
- package/src/lib/routes/enrollment-routes.js.map +1 -1
- package/src/lib/routes/platform-deps.d.ts +3 -1
- package/src/lib/routes/platform-deps.js +4 -2
- package/src/lib/routes/platform-deps.js.map +1 -1
- package/src/lib/routes/register-routes.js +12 -1
- package/src/lib/routes/register-routes.js.map +1 -1
- package/src/lib/routes/route-deps.d.ts +11 -0
- package/src/lib/routes/route-deps.js.map +1 -1
- package/src/lib/routes/sequence-routes.js +25 -2
- package/src/lib/routes/sequence-routes.js.map +1 -1
- package/src/lib/runtime/campaign-credit.d.ts +81 -0
- package/src/lib/runtime/campaign-credit.js +133 -0
- package/src/lib/runtime/campaign-credit.js.map +1 -0
- package/src/lib/runtime/campaign-detach.d.ts +21 -0
- package/src/lib/runtime/campaign-detach.js +85 -0
- package/src/lib/runtime/campaign-detach.js.map +1 -0
- package/src/lib/runtime/click-route.d.ts +1 -1
- package/src/lib/runtime/click-route.js +13 -1
- package/src/lib/runtime/click-route.js.map +1 -1
- package/src/lib/runtime/enrollment-events.d.ts +6 -2
- package/src/lib/runtime/enrollment-events.js +20 -3
- package/src/lib/runtime/enrollment-events.js.map +1 -1
- package/src/lib/runtime/platform-runtime-deps.d.ts +8 -1
- package/src/lib/runtime/platform-runtime-deps.js +52 -0
- package/src/lib/runtime/platform-runtime-deps.js.map +1 -1
- package/src/lib/runtime/runtime-deps.d.ts +73 -0
- package/src/lib/runtime/runtime-deps.js.map +1 -1
- package/src/lib/runtime/send-job.js +17 -2
- package/src/lib/runtime/send-job.js.map +1 -1
- package/src/lib/runtime/sync-job.js +59 -10
- package/src/lib/runtime/sync-job.js.map +1 -1
- package/src/lib/runtime/timeline.d.ts +10 -1
- package/src/lib/runtime/timeline.js +23 -1
- package/src/lib/runtime/timeline.js.map +1 -1
- package/src/lib/runtime/unsubscribe-route.d.ts +1 -1
- package/src/lib/runtime/unsubscribe-route.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/plugins-outreach",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.163",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,18 +25,18 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-ui-email-campaigns": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-ui-jsx": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-ui-next": "1.0.0-beta.
|
|
33
|
-
"@aglyn/shared-ui-snackstack": "1.0.0-beta.
|
|
34
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
35
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
36
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
37
|
-
"@aglyn/tenant-data-admin": "1.0.0-beta.
|
|
38
|
-
"@aglyn/tenant-feature-instance": "1.0.0-beta.
|
|
39
|
-
"@aglyn/tenant-runtime": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.163",
|
|
29
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.163",
|
|
30
|
+
"@aglyn/shared-ui-email-campaigns": "1.0.0-beta.163",
|
|
31
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.163",
|
|
32
|
+
"@aglyn/shared-ui-next": "1.0.0-beta.163",
|
|
33
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.163",
|
|
34
|
+
"@aglyn/shared-util-http": "1.0.0-beta.163",
|
|
35
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.163",
|
|
36
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.163",
|
|
37
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.163",
|
|
38
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.163",
|
|
39
|
+
"@aglyn/tenant-runtime": "1.0.0-beta.163",
|
|
40
40
|
"@swc/helpers": "0.5.23"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
@@ -20,8 +20,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
20
20
|
import { pluginDocsHelp } from "@aglyn/aglyn";
|
|
21
21
|
import { mdiPlus } from "@aglyn/shared-data-mdi";
|
|
22
22
|
import { CardDisplay, MdiIcon } from "@aglyn/shared-ui-jsx";
|
|
23
|
+
import CampaignPicker from "@aglyn/shared-ui-email-campaigns/components/campaign-picker.component";
|
|
23
24
|
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
24
|
-
import { useUser } from "@aglyn/tenant-feature-instance";
|
|
25
|
+
import { useHostCampaigns, useUser } from "@aglyn/tenant-feature-instance";
|
|
25
26
|
import { Alert, Autocomplete, Box, Button, Chip, FormControlLabel, Grid, Menu, MenuItem, Stack, Switch, TextField, Typography } from "@mui/material";
|
|
26
27
|
import { useEffect, useMemo, useState } from "react";
|
|
27
28
|
import { isInThreadEmailStep, OUTREACH_SEQUENCE_NAME_MAX, validateOutreachSequence } from "../engine/sequence-validation.js";
|
|
@@ -55,7 +56,7 @@ import { useOutreachEmailTemplates } from "./use-outreach-crm.js";
|
|
|
55
56
|
* no longer change once people are enrolled) comes back from the save and
|
|
56
57
|
* is shown in the same places.
|
|
57
58
|
*/ export function OutreachSequenceEditor(props) {
|
|
58
|
-
var _ref, _ref1, _props_mailboxes_mailboxes_find, _ref2, _issueAt;
|
|
59
|
+
var _ref, _ref1, _props_mailboxes_mailboxes_find, _ref2, _issueAt, _issueAt1;
|
|
59
60
|
var _settings_settings, _settings_settings1, _issues_find, _hosts_find;
|
|
60
61
|
const { orgId, orgMount, sequence, settings } = props;
|
|
61
62
|
const api = useOutreachApi(orgId);
|
|
@@ -123,6 +124,14 @@ import { useOutreachEmailTemplates } from "./use-outreach-crm.js";
|
|
|
123
124
|
const templates = useOutreachEmailTemplates(orgId, draft.hostId || null, uid);
|
|
124
125
|
const mailbox = (_props_mailboxes_mailboxes_find = props.mailboxes.mailboxes.find((entry)=>entry.id === draft.mailboxId)) != null ? _props_mailboxes_mailboxes_find : null;
|
|
125
126
|
const archived = (sequence == null ? void 0 : sequence.status) === 'archived';
|
|
127
|
+
/*
|
|
128
|
+
* The site's campaigns, for the picker (AGL-3254): read while the editor
|
|
129
|
+
* is open, the way the form's page reads them, and re-read when the
|
|
130
|
+
* sequence moves to another site — a campaign belongs to one site, so a
|
|
131
|
+
* draft moved elsewhere has to pick again from that site's.
|
|
132
|
+
*/ const campaigns = useHostCampaigns(draft.hostId || undefined, {
|
|
133
|
+
enabled: Boolean(draft.hostId)
|
|
134
|
+
});
|
|
126
135
|
const issues = useMemo(()=>{
|
|
127
136
|
const judged = [
|
|
128
137
|
...validateOutreachSequence(draft),
|
|
@@ -275,7 +284,19 @@ import { useOutreachEmailTemplates } from "./use-outreach-crm.js";
|
|
|
275
284
|
}),
|
|
276
285
|
error: issueAt('mailboxId'),
|
|
277
286
|
disabled: archived
|
|
278
|
-
})
|
|
287
|
+
}),
|
|
288
|
+
draft.hostId ? /*#__PURE__*/ _jsx(CampaignPicker, {
|
|
289
|
+
options: campaigns.options,
|
|
290
|
+
value: draft.campaignIds,
|
|
291
|
+
onChange: (campaignIds)=>update({
|
|
292
|
+
campaignIds
|
|
293
|
+
}),
|
|
294
|
+
label: "Campaigns",
|
|
295
|
+
helperText: (_issueAt1 = issueAt('campaignIds')) != null ? _issueAt1 : 'The campaigns this sequence is part of. Everyone enrolled joins them, and its sends, replies, meetings and conversions count on their pages.',
|
|
296
|
+
disabled: archived,
|
|
297
|
+
empty: campaigns.ready && !campaigns.options.length,
|
|
298
|
+
emptyText: "This site has no campaigns yet. Create one from Marketing to file this sequence under it."
|
|
299
|
+
}) : null
|
|
279
300
|
]
|
|
280
301
|
})
|
|
281
302
|
}),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/sequence-editor.tsx"],"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'use client'\n\nimport { pluginDocsHelp, type ConsolePluginOrgMount } from '@aglyn/aglyn'\nimport { mdiPlus } from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Autocomplete,\n Box,\n Button,\n Chip,\n FormControlLabel,\n Grid,\n Menu,\n MenuItem,\n Stack,\n Switch,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useEffect, useMemo, useState } from 'react'\nimport {\n isInThreadEmailStep,\n OUTREACH_SEQUENCE_NAME_MAX,\n validateOutreachSequence,\n} from '../engine/sequence-validation'\nimport { outreachCountryLabel } from '../model/compliance-settings'\nimport {\n OUTREACH_MAX_EMAIL_STEPS,\n OUTREACH_MAX_STEPS,\n OUTREACH_TASK_KIND_LABELS,\n OUTREACH_TASK_KINDS,\n type OutreachSequence,\n type OutreachSequenceStep,\n type OutreachTaskKind,\n} from '../model/outreach.types'\nimport {\n emptyOutreachSequenceDraft,\n newOutreachEmailStep,\n newOutreachTaskStep,\n outreachSequenceDraftOf,\n type OutreachSequenceDraft,\n type OutreachSequenceIssue,\n} from '../model/sequence-draft'\nimport { OutreachSendWindowFields } from './send-window-fields'\nimport { OutreachSequenceMailboxPicker } from './sequence-mailbox-picker'\nimport { OutreachSequencePreview } from './sequence-preview'\nimport { OutreachStepCard } from './sequence-step-card'\nimport { OutreachRouteError, useOutreachApi } from './use-outreach-api'\nimport { useOutreachEmailTemplates } from './use-outreach-crm'\nimport type { OutreachMailboxesResult } from './use-outreach-mailboxes'\nimport type { OutreachSettingsLoad } from './use-outreach-settings'\n\nexport interface OutreachSequenceEditorProps {\n orgId: string\n orgMount?: ConsolePluginOrgMount\n /** The stored sequence, or `null` for a new one. */\n sequence: OutreachSequence | null\n /** The organization's compliance settings: its countries, and the preview's footer. */\n settings: OutreachSettingsLoad\n /**\n * The organization's mailboxes, from the Mailboxes section's listener:\n * what the picker offers, and who the preview's emails are from.\n */\n mailboxes: OutreachMailboxesResult\n /** The Mailboxes section, where the picker sends a member to connect one. */\n mailboxesPath: string\n onSaved(sequence: OutreachSequence): void\n onCancel?(): void\n}\n\n/** The step a dotted issue path is about, and the field within it. */\nfunction stepIssueKey(path: string): { index: number; field: string } | null {\n const match = /^steps\\.(\\d+)(?:\\.(.+))?$/.exec(path)\n if (!match) return null\n const field = match[2] ?? 'step'\n return { index: Number(match[1]), field: field === 'id' ? 'step' : field }\n}\n\n/**\n * The sequence editor (AGL-2980): the name, the site and mailbox, the steps,\n * who it sends to and when, beside a live preview of each email.\n *\n * Every issue the engine's validator finds is shown beside its field — the\n * same validator the save route refuses with — errors once a save was\n * attempted, warnings as soon as they appear. What only the server can\n * judge (whose mailbox, which countries the organization allows, what may\n * no longer change once people are enrolled) comes back from the save and\n * is shown in the same places.\n */\nexport function OutreachSequenceEditor(props: OutreachSequenceEditorProps) {\n const { orgId, orgMount, sequence, settings } = props\n const api = useOutreachApi(orgId)\n const { enqueueSnackbar } = useSnackbar()\n const { data: user } = useUser()\n const uid = user?.uid ?? null\n const mountedHosts = orgMount?.hosts\n const hosts = useMemo(() => mountedHosts ?? [], [mountedHosts])\n const orgCountries = settings.settings?.allowedCountries ?? ['US']\n\n const [draft, setDraft] = useState<OutreachSequenceDraft>(() =>\n sequence\n ? outreachSequenceDraftOf(sequence)\n : emptyOutreachSequenceDraft({\n hostId: hosts.length === 1 ? hosts[0].id : '',\n allowedCountries: orgCountries,\n }),\n )\n const [attempted, setAttempted] = useState(false)\n const [serverIssues, setServerIssues] = useState<OutreachSequenceIssue[]>([])\n const [saving, setSaving] = useState(false)\n const [taskAnchor, setTaskAnchor] = useState<HTMLElement | null>(null)\n\n // A stored sequence the listener updated (a save, a status change) is the\n // new starting point.\n useEffect(() => {\n if (sequence) setDraft(outreachSequenceDraftOf(sequence))\n }, [sequence])\n // A new sequence starts on the organization's countries once they are read.\n const settingsCountries = settings.settings?.allowedCountries.join(',')\n useEffect(() => {\n if (!sequence && settingsCountries) {\n setDraft((previous) => ({\n ...previous,\n settings: {\n ...previous.settings,\n allowedCountries: settingsCountries.split(','),\n },\n }))\n }\n }, [sequence, settingsCountries])\n useEffect(() => {\n if (!sequence && !draft.hostId && hosts.length === 1)\n setDraft((previous) => ({ ...previous, hostId: hosts[0].id }))\n }, [sequence, draft.hostId, hosts])\n\n // A new sequence starts on the member's own mailbox when they have exactly\n // one that is sending.\n const ownSending = props.mailboxes.mailboxes.filter(\n (entry) => entry.connectedByUid === uid && entry.status === 'connected',\n )\n const onlyOwnMailbox = ownSending.length === 1 ? ownSending[0].id : ''\n useEffect(() => {\n if (!sequence && !draft.mailboxId && onlyOwnMailbox)\n setDraft((previous) => ({ ...previous, mailboxId: onlyOwnMailbox }))\n }, [sequence, draft.mailboxId, onlyOwnMailbox])\n\n const templates = useOutreachEmailTemplates(orgId, draft.hostId || null, uid)\n const mailbox =\n props.mailboxes.mailboxes.find((entry) => entry.id === draft.mailboxId) ??\n null\n const archived = sequence?.status === 'archived'\n\n const issues = useMemo(() => {\n const judged = [\n ...validateOutreachSequence(draft),\n ...serverIssues,\n ] as OutreachSequenceIssue[]\n return judged.filter(\n (issue) =>\n issue.severity === 'warning' ||\n attempted ||\n serverIssues.includes(issue),\n )\n }, [draft, serverIssues, attempted])\n const issueAt = (path: string) =>\n issues.find((issue) => issue.path === path)?.message\n const stepIssues = (index: number) => {\n const found: Record<string, string> = {}\n for (const issue of issues) {\n const key = stepIssueKey(issue.path)\n if (key?.index === index && !found[key.field])\n found[key.field] = issue.message\n }\n return found\n }\n const listIssue = issueAt('steps')\n const countriesIssue = issues.find((issue) =>\n issue.path.startsWith('settings.allowedCountries'),\n )?.message\n\n const update = (next: Partial<OutreachSequenceDraft>) => {\n setServerIssues([])\n setDraft((previous) => ({ ...previous, ...next }))\n }\n const setStep = (index: number, step: OutreachSequenceStep) =>\n update({\n steps: draft.steps.map((entry, position) =>\n position === index ? step : entry,\n ),\n })\n const moveStep = (index: number, direction: -1 | 1) => {\n const steps = [...draft.steps]\n const [moved] = steps.splice(index, 1)\n steps.splice(index + direction, 0, moved)\n update({ steps })\n }\n const emailCount = draft.steps.filter((step) => step.kind === 'email').length\n const full = draft.steps.length >= OUTREACH_MAX_STEPS\n const threadSubject = (index: number) => {\n for (let position = index - 1; position >= 0; position -= 1) {\n const step = draft.steps[position]\n if (step.kind === 'email' && !isInThreadEmailStep(draft.steps, position))\n return step.subject\n }\n return ''\n }\n\n const save = async () => {\n setAttempted(true)\n if (\n validateOutreachSequence(draft).some(\n (issue) => issue.severity === 'error',\n )\n )\n return\n setSaving(true)\n try {\n const answer = await api.saveSequence(sequence?.id ?? null, draft)\n setServerIssues([])\n setAttempted(false)\n enqueueSnackbar(\n answer.created ? 'Sequence created.' : 'Sequence saved.',\n { variant: 'success' },\n )\n props.onSaved(answer.sequence)\n } catch (error) {\n if (error instanceof OutreachRouteError && error.issues.length) {\n setServerIssues(\n error.issues.filter(\n (issue): issue is OutreachSequenceIssue => 'path' in issue,\n ),\n )\n }\n enqueueSnackbar((error as Error).message, {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setSaving(false)\n }\n }\n\n const siteName = hosts.find((host) => host.id === draft.hostId)?.name ?? ''\n const countryOptions = [\n ...new Set([...orgCountries, ...draft.settings.allowedCountries]),\n ]\n\n return (\n <Grid container spacing={2}>\n <Grid size={{ xs: 12, md: 7 }}>\n <Stack spacing={2}>\n {archived ? (\n <Alert severity=\"info\">\n This sequence is archived, so it can’t be edited.\n </Alert>\n ) : null}\n <CardDisplay\n header=\"Sequence\"\n help={pluginDocsHelp('sequences', { anchor: '#build-a-sequence' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <TextField\n label=\"Name\"\n value={draft.name}\n onChange={(event) => update({ name: event.target.value })}\n disabled={archived}\n error={Boolean(issueAt('name'))}\n helperText={issueAt('name')}\n slotProps={{\n htmlInput: { maxLength: OUTREACH_SEQUENCE_NAME_MAX + 20 },\n }}\n fullWidth\n />\n <TextField\n select\n label=\"Site\"\n value={\n hosts.some((host) => host.id === draft.hostId)\n ? draft.hostId\n : ''\n }\n onChange={(event) => update({ hostId: event.target.value })}\n disabled={archived || !hosts.length}\n error={Boolean(issueAt('hostId'))}\n helperText={\n issueAt('hostId') ??\n 'The site whose contacts this sequence emails.'\n }\n fullWidth\n >\n {hosts.map((host) => (\n <MenuItem key={host.id} value={host.id}>\n {host.name}\n </MenuItem>\n ))}\n </TextField>\n <OutreachSequenceMailboxPicker\n orgId={orgId}\n uid={uid}\n mailboxes={props.mailboxes}\n mailboxesPath={props.mailboxesPath}\n value={draft.mailboxId}\n onChange={(mailboxId) => update({ mailboxId })}\n error={issueAt('mailboxId')}\n disabled={archived}\n />\n </Stack>\n </CardDisplay>\n\n {listIssue ? <Alert severity=\"error\">{listIssue}</Alert> : null}\n {draft.steps.map((step, index) => (\n <OutreachStepCard\n key={step.id || `step-${index}`}\n step={step}\n index={index}\n count={draft.steps.length}\n firstEmail={\n draft.steps.findIndex((entry) => entry.kind === 'email') ===\n index\n }\n inThread={isInThreadEmailStep(draft.steps, index)}\n threadSubject={threadSubject(index)}\n templates={templates.data}\n issues={stepIssues(index)}\n disabled={archived}\n onChange={(next) => setStep(index, next)}\n onMove={(direction) => moveStep(index, direction)}\n onRemove={() =>\n update({\n steps: draft.steps.filter(\n (_entry, position) => position !== index,\n ),\n })\n }\n />\n ))}\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ flexWrap: 'wrap', rowGap: 1 }}\n >\n <Button\n variant=\"outlined\"\n startIcon={<MdiIcon path={mdiPlus.path} fontSize=\"small\" />}\n disabled={\n archived || full || emailCount >= OUTREACH_MAX_EMAIL_STEPS\n }\n onClick={() =>\n update({\n steps: [...draft.steps, newOutreachEmailStep(draft.steps)],\n })\n }\n >\n Add email\n </Button>\n <Button\n variant=\"outlined\"\n startIcon={<MdiIcon path={mdiPlus.path} fontSize=\"small\" />}\n disabled={archived || full}\n onClick={(event) => setTaskAnchor(event.currentTarget)}\n aria-haspopup=\"menu\"\n >\n Add task\n </Button>\n <Menu\n anchorEl={taskAnchor}\n open={Boolean(taskAnchor)}\n onClose={() => setTaskAnchor(null)}\n >\n {OUTREACH_TASK_KINDS.map((kind: OutreachTaskKind) => (\n <MenuItem\n key={kind}\n onClick={() => {\n setTaskAnchor(null)\n update({\n steps: [\n ...draft.steps,\n newOutreachTaskStep(draft.steps, kind),\n ],\n })\n }}\n >\n {OUTREACH_TASK_KIND_LABELS[kind]}\n </MenuItem>\n ))}\n </Menu>\n </Stack>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Up to ${OUTREACH_MAX_STEPS} steps, ${OUTREACH_MAX_EMAIL_STEPS} of them emails.`}\n </Typography>\n\n <CardDisplay\n header=\"Who it sends to, and when\"\n help={pluginDocsHelp('sequences', {\n anchor: '#build-a-sequence',\n excerpt:\n 'A sequence sends only to the countries both it and your organization allow, and in its mailbox’s sending hours unless it has its own.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Autocomplete<string, true>\n multiple\n options={countryOptions}\n value={draft.settings.allowedCountries}\n getOptionLabel={outreachCountryLabel}\n onChange={(_event, next) =>\n update({\n settings: { ...draft.settings, allowedCountries: next },\n })\n }\n disabled={archived}\n renderValue={(value, getItemProps) =>\n value.map((code, index) => {\n const { key, ...item } = getItemProps({ index })\n return (\n <Chip\n key={key}\n size=\"small\"\n label={outreachCountryLabel(code)}\n {...item}\n />\n )\n })\n }\n renderInput={(params) => (\n <TextField\n {...params}\n label=\"Countries\"\n error={Boolean(countriesIssue)}\n helperText={\n countriesIssue ??\n 'Among the countries your organization allows. Cold contacts are emailed only in the United States.'\n }\n />\n )}\n />\n <FormControlLabel\n control={\n <Switch\n checked={draft.settings.allowCustomers}\n onChange={(event) =>\n update({\n settings: {\n ...draft.settings,\n allowCustomers: event.target.checked,\n },\n })\n }\n disabled={archived}\n />\n }\n label=\"Include customers\"\n />\n {/*\n * Click tracking (AGL-3239), with its cost stated where it is\n * chosen rather than in a document nobody opens. Off by\n * default: it changes the link a cold recipient sees, and the\n * emails stay plain text either way — there is no open\n * tracking to turn on, because a tracking image needs an HTML\n * email and these are not.\n */}\n <Stack spacing={0}>\n <FormControlLabel\n control={\n <Switch\n checked={draft.settings.trackClicks}\n onChange={(event) =>\n update({\n settings: {\n ...draft.settings,\n trackClicks: event.target.checked,\n },\n })\n }\n disabled={archived}\n />\n }\n label=\"Count link clicks\"\n />\n <Typography variant=\"caption\" color=\"text.secondary\">\n Links in these emails are replaced with links of ours that\n forward to the same page, so clicks can be counted. The\n recipient sees the replacement, not your address. Opens\n aren’t counted either way: these emails are plain text, and\n counting an open needs a tracking image.\n </Typography>\n </Stack>\n <OutreachSendWindowFields\n window={draft.settings.window}\n onChange={(window) =>\n update({ settings: { ...draft.settings, window } })\n }\n daysIssue={issueAt('settings.window.days')}\n hoursIssue={issueAt('settings.window')}\n disabled={archived}\n />\n </Stack>\n </CardDisplay>\n\n {archived ? null : (\n <Stack\n direction={{ xs: 'column', sm: 'row' }}\n spacing={1}\n sx={{ justifyContent: 'flex-end' }}\n >\n {props.onCancel ? (\n <Button\n variant=\"text\"\n onClick={props.onCancel}\n disabled={saving}\n >\n Cancel\n </Button>\n ) : null}\n <Button\n variant=\"contained\"\n onClick={() => void save()}\n disabled={saving}\n >\n {saving ? 'Saving…' : sequence ? 'Save' : 'Create sequence'}\n </Button>\n </Stack>\n )}\n </Stack>\n </Grid>\n <Grid size={{ xs: 12, md: 5 }}>\n <Box sx={{ position: { md: 'sticky' }, top: { md: 16 } }}>\n <OutreachSequencePreview\n steps={draft.steps}\n orgSettings={settings.settings}\n orgSettingsStatus={settings.status}\n templates={templates.data}\n sender={\n mailbox\n ? {\n name: mailbox.displayName,\n email: mailbox.sendAs || mailbox.email,\n }\n : null\n }\n siteName={siteName}\n />\n </Box>\n </Grid>\n </Grid>\n )\n}\nOutreachSequenceEditor.displayName = 'OutreachSequenceEditor'\n\nexport default OutreachSequenceEditor\n"],"names":["pluginDocsHelp","mdiPlus","CardDisplay","MdiIcon","useSnackbar","useUser","Alert","Autocomplete","Box","Button","Chip","FormControlLabel","Grid","Menu","MenuItem","Stack","Switch","TextField","Typography","useEffect","useMemo","useState","isInThreadEmailStep","OUTREACH_SEQUENCE_NAME_MAX","validateOutreachSequence","outreachCountryLabel","OUTREACH_MAX_EMAIL_STEPS","OUTREACH_MAX_STEPS","OUTREACH_TASK_KIND_LABELS","OUTREACH_TASK_KINDS","emptyOutreachSequenceDraft","newOutreachEmailStep","newOutreachTaskStep","outreachSequenceDraftOf","OutreachSendWindowFields","OutreachSequenceMailboxPicker","OutreachSequencePreview","OutreachStepCard","OutreachRouteError","useOutreachApi","useOutreachEmailTemplates","stepIssueKey","path","match","exec","field","index","Number","OutreachSequenceEditor","props","issueAt","settings","issues","hosts","orgId","orgMount","sequence","api","enqueueSnackbar","data","user","uid","mountedHosts","orgCountries","allowedCountries","draft","setDraft","hostId","length","id","attempted","setAttempted","serverIssues","setServerIssues","saving","setSaving","taskAnchor","setTaskAnchor","settingsCountries","join","previous","split","ownSending","mailboxes","filter","entry","connectedByUid","status","onlyOwnMailbox","mailboxId","templates","mailbox","find","archived","judged","issue","severity","includes","message","stepIssues","found","key","listIssue","countriesIssue","startsWith","update","next","setStep","step","steps","map","position","moveStep","direction","moved","splice","emailCount","kind","full","threadSubject","subject","save","some","answer","saveSequence","created","variant","onSaved","error","allowDuplicate","siteName","host","name","countryOptions","Set","container","spacing","size","xs","md","header","help","anchor","contentGutterX","contentGutterY","label","value","onChange","event","target","disabled","Boolean","helperText","slotProps","htmlInput","maxLength","fullWidth","select","mailboxesPath","count","firstEmail","findIndex","inThread","onMove","onRemove","_entry","sx","flexWrap","rowGap","startIcon","fontSize","onClick","currentTarget","aria-haspopup","anchorEl","open","onClose","color","excerpt","multiple","options","getOptionLabel","_event","renderValue","getItemProps","code","item","renderInput","params","control","checked","allowCustomers","trackClicks","window","daysIssue","hoursIssue","sm","justifyContent","onCancel","top","orgSettings","orgSettingsStatus","sender","displayName","email","sendAs"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/sequence-editor.tsx"],"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'use client'\n\nimport { pluginDocsHelp, type ConsolePluginOrgMount } from '@aglyn/aglyn'\nimport { mdiPlus } from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport CampaignPicker from '@aglyn/shared-ui-email-campaigns/components/campaign-picker.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useHostCampaigns, useUser } from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Autocomplete,\n Box,\n Button,\n Chip,\n FormControlLabel,\n Grid,\n Menu,\n MenuItem,\n Stack,\n Switch,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useEffect, useMemo, useState } from 'react'\nimport {\n isInThreadEmailStep,\n OUTREACH_SEQUENCE_NAME_MAX,\n validateOutreachSequence,\n} from '../engine/sequence-validation'\nimport { outreachCountryLabel } from '../model/compliance-settings'\nimport {\n OUTREACH_MAX_EMAIL_STEPS,\n OUTREACH_MAX_STEPS,\n OUTREACH_TASK_KIND_LABELS,\n OUTREACH_TASK_KINDS,\n type OutreachSequence,\n type OutreachSequenceStep,\n type OutreachTaskKind,\n} from '../model/outreach.types'\nimport {\n emptyOutreachSequenceDraft,\n newOutreachEmailStep,\n newOutreachTaskStep,\n outreachSequenceDraftOf,\n type OutreachSequenceDraft,\n type OutreachSequenceIssue,\n} from '../model/sequence-draft'\nimport { OutreachSendWindowFields } from './send-window-fields'\nimport { OutreachSequenceMailboxPicker } from './sequence-mailbox-picker'\nimport { OutreachSequencePreview } from './sequence-preview'\nimport { OutreachStepCard } from './sequence-step-card'\nimport { OutreachRouteError, useOutreachApi } from './use-outreach-api'\nimport { useOutreachEmailTemplates } from './use-outreach-crm'\nimport type { OutreachMailboxesResult } from './use-outreach-mailboxes'\nimport type { OutreachSettingsLoad } from './use-outreach-settings'\n\nexport interface OutreachSequenceEditorProps {\n orgId: string\n orgMount?: ConsolePluginOrgMount\n /** The stored sequence, or `null` for a new one. */\n sequence: OutreachSequence | null\n /** The organization's compliance settings: its countries, and the preview's footer. */\n settings: OutreachSettingsLoad\n /**\n * The organization's mailboxes, from the Mailboxes section's listener:\n * what the picker offers, and who the preview's emails are from.\n */\n mailboxes: OutreachMailboxesResult\n /** The Mailboxes section, where the picker sends a member to connect one. */\n mailboxesPath: string\n onSaved(sequence: OutreachSequence): void\n onCancel?(): void\n}\n\n/** The step a dotted issue path is about, and the field within it. */\nfunction stepIssueKey(path: string): { index: number; field: string } | null {\n const match = /^steps\\.(\\d+)(?:\\.(.+))?$/.exec(path)\n if (!match) return null\n const field = match[2] ?? 'step'\n return { index: Number(match[1]), field: field === 'id' ? 'step' : field }\n}\n\n/**\n * The sequence editor (AGL-2980): the name, the site and mailbox, the steps,\n * who it sends to and when, beside a live preview of each email.\n *\n * Every issue the engine's validator finds is shown beside its field — the\n * same validator the save route refuses with — errors once a save was\n * attempted, warnings as soon as they appear. What only the server can\n * judge (whose mailbox, which countries the organization allows, what may\n * no longer change once people are enrolled) comes back from the save and\n * is shown in the same places.\n */\nexport function OutreachSequenceEditor(props: OutreachSequenceEditorProps) {\n const { orgId, orgMount, sequence, settings } = props\n const api = useOutreachApi(orgId)\n const { enqueueSnackbar } = useSnackbar()\n const { data: user } = useUser()\n const uid = user?.uid ?? null\n const mountedHosts = orgMount?.hosts\n const hosts = useMemo(() => mountedHosts ?? [], [mountedHosts])\n const orgCountries = settings.settings?.allowedCountries ?? ['US']\n\n const [draft, setDraft] = useState<OutreachSequenceDraft>(() =>\n sequence\n ? outreachSequenceDraftOf(sequence)\n : emptyOutreachSequenceDraft({\n hostId: hosts.length === 1 ? hosts[0].id : '',\n allowedCountries: orgCountries,\n }),\n )\n const [attempted, setAttempted] = useState(false)\n const [serverIssues, setServerIssues] = useState<OutreachSequenceIssue[]>([])\n const [saving, setSaving] = useState(false)\n const [taskAnchor, setTaskAnchor] = useState<HTMLElement | null>(null)\n\n // A stored sequence the listener updated (a save, a status change) is the\n // new starting point.\n useEffect(() => {\n if (sequence) setDraft(outreachSequenceDraftOf(sequence))\n }, [sequence])\n // A new sequence starts on the organization's countries once they are read.\n const settingsCountries = settings.settings?.allowedCountries.join(',')\n useEffect(() => {\n if (!sequence && settingsCountries) {\n setDraft((previous) => ({\n ...previous,\n settings: {\n ...previous.settings,\n allowedCountries: settingsCountries.split(','),\n },\n }))\n }\n }, [sequence, settingsCountries])\n useEffect(() => {\n if (!sequence && !draft.hostId && hosts.length === 1)\n setDraft((previous) => ({ ...previous, hostId: hosts[0].id }))\n }, [sequence, draft.hostId, hosts])\n\n // A new sequence starts on the member's own mailbox when they have exactly\n // one that is sending.\n const ownSending = props.mailboxes.mailboxes.filter(\n (entry) => entry.connectedByUid === uid && entry.status === 'connected',\n )\n const onlyOwnMailbox = ownSending.length === 1 ? ownSending[0].id : ''\n useEffect(() => {\n if (!sequence && !draft.mailboxId && onlyOwnMailbox)\n setDraft((previous) => ({ ...previous, mailboxId: onlyOwnMailbox }))\n }, [sequence, draft.mailboxId, onlyOwnMailbox])\n\n const templates = useOutreachEmailTemplates(orgId, draft.hostId || null, uid)\n const mailbox =\n props.mailboxes.mailboxes.find((entry) => entry.id === draft.mailboxId) ??\n null\n const archived = sequence?.status === 'archived'\n /*\n * The site's campaigns, for the picker (AGL-3254): read while the editor\n * is open, the way the form's page reads them, and re-read when the\n * sequence moves to another site — a campaign belongs to one site, so a\n * draft moved elsewhere has to pick again from that site's.\n */\n const campaigns = useHostCampaigns(draft.hostId || undefined, { enabled: Boolean(draft.hostId) })\n\n const issues = useMemo(() => {\n const judged = [\n ...validateOutreachSequence(draft),\n ...serverIssues,\n ] as OutreachSequenceIssue[]\n return judged.filter(\n (issue) =>\n issue.severity === 'warning' ||\n attempted ||\n serverIssues.includes(issue),\n )\n }, [draft, serverIssues, attempted])\n const issueAt = (path: string) =>\n issues.find((issue) => issue.path === path)?.message\n const stepIssues = (index: number) => {\n const found: Record<string, string> = {}\n for (const issue of issues) {\n const key = stepIssueKey(issue.path)\n if (key?.index === index && !found[key.field])\n found[key.field] = issue.message\n }\n return found\n }\n const listIssue = issueAt('steps')\n const countriesIssue = issues.find((issue) =>\n issue.path.startsWith('settings.allowedCountries'),\n )?.message\n\n const update = (next: Partial<OutreachSequenceDraft>) => {\n setServerIssues([])\n setDraft((previous) => ({ ...previous, ...next }))\n }\n const setStep = (index: number, step: OutreachSequenceStep) =>\n update({\n steps: draft.steps.map((entry, position) =>\n position === index ? step : entry,\n ),\n })\n const moveStep = (index: number, direction: -1 | 1) => {\n const steps = [...draft.steps]\n const [moved] = steps.splice(index, 1)\n steps.splice(index + direction, 0, moved)\n update({ steps })\n }\n const emailCount = draft.steps.filter((step) => step.kind === 'email').length\n const full = draft.steps.length >= OUTREACH_MAX_STEPS\n const threadSubject = (index: number) => {\n for (let position = index - 1; position >= 0; position -= 1) {\n const step = draft.steps[position]\n if (step.kind === 'email' && !isInThreadEmailStep(draft.steps, position))\n return step.subject\n }\n return ''\n }\n\n const save = async () => {\n setAttempted(true)\n if (\n validateOutreachSequence(draft).some(\n (issue) => issue.severity === 'error',\n )\n )\n return\n setSaving(true)\n try {\n const answer = await api.saveSequence(sequence?.id ?? null, draft)\n setServerIssues([])\n setAttempted(false)\n enqueueSnackbar(\n answer.created ? 'Sequence created.' : 'Sequence saved.',\n { variant: 'success' },\n )\n props.onSaved(answer.sequence)\n } catch (error) {\n if (error instanceof OutreachRouteError && error.issues.length) {\n setServerIssues(\n error.issues.filter(\n (issue): issue is OutreachSequenceIssue => 'path' in issue,\n ),\n )\n }\n enqueueSnackbar((error as Error).message, {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setSaving(false)\n }\n }\n\n const siteName = hosts.find((host) => host.id === draft.hostId)?.name ?? ''\n const countryOptions = [\n ...new Set([...orgCountries, ...draft.settings.allowedCountries]),\n ]\n\n return (\n <Grid container spacing={2}>\n <Grid size={{ xs: 12, md: 7 }}>\n <Stack spacing={2}>\n {archived ? (\n <Alert severity=\"info\">\n This sequence is archived, so it can’t be edited.\n </Alert>\n ) : null}\n <CardDisplay\n header=\"Sequence\"\n help={pluginDocsHelp('sequences', { anchor: '#build-a-sequence' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <TextField\n label=\"Name\"\n value={draft.name}\n onChange={(event) => update({ name: event.target.value })}\n disabled={archived}\n error={Boolean(issueAt('name'))}\n helperText={issueAt('name')}\n slotProps={{\n htmlInput: { maxLength: OUTREACH_SEQUENCE_NAME_MAX + 20 },\n }}\n fullWidth\n />\n <TextField\n select\n label=\"Site\"\n value={\n hosts.some((host) => host.id === draft.hostId)\n ? draft.hostId\n : ''\n }\n onChange={(event) => update({ hostId: event.target.value })}\n disabled={archived || !hosts.length}\n error={Boolean(issueAt('hostId'))}\n helperText={\n issueAt('hostId') ??\n 'The site whose contacts this sequence emails.'\n }\n fullWidth\n >\n {hosts.map((host) => (\n <MenuItem key={host.id} value={host.id}>\n {host.name}\n </MenuItem>\n ))}\n </TextField>\n <OutreachSequenceMailboxPicker\n orgId={orgId}\n uid={uid}\n mailboxes={props.mailboxes}\n mailboxesPath={props.mailboxesPath}\n value={draft.mailboxId}\n onChange={(mailboxId) => update({ mailboxId })}\n error={issueAt('mailboxId')}\n disabled={archived}\n />\n {/*\n The campaigns this sequence is part of (AGL-3254), picked the\n way a form's page picks them. Everyone enrolled joins them,\n and what the sequence produces is reported under them; it\n does not change who is enrolled or what is sent.\n */}\n {draft.hostId ? (\n <CampaignPicker\n options={campaigns.options}\n value={draft.campaignIds}\n onChange={(campaignIds) => update({ campaignIds })}\n label=\"Campaigns\"\n helperText={\n issueAt('campaignIds') ??\n 'The campaigns this sequence is part of. Everyone enrolled joins them, and its sends, replies, meetings and conversions count on their pages.'\n }\n disabled={archived}\n empty={campaigns.ready && !campaigns.options.length}\n emptyText=\"This site has no campaigns yet. Create one from Marketing to file this sequence under it.\"\n />\n ) : null}\n </Stack>\n </CardDisplay>\n\n {listIssue ? <Alert severity=\"error\">{listIssue}</Alert> : null}\n {draft.steps.map((step, index) => (\n <OutreachStepCard\n key={step.id || `step-${index}`}\n step={step}\n index={index}\n count={draft.steps.length}\n firstEmail={\n draft.steps.findIndex((entry) => entry.kind === 'email') ===\n index\n }\n inThread={isInThreadEmailStep(draft.steps, index)}\n threadSubject={threadSubject(index)}\n templates={templates.data}\n issues={stepIssues(index)}\n disabled={archived}\n onChange={(next) => setStep(index, next)}\n onMove={(direction) => moveStep(index, direction)}\n onRemove={() =>\n update({\n steps: draft.steps.filter(\n (_entry, position) => position !== index,\n ),\n })\n }\n />\n ))}\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ flexWrap: 'wrap', rowGap: 1 }}\n >\n <Button\n variant=\"outlined\"\n startIcon={<MdiIcon path={mdiPlus.path} fontSize=\"small\" />}\n disabled={\n archived || full || emailCount >= OUTREACH_MAX_EMAIL_STEPS\n }\n onClick={() =>\n update({\n steps: [...draft.steps, newOutreachEmailStep(draft.steps)],\n })\n }\n >\n Add email\n </Button>\n <Button\n variant=\"outlined\"\n startIcon={<MdiIcon path={mdiPlus.path} fontSize=\"small\" />}\n disabled={archived || full}\n onClick={(event) => setTaskAnchor(event.currentTarget)}\n aria-haspopup=\"menu\"\n >\n Add task\n </Button>\n <Menu\n anchorEl={taskAnchor}\n open={Boolean(taskAnchor)}\n onClose={() => setTaskAnchor(null)}\n >\n {OUTREACH_TASK_KINDS.map((kind: OutreachTaskKind) => (\n <MenuItem\n key={kind}\n onClick={() => {\n setTaskAnchor(null)\n update({\n steps: [\n ...draft.steps,\n newOutreachTaskStep(draft.steps, kind),\n ],\n })\n }}\n >\n {OUTREACH_TASK_KIND_LABELS[kind]}\n </MenuItem>\n ))}\n </Menu>\n </Stack>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Up to ${OUTREACH_MAX_STEPS} steps, ${OUTREACH_MAX_EMAIL_STEPS} of them emails.`}\n </Typography>\n\n <CardDisplay\n header=\"Who it sends to, and when\"\n help={pluginDocsHelp('sequences', {\n anchor: '#build-a-sequence',\n excerpt:\n 'A sequence sends only to the countries both it and your organization allow, and in its mailbox’s sending hours unless it has its own.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Autocomplete<string, true>\n multiple\n options={countryOptions}\n value={draft.settings.allowedCountries}\n getOptionLabel={outreachCountryLabel}\n onChange={(_event, next) =>\n update({\n settings: { ...draft.settings, allowedCountries: next },\n })\n }\n disabled={archived}\n renderValue={(value, getItemProps) =>\n value.map((code, index) => {\n const { key, ...item } = getItemProps({ index })\n return (\n <Chip\n key={key}\n size=\"small\"\n label={outreachCountryLabel(code)}\n {...item}\n />\n )\n })\n }\n renderInput={(params) => (\n <TextField\n {...params}\n label=\"Countries\"\n error={Boolean(countriesIssue)}\n helperText={\n countriesIssue ??\n 'Among the countries your organization allows. Cold contacts are emailed only in the United States.'\n }\n />\n )}\n />\n <FormControlLabel\n control={\n <Switch\n checked={draft.settings.allowCustomers}\n onChange={(event) =>\n update({\n settings: {\n ...draft.settings,\n allowCustomers: event.target.checked,\n },\n })\n }\n disabled={archived}\n />\n }\n label=\"Include customers\"\n />\n {/*\n * Click tracking (AGL-3239), with its cost stated where it is\n * chosen rather than in a document nobody opens. Off by\n * default: it changes the link a cold recipient sees, and the\n * emails stay plain text either way — there is no open\n * tracking to turn on, because a tracking image needs an HTML\n * email and these are not.\n */}\n <Stack spacing={0}>\n <FormControlLabel\n control={\n <Switch\n checked={draft.settings.trackClicks}\n onChange={(event) =>\n update({\n settings: {\n ...draft.settings,\n trackClicks: event.target.checked,\n },\n })\n }\n disabled={archived}\n />\n }\n label=\"Count link clicks\"\n />\n <Typography variant=\"caption\" color=\"text.secondary\">\n Links in these emails are replaced with links of ours that\n forward to the same page, so clicks can be counted. The\n recipient sees the replacement, not your address. Opens\n aren’t counted either way: these emails are plain text, and\n counting an open needs a tracking image.\n </Typography>\n </Stack>\n <OutreachSendWindowFields\n window={draft.settings.window}\n onChange={(window) =>\n update({ settings: { ...draft.settings, window } })\n }\n daysIssue={issueAt('settings.window.days')}\n hoursIssue={issueAt('settings.window')}\n disabled={archived}\n />\n </Stack>\n </CardDisplay>\n\n {archived ? null : (\n <Stack\n direction={{ xs: 'column', sm: 'row' }}\n spacing={1}\n sx={{ justifyContent: 'flex-end' }}\n >\n {props.onCancel ? (\n <Button\n variant=\"text\"\n onClick={props.onCancel}\n disabled={saving}\n >\n Cancel\n </Button>\n ) : null}\n <Button\n variant=\"contained\"\n onClick={() => void save()}\n disabled={saving}\n >\n {saving ? 'Saving…' : sequence ? 'Save' : 'Create sequence'}\n </Button>\n </Stack>\n )}\n </Stack>\n </Grid>\n <Grid size={{ xs: 12, md: 5 }}>\n <Box sx={{ position: { md: 'sticky' }, top: { md: 16 } }}>\n <OutreachSequencePreview\n steps={draft.steps}\n orgSettings={settings.settings}\n orgSettingsStatus={settings.status}\n templates={templates.data}\n sender={\n mailbox\n ? {\n name: mailbox.displayName,\n email: mailbox.sendAs || mailbox.email,\n }\n : null\n }\n siteName={siteName}\n />\n </Box>\n </Grid>\n </Grid>\n )\n}\nOutreachSequenceEditor.displayName = 'OutreachSequenceEditor'\n\nexport default OutreachSequenceEditor\n"],"names":["pluginDocsHelp","mdiPlus","CardDisplay","MdiIcon","CampaignPicker","useSnackbar","useHostCampaigns","useUser","Alert","Autocomplete","Box","Button","Chip","FormControlLabel","Grid","Menu","MenuItem","Stack","Switch","TextField","Typography","useEffect","useMemo","useState","isInThreadEmailStep","OUTREACH_SEQUENCE_NAME_MAX","validateOutreachSequence","outreachCountryLabel","OUTREACH_MAX_EMAIL_STEPS","OUTREACH_MAX_STEPS","OUTREACH_TASK_KIND_LABELS","OUTREACH_TASK_KINDS","emptyOutreachSequenceDraft","newOutreachEmailStep","newOutreachTaskStep","outreachSequenceDraftOf","OutreachSendWindowFields","OutreachSequenceMailboxPicker","OutreachSequencePreview","OutreachStepCard","OutreachRouteError","useOutreachApi","useOutreachEmailTemplates","stepIssueKey","path","match","exec","field","index","Number","OutreachSequenceEditor","props","issueAt","settings","issues","hosts","orgId","orgMount","sequence","api","enqueueSnackbar","data","user","uid","mountedHosts","orgCountries","allowedCountries","draft","setDraft","hostId","length","id","attempted","setAttempted","serverIssues","setServerIssues","saving","setSaving","taskAnchor","setTaskAnchor","settingsCountries","join","previous","split","ownSending","mailboxes","filter","entry","connectedByUid","status","onlyOwnMailbox","mailboxId","templates","mailbox","find","archived","campaigns","undefined","enabled","Boolean","judged","issue","severity","includes","message","stepIssues","found","key","listIssue","countriesIssue","startsWith","update","next","setStep","step","steps","map","position","moveStep","direction","moved","splice","emailCount","kind","full","threadSubject","subject","save","some","answer","saveSequence","created","variant","onSaved","error","allowDuplicate","siteName","host","name","countryOptions","Set","container","spacing","size","xs","md","header","help","anchor","contentGutterX","contentGutterY","label","value","onChange","event","target","disabled","helperText","slotProps","htmlInput","maxLength","fullWidth","select","mailboxesPath","options","campaignIds","empty","ready","emptyText","count","firstEmail","findIndex","inThread","onMove","onRemove","_entry","sx","flexWrap","rowGap","startIcon","fontSize","onClick","currentTarget","aria-haspopup","anchorEl","open","onClose","color","excerpt","multiple","getOptionLabel","_event","renderValue","getItemProps","code","item","renderInput","params","control","checked","allowCustomers","trackClicks","window","daysIssue","hoursIssue","sm","justifyContent","onCancel","top","orgSettings","orgSettingsStatus","sender","displayName","email","sendAs"],"mappings":"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"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
1
2
|
/**
|
|
2
3
|
* @license
|
|
3
4
|
* Copyright 2026 Aglyn LLC
|
|
@@ -16,6 +17,7 @@
|
|
|
16
17
|
*/ // Each registry from its own module, not the `@aglyn/aglyn/server` barrel:
|
|
17
18
|
// boot needs one registry, not the whole server surface.
|
|
18
19
|
import { registerPluginConsoleCron } from "@aglyn/aglyn/plugin-manager/plugin-console-crons";
|
|
20
|
+
import { listPluginMembershipDetachers, registerPluginMembershipDetacher } from "@aglyn/aglyn/plugin-manager/plugin-membership-detach";
|
|
19
21
|
import { listPluginOrgErasers, registerPluginOrgEraser } from "@aglyn/aglyn/plugin-manager/plugin-org-erasure";
|
|
20
22
|
import { listPluginLeadConversionListeners, registerPluginLeadConversionListener } from "@aglyn/aglyn/plugin-manager/plugin-lead-conversion";
|
|
21
23
|
import { registerPluginPersonEraser } from "@aglyn/aglyn/plugin-manager/plugin-person-erasure";
|
|
@@ -66,14 +68,36 @@ import { OUTREACH_SEND_JOB_ID, OUTREACH_SYNC_JOB_ID } from "./constants/runtime-
|
|
|
66
68
|
pluginId: OUTREACH_PLUGIN_ID
|
|
67
69
|
});
|
|
68
70
|
// A lead that converts takes its enrollments to the contact it became
|
|
69
|
-
// (AGL-3234): the same document, now naming the contact
|
|
71
|
+
// (AGL-3234): the same document, now naming the contact — and every
|
|
72
|
+
// enrollment made on it credits `converted` to its campaigns (AGL-3254).
|
|
70
73
|
if (!listPluginLeadConversionListeners().includes(OUTREACH_PLUGIN_ID)) {
|
|
71
74
|
registerPluginLeadConversionListener(async (request)=>{
|
|
72
|
-
const [{ followOutreachLeadToContact }, platform] = await Promise.all([
|
|
75
|
+
const [{ followOutreachLeadToContact }, { creditOutreachLeadConversion }, platform] = await Promise.all([
|
|
73
76
|
import("./runtime/lead-records.js"),
|
|
77
|
+
import("./runtime/campaign-credit.js"),
|
|
74
78
|
runtime()
|
|
75
79
|
]);
|
|
76
|
-
|
|
80
|
+
const deps = platform.platformOutreachRuntimeDeps();
|
|
81
|
+
const credited = await creditOutreachLeadConversion(deps, request);
|
|
82
|
+
const followed = await followOutreachLeadToContact(deps.firestore(), request);
|
|
83
|
+
return _extends({}, followed, {
|
|
84
|
+
campaignsCredited: credited
|
|
85
|
+
});
|
|
86
|
+
}, {
|
|
87
|
+
pluginId: OUTREACH_PLUGIN_ID
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
// A deleted campaign comes off every sequence and enrollment naming it
|
|
91
|
+
// (AGL-3254), before the marketing plugin removes the container.
|
|
92
|
+
if (!listPluginMembershipDetachers().includes(OUTREACH_PLUGIN_ID)) {
|
|
93
|
+
registerPluginMembershipDetacher(async (request)=>{
|
|
94
|
+
const [{ createOutreachCampaignDetacher }, platform] = await Promise.all([
|
|
95
|
+
import("./runtime/campaign-detach.js"),
|
|
96
|
+
runtime()
|
|
97
|
+
]);
|
|
98
|
+
return createOutreachCampaignDetacher({
|
|
99
|
+
firestore: platform.platformOutreachRuntimeDeps().firestore
|
|
100
|
+
})(request);
|
|
77
101
|
}, {
|
|
78
102
|
pluginId: OUTREACH_PLUGIN_ID
|
|
79
103
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/plugins/outreach/src/lib/declarations.console-server.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// Each registry from its own module, not the `@aglyn/aglyn/server` barrel:\n// boot needs one registry, not the whole server surface.\nimport { registerPluginConsoleCron } from '@aglyn/aglyn/plugin-manager/plugin-console-crons'\nimport {\n listPluginOrgErasers,\n registerPluginOrgEraser,\n} from '@aglyn/aglyn/plugin-manager/plugin-org-erasure'\nimport {\n listPluginLeadConversionListeners,\n registerPluginLeadConversionListener,\n} from '@aglyn/aglyn/plugin-manager/plugin-lead-conversion'\nimport { registerPluginPersonEraser } from '@aglyn/aglyn/plugin-manager/plugin-person-erasure'\nimport {\n listPluginUserErasers,\n registerPluginUserEraser,\n} from '@aglyn/aglyn/plugin-manager/plugin-user-erasure'\nimport { OUTREACH_PLUGIN_ID } from './constants/bundle-common'\nimport { OUTREACH_SEND_JOB_ID, OUTREACH_SYNC_JOB_ID } from './constants/runtime-jobs'\n\n/**\n * Outreach's CONSOLE-ONLY server declarations (AGL-2978), named under\n * `consoleServerDeclarations` in `plugins.config.json` and run at the\n * console's boot.\n *\n * The erasers are registered here rather than from `serverDeclarations`,\n * which the tenant runtime also loads: the workspace eraser revokes each\n * rep's Google grant, and the account eraser (AGL-3106) revokes and deletes\n * the person's own, and both open a sealed token with `OUTREACH_TOKEN_KEY`,\n * which only the console holds. The tenant may not so much as bundle the code\n * that does (`outreach-credential-isolation.spec.ts`). Every erasure runs in\n * the console, so this is where the erasers are needed.\n *\n * The sending runtime (AGL-2981) is declared here too, for the same reason:\n * the send and sync jobs open each rep's sealed grant, so they run on the\n * console's `plugin-console-crons` tick and nowhere else, and the person\n * eraser removes the enrollments a workspace kept about someone it erases.\n *\n * Light at boot: every module that does the work is imported when a job, an\n * erasure or a tick first asks for it, so the boot cost is the registration.\n */\nexport function registerOutreachConsoleServerDeclarations(): void {\n // Idempotent against the REGISTRY, so a reset (a spec) registers again.\n if (!listPluginOrgErasers().includes(OUTREACH_PLUGIN_ID)) {\n registerPluginOrgEraser(\n async (request) => {\n const { createOutreachOrgEraser, defaultOutreachErasureDeps } = await import(\n './mailboxes/mailbox-erasure'\n )\n return createOutreachOrgEraser(defaultOutreachErasureDeps())(request)\n },\n { pluginId: OUTREACH_PLUGIN_ID },\n )\n }\n if (!listPluginUserErasers().includes(OUTREACH_PLUGIN_ID)) {\n registerPluginUserEraser(\n async (request) => {\n const { createOutreachUserEraser, defaultOutreachErasureDeps } = await import(\n './mailboxes/mailbox-erasure'\n )\n return createOutreachUserEraser(defaultOutreachErasureDeps())(request)\n },\n { pluginId: OUTREACH_PLUGIN_ID },\n )\n }\n // A person erased from a workspace takes their enrollments with them; the\n // do-not-contact entry stays, stripped of anything that names them.\n registerPluginPersonEraser(\n async (request) => (await runtime()).platformOutreachPersonEraser()(request),\n { pluginId: OUTREACH_PLUGIN_ID },\n )\n // A lead that converts takes its enrollments to the contact it became\n // (AGL-3234): the same document, now naming the contact.\n if (!listPluginLeadConversionListeners().includes(OUTREACH_PLUGIN_ID)) {\n registerPluginLeadConversionListener(\n async (request) => {\n const [{ followOutreachLeadToContact }, platform] = await Promise.all([\n import('./runtime/lead-records'),\n runtime(),\n ])\n return followOutreachLeadToContact(platform.platformOutreachRuntimeDeps().firestore(), request)\n },\n { pluginId: OUTREACH_PLUGIN_ID },\n )\n }\n // The sending runtime, on the console's fifteen-minute tick.\n registerPluginConsoleCron(\n {\n id: OUTREACH_SEND_JOB_ID,\n label: 'Sequence sends',\n drives:\n 'Sends every sequence step that has come due, from the rep’s own connected mailbox, and files each on the contact’s timeline. If it stops, no sequence sends anything and every task step waits.',\n run: async (context) => {\n const [{ runOutreachSendJob }, platform] = await Promise.all([import('./runtime/send-job'), runtime()])\n return runOutreachSendJob(platform.platformOutreachRuntimeDeps(), context)\n },\n },\n { pluginId: OUTREACH_PLUGIN_ID },\n )\n registerPluginConsoleCron(\n {\n id: OUTREACH_SYNC_JOB_ID,\n label: 'Sequence replies and bounces',\n drives:\n 'Reads each connected mailbox for replies, out-of-office answers, opt-outs and bounces, and stops or postpones the sequence each one is about. If it stops, a person who replied or asked to be left alone keeps getting follow-ups, and a mailbox that bounces never pauses itself.',\n run: async (context) => {\n const [{ runOutreachSyncJob }, platform] = await Promise.all([import('./runtime/sync-job'), runtime()])\n return runOutreachSyncJob(platform.platformOutreachRuntimeDeps(), context)\n },\n },\n { pluginId: OUTREACH_PLUGIN_ID },\n )\n}\n\n/** The runtime's platform reach, loaded when a job or an erasure first runs. */\nconst runtime = () => import('./runtime/platform-runtime-deps')\n"],"names":["registerPluginConsoleCron","listPluginOrgErasers","registerPluginOrgEraser","listPluginLeadConversionListeners","registerPluginLeadConversionListener","registerPluginPersonEraser","listPluginUserErasers","registerPluginUserEraser","OUTREACH_PLUGIN_ID","OUTREACH_SEND_JOB_ID","OUTREACH_SYNC_JOB_ID","registerOutreachConsoleServerDeclarations","includes","request","createOutreachOrgEraser","defaultOutreachErasureDeps","pluginId","createOutreachUserEraser","runtime","platformOutreachPersonEraser","followOutreachLeadToContact","platform","Promise","all","platformOutreachRuntimeDeps","firestore","id","label","drives","run","context","runOutreachSendJob","runOutreachSyncJob"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,2EAA2E;AAC3E,yDAAyD;AACzD,SAASA,yBAAyB,QAAQ,mDAAkD;AAC5F,SACEC,oBAAoB,EACpBC,uBAAuB,QAClB,iDAAgD;AACvD,SACEC,iCAAiC,EACjCC,oCAAoC,QAC/B,qDAAoD;AAC3D,SAASC,0BAA0B,QAAQ,oDAAmD;AAC9F,SACEC,qBAAqB,EACrBC,wBAAwB,QACnB,kDAAiD;AACxD,SAASC,kBAAkB,QAAQ,+BAA2B;AAC9D,SAASC,oBAAoB,EAAEC,oBAAoB,QAAQ,8BAA0B;AAErF;;;;;;;;;;;;;;;;;;;;CAoBC,GACD,OAAO,SAASC;IACd,wEAAwE;IACxE,IAAI,CAACV,uBAAuBW,QAAQ,CAACJ,qBAAqB;QACxDN,wBACE,OAAOW;YACL,MAAM,EAAEC,uBAAuB,EAAEC,0BAA0B,EAAE,GAAG,MAAM,MAAM,CAC1E;YAEF,OAAOD,wBAAwBC,8BAA8BF;QAC/D,GACA;YAAEG,UAAUR;QAAmB;IAEnC;IACA,IAAI,CAACF,wBAAwBM,QAAQ,CAACJ,qBAAqB;QACzDD,yBACE,OAAOM;YACL,MAAM,EAAEI,wBAAwB,EAAEF,0BAA0B,EAAE,GAAG,MAAM,MAAM,CAC3E;YAEF,OAAOE,yBAAyBF,8BAA8BF;QAChE,GACA;YAAEG,UAAUR;QAAmB;IAEnC;IACA,0EAA0E;IAC1E,oEAAoE;IACpEH,2BACE,OAAOQ,UAAY,AAAC,CAAA,MAAMK,SAAQ,EAAGC,4BAA4B,GAAGN,UACpE;QAAEG,UAAUR;IAAmB;IAEjC,sEAAsE;IACtE,yDAAyD;IACzD,IAAI,CAACL,oCAAoCS,QAAQ,CAACJ,qBAAqB;QACrEJ,qCACE,OAAOS;YACL,MAAM,CAAC,EAAEO,2BAA2B,EAAE,EAAEC,SAAS,GAAG,MAAMC,QAAQC,GAAG,CAAC;gBACpE,MAAM,CAAC;gBACPL;aACD;YACD,OAAOE,4BAA4BC,SAASG,2BAA2B,GAAGC,SAAS,IAAIZ;QACzF,GACA;YAAEG,UAAUR;QAAmB;IAEnC;IACA,6DAA6D;IAC7DR,0BACE;QACE0B,IAAIjB;QACJkB,OAAO;QACPC,QACE;QACFC,KAAK,OAAOC;YACV,MAAM,CAAC,EAAEC,kBAAkB,EAAE,EAAEV,SAAS,GAAG,MAAMC,QAAQC,GAAG,CAAC;gBAAC,MAAM,CAAC;gBAAuBL;aAAU;YACtG,OAAOa,mBAAmBV,SAASG,2BAA2B,IAAIM;QACpE;IACF,GACA;QAAEd,UAAUR;IAAmB;IAEjCR,0BACE;QACE0B,IAAIhB;QACJiB,OAAO;QACPC,QACE;QACFC,KAAK,OAAOC;YACV,MAAM,CAAC,EAAEE,kBAAkB,EAAE,EAAEX,SAAS,GAAG,MAAMC,QAAQC,GAAG,CAAC;gBAAC,MAAM,CAAC;gBAAuBL;aAAU;YACtG,OAAOc,mBAAmBX,SAASG,2BAA2B,IAAIM;QACpE;IACF,GACA;QAAEd,UAAUR;IAAmB;AAEnC;AAEA,8EAA8E,GAC9E,MAAMU,UAAU,IAAM,MAAM,CAAC"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/plugins/outreach/src/lib/declarations.console-server.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// Each registry from its own module, not the `@aglyn/aglyn/server` barrel:\n// boot needs one registry, not the whole server surface.\nimport { registerPluginConsoleCron } from '@aglyn/aglyn/plugin-manager/plugin-console-crons'\nimport {\n listPluginMembershipDetachers,\n registerPluginMembershipDetacher,\n} from '@aglyn/aglyn/plugin-manager/plugin-membership-detach'\nimport {\n listPluginOrgErasers,\n registerPluginOrgEraser,\n} from '@aglyn/aglyn/plugin-manager/plugin-org-erasure'\nimport {\n listPluginLeadConversionListeners,\n registerPluginLeadConversionListener,\n} from '@aglyn/aglyn/plugin-manager/plugin-lead-conversion'\nimport { registerPluginPersonEraser } from '@aglyn/aglyn/plugin-manager/plugin-person-erasure'\nimport {\n listPluginUserErasers,\n registerPluginUserEraser,\n} from '@aglyn/aglyn/plugin-manager/plugin-user-erasure'\nimport { OUTREACH_PLUGIN_ID } from './constants/bundle-common'\nimport { OUTREACH_SEND_JOB_ID, OUTREACH_SYNC_JOB_ID } from './constants/runtime-jobs'\n\n/**\n * Outreach's CONSOLE-ONLY server declarations (AGL-2978), named under\n * `consoleServerDeclarations` in `plugins.config.json` and run at the\n * console's boot.\n *\n * The erasers are registered here rather than from `serverDeclarations`,\n * which the tenant runtime also loads: the workspace eraser revokes each\n * rep's Google grant, and the account eraser (AGL-3106) revokes and deletes\n * the person's own, and both open a sealed token with `OUTREACH_TOKEN_KEY`,\n * which only the console holds. The tenant may not so much as bundle the code\n * that does (`outreach-credential-isolation.spec.ts`). Every erasure runs in\n * the console, so this is where the erasers are needed.\n *\n * The sending runtime (AGL-2981) is declared here too, for the same reason:\n * the send and sync jobs open each rep's sealed grant, so they run on the\n * console's `plugin-console-crons` tick and nowhere else, and the person\n * eraser removes the enrollments a workspace kept about someone it erases.\n *\n * Light at boot: every module that does the work is imported when a job, an\n * erasure or a tick first asks for it, so the boot cost is the registration.\n */\nexport function registerOutreachConsoleServerDeclarations(): void {\n // Idempotent against the REGISTRY, so a reset (a spec) registers again.\n if (!listPluginOrgErasers().includes(OUTREACH_PLUGIN_ID)) {\n registerPluginOrgEraser(\n async (request) => {\n const { createOutreachOrgEraser, defaultOutreachErasureDeps } = await import(\n './mailboxes/mailbox-erasure'\n )\n return createOutreachOrgEraser(defaultOutreachErasureDeps())(request)\n },\n { pluginId: OUTREACH_PLUGIN_ID },\n )\n }\n if (!listPluginUserErasers().includes(OUTREACH_PLUGIN_ID)) {\n registerPluginUserEraser(\n async (request) => {\n const { createOutreachUserEraser, defaultOutreachErasureDeps } = await import(\n './mailboxes/mailbox-erasure'\n )\n return createOutreachUserEraser(defaultOutreachErasureDeps())(request)\n },\n { pluginId: OUTREACH_PLUGIN_ID },\n )\n }\n // A person erased from a workspace takes their enrollments with them; the\n // do-not-contact entry stays, stripped of anything that names them.\n registerPluginPersonEraser(\n async (request) => (await runtime()).platformOutreachPersonEraser()(request),\n { pluginId: OUTREACH_PLUGIN_ID },\n )\n // A lead that converts takes its enrollments to the contact it became\n // (AGL-3234): the same document, now naming the contact — and every\n // enrollment made on it credits `converted` to its campaigns (AGL-3254).\n if (!listPluginLeadConversionListeners().includes(OUTREACH_PLUGIN_ID)) {\n registerPluginLeadConversionListener(\n async (request) => {\n const [{ followOutreachLeadToContact }, { creditOutreachLeadConversion }, platform] = await Promise.all([\n import('./runtime/lead-records'),\n import('./runtime/campaign-credit'),\n runtime(),\n ])\n const deps = platform.platformOutreachRuntimeDeps()\n const credited = await creditOutreachLeadConversion(deps, request)\n const followed = await followOutreachLeadToContact(deps.firestore(), request)\n return { ...followed, campaignsCredited: credited }\n },\n { pluginId: OUTREACH_PLUGIN_ID },\n )\n }\n // A deleted campaign comes off every sequence and enrollment naming it\n // (AGL-3254), before the marketing plugin removes the container.\n if (!listPluginMembershipDetachers().includes(OUTREACH_PLUGIN_ID)) {\n registerPluginMembershipDetacher(\n async (request) => {\n const [{ createOutreachCampaignDetacher }, platform] = await Promise.all([\n import('./runtime/campaign-detach'),\n runtime(),\n ])\n return createOutreachCampaignDetacher({ firestore: platform.platformOutreachRuntimeDeps().firestore })(\n request,\n )\n },\n { pluginId: OUTREACH_PLUGIN_ID },\n )\n }\n // The sending runtime, on the console's fifteen-minute tick.\n registerPluginConsoleCron(\n {\n id: OUTREACH_SEND_JOB_ID,\n label: 'Sequence sends',\n drives:\n 'Sends every sequence step that has come due, from the rep’s own connected mailbox, and files each on the contact’s timeline. If it stops, no sequence sends anything and every task step waits.',\n run: async (context) => {\n const [{ runOutreachSendJob }, platform] = await Promise.all([import('./runtime/send-job'), runtime()])\n return runOutreachSendJob(platform.platformOutreachRuntimeDeps(), context)\n },\n },\n { pluginId: OUTREACH_PLUGIN_ID },\n )\n registerPluginConsoleCron(\n {\n id: OUTREACH_SYNC_JOB_ID,\n label: 'Sequence replies and bounces',\n drives:\n 'Reads each connected mailbox for replies, out-of-office answers, opt-outs and bounces, and stops or postpones the sequence each one is about. If it stops, a person who replied or asked to be left alone keeps getting follow-ups, and a mailbox that bounces never pauses itself.',\n run: async (context) => {\n const [{ runOutreachSyncJob }, platform] = await Promise.all([import('./runtime/sync-job'), runtime()])\n return runOutreachSyncJob(platform.platformOutreachRuntimeDeps(), context)\n },\n },\n { pluginId: OUTREACH_PLUGIN_ID },\n )\n}\n\n/** The runtime's platform reach, loaded when a job or an erasure first runs. */\nconst runtime = () => import('./runtime/platform-runtime-deps')\n"],"names":["registerPluginConsoleCron","listPluginMembershipDetachers","registerPluginMembershipDetacher","listPluginOrgErasers","registerPluginOrgEraser","listPluginLeadConversionListeners","registerPluginLeadConversionListener","registerPluginPersonEraser","listPluginUserErasers","registerPluginUserEraser","OUTREACH_PLUGIN_ID","OUTREACH_SEND_JOB_ID","OUTREACH_SYNC_JOB_ID","registerOutreachConsoleServerDeclarations","includes","request","createOutreachOrgEraser","defaultOutreachErasureDeps","pluginId","createOutreachUserEraser","runtime","platformOutreachPersonEraser","followOutreachLeadToContact","creditOutreachLeadConversion","platform","Promise","all","deps","platformOutreachRuntimeDeps","credited","followed","firestore","campaignsCredited","createOutreachCampaignDetacher","id","label","drives","run","context","runOutreachSendJob","runOutreachSyncJob"],"mappings":"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"}
|
|
@@ -86,7 +86,7 @@ export declare function planOutreachFirstDue(input: {
|
|
|
86
86
|
export declare function outreachAttestationsFrom(kinds: readonly unknown[], uid: string, atMs: number): OutreachAttestations;
|
|
87
87
|
export interface OutreachEnrollmentDraft {
|
|
88
88
|
id: string;
|
|
89
|
-
sequence: Pick<OutreachSequence, 'id' | 'hostId' | 'mailboxId' | 'steps' | 'settings'>;
|
|
89
|
+
sequence: Pick<OutreachSequence, 'id' | 'hostId' | 'mailboxId' | 'steps' | 'settings' | 'campaignIds'>;
|
|
90
90
|
mailbox: ScheduleMailbox;
|
|
91
91
|
/** The record the person is (AGL-3234); a contact when absent. */
|
|
92
92
|
target?: OutreachEnrollmentTarget;
|
|
@@ -37,6 +37,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
37
37
|
* whole document, so a writer applies it with its own SDK and its own
|
|
38
38
|
* timestamps.
|
|
39
39
|
*==========================================*/ import { OUTREACH_ATTESTATION_KINDS, OUTREACH_STOP_REASONS_BY_STATUS } from "../model/outreach.types.js";
|
|
40
|
+
import { normalizeCampaignIds } from "@aglyn/aglyn/app-utils/campaign-membership";
|
|
40
41
|
import { normalizeOutreachPersonalLine } from "./gates.js";
|
|
41
42
|
import { effectiveOutreachWindow, scheduleOutreachDue } from "./schedule.js";
|
|
42
43
|
import { isInThreadEmailStep } from "./sequence-validation.js";
|
|
@@ -220,6 +221,9 @@ const detailOf = (value)=>{
|
|
|
220
221
|
threadSubject: null,
|
|
221
222
|
messageIds: [],
|
|
222
223
|
lastSentAtMs: null,
|
|
224
|
+
// The sequence's campaigns as they stand now (AGL-3254): what this
|
|
225
|
+
// enrollment's outcomes are credited to, whatever the sequence joins later.
|
|
226
|
+
campaignIds: normalizeCampaignIds(draft.sequence.campaignIds),
|
|
223
227
|
createdAtMs: draft.nowMs,
|
|
224
228
|
updatedAtMs: draft.nowMs
|
|
225
229
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/engine/enrollment-state.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 * ONE PERSON'S RUN THROUGH A SEQUENCE (AGL-2979).\n *\n * ## The state machine\n *\n * active ──► paused ──► active\n * │ │\n * ├──────────┴──► replied · bounced · opted_out · stopped\n * └─► finished · failed\n *\n * An enrollment sends only while `active`. Everything that ends the sending\n * records why (`stopReason`, `stopDetail`, when, and who). Ending the\n * sending does not end what can still be learned from the thread: a reply,\n * a bounce or an opt-out that arrives after the last step, after a member\n * stopped it or after a send failed is still recorded, and a person who\n * replied and then asked to be left alone moves on to `opted_out` — an\n * opt-out is the one fact every later sequence has to be able to find.\n * `bounced` and `opted_out` are final.\n *\n * Every function returns a PATCH — the fields to write — and never the\n * whole document, so a writer applies it with its own SDK and its own\n * timestamps.\n *==========================================*/\n\nimport {\n OUTREACH_ATTESTATION_KINDS,\n OUTREACH_STOP_REASONS_BY_STATUS,\n type OutreachAttestationKind,\n type OutreachAttestations,\n type OutreachEnrollment,\n type OutreachEnrollmentStatus,\n type OutreachEnrollmentTarget,\n type OutreachMailbox,\n type OutreachSequence,\n type OutreachStopReason,\n} from '../model/outreach.types'\nimport { normalizeOutreachPersonalLine } from './gates'\nimport { effectiveOutreachWindow, type OutreachRandom, scheduleOutreachDue } from './schedule'\nimport { isInThreadEmailStep } from './sequence-validation'\n\n/** Where each status may go next. */\nexport const OUTREACH_ENROLLMENT_TRANSITIONS: Readonly<\n Record<OutreachEnrollmentStatus, readonly OutreachEnrollmentStatus[]>\n> = {\n active: ['paused', 'finished', 'replied', 'bounced', 'opted_out', 'stopped', 'failed'],\n paused: ['active', 'replied', 'bounced', 'opted_out', 'stopped'],\n finished: ['replied', 'bounced', 'opted_out'],\n stopped: ['replied', 'bounced', 'opted_out'],\n failed: ['replied', 'bounced', 'opted_out'],\n replied: ['opted_out'],\n bounced: [],\n opted_out: [],\n}\n\nexport function canTransitionOutreachEnrollment(\n from: OutreachEnrollmentStatus,\n to: OutreachEnrollmentStatus,\n): boolean {\n return OUTREACH_ENROLLMENT_TRANSITIONS[from]?.includes(to) === true\n}\n\n/** Something that happened to an enrollment. `atMs` is when it happened. */\nexport type OutreachEnrollmentEvent =\n | { type: 'pause'; atMs: number; byUid: string | null; detail?: string | null }\n | { type: 'resume'; atMs: number; byUid: string | null }\n | {\n type: 'stop'\n atMs: number\n byUid: string | null\n reason?: Extract<OutreachStopReason, 'manual' | 'gate' | 'sequence_archived'>\n detail?: string | null\n }\n | { type: 'reply'; atMs: number; detail?: string | null }\n | {\n type: 'opt_out'\n atMs: number\n reason: Extract<OutreachStopReason, 'opt_out_reply' | 'unsubscribe' | 'do_not_contact'>\n detail?: string | null\n }\n | { type: 'bounce'; atMs: number; detail?: string | null }\n | { type: 'send_failed'; atMs: number; detail?: string | null }\n\n/** The status fields an event writes. */\nexport interface OutreachEnrollmentStatusPatch {\n status: OutreachEnrollmentStatus\n stopReason: OutreachStopReason | null\n stopDetail: string | null\n stoppedAtMs: number | null\n stoppedByUid: string | null\n nextDueAtMs: number | null\n}\n\n/**\n * `patch` is what to write, or `null` when there is nothing to write — the\n * enrollment is already there, or the event is not allowed, in which case\n * `error` says why.\n */\nexport interface OutreachEnrollmentEventResult {\n patch: OutreachEnrollmentStatusPatch | null\n error: string | null\n}\n\nconst STATUS_WORDS: Record<OutreachEnrollmentStatus, string> = {\n active: 'is active',\n paused: 'is paused',\n finished: 'finished',\n replied: 'got a reply',\n bounced: 'bounced',\n opted_out: 'opted out',\n stopped: 'was stopped',\n failed: 'failed',\n}\n\nconst EVENT_TARGET: Record<OutreachEnrollmentEvent['type'], OutreachEnrollmentStatus> = {\n pause: 'paused',\n resume: 'active',\n stop: 'stopped',\n reply: 'replied',\n opt_out: 'opted_out',\n bounce: 'bounced',\n send_failed: 'failed',\n}\n\nconst detailOf = (value: unknown): string | null => {\n const text = typeof value === 'string' ? value.replace(/\\s+/g, ' ').trim() : ''\n return text ? text.slice(0, 500) : null\n}\n\n/** What an event does to an enrollment's status — see the module note. */\nexport function applyOutreachEnrollmentEvent(\n enrollment: Pick<OutreachEnrollment, 'status' | 'nextDueAtMs'>,\n event: OutreachEnrollmentEvent,\n): OutreachEnrollmentEventResult {\n const from = enrollment.status\n const to = EVENT_TARGET[event.type]\n if (!to) return { patch: null, error: 'That is not something that happens to an enrollment.' }\n if (from === to) return { patch: null, error: null }\n if (!canTransitionOutreachEnrollment(from, to)) {\n return {\n patch: null,\n error: `This enrollment ${STATUS_WORDS[from]}, so it can't be marked as one that ${STATUS_WORDS[to]}.`,\n }\n }\n if (event.type === 'resume') {\n return {\n patch: {\n status: 'active',\n stopReason: null,\n stopDetail: null,\n stoppedAtMs: null,\n stoppedByUid: null,\n nextDueAtMs: enrollment.nextDueAtMs,\n },\n error: null,\n }\n }\n const reason: OutreachStopReason =\n event.type === 'pause'\n ? 'manual'\n : event.type === 'stop'\n ? (event.reason ?? 'manual')\n : event.type === 'reply'\n ? 'reply'\n : event.type === 'opt_out'\n ? event.reason\n : event.type === 'bounce'\n ? 'hard_bounce'\n : 'send_failed'\n const allowed = OUTREACH_STOP_REASONS_BY_STATUS[to as keyof typeof OUTREACH_STOP_REASONS_BY_STATUS]\n if (!allowed?.includes(reason)) {\n return { patch: null, error: `\"${reason}\" isn't a reason an enrollment ${STATUS_WORDS[to]}.` }\n }\n const byUid = 'byUid' in event && typeof event.byUid === 'string' && event.byUid ? event.byUid : null\n return {\n patch: {\n status: to,\n stopReason: reason,\n stopDetail: detailOf(event.detail),\n stoppedAtMs: event.atMs,\n stoppedByUid: byUid,\n // A paused enrollment keeps its place, so resuming it picks up where it\n // stopped; every other status has nothing left waiting.\n nextDueAtMs: to === 'paused' ? enrollment.nextDueAtMs : null,\n },\n error: null,\n }\n}\n\ntype ScheduleMailbox = Pick<OutreachMailbox, 'timezone' | 'window'>\ntype ScheduleSequence = Pick<OutreachSequence, 'steps' | 'settings'>\n\n/** When a new enrollment's first step comes due, or `null` when it cannot be placed. */\nexport function planOutreachFirstDue(input: {\n sequence: ScheduleSequence\n mailbox: ScheduleMailbox\n enrolledAtMs: number\n random: OutreachRandom\n}): number | null {\n const first = input.sequence.steps?.[0]\n if (!first) return null\n return scheduleOutreachDue({\n fromMs: input.enrolledAtMs,\n delayBusinessDays: first.delayBusinessDays,\n timeZone: input.mailbox.timezone,\n window: effectiveOutreachWindow(input.sequence.settings?.window, input.mailbox.window),\n random: input.random,\n })\n}\n\n/** Stamps the attestations a rep ticked with who ticked them and when; unknown kinds are dropped. */\nexport function outreachAttestationsFrom(\n kinds: readonly unknown[],\n uid: string,\n atMs: number,\n): OutreachAttestations {\n const attestations: OutreachAttestations = {}\n for (const kind of kinds ?? []) {\n if ((OUTREACH_ATTESTATION_KINDS as readonly unknown[]).includes(kind)) {\n attestations[kind as OutreachAttestationKind] = { uid, atMs }\n }\n }\n return attestations\n}\n\nexport interface OutreachEnrollmentDraft {\n id: string\n sequence: Pick<OutreachSequence, 'id' | 'hostId' | 'mailboxId' | 'steps' | 'settings'>\n mailbox: ScheduleMailbox\n /** The record the person is (AGL-3234); a contact when absent. */\n target?: OutreachEnrollmentTarget\n /** The contact's id; `''` for a lead. */\n contactId: string\n /** The lead's person key, for an enrollment made on a lead. */\n leadId?: string | null\n /** The contact's name as the sending site knows it; `''` for none. */\n contactName?: string | null\n /** The address, as the gates normalized it. */\n email: string\n /** The gates' `cold`. */\n cold: boolean\n personalLine: string\n attestations: OutreachAttestations\n enrolledByUid: string\n nowMs: number\n random: OutreachRandom\n}\n\n/**\n * A new enrollment document, first step scheduled — one builder for every\n * door that enrolls, so an enrollment made from a contact and one made from\n * a list are the same shape. Call it after the gates allowed the person.\n */\nexport function buildOutreachEnrollment(draft: OutreachEnrollmentDraft): OutreachEnrollment {\n return {\n id: draft.id,\n sequenceId: draft.sequence.id,\n target: draft.target ?? 'contact',\n contactId: draft.contactId,\n leadId: draft.leadId ?? null,\n contactName: String(draft.contactName ?? '').replace(/\\s+/g, ' ').trim().slice(0, 200),\n email: String(draft.email ?? '').trim().toLowerCase(),\n hostId: draft.sequence.hostId,\n mailboxId: draft.sequence.mailboxId,\n stepIndex: 0,\n nextDueAtMs: planOutreachFirstDue({\n sequence: draft.sequence,\n mailbox: draft.mailbox,\n enrolledAtMs: draft.nowMs,\n random: draft.random,\n }),\n status: 'active',\n stopReason: null,\n stopDetail: null,\n stoppedAtMs: null,\n stoppedByUid: null,\n personalLine: normalizeOutreachPersonalLine(draft.personalLine),\n cold: draft.cold === true,\n attestations: { ...(draft.attestations ?? {}) },\n enrolledByUid: draft.enrolledByUid,\n gmailThreadId: null,\n gmailThreadIds: [],\n threadSubject: null,\n messageIds: [],\n lastSentAtMs: null,\n createdAtMs: draft.nowMs,\n updatedAtMs: draft.nowMs,\n }\n}\n\n/** What the provider reported for an email step that was sent. */\nexport interface OutreachSentEmail {\n /** The `Message-ID` header the message went out with, angle brackets included. */\n messageId: string\n /** The provider's thread id. */\n threadId: string\n /** The subject as sent. */\n subject: string\n}\n\nexport interface OutreachStepCompletionInput {\n enrollment: Pick<\n OutreachEnrollment,\n 'status' | 'stepIndex' | 'gmailThreadId' | 'gmailThreadIds' | 'messageIds' | 'threadSubject'\n >\n sequence: ScheduleSequence\n mailbox: ScheduleMailbox\n /** When the step ran. */\n completedAtMs: number\n /** For an email step, what was sent; not read for a task. */\n sent?: OutreachSentEmail | null\n random: OutreachRandom\n}\n\n/** The fields a completed step writes. */\nexport interface OutreachStepCompletionPatch {\n status: 'active' | 'finished'\n stepIndex: number\n nextDueAtMs: number | null\n lastSentAtMs: number\n gmailThreadId?: string\n gmailThreadIds?: string[]\n threadSubject?: string\n messageIds?: string[]\n}\n\nexport interface OutreachStepCompletionResult {\n patch: OutreachStepCompletionPatch | null\n /**\n * Why there is no patch, or — beside one — that the next step could not\n * be placed on the calendar (a window that never opens, a zone `Intl` does\n * not know), which leaves the enrollment with nothing due until a member\n * fixes the window and resumes it.\n */\n error: string | null\n}\n\n/**\n * What running the current step writes: the send recorded on the thread\n * fields, and the next step scheduled — or the enrollment `finished` when\n * that was the last.\n */\nexport function planOutreachStepCompletion(\n input: OutreachStepCompletionInput,\n): OutreachStepCompletionResult {\n const { enrollment, sequence } = input\n if (enrollment.status !== 'active') {\n return { patch: null, error: 'Only an active enrollment runs its steps.' }\n }\n const steps = sequence.steps ?? []\n const step = steps[enrollment.stepIndex]\n if (!step) return { patch: null, error: 'This enrollment has no step at its position.' }\n const patch: OutreachStepCompletionPatch = {\n status: 'active',\n stepIndex: enrollment.stepIndex + 1,\n nextDueAtMs: null,\n lastSentAtMs: input.completedAtMs,\n }\n if (step.kind === 'email') {\n const sent = input.sent\n const messageId = String(sent?.messageId ?? '').trim()\n const threadId = String(sent?.threadId ?? '').trim()\n if (!messageId || !threadId) {\n return { patch: null, error: 'An email step needs the sent message and thread ids.' }\n }\n const threadIds = [...(enrollment.gmailThreadIds ?? [])]\n if (!threadIds.includes(threadId)) threadIds.push(threadId)\n patch.gmailThreadId = threadId\n patch.gmailThreadIds = threadIds\n if (isInThreadEmailStep(steps, enrollment.stepIndex) && enrollment.threadSubject) {\n patch.messageIds = [...(enrollment.messageIds ?? []), messageId]\n } else {\n patch.threadSubject = String(sent.subject ?? '').trim()\n patch.messageIds = [messageId]\n }\n }\n const next = steps[patch.stepIndex]\n if (!next) {\n patch.status = 'finished'\n return { patch, error: null }\n }\n patch.nextDueAtMs = scheduleOutreachDue({\n fromMs: input.completedAtMs,\n delayBusinessDays: next.delayBusinessDays,\n timeZone: input.mailbox.timezone,\n window: effectiveOutreachWindow(sequence.settings?.window, input.mailbox.window),\n random: input.random,\n })\n return {\n patch,\n error:\n patch.nextDueAtMs === null\n ? \"The next step couldn't be scheduled: check the sending window and the mailbox's timezone.\"\n : null,\n }\n}\n"],"names":["OUTREACH_ATTESTATION_KINDS","OUTREACH_STOP_REASONS_BY_STATUS","normalizeOutreachPersonalLine","effectiveOutreachWindow","scheduleOutreachDue","isInThreadEmailStep","OUTREACH_ENROLLMENT_TRANSITIONS","active","paused","finished","stopped","failed","replied","bounced","opted_out","canTransitionOutreachEnrollment","from","to","includes","STATUS_WORDS","EVENT_TARGET","pause","resume","stop","reply","opt_out","bounce","send_failed","detailOf","value","text","replace","trim","slice","applyOutreachEnrollmentEvent","enrollment","event","status","type","patch","error","stopReason","stopDetail","stoppedAtMs","stoppedByUid","nextDueAtMs","reason","allowed","byUid","detail","atMs","planOutreachFirstDue","input","first","sequence","steps","fromMs","enrolledAtMs","delayBusinessDays","timeZone","mailbox","timezone","window","settings","random","outreachAttestationsFrom","kinds","uid","attestations","kind","buildOutreachEnrollment","draft","id","sequenceId","target","contactId","leadId","contactName","String","email","toLowerCase","hostId","mailboxId","stepIndex","nowMs","personalLine","cold","enrolledByUid","gmailThreadId","gmailThreadIds","threadSubject","messageIds","lastSentAtMs","createdAtMs","updatedAtMs","planOutreachStepCompletion","step","completedAtMs","sent","messageId","threadId","threadIds","push","subject","next"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/engine/enrollment-state.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 * ONE PERSON'S RUN THROUGH A SEQUENCE (AGL-2979).\n *\n * ## The state machine\n *\n * active ──► paused ──► active\n * │ │\n * ├──────────┴──► replied · bounced · opted_out · stopped\n * └─► finished · failed\n *\n * An enrollment sends only while `active`. Everything that ends the sending\n * records why (`stopReason`, `stopDetail`, when, and who). Ending the\n * sending does not end what can still be learned from the thread: a reply,\n * a bounce or an opt-out that arrives after the last step, after a member\n * stopped it or after a send failed is still recorded, and a person who\n * replied and then asked to be left alone moves on to `opted_out` — an\n * opt-out is the one fact every later sequence has to be able to find.\n * `bounced` and `opted_out` are final.\n *\n * Every function returns a PATCH — the fields to write — and never the\n * whole document, so a writer applies it with its own SDK and its own\n * timestamps.\n *==========================================*/\n\nimport {\n OUTREACH_ATTESTATION_KINDS,\n OUTREACH_STOP_REASONS_BY_STATUS,\n type OutreachAttestationKind,\n type OutreachAttestations,\n type OutreachEnrollment,\n type OutreachEnrollmentStatus,\n type OutreachEnrollmentTarget,\n type OutreachMailbox,\n type OutreachSequence,\n type OutreachStopReason,\n} from '../model/outreach.types'\nimport { normalizeCampaignIds } from '@aglyn/aglyn/app-utils/campaign-membership'\nimport { normalizeOutreachPersonalLine } from './gates'\nimport { effectiveOutreachWindow, type OutreachRandom, scheduleOutreachDue } from './schedule'\nimport { isInThreadEmailStep } from './sequence-validation'\n\n/** Where each status may go next. */\nexport const OUTREACH_ENROLLMENT_TRANSITIONS: Readonly<\n Record<OutreachEnrollmentStatus, readonly OutreachEnrollmentStatus[]>\n> = {\n active: ['paused', 'finished', 'replied', 'bounced', 'opted_out', 'stopped', 'failed'],\n paused: ['active', 'replied', 'bounced', 'opted_out', 'stopped'],\n finished: ['replied', 'bounced', 'opted_out'],\n stopped: ['replied', 'bounced', 'opted_out'],\n failed: ['replied', 'bounced', 'opted_out'],\n replied: ['opted_out'],\n bounced: [],\n opted_out: [],\n}\n\nexport function canTransitionOutreachEnrollment(\n from: OutreachEnrollmentStatus,\n to: OutreachEnrollmentStatus,\n): boolean {\n return OUTREACH_ENROLLMENT_TRANSITIONS[from]?.includes(to) === true\n}\n\n/** Something that happened to an enrollment. `atMs` is when it happened. */\nexport type OutreachEnrollmentEvent =\n | { type: 'pause'; atMs: number; byUid: string | null; detail?: string | null }\n | { type: 'resume'; atMs: number; byUid: string | null }\n | {\n type: 'stop'\n atMs: number\n byUid: string | null\n reason?: Extract<OutreachStopReason, 'manual' | 'gate' | 'sequence_archived'>\n detail?: string | null\n }\n | { type: 'reply'; atMs: number; detail?: string | null }\n | {\n type: 'opt_out'\n atMs: number\n reason: Extract<OutreachStopReason, 'opt_out_reply' | 'unsubscribe' | 'do_not_contact'>\n detail?: string | null\n }\n | { type: 'bounce'; atMs: number; detail?: string | null }\n | { type: 'send_failed'; atMs: number; detail?: string | null }\n\n/** The status fields an event writes. */\nexport interface OutreachEnrollmentStatusPatch {\n status: OutreachEnrollmentStatus\n stopReason: OutreachStopReason | null\n stopDetail: string | null\n stoppedAtMs: number | null\n stoppedByUid: string | null\n nextDueAtMs: number | null\n}\n\n/**\n * `patch` is what to write, or `null` when there is nothing to write — the\n * enrollment is already there, or the event is not allowed, in which case\n * `error` says why.\n */\nexport interface OutreachEnrollmentEventResult {\n patch: OutreachEnrollmentStatusPatch | null\n error: string | null\n}\n\nconst STATUS_WORDS: Record<OutreachEnrollmentStatus, string> = {\n active: 'is active',\n paused: 'is paused',\n finished: 'finished',\n replied: 'got a reply',\n bounced: 'bounced',\n opted_out: 'opted out',\n stopped: 'was stopped',\n failed: 'failed',\n}\n\nconst EVENT_TARGET: Record<OutreachEnrollmentEvent['type'], OutreachEnrollmentStatus> = {\n pause: 'paused',\n resume: 'active',\n stop: 'stopped',\n reply: 'replied',\n opt_out: 'opted_out',\n bounce: 'bounced',\n send_failed: 'failed',\n}\n\nconst detailOf = (value: unknown): string | null => {\n const text = typeof value === 'string' ? value.replace(/\\s+/g, ' ').trim() : ''\n return text ? text.slice(0, 500) : null\n}\n\n/** What an event does to an enrollment's status — see the module note. */\nexport function applyOutreachEnrollmentEvent(\n enrollment: Pick<OutreachEnrollment, 'status' | 'nextDueAtMs'>,\n event: OutreachEnrollmentEvent,\n): OutreachEnrollmentEventResult {\n const from = enrollment.status\n const to = EVENT_TARGET[event.type]\n if (!to) return { patch: null, error: 'That is not something that happens to an enrollment.' }\n if (from === to) return { patch: null, error: null }\n if (!canTransitionOutreachEnrollment(from, to)) {\n return {\n patch: null,\n error: `This enrollment ${STATUS_WORDS[from]}, so it can't be marked as one that ${STATUS_WORDS[to]}.`,\n }\n }\n if (event.type === 'resume') {\n return {\n patch: {\n status: 'active',\n stopReason: null,\n stopDetail: null,\n stoppedAtMs: null,\n stoppedByUid: null,\n nextDueAtMs: enrollment.nextDueAtMs,\n },\n error: null,\n }\n }\n const reason: OutreachStopReason =\n event.type === 'pause'\n ? 'manual'\n : event.type === 'stop'\n ? (event.reason ?? 'manual')\n : event.type === 'reply'\n ? 'reply'\n : event.type === 'opt_out'\n ? event.reason\n : event.type === 'bounce'\n ? 'hard_bounce'\n : 'send_failed'\n const allowed = OUTREACH_STOP_REASONS_BY_STATUS[to as keyof typeof OUTREACH_STOP_REASONS_BY_STATUS]\n if (!allowed?.includes(reason)) {\n return { patch: null, error: `\"${reason}\" isn't a reason an enrollment ${STATUS_WORDS[to]}.` }\n }\n const byUid = 'byUid' in event && typeof event.byUid === 'string' && event.byUid ? event.byUid : null\n return {\n patch: {\n status: to,\n stopReason: reason,\n stopDetail: detailOf(event.detail),\n stoppedAtMs: event.atMs,\n stoppedByUid: byUid,\n // A paused enrollment keeps its place, so resuming it picks up where it\n // stopped; every other status has nothing left waiting.\n nextDueAtMs: to === 'paused' ? enrollment.nextDueAtMs : null,\n },\n error: null,\n }\n}\n\ntype ScheduleMailbox = Pick<OutreachMailbox, 'timezone' | 'window'>\ntype ScheduleSequence = Pick<OutreachSequence, 'steps' | 'settings'>\n\n/** When a new enrollment's first step comes due, or `null` when it cannot be placed. */\nexport function planOutreachFirstDue(input: {\n sequence: ScheduleSequence\n mailbox: ScheduleMailbox\n enrolledAtMs: number\n random: OutreachRandom\n}): number | null {\n const first = input.sequence.steps?.[0]\n if (!first) return null\n return scheduleOutreachDue({\n fromMs: input.enrolledAtMs,\n delayBusinessDays: first.delayBusinessDays,\n timeZone: input.mailbox.timezone,\n window: effectiveOutreachWindow(input.sequence.settings?.window, input.mailbox.window),\n random: input.random,\n })\n}\n\n/** Stamps the attestations a rep ticked with who ticked them and when; unknown kinds are dropped. */\nexport function outreachAttestationsFrom(\n kinds: readonly unknown[],\n uid: string,\n atMs: number,\n): OutreachAttestations {\n const attestations: OutreachAttestations = {}\n for (const kind of kinds ?? []) {\n if ((OUTREACH_ATTESTATION_KINDS as readonly unknown[]).includes(kind)) {\n attestations[kind as OutreachAttestationKind] = { uid, atMs }\n }\n }\n return attestations\n}\n\nexport interface OutreachEnrollmentDraft {\n id: string\n sequence: Pick<OutreachSequence, 'id' | 'hostId' | 'mailboxId' | 'steps' | 'settings' | 'campaignIds'>\n mailbox: ScheduleMailbox\n /** The record the person is (AGL-3234); a contact when absent. */\n target?: OutreachEnrollmentTarget\n /** The contact's id; `''` for a lead. */\n contactId: string\n /** The lead's person key, for an enrollment made on a lead. */\n leadId?: string | null\n /** The contact's name as the sending site knows it; `''` for none. */\n contactName?: string | null\n /** The address, as the gates normalized it. */\n email: string\n /** The gates' `cold`. */\n cold: boolean\n personalLine: string\n attestations: OutreachAttestations\n enrolledByUid: string\n nowMs: number\n random: OutreachRandom\n}\n\n/**\n * A new enrollment document, first step scheduled — one builder for every\n * door that enrolls, so an enrollment made from a contact and one made from\n * a list are the same shape. Call it after the gates allowed the person.\n */\nexport function buildOutreachEnrollment(draft: OutreachEnrollmentDraft): OutreachEnrollment {\n return {\n id: draft.id,\n sequenceId: draft.sequence.id,\n target: draft.target ?? 'contact',\n contactId: draft.contactId,\n leadId: draft.leadId ?? null,\n contactName: String(draft.contactName ?? '').replace(/\\s+/g, ' ').trim().slice(0, 200),\n email: String(draft.email ?? '').trim().toLowerCase(),\n hostId: draft.sequence.hostId,\n mailboxId: draft.sequence.mailboxId,\n stepIndex: 0,\n nextDueAtMs: planOutreachFirstDue({\n sequence: draft.sequence,\n mailbox: draft.mailbox,\n enrolledAtMs: draft.nowMs,\n random: draft.random,\n }),\n status: 'active',\n stopReason: null,\n stopDetail: null,\n stoppedAtMs: null,\n stoppedByUid: null,\n personalLine: normalizeOutreachPersonalLine(draft.personalLine),\n cold: draft.cold === true,\n attestations: { ...(draft.attestations ?? {}) },\n enrolledByUid: draft.enrolledByUid,\n gmailThreadId: null,\n gmailThreadIds: [],\n threadSubject: null,\n messageIds: [],\n lastSentAtMs: null,\n // The sequence's campaigns as they stand now (AGL-3254): what this\n // enrollment's outcomes are credited to, whatever the sequence joins later.\n campaignIds: normalizeCampaignIds(draft.sequence.campaignIds),\n createdAtMs: draft.nowMs,\n updatedAtMs: draft.nowMs,\n }\n}\n\n/** What the provider reported for an email step that was sent. */\nexport interface OutreachSentEmail {\n /** The `Message-ID` header the message went out with, angle brackets included. */\n messageId: string\n /** The provider's thread id. */\n threadId: string\n /** The subject as sent. */\n subject: string\n}\n\nexport interface OutreachStepCompletionInput {\n enrollment: Pick<\n OutreachEnrollment,\n 'status' | 'stepIndex' | 'gmailThreadId' | 'gmailThreadIds' | 'messageIds' | 'threadSubject'\n >\n sequence: ScheduleSequence\n mailbox: ScheduleMailbox\n /** When the step ran. */\n completedAtMs: number\n /** For an email step, what was sent; not read for a task. */\n sent?: OutreachSentEmail | null\n random: OutreachRandom\n}\n\n/** The fields a completed step writes. */\nexport interface OutreachStepCompletionPatch {\n status: 'active' | 'finished'\n stepIndex: number\n nextDueAtMs: number | null\n lastSentAtMs: number\n gmailThreadId?: string\n gmailThreadIds?: string[]\n threadSubject?: string\n messageIds?: string[]\n}\n\nexport interface OutreachStepCompletionResult {\n patch: OutreachStepCompletionPatch | null\n /**\n * Why there is no patch, or — beside one — that the next step could not\n * be placed on the calendar (a window that never opens, a zone `Intl` does\n * not know), which leaves the enrollment with nothing due until a member\n * fixes the window and resumes it.\n */\n error: string | null\n}\n\n/**\n * What running the current step writes: the send recorded on the thread\n * fields, and the next step scheduled — or the enrollment `finished` when\n * that was the last.\n */\nexport function planOutreachStepCompletion(\n input: OutreachStepCompletionInput,\n): OutreachStepCompletionResult {\n const { enrollment, sequence } = input\n if (enrollment.status !== 'active') {\n return { patch: null, error: 'Only an active enrollment runs its steps.' }\n }\n const steps = sequence.steps ?? []\n const step = steps[enrollment.stepIndex]\n if (!step) return { patch: null, error: 'This enrollment has no step at its position.' }\n const patch: OutreachStepCompletionPatch = {\n status: 'active',\n stepIndex: enrollment.stepIndex + 1,\n nextDueAtMs: null,\n lastSentAtMs: input.completedAtMs,\n }\n if (step.kind === 'email') {\n const sent = input.sent\n const messageId = String(sent?.messageId ?? '').trim()\n const threadId = String(sent?.threadId ?? '').trim()\n if (!messageId || !threadId) {\n return { patch: null, error: 'An email step needs the sent message and thread ids.' }\n }\n const threadIds = [...(enrollment.gmailThreadIds ?? [])]\n if (!threadIds.includes(threadId)) threadIds.push(threadId)\n patch.gmailThreadId = threadId\n patch.gmailThreadIds = threadIds\n if (isInThreadEmailStep(steps, enrollment.stepIndex) && enrollment.threadSubject) {\n patch.messageIds = [...(enrollment.messageIds ?? []), messageId]\n } else {\n patch.threadSubject = String(sent.subject ?? '').trim()\n patch.messageIds = [messageId]\n }\n }\n const next = steps[patch.stepIndex]\n if (!next) {\n patch.status = 'finished'\n return { patch, error: null }\n }\n patch.nextDueAtMs = scheduleOutreachDue({\n fromMs: input.completedAtMs,\n delayBusinessDays: next.delayBusinessDays,\n timeZone: input.mailbox.timezone,\n window: effectiveOutreachWindow(sequence.settings?.window, input.mailbox.window),\n random: input.random,\n })\n return {\n patch,\n error:\n patch.nextDueAtMs === null\n ? \"The next step couldn't be scheduled: check the sending window and the mailbox's timezone.\"\n : null,\n }\n}\n"],"names":["OUTREACH_ATTESTATION_KINDS","OUTREACH_STOP_REASONS_BY_STATUS","normalizeCampaignIds","normalizeOutreachPersonalLine","effectiveOutreachWindow","scheduleOutreachDue","isInThreadEmailStep","OUTREACH_ENROLLMENT_TRANSITIONS","active","paused","finished","stopped","failed","replied","bounced","opted_out","canTransitionOutreachEnrollment","from","to","includes","STATUS_WORDS","EVENT_TARGET","pause","resume","stop","reply","opt_out","bounce","send_failed","detailOf","value","text","replace","trim","slice","applyOutreachEnrollmentEvent","enrollment","event","status","type","patch","error","stopReason","stopDetail","stoppedAtMs","stoppedByUid","nextDueAtMs","reason","allowed","byUid","detail","atMs","planOutreachFirstDue","input","first","sequence","steps","fromMs","enrolledAtMs","delayBusinessDays","timeZone","mailbox","timezone","window","settings","random","outreachAttestationsFrom","kinds","uid","attestations","kind","buildOutreachEnrollment","draft","id","sequenceId","target","contactId","leadId","contactName","String","email","toLowerCase","hostId","mailboxId","stepIndex","nowMs","personalLine","cold","enrolledByUid","gmailThreadId","gmailThreadIds","threadSubject","messageIds","lastSentAtMs","campaignIds","createdAtMs","updatedAtMs","planOutreachStepCompletion","step","completedAtMs","sent","messageId","threadId","threadIds","push","subject","next"],"mappings":"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"}
|
|
@@ -95,7 +95,7 @@ export interface OutreachGateInput {
|
|
|
95
95
|
enrollmentId?: string | null;
|
|
96
96
|
lookups: OutreachGateLookups;
|
|
97
97
|
}
|
|
98
|
-
export type OutreachGateCode = 'contact_missing' | 'invalid_email' | 'email_not_on_contact' | 'free_mail' | 'country_not_allowed' | 'country_unknown' | 'platform_suppressed' | 'host_suppressed' | 'sales_opted_out' | 'do_not_contact' | 'do_not_contact_domain' | 'workspace_member' | 'customer' | 'already_enrolled' | 'attestations_missing' | 'personal_line_missing' | 'personal_line_too_long';
|
|
98
|
+
export type OutreachGateCode = 'contact_missing' | 'invalid_email' | 'email_not_on_contact' | 'free_mail' | 'country_not_allowed' | 'country_unknown' | 'platform_suppressed' | 'host_suppressed' | 'email_state' | 'sales_opted_out' | 'do_not_contact' | 'do_not_contact_domain' | 'workspace_member' | 'customer' | 'already_enrolled' | 'attestations_missing' | 'personal_line_missing' | 'personal_line_too_long';
|
|
99
99
|
export interface OutreachGateBlock {
|
|
100
100
|
code: OutreachGateCode;
|
|
101
101
|
/** A sentence a rep can act on. */
|
package/src/lib/engine/gates.js
CHANGED
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
* be emailed must not be emailed because a read timed out.
|
|
48
48
|
*==========================================*/ import { contactEmails, normalizeContactEmail, readContactFacet } from "@aglyn/aglyn/app-utils/contacts";
|
|
49
49
|
import { isPublicMailboxDomain } from "@aglyn/aglyn/app-utils/crm";
|
|
50
|
+
import { emailStateRefusal, readEmailState } from "@aglyn/aglyn/app-utils/email-state";
|
|
50
51
|
import { findMemberByEmailAddress } from "@aglyn/aglyn/app-utils/member-email-aliases";
|
|
51
52
|
import { OUTREACH_ATTESTATION_KINDS, OUTREACH_PERSONAL_LINE_MAX } from "../model/outreach.types.js";
|
|
52
53
|
import { outreachCountryName, resolveOutreachRecipientCountry } from "./recipient-country.js";
|
|
@@ -167,7 +168,11 @@ const COULD_NOT_CHECK = 'Try again in a moment.';
|
|
|
167
168
|
} else if (!attestedUs) {
|
|
168
169
|
block('country_unknown', `We don't know which country ${email} is in. Confirm it's a US business address, or add the country to the contact.`);
|
|
169
170
|
}
|
|
170
|
-
// 4. Every list that says not to
|
|
171
|
+
// 4. Every list that says not to — the record's own verdict first
|
|
172
|
+
// (AGL-3245), in the words the record page shows: what the lists below
|
|
173
|
+
// hold, said by the reason it is held.
|
|
174
|
+
const recordRefusal = emailStateRefusal(readEmailState(contact));
|
|
175
|
+
if (recordRefusal) block('email_state', recordRefusal);
|
|
171
176
|
if ((lookups == null ? void 0 : lookups.platformSuppressed) === true) {
|
|
172
177
|
block('platform_suppressed', `${email} is on the platform's suppression list after a hard bounce or a spam complaint.`);
|
|
173
178
|
} else if ((lookups == null ? void 0 : lookups.platformSuppressed) !== false) {
|