@aglyn/plugins-outreach 1.0.0-beta.173 → 1.0.0-beta.174

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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/enroll-routes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n CAMPAIGN_MEMBERSHIP_FIELD,\n contactCampaignFieldPath,\n normalizeCampaignIds,\n} from '@aglyn/aglyn/app-utils/campaign-membership'\nimport { consentGroupForHost } from '@aglyn/aglyn/app-utils/consent-groups'\nimport { scopeTokensForHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport {\n CRM_COLLECTIONS,\n crmLeadStatus,\n crmViewIsListed,\n isCrmLeadOpen,\n normalizeCrmViewFilters,\n} from '@aglyn/aglyn/app-utils/crm'\nimport { dynamicListDimensionsForCrmView } from '@aglyn/aglyn/app-utils/dynamic-list-rule'\nimport type { PluginRecordTimelineWriter } from '@aglyn/aglyn/plugin-manager/plugin-record-timeline'\nimport type { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport { findContactByEmail } from '@aglyn/tenant-data-admin/server/contact-email-index'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { outreachEnrolledEntry } from '../engine/enrollment-activity'\nimport { buildOutreachEnrollment, planOutreachFirstDue } from '../engine/enrollment-state'\nimport type { OutreachGateLookups } from '../engine/gates'\nimport {\n decideOutreachEnrollment,\n previewOutreachPerson,\n readOutreachEnrollCandidates,\n type OutreachEnrollCandidate,\n type OutreachGateQuestion,\n} from '../enrollment/enroll-people'\nimport { readOutreachGateLookups } from '../enrollment/gate-lookups'\nimport { effectiveOutreachAllowedCountries } from '../model/compliance-settings'\nimport {\n OUTREACH_ENROLL_BATCH_MAX,\n type OutreachEnrollOutcome,\n type OutreachEnrollPreviewResponse,\n type OutreachEnrollResponse,\n type OutreachPersonRef,\n} from '../model/outreach-api'\nimport type { OutreachSequence } from '../model/outreach.types'\nimport { readOutreachComplianceSettingsDoc } from '../storage/compliance-settings-store'\nimport { fileOutreachNote } from '../runtime/timeline'\nimport {\n outreachEnrollmentId,\n outreachEnrollmentLink,\n outreachOrgCollection,\n readStoredOutreachMailbox,\n readStoredOutreachSequence,\n} from '../storage/outreach-records'\nimport { OUTREACH_SEQUENCE_ACTIVITY_TARGET, type OutreachRouteDeps } from './route-deps'\nimport { outreachRouteGate, type OutreachRouteCaller, type OutreachRouteExtraPermission } from './route-gate'\nimport {\n outreachMethodNotAllowed,\n outreachOk,\n outreachRefusal,\n readOutreachDocumentId,\n readOutreachJsonBody,\n} from './route-http'\n\n/**\n * ENROLLING PEOPLE IN A SEQUENCE (AGL-2980): `outreach/enroll/preview` and\n * `outreach/enroll`.\n *\n * The preview reads the people a saved Contacts view or a search named —\n * at most {@link OUTREACH_ENROLL_BATCH_MAX} — and says where each stands:\n * eligible, blocked with the gate's own sentence, or waiting on the rep for\n * a personal line and the attestations only they can make. Confirm reads\n * everything again and asks the gates again with what the rep supplied,\n * because a preview is a picture of a moment: someone can unsubscribe\n * between the two, and a request can claim anything. Nothing the preview\n * answered is trusted by the enroll.\n *\n * Both read the CRM's people, so both ask for `data.manage` beside\n * `outreach.use` — the permission the contacts' own rules read with, so\n * Outreach is no side door into the address book.\n *\n * Only an ACTIVE sequence takes people: enrolling schedules the first step\n * in the mailbox's hours, and a draft's steps and a paused sequence's timing\n * are not yet what will be sent.\n */\n\n/** The permission enrolling reads the CRM with. */\nexport const OUTREACH_READS_PEOPLE: OutreachRouteExtraPermission = {\n key: 'data.manage',\n refusal: \"Enrolling reads your CRM's contacts, and your role does not include Manage data.\",\n}\n\n/** Everything enrolling reaches beyond the routes' own dependencies. */\nexport interface OutreachEnrollRouteDeps extends OutreachRouteDeps {\n /**\n * The addresses a saved Contacts view selects among one site's contacts,\n * read the way the dynamic-list sweep reads one, and whether the read\n * reached the whole view before its budget.\n */\n crmViewEmails(input: { hostId: string; viewId: string }): Promise<{ emails: string[]; complete: boolean }>\n /**\n * The workspace's record system on the timeline seam (AGL-3274), when a\n * plugin keeps one: the enroll files \"Enrolled in\" on the person's record\n * — the runtime's `timeline`, reached the same way.\n */\n timeline(): PluginRecordTimelineWriter | null\n}\n\nexport interface OutreachEnrollRoutes {\n preview: PluginWebApiHandler\n confirm: PluginWebApiHandler\n}\n\n/** The activity line an enroll writes. */\nexport function outreachEnrolledActivity(count: number): string {\n return count === 1\n ? 'Enrolled 1 person in a sequence'\n : `Enrolled ${count} people in a sequence`\n}\n\n/** Lookups for a person the gates will never be asked about: every one unchecked. */\nconst UNCHECKED: OutreachGateLookups = {\n platformSuppressed: null,\n hostSuppressed: null,\n salesTopicState: null,\n doNotContact: null,\n doNotContactDomain: null,\n workspaceMembers: null,\n openEnrollments: null,\n hasInboundEmail: null,\n}\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** gRPC `ALREADY_EXISTS`, which `create()` rejects with when the document is there. */\nconst ALREADY_EXISTS = 6\n\nasync function loadActiveSequence(\n firestore: Firestore,\n orgId: string,\n rawSequenceId: unknown,\n): Promise<Response | OutreachSequence> {\n const sequenceId = readOutreachDocumentId(rawSequenceId)\n if (!sequenceId) return outreachRefusal(400, 'invalid-request', 'Name the sequence to enroll people in.')\n const snapshot = await outreachOrgCollection(firestore, orgId, 'sequences').doc(sequenceId).get()\n const sequence = readStoredOutreachSequence(sequenceId, snapshot.exists ? snapshot.data() : undefined)\n if (!sequence) return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n if (sequence.status === 'archived') {\n return outreachRefusal(409, 'sequence-archived', 'This sequence is archived and takes no one new.')\n }\n if (sequence.status !== 'active') {\n return outreachRefusal(409, 'sequence-not-active', 'Activate this sequence before enrolling people.')\n }\n return sequence\n}\n\n/** What every person in one request is judged against. */\ninterface EnrollContext {\n settings: OutreachGateQuestion['settings']\n contactGroupId: string\n siteName: string\n}\n\nasync function enrollContext(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n sequence: OutreachSequence,\n): Promise<EnrollContext> {\n const [orgSettings, host] = await Promise.all([\n readOutreachComplianceSettingsDoc(firestore, caller.orgId),\n firestore.collection('hosts').doc(sequence.hostId).get(),\n ])\n return {\n // The organization's countries are a ceiling over the sequence's.\n settings: {\n ...sequence.settings,\n allowedCountries: effectiveOutreachAllowedCountries(\n sequence.settings.allowedCountries,\n orgSettings.allowedCountries,\n ),\n },\n contactGroupId: consentGroupForHost(caller.org, sequence.hostId).groupId,\n siteName: host.exists ? String(host.get('name') ?? '') : '',\n }\n}\n\n/**\n * The candidates, the enrollments they already have here, and the gates'\n * lookups — every map keyed by the person's own id.\n *\n * \"Already in this sequence\" is answered two ways (AGL-3234): by the id the\n * person's enrollment would have, and by any enrollment in this sequence\n * that carries the person's ADDRESS. The second is what keeps a contact\n * from being enrolled beside the lead they were converted from: the lead's\n * enrollment followed them, under the lead's key, and still names them.\n */\nasync function readPeople(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n sequence: OutreachSequence,\n context: EnrollContext,\n people: readonly OutreachPersonRef[],\n): Promise<{\n candidates: OutreachEnrollCandidate[]\n enrolledHere: Set<string>\n lookups: Map<string, OutreachGateLookups>\n}> {\n const candidates = await readOutreachEnrollCandidates(firestore, {\n orgId: caller.orgId,\n hostId: sequence.hostId,\n contactGroupId: context.contactGroupId,\n people,\n })\n const enrollments = outreachOrgCollection(firestore, caller.orgId, 'enrollments')\n const existing = candidates.length\n ? await firestore.getAll(\n ...candidates.map((candidate) => enrollments.doc(outreachEnrollmentId(sequence.id, candidate.personId))),\n )\n : []\n const enrolledHere = new Set(\n candidates.filter((_candidate, index) => existing[index]?.exists).map((candidate) => candidate.personId),\n )\n const askable = candidates.filter((candidate) => candidate.contact && candidate.visible && candidate.email)\n const lookups = askable.length\n ? await readOutreachGateLookups(firestore, {\n orgId: caller.orgId,\n hostId: sequence.hostId,\n people: askable.map((candidate) => ({\n personId: candidate.personId,\n contactId: candidate.contactId || null,\n leadId: candidate.leadId,\n email: String(candidate.email),\n })),\n })\n : new Map<string, OutreachGateLookups>()\n for (const candidate of askable) {\n const open = lookups.get(candidate.personId)?.openEnrollments ?? []\n if (open.some((entry) => entry.sequenceId === sequence.id)) enrolledHere.add(candidate.personId)\n }\n return { candidates, enrolledHere, lookups }\n}\n\nconst uniqueIds = (values: unknown): string[] =>\n Array.isArray(values)\n ? [...new Set(values.map(readOutreachDocumentId).filter((id): id is string => id !== null))]\n : []\n\n/** The most leads a saved Leads view reads — the Leads list's own window. */\nconst LEADS_VIEW_WINDOW = 200\n\n/**\n * The people a saved LEADS view selects (AGL-3234): the sequence's site's\n * most recently seen leads, narrowed by the view's status clause the way\n * the Leads list narrows its window — open leads when the view names no\n * status, one status when it does, everything for `all`. The person key IS\n * the id, so there is no address to resolve — and since AGL-3275 the window is\n * narrowed by `visibleTo` rather than by the collection's parent.\n */\nasync function leadsViewPeople(\n firestore: Firestore,\n orgId: string,\n sequence: OutreachSequence,\n filters: ReturnType<typeof normalizeCrmViewFilters>,\n): Promise<{ people: OutreachPersonRef[]; total: number; truncated: boolean }> {\n const statusClause = filters.find((clause) => clause.field === 'status' && clause.op === 'equals')\n const wanted = String(statusClause?.value ?? 'open')\n /*\n * SCOPED (AGL-3275). The collection is org-wide, so the window is narrowed\n * to what the sequence's site may see before it is ordered — otherwise an\n * agency's sequence would offer another client's people to enroll. The\n * composite index for `visibleTo array-contains-any` + `lastSeenAtMs desc`\n * is in `cloud/firestore.indexes.json`.\n */\n const window = await firestore\n .collection('orgs')\n .doc(orgId)\n .collection('leads')\n .where('visibleTo', 'array-contains-any', scopeTokensForHost(sequence.hostId))\n .orderBy('lastSeenAtMs', 'desc')\n .limit(LEADS_VIEW_WINDOW + 1)\n .get()\n const matching = window.docs.slice(0, LEADS_VIEW_WINDOW).filter((doc) => {\n const lead = doc.data() as Record<string, unknown>\n if (wanted === 'all') return true\n if (wanted === 'open') return isCrmLeadOpen(lead as never) && !lead['convertedContactId']\n return crmLeadStatus(lead as never) === wanted\n })\n return {\n people: matching.slice(0, OUTREACH_ENROLL_BATCH_MAX).map((doc) => ({ kind: 'lead', id: doc.id })),\n total: matching.length,\n truncated: matching.length > OUTREACH_ENROLL_BATCH_MAX || window.docs.length > LEADS_VIEW_WINDOW,\n }\n}\n\nexport function createOutreachEnrollRoutes(deps: OutreachEnrollRouteDeps): OutreachEnrollRoutes {\n /**\n * The person joins the sequence's campaigns (AGL-3254), the moment they\n * are enrolled: `arrayUnion` on the lead's own `campaignIds`, or on the\n * contact's facet for the sequence's site — the same field, the same\n * shape, every other campaign member carries — and `enrolled` credited\n * to each campaign. After the enrollment is created and never before:\n * a person the create refused joins nothing. Neither write may fail the\n * enrollment, which already exists; a membership that could not be\n * stamped is one the list's bulk bar can add by hand.\n */\n async function joinSequenceCampaigns(\n firestore: Firestore,\n orgId: string,\n sequence: OutreachSequence,\n contactGroupId: string,\n candidate: OutreachEnrollCandidate,\n nowMs: number,\n ): Promise<void> {\n const campaignIds = normalizeCampaignIds(sequence.campaignIds)\n if (!campaignIds.length) return\n try {\n const ref =\n candidate.target === 'lead' && candidate.leadId\n ? firestore.collection('orgs').doc(orgId).collection('leads').doc(candidate.leadId)\n : candidate.contactId\n ? firestore.collection('orgs').doc(orgId).collection('contacts').doc(candidate.contactId)\n : null\n if (ref) {\n const field =\n candidate.target === 'lead' ? CAMPAIGN_MEMBERSHIP_FIELD : contactCampaignFieldPath(contactGroupId)\n await ref.update({ [field]: FieldValue.arrayUnion(...campaignIds), updatedAt: FieldValue.serverTimestamp() })\n }\n } catch (error) {\n console.error('[outreach] the enrolled person could not join the sequence’s campaigns', error)\n }\n try {\n await deps.creditCampaign({ hostId: sequence.hostId, campaignIds, outcome: 'enrolled', atMs: nowMs })\n } catch (error) {\n console.error('[outreach] the enrollment could not be credited to the sequence’s campaigns', error)\n }\n }\n\n /**\n * The names of the sequence's campaigns as they stand, for the entry the\n * enroll files on the person's record (AGL-3274). Read once per request,\n * not per person; a container that is gone answers nothing and the entry\n * names the rest.\n */\n async function sequenceCampaignNames(firestore: Firestore, sequence: OutreachSequence): Promise<string[]> {\n const campaignIds = normalizeCampaignIds(sequence.campaignIds)\n if (!campaignIds.length) return []\n try {\n const containers = firestore.collection('hosts').doc(sequence.hostId).collection('emailCampaigns')\n const found = await firestore.getAll(...campaignIds.map((id) => containers.doc(id)))\n return found.map((snapshot) => String(snapshot.get('name') ?? '').trim()).filter(Boolean)\n } catch (error) {\n console.error('[outreach] the sequence’s campaigns could not be named for the record', error)\n return []\n }\n }\n\n /**\n * The people a source names, the batch limit applied: every contact a\n * search picked, every lead picked from the sequence's site, or the\n * people a saved Contacts or Leads view selects there — contacts found by\n * address, leads by their own key.\n */\n async function sourcePeople(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n sequence: OutreachSequence,\n source: Record<string, unknown>,\n ): Promise<Response | { people: OutreachPersonRef[]; total: number; truncated: boolean }> {\n if (source['kind'] === 'contacts' || source['kind'] === 'leads') {\n const kind = source['kind'] === 'leads' ? 'lead' : 'contact'\n const ids = uniqueIds(kind === 'lead' ? source['leadIds'] : source['contactIds'])\n if (!ids.length) return outreachRefusal(400, 'invalid-request', 'Pick the people to enroll.')\n return {\n people: ids.slice(0, OUTREACH_ENROLL_BATCH_MAX).map((id) => ({ kind, id })),\n total: ids.length,\n truncated: ids.length > OUTREACH_ENROLL_BATCH_MAX,\n }\n }\n if (source['kind'] !== 'view') {\n return outreachRefusal(400, 'invalid-request', 'Enroll people from a saved view or a search.')\n }\n const viewId = readOutreachDocumentId(source['viewId'])\n const view = viewId\n ? await firestore.collection('orgs').doc(caller.orgId).collection(CRM_COLLECTIONS.views).doc(viewId).get()\n : null\n const data = view?.exists ? (view.data() as Record<string, unknown>) : null\n // A colleague's private view is theirs: it is not listed for this\n // reader, so it does not exist for them here either.\n if (\n !viewId ||\n !data ||\n !crmViewIsListed({ shared: data['shared'] === true, ownerUid: String(data['ownerUid'] ?? '') }, caller.uid)\n ) {\n return outreachRefusal(404, 'view-not-found', 'That saved view no longer exists.')\n }\n if (data['section'] === 'leads') {\n return leadsViewPeople(firestore, caller.orgId, sequence, normalizeCrmViewFilters(data['filters']))\n }\n if (data['section'] !== 'contacts') {\n return outreachRefusal(400, 'view-unsupported', 'Enroll from a saved view of Contacts or Leads.')\n }\n const { unsupported } = dynamicListDimensionsForCrmView(normalizeCrmViewFilters(data['filters']))\n if (unsupported.length) {\n // Dropping the filter enrolling cannot read would enroll more people\n // than the view shows, so the view is refused whole.\n return outreachRefusal(\n 400,\n 'view-unsupported',\n `This view filters on ${unsupported.map((clause) => clause.label || clause.field).join(', ')}, which enrolling can't apply. Pick another view, or search.`,\n )\n }\n const { emails, complete } = await deps.crmViewEmails({ hostId: sequence.hostId, viewId })\n const ordered = [...new Set(emails)].sort()\n const contacts = firestore.collection('orgs').doc(caller.orgId).collection('contacts')\n const found = await Promise.all(\n ordered\n .slice(0, OUTREACH_ENROLL_BATCH_MAX)\n .map((email) => findContactByEmail(contacts, email, { hostId: sequence.hostId })),\n )\n return {\n people: [...new Set(found.filter((snapshot) => snapshot).map((snapshot) => String(snapshot?.id)))].map(\n (id) => ({ kind: 'contact', id }),\n ),\n total: ordered.length,\n truncated: ordered.length > OUTREACH_ENROLL_BATCH_MAX || !complete,\n }\n }\n\n const preview: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate, [OUTREACH_READS_PEOPLE])\n if (caller instanceof Response) return caller\n const firestore = deps.firestore()\n const sequence = await loadActiveSequence(firestore, caller.orgId, body['sequenceId'])\n if (sequence instanceof Response) return sequence\n const source = body['source'] && typeof body['source'] === 'object' ? (body['source'] as Record<string, unknown>) : {}\n const named = await sourcePeople(firestore, caller, sequence, source)\n if (named instanceof Response) return named\n const context = await enrollContext(firestore, caller, sequence)\n const { candidates, enrolledHere, lookups } = await readPeople(\n firestore,\n caller,\n sequence,\n context,\n named.people,\n )\n const people = candidates.map((candidate) =>\n previewOutreachPerson(\n {\n candidate,\n lookups: lookups.get(candidate.personId) ?? UNCHECKED,\n settings: context.settings,\n contactGroupId: context.contactGroupId,\n },\n { siteName: context.siteName, alreadyInSequence: enrolledHere.has(candidate.personId) },\n ),\n )\n return outreachOk({\n ok: true,\n people,\n total: named.total,\n truncated: named.truncated,\n } satisfies OutreachEnrollPreviewResponse)\n }\n\n const confirm: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate, [OUTREACH_READS_PEOPLE])\n if (caller instanceof Response) return caller\n const firestore = deps.firestore()\n const sequence = await loadActiveSequence(firestore, caller.orgId, body['sequenceId'])\n if (sequence instanceof Response) return sequence\n\n // Each person once, by whichever record they are: a contact by id, or a\n // lead by its key (AGL-3234). A person named both ways is a contact.\n const requested = new Map<\n string,\n { ref: OutreachPersonRef; personalLine: string; attestations: unknown[] }\n >()\n for (const entry of Array.isArray(body['people']) ? body['people'] : []) {\n const person = (entry && typeof entry === 'object' ? entry : {}) as Record<string, unknown>\n const contactId = readOutreachDocumentId(person['contactId'])\n const leadId = readOutreachDocumentId(person['leadId'])\n const ref: OutreachPersonRef | null = contactId\n ? { kind: 'contact', id: contactId }\n : leadId\n ? { kind: 'lead', id: leadId }\n : null\n if (!ref || requested.has(ref.id)) continue\n requested.set(ref.id, {\n ref,\n personalLine: typeof person['personalLine'] === 'string' ? person['personalLine'] : '',\n attestations: Array.isArray(person['attestations']) ? person['attestations'] : [],\n })\n }\n if (!requested.size) return outreachRefusal(400, 'invalid-request', 'Pick the people to enroll.')\n if (requested.size > OUTREACH_ENROLL_BATCH_MAX) {\n return outreachRefusal(\n 400,\n 'invalid-request',\n `Enroll at most ${OUTREACH_ENROLL_BATCH_MAX} people at a time.`,\n )\n }\n\n const mailboxSnapshot = sequence.mailboxId\n ? await outreachOrgCollection(firestore, caller.orgId, 'mailboxes').doc(sequence.mailboxId).get()\n : null\n const mailbox = readStoredOutreachMailbox(\n sequence.mailboxId,\n mailboxSnapshot?.exists ? mailboxSnapshot.data() : undefined,\n )\n if (!mailbox || mailbox.status === 'disconnected') {\n return outreachRefusal(\n 409,\n 'mailbox-unavailable',\n \"This sequence's mailbox is no longer connected. Choose another mailbox for it before enrolling people.\",\n )\n }\n const nowMs = deps.now()\n if (planOutreachFirstDue({ sequence, mailbox, enrolledAtMs: nowMs, random: deps.random }) === null) {\n return outreachRefusal(\n 409,\n 'mailbox-unavailable',\n \"This sequence's sending hours never open in its mailbox's timezone. Fix the hours before enrolling people.\",\n )\n }\n\n const context = await enrollContext(firestore, caller, sequence)\n const { candidates, enrolledHere, lookups } = await readPeople(\n firestore,\n caller,\n sequence,\n context,\n [...requested.values()].map((entry) => entry.ref),\n )\n const enrollments = outreachOrgCollection(firestore, caller.orgId, 'enrollments')\n const campaignNames = await sequenceCampaignNames(firestore, sequence)\n const results: OutreachEnrollOutcome[] = await Promise.all(\n candidates.map(async (candidate): Promise<OutreachEnrollOutcome> => {\n const named = {\n personId: candidate.personId,\n target: candidate.target,\n contactId: candidate.contactId,\n leadId: candidate.leadId,\n }\n const supplied = requested.get(candidate.personId) ?? { personalLine: '', attestations: [] }\n const decision = decideOutreachEnrollment(\n {\n candidate,\n lookups: lookups.get(candidate.personId) ?? UNCHECKED,\n settings: context.settings,\n contactGroupId: context.contactGroupId,\n },\n { siteName: context.siteName, alreadyInSequence: enrolledHere.has(candidate.personId) },\n { personalLine: supplied.personalLine, attestations: supplied.attestations, uid: caller.uid, nowMs },\n )\n if (!decision.allowed || !decision.email) {\n return { ...named, email: candidate.email, outcome: 'blocked', blocks: decision.blocks }\n }\n const id = outreachEnrollmentId(sequence.id, candidate.personId)\n const enrollment = buildOutreachEnrollment({\n id,\n sequence,\n mailbox,\n target: candidate.target,\n contactId: candidate.contactId,\n leadId: candidate.leadId,\n contactName: candidate.name,\n email: decision.email,\n cold: decision.cold,\n personalLine: supplied.personalLine,\n attestations: decision.attestations,\n enrolledByUid: caller.uid,\n nowMs,\n random: deps.random,\n })\n try {\n await enrollments.doc(id).create(enrollment)\n } catch (error) {\n if ((error as { code?: unknown })?.code !== ALREADY_EXISTS) throw error\n return {\n ...named,\n email: candidate.email,\n outcome: 'blocked',\n blocks: [\n {\n code: 'already_in_sequence',\n reason: \"This person has already been in this sequence, so they won't be sent its emails again.\",\n },\n ],\n }\n }\n await joinSequenceCampaigns(firestore, caller.orgId, sequence, context.contactGroupId, candidate, nowMs)\n // The person's record says so (AGL-3274): \"Enrolled in <sequence>\",\n // with the campaigns it carried them into, once per enrollment.\n const entry = outreachEnrolledEntry({ enrollmentId: id, sequenceName: sequence.name, campaignNames })\n await fileOutreachNote(deps, {\n orgId: caller.orgId,\n hostId: sequence.hostId,\n link: outreachEnrollmentLink(enrollment),\n dedupeKey: entry.dedupeKey,\n body: entry.body,\n atMs: nowMs,\n })\n return { ...named, email: decision.email, outcome: 'enrolled', enrollmentId: id }\n }),\n )\n const enrolled = results.filter((result) => result.outcome === 'enrolled').length\n if (enrolled) {\n await deps.logOrgActivity(\n caller.orgId,\n { uid: caller.uid, email: caller.email },\n outreachEnrolledActivity(enrolled),\n { type: OUTREACH_SEQUENCE_ACTIVITY_TARGET, id: sequence.id, name: sequence.name },\n )\n }\n return outreachOk({ ok: true, results, enrolled } satisfies OutreachEnrollResponse)\n }\n\n return { preview, confirm }\n}\n"],"names":["CAMPAIGN_MEMBERSHIP_FIELD","contactCampaignFieldPath","normalizeCampaignIds","consentGroupForHost","scopeTokensForHost","CRM_COLLECTIONS","crmLeadStatus","crmViewIsListed","isCrmLeadOpen","normalizeCrmViewFilters","dynamicListDimensionsForCrmView","findContactByEmail","FieldValue","outreachEnrolledEntry","buildOutreachEnrollment","planOutreachFirstDue","decideOutreachEnrollment","previewOutreachPerson","readOutreachEnrollCandidates","readOutreachGateLookups","effectiveOutreachAllowedCountries","OUTREACH_ENROLL_BATCH_MAX","readOutreachComplianceSettingsDoc","fileOutreachNote","outreachEnrollmentId","outreachEnrollmentLink","outreachOrgCollection","readStoredOutreachMailbox","readStoredOutreachSequence","OUTREACH_SEQUENCE_ACTIVITY_TARGET","outreachRouteGate","outreachMethodNotAllowed","outreachOk","outreachRefusal","readOutreachDocumentId","readOutreachJsonBody","OUTREACH_READS_PEOPLE","key","refusal","outreachEnrolledActivity","count","UNCHECKED","platformSuppressed","hostSuppressed","salesTopicState","doNotContact","doNotContactDomain","workspaceMembers","openEnrollments","hasInboundEmail","ALREADY_EXISTS","loadActiveSequence","firestore","orgId","rawSequenceId","sequenceId","snapshot","doc","get","sequence","exists","data","undefined","status","enrollContext","caller","host","orgSettings","Promise","all","collection","hostId","settings","allowedCountries","contactGroupId","org","groupId","siteName","String","readPeople","context","people","candidates","enrollments","existing","length","getAll","map","candidate","id","personId","enrolledHere","Set","filter","_candidate","index","askable","contact","visible","email","lookups","contactId","leadId","Map","open","some","entry","add","uniqueIds","values","Array","isArray","LEADS_VIEW_WINDOW","leadsViewPeople","filters","statusClause","find","clause","field","op","wanted","value","window","where","orderBy","limit","matching","docs","slice","lead","kind","total","truncated","createOutreachEnrollRoutes","deps","joinSequenceCampaigns","nowMs","campaignIds","ref","target","update","arrayUnion","updatedAt","serverTimestamp","error","console","creditCampaign","outcome","atMs","sequenceCampaignNames","containers","found","trim","Boolean","sourcePeople","source","ids","viewId","view","views","shared","ownerUid","uid","unsupported","label","join","emails","complete","crmViewEmails","ordered","sort","contacts","preview","request","method","body","gate","Response","named","alreadyInSequence","has","ok","confirm","requested","person","set","personalLine","attestations","size","mailboxSnapshot","mailboxId","mailbox","now","enrolledAtMs","random","campaignNames","results","supplied","decision","allowed","blocks","enrollment","contactName","name","cold","enrolledByUid","create","code","reason","enrollmentId","sequenceName","link","dedupeKey","enrolled","result","logOrgActivity","type"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/enroll-routes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n CAMPAIGN_MEMBERSHIP_FIELD,\n contactCampaignFieldPath,\n normalizeCampaignIds,\n} from '@aglyn/aglyn/app-utils/campaign-membership'\nimport { consentGroupForHost } from '@aglyn/aglyn/app-utils/consent-groups'\nimport { scopeTokensForHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport {\n CRM_COLLECTIONS,\n crmLeadStatus,\n crmViewIsListed,\n isCrmLeadOpen,\n normalizeCrmViewFilters,\n} from '@aglyn/aglyn/app-utils/crm'\nimport { dynamicListDimensionsForCrmView } from '@aglyn/aglyn/app-utils/dynamic-list-rule'\nimport type { PluginRecordTimelineWriter } from '@aglyn/aglyn/plugin-manager/plugin-record-timeline'\nimport type { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport { findContactByEmail } from '@aglyn/tenant-data-admin/server/contact-email-index'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { outreachEnrolledEntry } from '../engine/enrollment-activity'\nimport { buildOutreachEnrollment, planOutreachFirstDue } from '../engine/enrollment-state'\nimport type { OutreachGateLookups } from '../engine/gates'\nimport {\n decideOutreachEnrollment,\n previewOutreachPerson,\n readOutreachEnrollCandidates,\n type OutreachEnrollCandidate,\n type OutreachGateQuestion,\n} from '../enrollment/enroll-people'\nimport { readOutreachGateLookups } from '../enrollment/gate-lookups'\nimport { effectiveOutreachAllowedCountries } from '../model/compliance-settings'\nimport {\n OUTREACH_ENROLL_BATCH_MAX,\n type OutreachEnrollOutcome,\n type OutreachEnrollPreviewResponse,\n type OutreachEnrollResponse,\n type OutreachPersonRef,\n} from '../model/outreach-api'\nimport type { OutreachSequence } from '../model/outreach.types'\nimport { readOutreachComplianceSettingsDoc } from '../storage/compliance-settings-store'\nimport { fileOutreachNote } from '../runtime/timeline'\nimport {\n outreachEnrollmentId,\n outreachEnrollmentLink,\n outreachOrgCollection,\n readStoredOutreachMailbox,\n readStoredOutreachSequence,\n} from '../storage/outreach-records'\nimport { OUTREACH_SEQUENCE_ACTIVITY_TARGET, type OutreachRouteDeps } from './route-deps'\nimport { outreachRouteGate, type OutreachRouteCaller, type OutreachRouteExtraPermission } from './route-gate'\nimport {\n outreachMethodNotAllowed,\n outreachOk,\n outreachRefusal,\n readOutreachDocumentId,\n readOutreachJsonBody,\n} from './route-http'\n\n/**\n * ENROLLING PEOPLE IN A SEQUENCE (AGL-2980): `outreach/enroll/preview` and\n * `outreach/enroll`.\n *\n * The preview reads the people a saved Contacts view or a search named —\n * at most {@link OUTREACH_ENROLL_BATCH_MAX} — and says where each stands:\n * eligible, blocked with the gate's own sentence, or waiting on the rep for\n * a personal line and the attestations only they can make. Confirm reads\n * everything again and asks the gates again with what the rep supplied,\n * because a preview is a picture of a moment: someone can unsubscribe\n * between the two, and a request can claim anything. Nothing the preview\n * answered is trusted by the enroll.\n *\n * Both read the CRM's people, so both ask for `data.manage` beside\n * `outreach.use` — the permission the contacts' own rules read with, so\n * Outreach is no side door into the address book.\n *\n * Only an ACTIVE sequence takes people: enrolling schedules the first step\n * in the mailbox's hours, and a draft's steps and a paused sequence's timing\n * are not yet what will be sent.\n */\n\n/** The permission enrolling reads the CRM with. */\nexport const OUTREACH_READS_PEOPLE: OutreachRouteExtraPermission = {\n key: 'data.manage',\n refusal: \"Enrolling reads your CRM's contacts, and your role does not include Manage data.\",\n}\n\n/** Everything enrolling reaches beyond the routes' own dependencies. */\nexport interface OutreachEnrollRouteDeps extends OutreachRouteDeps {\n /**\n * The addresses a saved Contacts view selects among one site's contacts,\n * read the way the dynamic-list sweep reads one, and whether the read\n * reached the whole view before its budget.\n */\n crmViewEmails(input: { hostId: string; viewId: string }): Promise<{ emails: string[]; complete: boolean }>\n /**\n * The workspace's record system on the timeline seam (AGL-3274), when a\n * plugin keeps one: the enroll files \"Enrolled in\" on the person's record\n * — the runtime's `timeline`, reached the same way.\n */\n timeline(): PluginRecordTimelineWriter | null\n}\n\nexport interface OutreachEnrollRoutes {\n preview: PluginWebApiHandler\n confirm: PluginWebApiHandler\n}\n\n/** The activity line an enroll writes. */\nexport function outreachEnrolledActivity(count: number): string {\n return count === 1\n ? 'Enrolled 1 person in a sequence'\n : `Enrolled ${count} people in a sequence`\n}\n\n/** Lookups for a person the gates will never be asked about: every one unchecked. */\nconst UNCHECKED: OutreachGateLookups = {\n platformSuppressed: null,\n hostSuppressed: null,\n salesTopicState: null,\n doNotContact: null,\n doNotContactDomain: null,\n workspaceMembers: null,\n openEnrollments: null,\n hasInboundEmail: null,\n}\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** gRPC `ALREADY_EXISTS`, which `create()` rejects with when the document is there. */\nconst ALREADY_EXISTS = 6\n\nasync function loadActiveSequence(\n firestore: Firestore,\n orgId: string,\n rawSequenceId: unknown,\n): Promise<Response | OutreachSequence> {\n const sequenceId = readOutreachDocumentId(rawSequenceId)\n if (!sequenceId) return outreachRefusal(400, 'invalid-request', 'Name the sequence to enroll people in.')\n const snapshot = await outreachOrgCollection(firestore, orgId, 'sequences').doc(sequenceId).get()\n const sequence = readStoredOutreachSequence(sequenceId, snapshot.exists ? snapshot.data() : undefined)\n if (!sequence) return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n if (sequence.status === 'archived') {\n return outreachRefusal(409, 'sequence-archived', 'This sequence is archived and takes no one new.')\n }\n if (sequence.status !== 'active') {\n return outreachRefusal(409, 'sequence-not-active', 'Activate this sequence before enrolling people.')\n }\n return sequence\n}\n\n/** What every person in one request is judged against. */\ninterface EnrollContext {\n settings: OutreachGateQuestion['settings']\n contactGroupId: string\n siteName: string\n}\n\nasync function enrollContext(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n sequence: OutreachSequence,\n): Promise<EnrollContext> {\n const [orgSettings, host] = await Promise.all([\n readOutreachComplianceSettingsDoc(firestore, caller.orgId),\n firestore.collection('hosts').doc(sequence.hostId).get(),\n ])\n return {\n // The organization's countries are a ceiling over the sequence's.\n settings: {\n ...sequence.settings,\n allowedCountries: effectiveOutreachAllowedCountries(\n sequence.settings.allowedCountries,\n orgSettings.allowedCountries,\n ),\n },\n contactGroupId: consentGroupForHost(caller.org, sequence.hostId).groupId,\n siteName: host.exists ? String(host.get('name') ?? '') : '',\n }\n}\n\n/**\n * The candidates, the enrollments they already have here, and the gates'\n * lookups — every map keyed by the person's own id.\n *\n * \"Already in this sequence\" is answered two ways (AGL-3234): by the id the\n * person's enrollment would have, and by any enrollment in this sequence\n * that carries the person's ADDRESS. The second is what keeps a contact\n * from being enrolled beside the lead they were converted from: the lead's\n * enrollment followed them, under the lead's key, and still names them.\n */\nasync function readPeople(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n sequence: OutreachSequence,\n context: EnrollContext,\n people: readonly OutreachPersonRef[],\n): Promise<{\n candidates: OutreachEnrollCandidate[]\n enrolledHere: Set<string>\n lookups: Map<string, OutreachGateLookups>\n}> {\n const candidates = await readOutreachEnrollCandidates(firestore, {\n orgId: caller.orgId,\n hostId: sequence.hostId,\n contactGroupId: context.contactGroupId,\n people,\n })\n const enrollments = outreachOrgCollection(firestore, caller.orgId, 'enrollments')\n const existing = candidates.length\n ? await firestore.getAll(\n ...candidates.map((candidate) => enrollments.doc(outreachEnrollmentId(sequence.id, candidate.personId))),\n )\n : []\n const enrolledHere = new Set(\n candidates.filter((_candidate, index) => existing[index]?.exists).map((candidate) => candidate.personId),\n )\n const askable = candidates.filter((candidate) => candidate.contact && candidate.visible && candidate.email)\n const lookups = askable.length\n ? await readOutreachGateLookups(firestore, {\n orgId: caller.orgId,\n hostId: sequence.hostId,\n people: askable.map((candidate) => ({\n personId: candidate.personId,\n contactId: candidate.contactId || null,\n leadId: candidate.leadId,\n email: String(candidate.email),\n })),\n })\n : new Map<string, OutreachGateLookups>()\n for (const candidate of askable) {\n const open = lookups.get(candidate.personId)?.openEnrollments ?? []\n if (open.some((entry) => entry.sequenceId === sequence.id)) enrolledHere.add(candidate.personId)\n }\n return { candidates, enrolledHere, lookups }\n}\n\nconst uniqueIds = (values: unknown): string[] =>\n Array.isArray(values)\n ? [...new Set(values.map(readOutreachDocumentId).filter((id): id is string => id !== null))]\n : []\n\n/** The most leads a saved Leads view reads — the Leads list's own window. */\nconst LEADS_VIEW_WINDOW = 200\n\n/**\n * The people a saved LEADS view selects (AGL-3234): the sequence's site's\n * most recently seen leads, narrowed by the view's status clause the way\n * the Leads list narrows its window — open leads when the view names no\n * status, one status when it does, everything for `all`. The person key IS\n * the id, so there is no address to resolve — and since AGL-3275 the window is\n * narrowed by `visibleTo` rather than by the collection's parent.\n */\nasync function leadsViewPeople(\n firestore: Firestore,\n orgId: string,\n sequence: OutreachSequence,\n filters: ReturnType<typeof normalizeCrmViewFilters>,\n): Promise<{ people: OutreachPersonRef[]; total: number; truncated: boolean }> {\n const statusClause = filters.find((clause) => clause.field === 'status' && clause.op === 'equals')\n const wanted = String(statusClause?.value ?? 'open')\n /*\n * SCOPED (AGL-3275). The collection is org-wide, so the window is narrowed\n * to what the sequence's site may see before it is ordered — otherwise an\n * agency's sequence would offer another client's people to enroll. The\n * composite index for `visibleTo array-contains-any` + `lastSeenAtMs desc`\n * is in `cloud/firestore.indexes.json`.\n */\n const window = await firestore\n .collection('orgs')\n .doc(orgId)\n .collection('leads')\n .where('visibleTo', 'array-contains-any', scopeTokensForHost(sequence.hostId))\n .orderBy('lastSeenAtMs', 'desc')\n .limit(LEADS_VIEW_WINDOW + 1)\n .get()\n const matching = window.docs.slice(0, LEADS_VIEW_WINDOW).filter((doc) => {\n const lead = doc.data() as Record<string, unknown>\n if (wanted === 'all') return true\n if (wanted === 'open') return isCrmLeadOpen(lead as never) && !lead['convertedContactId']\n return crmLeadStatus(lead as never) === wanted\n })\n return {\n people: matching.slice(0, OUTREACH_ENROLL_BATCH_MAX).map((doc) => ({ kind: 'lead', id: doc.id })),\n total: matching.length,\n truncated: matching.length > OUTREACH_ENROLL_BATCH_MAX || window.docs.length > LEADS_VIEW_WINDOW,\n }\n}\n\nexport function createOutreachEnrollRoutes(deps: OutreachEnrollRouteDeps): OutreachEnrollRoutes {\n /**\n * The person joins the sequence's campaigns (AGL-3254), the moment they\n * are enrolled: `arrayUnion` on the lead's own `campaignIds`, or on the\n * contact's facet for the sequence's site — the same field, the same\n * shape, every other campaign member carries — and `enrolled` credited\n * to each campaign. After the enrollment is created and never before:\n * a person the create refused joins nothing. Neither write may fail the\n * enrollment, which already exists; a membership that could not be\n * stamped is one the list's bulk bar can add by hand.\n */\n async function joinSequenceCampaigns(\n firestore: Firestore,\n orgId: string,\n sequence: OutreachSequence,\n contactGroupId: string,\n candidate: OutreachEnrollCandidate,\n nowMs: number,\n ): Promise<void> {\n const campaignIds = normalizeCampaignIds(sequence.campaignIds)\n if (!campaignIds.length) return\n try {\n const ref =\n candidate.target === 'lead' && candidate.leadId\n ? firestore.collection('orgs').doc(orgId).collection('leads').doc(candidate.leadId)\n : candidate.contactId\n ? firestore.collection('orgs').doc(orgId).collection('contacts').doc(candidate.contactId)\n : null\n if (ref) {\n const field =\n candidate.target === 'lead' ? CAMPAIGN_MEMBERSHIP_FIELD : contactCampaignFieldPath(contactGroupId)\n await ref.update({ [field]: FieldValue.arrayUnion(...campaignIds), updatedAt: FieldValue.serverTimestamp() })\n }\n } catch (error) {\n console.error('[outreach] the enrolled person could not join the sequence’s campaigns', error)\n }\n try {\n await deps.creditCampaign({ hostId: sequence.hostId, orgId, campaignIds, outcome: 'enrolled', atMs: nowMs })\n } catch (error) {\n console.error('[outreach] the enrollment could not be credited to the sequence’s campaigns', error)\n }\n }\n\n /**\n * The names of the sequence's campaigns as they stand, for the entry the\n * enroll files on the person's record (AGL-3274) — the org's containers,\n * `orgs/{orgId}/emailCampaigns`. Read once per request, not per person; a\n * container that is gone answers nothing and the entry names the rest.\n */\n async function sequenceCampaignNames(\n firestore: Firestore,\n orgId: string,\n sequence: OutreachSequence,\n ): Promise<string[]> {\n const campaignIds = normalizeCampaignIds(sequence.campaignIds)\n if (!campaignIds.length || !orgId) return []\n try {\n const containers = firestore.collection('orgs').doc(orgId).collection('emailCampaigns')\n const found = await firestore.getAll(...campaignIds.map((id) => containers.doc(id)))\n return found.map((snapshot) => String(snapshot.get('name') ?? '').trim()).filter(Boolean)\n } catch (error) {\n console.error('[outreach] the sequence’s campaigns could not be named for the record', error)\n return []\n }\n }\n\n /**\n * The people a source names, the batch limit applied: every contact a\n * search picked, every lead picked from the sequence's site, or the\n * people a saved Contacts or Leads view selects there — contacts found by\n * address, leads by their own key.\n */\n async function sourcePeople(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n sequence: OutreachSequence,\n source: Record<string, unknown>,\n ): Promise<Response | { people: OutreachPersonRef[]; total: number; truncated: boolean }> {\n if (source['kind'] === 'contacts' || source['kind'] === 'leads') {\n const kind = source['kind'] === 'leads' ? 'lead' : 'contact'\n const ids = uniqueIds(kind === 'lead' ? source['leadIds'] : source['contactIds'])\n if (!ids.length) return outreachRefusal(400, 'invalid-request', 'Pick the people to enroll.')\n return {\n people: ids.slice(0, OUTREACH_ENROLL_BATCH_MAX).map((id) => ({ kind, id })),\n total: ids.length,\n truncated: ids.length > OUTREACH_ENROLL_BATCH_MAX,\n }\n }\n if (source['kind'] !== 'view') {\n return outreachRefusal(400, 'invalid-request', 'Enroll people from a saved view or a search.')\n }\n const viewId = readOutreachDocumentId(source['viewId'])\n const view = viewId\n ? await firestore.collection('orgs').doc(caller.orgId).collection(CRM_COLLECTIONS.views).doc(viewId).get()\n : null\n const data = view?.exists ? (view.data() as Record<string, unknown>) : null\n // A colleague's private view is theirs: it is not listed for this\n // reader, so it does not exist for them here either.\n if (\n !viewId ||\n !data ||\n !crmViewIsListed({ shared: data['shared'] === true, ownerUid: String(data['ownerUid'] ?? '') }, caller.uid)\n ) {\n return outreachRefusal(404, 'view-not-found', 'That saved view no longer exists.')\n }\n if (data['section'] === 'leads') {\n return leadsViewPeople(firestore, caller.orgId, sequence, normalizeCrmViewFilters(data['filters']))\n }\n if (data['section'] !== 'contacts') {\n return outreachRefusal(400, 'view-unsupported', 'Enroll from a saved view of Contacts or Leads.')\n }\n const { unsupported } = dynamicListDimensionsForCrmView(normalizeCrmViewFilters(data['filters']))\n if (unsupported.length) {\n // Dropping the filter enrolling cannot read would enroll more people\n // than the view shows, so the view is refused whole.\n return outreachRefusal(\n 400,\n 'view-unsupported',\n `This view filters on ${unsupported.map((clause) => clause.label || clause.field).join(', ')}, which enrolling can't apply. Pick another view, or search.`,\n )\n }\n const { emails, complete } = await deps.crmViewEmails({ hostId: sequence.hostId, viewId })\n const ordered = [...new Set(emails)].sort()\n const contacts = firestore.collection('orgs').doc(caller.orgId).collection('contacts')\n const found = await Promise.all(\n ordered\n .slice(0, OUTREACH_ENROLL_BATCH_MAX)\n .map((email) => findContactByEmail(contacts, email, { hostId: sequence.hostId })),\n )\n return {\n people: [...new Set(found.filter((snapshot) => snapshot).map((snapshot) => String(snapshot?.id)))].map(\n (id) => ({ kind: 'contact', id }),\n ),\n total: ordered.length,\n truncated: ordered.length > OUTREACH_ENROLL_BATCH_MAX || !complete,\n }\n }\n\n const preview: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate, [OUTREACH_READS_PEOPLE])\n if (caller instanceof Response) return caller\n const firestore = deps.firestore()\n const sequence = await loadActiveSequence(firestore, caller.orgId, body['sequenceId'])\n if (sequence instanceof Response) return sequence\n const source = body['source'] && typeof body['source'] === 'object' ? (body['source'] as Record<string, unknown>) : {}\n const named = await sourcePeople(firestore, caller, sequence, source)\n if (named instanceof Response) return named\n const context = await enrollContext(firestore, caller, sequence)\n const { candidates, enrolledHere, lookups } = await readPeople(\n firestore,\n caller,\n sequence,\n context,\n named.people,\n )\n const people = candidates.map((candidate) =>\n previewOutreachPerson(\n {\n candidate,\n lookups: lookups.get(candidate.personId) ?? UNCHECKED,\n settings: context.settings,\n contactGroupId: context.contactGroupId,\n },\n { siteName: context.siteName, alreadyInSequence: enrolledHere.has(candidate.personId) },\n ),\n )\n return outreachOk({\n ok: true,\n people,\n total: named.total,\n truncated: named.truncated,\n } satisfies OutreachEnrollPreviewResponse)\n }\n\n const confirm: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate, [OUTREACH_READS_PEOPLE])\n if (caller instanceof Response) return caller\n const firestore = deps.firestore()\n const sequence = await loadActiveSequence(firestore, caller.orgId, body['sequenceId'])\n if (sequence instanceof Response) return sequence\n\n // Each person once, by whichever record they are: a contact by id, or a\n // lead by its key (AGL-3234). A person named both ways is a contact.\n const requested = new Map<\n string,\n { ref: OutreachPersonRef; personalLine: string; attestations: unknown[] }\n >()\n for (const entry of Array.isArray(body['people']) ? body['people'] : []) {\n const person = (entry && typeof entry === 'object' ? entry : {}) as Record<string, unknown>\n const contactId = readOutreachDocumentId(person['contactId'])\n const leadId = readOutreachDocumentId(person['leadId'])\n const ref: OutreachPersonRef | null = contactId\n ? { kind: 'contact', id: contactId }\n : leadId\n ? { kind: 'lead', id: leadId }\n : null\n if (!ref || requested.has(ref.id)) continue\n requested.set(ref.id, {\n ref,\n personalLine: typeof person['personalLine'] === 'string' ? person['personalLine'] : '',\n attestations: Array.isArray(person['attestations']) ? person['attestations'] : [],\n })\n }\n if (!requested.size) return outreachRefusal(400, 'invalid-request', 'Pick the people to enroll.')\n if (requested.size > OUTREACH_ENROLL_BATCH_MAX) {\n return outreachRefusal(\n 400,\n 'invalid-request',\n `Enroll at most ${OUTREACH_ENROLL_BATCH_MAX} people at a time.`,\n )\n }\n\n const mailboxSnapshot = sequence.mailboxId\n ? await outreachOrgCollection(firestore, caller.orgId, 'mailboxes').doc(sequence.mailboxId).get()\n : null\n const mailbox = readStoredOutreachMailbox(\n sequence.mailboxId,\n mailboxSnapshot?.exists ? mailboxSnapshot.data() : undefined,\n )\n if (!mailbox || mailbox.status === 'disconnected') {\n return outreachRefusal(\n 409,\n 'mailbox-unavailable',\n \"This sequence's mailbox is no longer connected. Choose another mailbox for it before enrolling people.\",\n )\n }\n const nowMs = deps.now()\n if (planOutreachFirstDue({ sequence, mailbox, enrolledAtMs: nowMs, random: deps.random }) === null) {\n return outreachRefusal(\n 409,\n 'mailbox-unavailable',\n \"This sequence's sending hours never open in its mailbox's timezone. Fix the hours before enrolling people.\",\n )\n }\n\n const context = await enrollContext(firestore, caller, sequence)\n const { candidates, enrolledHere, lookups } = await readPeople(\n firestore,\n caller,\n sequence,\n context,\n [...requested.values()].map((entry) => entry.ref),\n )\n const enrollments = outreachOrgCollection(firestore, caller.orgId, 'enrollments')\n const campaignNames = await sequenceCampaignNames(firestore, caller.orgId, sequence)\n const results: OutreachEnrollOutcome[] = await Promise.all(\n candidates.map(async (candidate): Promise<OutreachEnrollOutcome> => {\n const named = {\n personId: candidate.personId,\n target: candidate.target,\n contactId: candidate.contactId,\n leadId: candidate.leadId,\n }\n const supplied = requested.get(candidate.personId) ?? { personalLine: '', attestations: [] }\n const decision = decideOutreachEnrollment(\n {\n candidate,\n lookups: lookups.get(candidate.personId) ?? UNCHECKED,\n settings: context.settings,\n contactGroupId: context.contactGroupId,\n },\n { siteName: context.siteName, alreadyInSequence: enrolledHere.has(candidate.personId) },\n { personalLine: supplied.personalLine, attestations: supplied.attestations, uid: caller.uid, nowMs },\n )\n if (!decision.allowed || !decision.email) {\n return { ...named, email: candidate.email, outcome: 'blocked', blocks: decision.blocks }\n }\n const id = outreachEnrollmentId(sequence.id, candidate.personId)\n const enrollment = buildOutreachEnrollment({\n id,\n sequence,\n mailbox,\n target: candidate.target,\n contactId: candidate.contactId,\n leadId: candidate.leadId,\n contactName: candidate.name,\n email: decision.email,\n cold: decision.cold,\n personalLine: supplied.personalLine,\n attestations: decision.attestations,\n enrolledByUid: caller.uid,\n nowMs,\n random: deps.random,\n })\n try {\n await enrollments.doc(id).create(enrollment)\n } catch (error) {\n if ((error as { code?: unknown })?.code !== ALREADY_EXISTS) throw error\n return {\n ...named,\n email: candidate.email,\n outcome: 'blocked',\n blocks: [\n {\n code: 'already_in_sequence',\n reason: \"This person has already been in this sequence, so they won't be sent its emails again.\",\n },\n ],\n }\n }\n await joinSequenceCampaigns(firestore, caller.orgId, sequence, context.contactGroupId, candidate, nowMs)\n // The person's record says so (AGL-3274): \"Enrolled in <sequence>\",\n // with the campaigns it carried them into, once per enrollment.\n const entry = outreachEnrolledEntry({ enrollmentId: id, sequenceName: sequence.name, campaignNames })\n await fileOutreachNote(deps, {\n orgId: caller.orgId,\n hostId: sequence.hostId,\n link: outreachEnrollmentLink(enrollment),\n dedupeKey: entry.dedupeKey,\n body: entry.body,\n atMs: nowMs,\n })\n return { ...named, email: decision.email, outcome: 'enrolled', enrollmentId: id }\n }),\n )\n const enrolled = results.filter((result) => result.outcome === 'enrolled').length\n if (enrolled) {\n await deps.logOrgActivity(\n caller.orgId,\n { uid: caller.uid, email: caller.email },\n outreachEnrolledActivity(enrolled),\n { type: OUTREACH_SEQUENCE_ACTIVITY_TARGET, id: sequence.id, name: sequence.name },\n )\n }\n return outreachOk({ ok: true, results, enrolled } satisfies OutreachEnrollResponse)\n }\n\n return { preview, confirm }\n}\n"],"names":["CAMPAIGN_MEMBERSHIP_FIELD","contactCampaignFieldPath","normalizeCampaignIds","consentGroupForHost","scopeTokensForHost","CRM_COLLECTIONS","crmLeadStatus","crmViewIsListed","isCrmLeadOpen","normalizeCrmViewFilters","dynamicListDimensionsForCrmView","findContactByEmail","FieldValue","outreachEnrolledEntry","buildOutreachEnrollment","planOutreachFirstDue","decideOutreachEnrollment","previewOutreachPerson","readOutreachEnrollCandidates","readOutreachGateLookups","effectiveOutreachAllowedCountries","OUTREACH_ENROLL_BATCH_MAX","readOutreachComplianceSettingsDoc","fileOutreachNote","outreachEnrollmentId","outreachEnrollmentLink","outreachOrgCollection","readStoredOutreachMailbox","readStoredOutreachSequence","OUTREACH_SEQUENCE_ACTIVITY_TARGET","outreachRouteGate","outreachMethodNotAllowed","outreachOk","outreachRefusal","readOutreachDocumentId","readOutreachJsonBody","OUTREACH_READS_PEOPLE","key","refusal","outreachEnrolledActivity","count","UNCHECKED","platformSuppressed","hostSuppressed","salesTopicState","doNotContact","doNotContactDomain","workspaceMembers","openEnrollments","hasInboundEmail","ALREADY_EXISTS","loadActiveSequence","firestore","orgId","rawSequenceId","sequenceId","snapshot","doc","get","sequence","exists","data","undefined","status","enrollContext","caller","host","orgSettings","Promise","all","collection","hostId","settings","allowedCountries","contactGroupId","org","groupId","siteName","String","readPeople","context","people","candidates","enrollments","existing","length","getAll","map","candidate","id","personId","enrolledHere","Set","filter","_candidate","index","askable","contact","visible","email","lookups","contactId","leadId","Map","open","some","entry","add","uniqueIds","values","Array","isArray","LEADS_VIEW_WINDOW","leadsViewPeople","filters","statusClause","find","clause","field","op","wanted","value","window","where","orderBy","limit","matching","docs","slice","lead","kind","total","truncated","createOutreachEnrollRoutes","deps","joinSequenceCampaigns","nowMs","campaignIds","ref","target","update","arrayUnion","updatedAt","serverTimestamp","error","console","creditCampaign","outcome","atMs","sequenceCampaignNames","containers","found","trim","Boolean","sourcePeople","source","ids","viewId","view","views","shared","ownerUid","uid","unsupported","label","join","emails","complete","crmViewEmails","ordered","sort","contacts","preview","request","method","body","gate","Response","named","alreadyInSequence","has","ok","confirm","requested","person","set","personalLine","attestations","size","mailboxSnapshot","mailboxId","mailbox","now","enrolledAtMs","random","campaignNames","results","supplied","decision","allowed","blocks","enrollment","contactName","name","cold","enrolledByUid","create","code","reason","enrollmentId","sequenceName","link","dedupeKey","enrolled","result","logOrgActivity","type"],"mappings":"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"}
@@ -60,9 +60,10 @@ export function defaultOutreachRouteDeps() {
60
60
  stampRecordEmailState: async (stamp)=>{
61
61
  await (await platform()).stampRecordEmailState(stamp);
62
62
  },
63
- creditCampaign: async ({ hostId, campaignIds, outcome, atMs })=>{
63
+ creditCampaign: async ({ hostId, orgId, campaignIds, outcome, atMs })=>{
64
64
  await (await platform()).creditCampaignSequenceOutcome({
65
65
  hostId,
66
+ orgId,
66
67
  campaignIds,
67
68
  outcome,
68
69
  atMs
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/register-routes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { pluginRecordTimelineWriter } from '@aglyn/aglyn/plugin-manager/plugin-record-timeline'\nimport { registerPluginApiRoute } from '@aglyn/aglyn/server'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { OUTREACH_API_ROUTES } from '../constants/api-routes'\nimport { outreachOrgSubject } from '../mailboxes/register-mailbox-routes'\nimport { createOutreachDoNotContactDomainsRoute } from './do-not-contact-routes'\nimport { createOutreachEnrollRoutes, type OutreachEnrollRouteDeps } from './enroll-routes'\nimport { createOutreachEnrollmentActionRoute } from './enrollment-routes'\nimport { createOutreachPreviewRoute } from './preview-routes'\nimport type { OutreachRouteGateDeps } from './route-gate'\nimport { createOutreachSequenceRoutes } from './sequence-routes'\nimport { createOutreachSettingsRoute } from './settings-routes'\n\n/**\n * Wires the settings, sequence and enrollment routes into the console\n * dispatcher (AGL-2980) with the platform's own dependencies. Specs build\n * their own.\n *\n * Every one names its organization (`orgId`, in the query or the JSON body)\n * and none names a site, so each declares the mailbox routes' subject reader:\n * the dispatcher's release gate then asks about that organization, and a\n * `release_outreach` override for one organization reaches these routes as it\n * reaches Mailboxes. Without it the gate reads the request as anonymous, which\n * only a fully enabled flag or a staff session passes.\n */\n\n/** The platform's heavier modules, loaded the first time a request needs one. */\nconst platform = () => import('./platform-deps')\n\n/** The gate's reach into the platform. */\nexport function defaultOutreachRouteGateDeps(): OutreachRouteGateDeps {\n return {\n verifyIdToken: (idToken) => firebaseAdmin.app().auth().verifyIdToken(idToken),\n resolveOrgPermissions: async (uid, context) => (await platform()).resolveOrgPermissions(uid, context),\n holdsOrgCatalogPermission: async (uid, orgId, key) =>\n (await platform()).holdsOrgCatalogPermission(uid, orgId, key),\n readOrg: async (orgId) => {\n const snapshot = await firebaseAdmin.app().firestore().collection('orgs').doc(orgId).get()\n return snapshot.exists ? (snapshot.data() ?? {}) : null\n },\n lockdownRefusal: async (options) => (await platform()).lockdownRefusal(options),\n }\n}\n\nexport function defaultOutreachRouteDeps(): OutreachEnrollRouteDeps {\n return {\n firestore: () => firebaseAdmin.app().firestore(),\n gate: defaultOutreachRouteGateDeps(),\n now: Date.now,\n random: Math.random,\n logOrgActivity: async (orgId, actor, action, target) =>\n (await platform()).logOrgActivity(orgId, actor, action, target),\n crmViewEmails: async (input) => (await platform()).crmViewEmails(input),\n stampRecordEmailState: async (stamp) => {\n await (await platform()).stampRecordEmailState(stamp)\n },\n creditCampaign: async ({ hostId, campaignIds, outcome, atMs }) => {\n await (await platform()).creditCampaignSequenceOutcome(\n { hostId, campaignIds, outcome, atMs },\n firebaseAdmin.app().firestore(),\n )\n },\n // The record system's writer (AGL-3274), resolved per call the way the\n // runtime resolves it: the CRM registers at boot, and a request that\n // arrives before it did files nothing rather than caching a `null`.\n timeline: () => pluginRecordTimelineWriter()?.writer ?? null,\n }\n}\n\nexport function registerOutreachRoutes(\n deps: OutreachEnrollRouteDeps = defaultOutreachRouteDeps(),\n): void {\n const sequences = createOutreachSequenceRoutes(deps)\n const enroll = createOutreachEnrollRoutes(deps)\n const orgSubject = { subject: outreachOrgSubject }\n registerPluginApiRoute(OUTREACH_API_ROUTES.settings, { web: createOutreachSettingsRoute(deps) }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.sequencesSave, { web: sequences.save }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.sequencesStatus, { web: sequences.status }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.sequencesDelete, { web: sequences.remove }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.enrollPreview, { web: enroll.preview }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.enroll, { web: enroll.confirm }, orgSubject)\n registerPluginApiRoute(\n OUTREACH_API_ROUTES.enrollmentsAction,\n { web: createOutreachEnrollmentActionRoute(deps) },\n orgSubject,\n )\n registerPluginApiRoute(OUTREACH_API_ROUTES.preview, { web: createOutreachPreviewRoute(deps) }, orgSubject)\n registerPluginApiRoute(\n OUTREACH_API_ROUTES.doNotContactDomains,\n { web: createOutreachDoNotContactDomainsRoute(deps) },\n orgSubject,\n )\n}\n"],"names":["pluginRecordTimelineWriter","registerPluginApiRoute","firebaseAdmin","OUTREACH_API_ROUTES","outreachOrgSubject","createOutreachDoNotContactDomainsRoute","createOutreachEnrollRoutes","createOutreachEnrollmentActionRoute","createOutreachPreviewRoute","createOutreachSequenceRoutes","createOutreachSettingsRoute","platform","defaultOutreachRouteGateDeps","verifyIdToken","idToken","app","auth","resolveOrgPermissions","uid","context","holdsOrgCatalogPermission","orgId","key","readOrg","snapshot","firestore","collection","doc","get","exists","data","lockdownRefusal","options","defaultOutreachRouteDeps","gate","now","Date","random","Math","logOrgActivity","actor","action","target","crmViewEmails","input","stampRecordEmailState","stamp","creditCampaign","hostId","campaignIds","outcome","atMs","creditCampaignSequenceOutcome","timeline","writer","registerOutreachRoutes","deps","sequences","enroll","orgSubject","subject","settings","web","sequencesSave","save","sequencesStatus","status","sequencesDelete","remove","enrollPreview","preview","confirm","enrollmentsAction","doNotContactDomains"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/register-routes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { pluginRecordTimelineWriter } from '@aglyn/aglyn/plugin-manager/plugin-record-timeline'\nimport { registerPluginApiRoute } from '@aglyn/aglyn/server'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { OUTREACH_API_ROUTES } from '../constants/api-routes'\nimport { outreachOrgSubject } from '../mailboxes/register-mailbox-routes'\nimport { createOutreachDoNotContactDomainsRoute } from './do-not-contact-routes'\nimport { createOutreachEnrollRoutes, type OutreachEnrollRouteDeps } from './enroll-routes'\nimport { createOutreachEnrollmentActionRoute } from './enrollment-routes'\nimport { createOutreachPreviewRoute } from './preview-routes'\nimport type { OutreachRouteGateDeps } from './route-gate'\nimport { createOutreachSequenceRoutes } from './sequence-routes'\nimport { createOutreachSettingsRoute } from './settings-routes'\n\n/**\n * Wires the settings, sequence and enrollment routes into the console\n * dispatcher (AGL-2980) with the platform's own dependencies. Specs build\n * their own.\n *\n * Every one names its organization (`orgId`, in the query or the JSON body)\n * and none names a site, so each declares the mailbox routes' subject reader:\n * the dispatcher's release gate then asks about that organization, and a\n * `release_outreach` override for one organization reaches these routes as it\n * reaches Mailboxes. Without it the gate reads the request as anonymous, which\n * only a fully enabled flag or a staff session passes.\n */\n\n/** The platform's heavier modules, loaded the first time a request needs one. */\nconst platform = () => import('./platform-deps')\n\n/** The gate's reach into the platform. */\nexport function defaultOutreachRouteGateDeps(): OutreachRouteGateDeps {\n return {\n verifyIdToken: (idToken) => firebaseAdmin.app().auth().verifyIdToken(idToken),\n resolveOrgPermissions: async (uid, context) => (await platform()).resolveOrgPermissions(uid, context),\n holdsOrgCatalogPermission: async (uid, orgId, key) =>\n (await platform()).holdsOrgCatalogPermission(uid, orgId, key),\n readOrg: async (orgId) => {\n const snapshot = await firebaseAdmin.app().firestore().collection('orgs').doc(orgId).get()\n return snapshot.exists ? (snapshot.data() ?? {}) : null\n },\n lockdownRefusal: async (options) => (await platform()).lockdownRefusal(options),\n }\n}\n\nexport function defaultOutreachRouteDeps(): OutreachEnrollRouteDeps {\n return {\n firestore: () => firebaseAdmin.app().firestore(),\n gate: defaultOutreachRouteGateDeps(),\n now: Date.now,\n random: Math.random,\n logOrgActivity: async (orgId, actor, action, target) =>\n (await platform()).logOrgActivity(orgId, actor, action, target),\n crmViewEmails: async (input) => (await platform()).crmViewEmails(input),\n stampRecordEmailState: async (stamp) => {\n await (await platform()).stampRecordEmailState(stamp)\n },\n creditCampaign: async ({ hostId, orgId, campaignIds, outcome, atMs }) => {\n await (await platform()).creditCampaignSequenceOutcome(\n { hostId, orgId, campaignIds, outcome, atMs },\n firebaseAdmin.app().firestore(),\n )\n },\n // The record system's writer (AGL-3274), resolved per call the way the\n // runtime resolves it: the CRM registers at boot, and a request that\n // arrives before it did files nothing rather than caching a `null`.\n timeline: () => pluginRecordTimelineWriter()?.writer ?? null,\n }\n}\n\nexport function registerOutreachRoutes(\n deps: OutreachEnrollRouteDeps = defaultOutreachRouteDeps(),\n): void {\n const sequences = createOutreachSequenceRoutes(deps)\n const enroll = createOutreachEnrollRoutes(deps)\n const orgSubject = { subject: outreachOrgSubject }\n registerPluginApiRoute(OUTREACH_API_ROUTES.settings, { web: createOutreachSettingsRoute(deps) }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.sequencesSave, { web: sequences.save }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.sequencesStatus, { web: sequences.status }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.sequencesDelete, { web: sequences.remove }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.enrollPreview, { web: enroll.preview }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.enroll, { web: enroll.confirm }, orgSubject)\n registerPluginApiRoute(\n OUTREACH_API_ROUTES.enrollmentsAction,\n { web: createOutreachEnrollmentActionRoute(deps) },\n orgSubject,\n )\n registerPluginApiRoute(OUTREACH_API_ROUTES.preview, { web: createOutreachPreviewRoute(deps) }, orgSubject)\n registerPluginApiRoute(\n OUTREACH_API_ROUTES.doNotContactDomains,\n { web: createOutreachDoNotContactDomainsRoute(deps) },\n orgSubject,\n )\n}\n"],"names":["pluginRecordTimelineWriter","registerPluginApiRoute","firebaseAdmin","OUTREACH_API_ROUTES","outreachOrgSubject","createOutreachDoNotContactDomainsRoute","createOutreachEnrollRoutes","createOutreachEnrollmentActionRoute","createOutreachPreviewRoute","createOutreachSequenceRoutes","createOutreachSettingsRoute","platform","defaultOutreachRouteGateDeps","verifyIdToken","idToken","app","auth","resolveOrgPermissions","uid","context","holdsOrgCatalogPermission","orgId","key","readOrg","snapshot","firestore","collection","doc","get","exists","data","lockdownRefusal","options","defaultOutreachRouteDeps","gate","now","Date","random","Math","logOrgActivity","actor","action","target","crmViewEmails","input","stampRecordEmailState","stamp","creditCampaign","hostId","campaignIds","outcome","atMs","creditCampaignSequenceOutcome","timeline","writer","registerOutreachRoutes","deps","sequences","enroll","orgSubject","subject","settings","web","sequencesSave","save","sequencesStatus","status","sequencesDelete","remove","enrollPreview","preview","confirm","enrollmentsAction","doNotContactDomains"],"mappings":"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"}
@@ -145,12 +145,14 @@ draft, options = {}) {
145
145
  issues.push(issue('settings.allowedCountries', 'country_not_in_org', `${outside.join(', ')} ${outside.length === 1 ? "isn't" : "aren't"} among the countries your organization allows. Add ${outside.length === 1 ? 'it' : 'them'} in Sequences compliance settings first.`));
146
146
  }
147
147
  /*
148
- * The campaigns must be the SITE's live containers (AGL-3254): a request
149
- * can claim any id, and a sequence filed under another site's campaign,
150
- * or under one the console soft-deleted, would credit its outcomes to a
151
- * page nobody at this site can open.
152
- */ if (campaignIds.length && draft.hostId && readOutreachDocumentId(draft.hostId)) {
153
- const containers = firestore.collection('hosts').doc(draft.hostId).collection('emailCampaigns');
148
+ * The campaigns must be the ORGANIZATION's live containers
149
+ * (`orgs/{orgId}/emailCampaigns`, AGL-3254): a request can claim any id,
150
+ * and a sequence filed under another org's campaign, or under one the
151
+ * console soft-deleted, would credit its outcomes to a page nobody here
152
+ * can open. A sequence is an org record, so any live campaign of the org
153
+ * qualifies, whichever sites it is placed on.
154
+ */ if (campaignIds.length) {
155
+ const containers = firestore.collection('orgs').doc(caller.orgId).collection('emailCampaigns');
154
156
  const found = await firestore.getAll(...campaignIds.map((campaignId)=>{
155
157
  var _readOutreachDocumentId;
156
158
  return containers.doc((_readOutreachDocumentId = readOutreachDocumentId(campaignId)) != null ? _readOutreachDocumentId : '-');
@@ -160,7 +162,7 @@ draft, options = {}) {
160
162
  return !readOutreachDocumentId(campaignId) || !(snapshot == null ? void 0 : snapshot.exists) || Boolean(snapshot.get('deletedAt'));
161
163
  });
162
164
  if (unknown.length) {
163
- issues.push(issue('campaignIds', 'campaign_unknown', unknown.length === 1 ? "One of the campaigns picked isn't a campaign of this sequence's site any more. Pick it again." : "Some of the campaigns picked aren't campaigns of this sequence's site any more. Pick them again."));
165
+ issues.push(issue('campaignIds', 'campaign_unknown', unknown.length === 1 ? "One of the campaigns picked isn't one of this organization's campaigns any more. Pick it again." : "Some of the campaigns picked aren't this organization's campaigns any more. Pick them again."));
164
166
  }
165
167
  }
166
168
  return issues;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/sequence-routes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { normalizeCampaignIds } from '@aglyn/aglyn/app-utils/campaign-membership'\nimport { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport type { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport { applyOutreachEnrollmentEvent } from '../engine/enrollment-state'\nimport {\n validateOutreachSequence,\n validateOutreachSequenceActivation,\n} from '../engine/sequence-validation'\nimport type {\n OutreachSequenceAction,\n OutreachSequenceSaveResponse,\n OutreachSequenceStatusResponse,\n} from '../model/outreach-api'\nimport { OUTREACH_OPEN_ENROLLMENT_STATUSES } from '../engine/gates'\nimport type { OutreachMailbox, OutreachSequence, OutreachSequenceStatus } from '../model/outreach.types'\nimport {\n outreachEnrolledSequenceIssues,\n outreachMailboxActivationIssue,\n readOutreachSequenceDraft,\n type OutreachSequenceDraft,\n type OutreachSequenceIssue,\n} from '../model/sequence-draft'\nimport { readOutreachComplianceSettingsDoc } from '../storage/compliance-settings-store'\nimport {\n outreachOrgCollection,\n readStoredOutreachEnrollment,\n readStoredOutreachMailbox,\n readStoredOutreachSequence,\n} from '../storage/outreach-records'\nimport { OUTREACH_SEQUENCE_ACTIVITY_TARGET, type OutreachRouteDeps } from './route-deps'\nimport { outreachRouteGate, type OutreachRouteCaller } from './route-gate'\nimport {\n outreachMethodNotAllowed,\n outreachOk,\n outreachRefusal,\n readOutreachDocumentId,\n readOutreachJsonBody,\n} from './route-http'\n\n/**\n * THE SEQUENCE ROUTES (AGL-2980): save, activate, pause, archive, delete.\n *\n * ## Who may change a sequence\n *\n * A sequence sends as the member whose mailbox it names, so it is theirs to\n * change: the member who connected the mailbox, or an organization owner or\n * admin, may save it, activate it and send from it — the same two the\n * mailbox's own routes let manage the mailbox. Anyone with `outreach.use`\n * may pause or archive any sequence, because stopping mail is never the act\n * that needs guarding.\n *\n * ## Saving\n *\n * The draft is read with the editor's reader and judged by the engine's\n * validator, so the save refuses exactly what the editor showed beside a\n * field. On top of that, the site must be one of the organization's, the\n * mailbox one of its connected mailboxes, the countries ones the\n * organization allows, and — once anyone is enrolled — the steps, site and\n * mailbox fixed (`outreachEnrolledSequenceIssues`). A new sequence is a\n * draft; an edit keeps the status it had, and an active sequence stays\n * active only because the edit passed the same validator activation does.\n *\n * ## Activating\n *\n * `validateOutreachSequenceActivation`: everything a save refuses on, and an\n * organization whose footer cannot be written — no legal name, or no postal\n * address. And its mailbox must be sending (`outreachMailboxActivationIssue`):\n * chosen, still connected, not paused, and not waiting to be reconnected —\n * the same words the sequence page shows beside its disabled Activate.\n *\n * ## Archiving\n *\n * Final. Every person still active or paused in the sequence is stopped\n * with `sequence_archived`, in batches, so no archived sequence goes on\n * sending.\n */\n\n/** A batch holds 500 writes; a page of enrollments stops this many. */\nconst ARCHIVE_PAGE = 400\n\n/** The activity line each lifecycle act writes. */\nexport const OUTREACH_SEQUENCE_ACTIVITY = {\n create: 'Created a sequence',\n activate: 'Activated a sequence',\n pause: 'Paused a sequence',\n archive: 'Archived a sequence',\n delete: 'Deleted a sequence',\n} as const\n\n/** Where each action may start, and where it ends. */\nconst TRANSITIONS: Record<\n OutreachSequenceAction,\n { from: readonly OutreachSequenceStatus[]; to: OutreachSequenceStatus }\n> = {\n activate: { from: ['draft', 'paused'], to: 'active' },\n pause: { from: ['active'], to: 'paused' },\n archive: { from: ['draft', 'active', 'paused'], to: 'archived' },\n}\n\nconst ACTION_REFUSALS: Record<OutreachSequenceAction, string> = {\n activate: 'Only a draft or paused sequence can be activated.',\n pause: 'Only an active sequence can be paused.',\n archive: 'This sequence is already archived.',\n}\n\nexport interface OutreachSequenceRoutes {\n save: PluginWebApiHandler\n status: PluginWebApiHandler\n remove: PluginWebApiHandler\n}\n\ntype Firestore = FirebaseFirestore.Firestore\n\nasync function loadSequence(\n firestore: Firestore,\n orgId: string,\n sequenceId: string,\n): Promise<OutreachSequence | null> {\n const snapshot = await outreachOrgCollection(firestore, orgId, 'sequences').doc(sequenceId).get()\n return readStoredOutreachSequence(sequenceId, snapshot.exists ? snapshot.data() : undefined)\n}\n\n/** Whether anyone was ever enrolled in the sequence. */\nasync function hasEnrollments(firestore: Firestore, orgId: string, sequenceId: string): Promise<boolean> {\n const found = await outreachOrgCollection(firestore, orgId, 'enrollments')\n .where('sequenceId', '==', sequenceId)\n .limit(1)\n .get()\n return !found.empty\n}\n\nconst issue = (\n path: string,\n code: OutreachSequenceIssue['code'],\n message: string,\n): OutreachSequenceIssue => ({ path, code, message, severity: 'error' })\n\n/**\n * The organization-level facts a draft has to agree with: its site is the\n * organization's, its mailbox is connected and the caller may send from it,\n * and its countries are ones the organization allows. `activating` adds\n * that the mailbox is sending now — chosen, and neither paused nor waiting\n * to be reconnected.\n */\nasync function draftPlacementIssues(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n // A draft, or a stored sequence being activated, which may predate the\n // campaign field (AGL-3254).\n draft: Pick<OutreachSequenceDraft, 'hostId' | 'mailboxId' | 'settings'> & Pick<OutreachSequence, 'campaignIds'>,\n options: { activating?: boolean } = {},\n): Promise<OutreachSequenceIssue[]> {\n const issues: OutreachSequenceIssue[] = []\n const campaignIds = normalizeCampaignIds(draft.campaignIds)\n if (draft.hostId) {\n const host = readOutreachDocumentId(draft.hostId)\n ? await firestore.collection('hosts').doc(draft.hostId).get()\n : null\n if (!host?.exists || host.get('orgId') !== caller.orgId) {\n issues.push(issue('hostId', 'host_unknown', \"Choose one of this organization's sites.\"))\n }\n } else {\n issues.push(issue('hostId', 'host_unknown', \"Choose the site whose contacts this sequence emails.\"))\n }\n const mailbox = draft.mailboxId ? await loadMailbox(firestore, caller.orgId, draft.mailboxId) : null\n const activation = options.activating ? outreachMailboxActivationIssue(draft.mailboxId, mailbox) : null\n if (draft.mailboxId) {\n const placement = mailboxIssues(caller, mailbox)\n // A mailbox that is gone is refused once, in activation's words when\n // activating: they say what to do next.\n issues.push(\n ...(activation?.code === 'mailbox_unknown'\n ? placement.filter((entry) => entry.code !== 'mailbox_unknown')\n : placement),\n )\n }\n if (activation && !issues.some((entry) => entry.code === 'mailbox_not_yours')) issues.push(activation)\n const settings = await readOutreachComplianceSettingsDoc(firestore, caller.orgId)\n const outside = draft.settings.allowedCountries.filter(\n (code) => !settings.allowedCountries.includes(code),\n )\n if (outside.length) {\n issues.push(\n issue(\n 'settings.allowedCountries',\n 'country_not_in_org',\n `${outside.join(', ')} ${outside.length === 1 ? \"isn't\" : \"aren't\"} among the countries your organization allows. Add ${outside.length === 1 ? 'it' : 'them'} in Sequences compliance settings first.`,\n ),\n )\n }\n /*\n * The campaigns must be the SITE's live containers (AGL-3254): a request\n * can claim any id, and a sequence filed under another site's campaign,\n * or under one the console soft-deleted, would credit its outcomes to a\n * page nobody at this site can open.\n */\n if (campaignIds.length && draft.hostId && readOutreachDocumentId(draft.hostId)) {\n const containers = firestore.collection('hosts').doc(draft.hostId).collection('emailCampaigns')\n const found = await firestore.getAll(\n ...campaignIds.map((campaignId) => containers.doc(readOutreachDocumentId(campaignId) ?? '-')),\n )\n const unknown = campaignIds.filter((campaignId, index) => {\n const snapshot = found[index]\n return !readOutreachDocumentId(campaignId) || !snapshot?.exists || Boolean(snapshot.get('deletedAt'))\n })\n if (unknown.length) {\n issues.push(\n issue(\n 'campaignIds',\n 'campaign_unknown',\n unknown.length === 1\n ? \"One of the campaigns picked isn't a campaign of this sequence's site any more. Pick it again.\"\n : \"Some of the campaigns picked aren't campaigns of this sequence's site any more. Pick them again.\",\n ),\n )\n }\n }\n return issues\n}\n\n/** A mailbox of this organization's, as stored; `null` when there is none by that id. */\nasync function loadMailbox(firestore: Firestore, orgId: string, mailboxId: string): Promise<OutreachMailbox | null> {\n const snapshot = readOutreachDocumentId(mailboxId)\n ? await outreachOrgCollection(firestore, orgId, 'mailboxes').doc(mailboxId).get()\n : null\n return readStoredOutreachMailbox(mailboxId, snapshot?.exists ? snapshot.data() : undefined)\n}\n\n/** Whether the mailbox is connected here, and the caller may send from it. */\nfunction mailboxIssues(caller: OutreachRouteCaller, mailbox: OutreachMailbox | null): OutreachSequenceIssue[] {\n if (!mailbox || mailbox.status === 'disconnected') {\n return [issue('mailboxId', 'mailbox_unknown', 'That mailbox is not connected to this organization.')]\n }\n if (mailbox.connectedByUid !== caller.uid && !caller.isOrgAdmin) {\n return [\n issue(\n 'mailboxId',\n 'mailbox_not_yours',\n 'Only the member who connected this mailbox, or an organization owner or admin, can send from it.',\n ),\n ]\n }\n return []\n}\n\n/** One issue per field and code: activation and the engine can both ask for a mailbox. */\nfunction distinctIssues(issues: readonly OutreachSequenceIssue[]): OutreachSequenceIssue[] {\n const seen = new Set<string>()\n return issues.filter((entry) => {\n const key = `${entry.path}|${entry.code}`\n if (seen.has(key)) return false\n seen.add(key)\n return true\n })\n}\n\nexport function createOutreachSequenceRoutes(deps: OutreachRouteDeps): OutreachSequenceRoutes {\n const activity = (caller: OutreachRouteCaller, action: string, sequence: Pick<OutreachSequence, 'id' | 'name'>) =>\n deps.logOrgActivity(caller.orgId, { uid: caller.uid, email: caller.email }, action, {\n type: OUTREACH_SEQUENCE_ACTIVITY_TARGET,\n id: sequence.id,\n name: sequence.name,\n })\n\n const save: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate)\n if (caller instanceof Response) return caller\n const firestore = deps.firestore()\n\n const rawId = body['sequenceId']\n const sequenceId = rawId === undefined || rawId === null || rawId === '' ? null : readOutreachDocumentId(rawId)\n if (rawId && !sequenceId) return outreachRefusal(400, 'invalid-request', 'Name the sequence to save.')\n const existing = sequenceId ? await loadSequence(firestore, caller.orgId, sequenceId) : null\n if (sequenceId && !existing) {\n return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n }\n if (existing?.status === 'archived') {\n return outreachRefusal(409, 'sequence-archived', \"An archived sequence can't be edited.\")\n }\n\n const draft = readOutreachSequenceDraft(body['sequence'])\n // A sequence sends as its mailbox's member, so it is theirs (or an\n // admin's) to change — the stored mailbox as well as a new one: moving\n // a colleague's sequence onto your own mailbox is still changing theirs.\n if (existing?.mailboxId && existing.mailboxId !== draft.mailboxId) {\n const stored = mailboxIssues(caller, await loadMailbox(firestore, caller.orgId, existing.mailboxId))\n const refused = stored.find((entry) => entry.code === 'mailbox_not_yours')\n if (refused) return outreachRefusal(403, 'permission', refused.message)\n }\n const judged: OutreachSequenceIssue[] = [\n ...validateOutreachSequence(draft),\n ...(await draftPlacementIssues(firestore, caller, draft)),\n ]\n const ownership = judged.find((entry) => entry.code === 'mailbox_not_yours')\n if (ownership) return outreachRefusal(403, 'permission', ownership.message)\n if (existing && (await hasEnrollments(firestore, caller.orgId, existing.id))) {\n judged.push(...outreachEnrolledSequenceIssues(existing, draft))\n }\n const errors = judged.filter((entry) => entry.severity === 'error')\n if (errors.length) {\n return outreachRefusal(400, 'invalid-sequence', errors[0].message, { issues: judged })\n }\n\n const nowMs = deps.now()\n const ref = existing\n ? outreachOrgCollection(firestore, caller.orgId, 'sequences').doc(existing.id)\n : outreachOrgCollection(firestore, caller.orgId, 'sequences').doc(createResourceUid())\n const sequence: OutreachSequence = {\n id: ref.id,\n ...draft,\n status: existing?.status ?? 'draft',\n /*\n * Carried through explicitly, because the write below REPLACES the\n * document (AGL-3239). The counters are the runtime's and the click\n * route's, never the editor's, and a member renaming a sequence must\n * not reset what it measured.\n */\n ...(existing?.stats ? { stats: existing.stats } : {}),\n createdAtMs: existing?.createdAtMs || nowMs,\n updatedAtMs: nowMs,\n }\n await ref.set(sequence)\n if (!existing) await activity(caller, OUTREACH_SEQUENCE_ACTIVITY.create, sequence)\n return outreachOk({\n ok: true,\n sequence,\n created: !existing,\n warnings: judged.filter((entry) => entry.severity === 'warning'),\n } satisfies OutreachSequenceSaveResponse)\n }\n\n const status: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate)\n if (caller instanceof Response) return caller\n const action = body['action'] as OutreachSequenceAction\n const sequenceId = readOutreachDocumentId(body['sequenceId'])\n if (!sequenceId || !Object.prototype.hasOwnProperty.call(TRANSITIONS, action)) {\n return outreachRefusal(400, 'invalid-request', 'Name the sequence, and activate, pause or archive.')\n }\n const firestore = deps.firestore()\n const sequence = await loadSequence(firestore, caller.orgId, sequenceId)\n if (!sequence) return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n const transition = TRANSITIONS[action]\n if (sequence.status === transition.to) {\n return outreachOk({ ok: true, sequence, stoppedEnrollments: 0 } satisfies OutreachSequenceStatusResponse)\n }\n if (!transition.from.includes(sequence.status)) {\n return outreachRefusal(409, 'transition-refused', ACTION_REFUSALS[action])\n }\n\n if (action === 'activate') {\n const orgSettings = await readOutreachComplianceSettingsDoc(firestore, caller.orgId)\n const judged: OutreachSequenceIssue[] = distinctIssues([\n ...validateOutreachSequenceActivation(sequence, orgSettings),\n ...(await draftPlacementIssues(firestore, caller, sequence, { activating: true })),\n ]).filter((entry) => entry.severity === 'error')\n if (judged.length) {\n const ownership = judged.find((entry) => entry.code === 'mailbox_not_yours')\n return ownership\n ? outreachRefusal(403, 'permission', ownership.message)\n : outreachRefusal(409, 'activation-refused', judged[0].message, { issues: judged })\n }\n }\n\n const nowMs = deps.now()\n const next: OutreachSequence = { ...sequence, status: transition.to, updatedAtMs: nowMs }\n await outreachOrgCollection(firestore, caller.orgId, 'sequences')\n .doc(sequence.id)\n .update({ status: next.status, updatedAtMs: nowMs })\n const stoppedEnrollments =\n action === 'archive' ? await stopOpenEnrollments(firestore, caller, sequence.id, nowMs) : 0\n await activity(caller, OUTREACH_SEQUENCE_ACTIVITY[action], sequence)\n return outreachOk({ ok: true, sequence: next, stoppedEnrollments } satisfies OutreachSequenceStatusResponse)\n }\n\n const remove: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate)\n if (caller instanceof Response) return caller\n const sequenceId = readOutreachDocumentId(body['sequenceId'])\n if (!sequenceId) return outreachRefusal(400, 'invalid-request', 'Name the sequence to delete.')\n const firestore = deps.firestore()\n const sequence = await loadSequence(firestore, caller.orgId, sequenceId)\n if (!sequence) return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n if (sequence.status !== 'draft' || (await hasEnrollments(firestore, caller.orgId, sequence.id))) {\n // What people were sent, and why they stopped, is the record of what\n // this organization did; only a draft nobody was enrolled in has none.\n return outreachRefusal(\n 409,\n 'delete-refused',\n 'Only a draft nobody was enrolled in can be deleted. Archive this sequence instead.',\n )\n }\n await outreachOrgCollection(firestore, caller.orgId, 'sequences').doc(sequence.id).delete()\n await activity(caller, OUTREACH_SEQUENCE_ACTIVITY.delete, sequence)\n return outreachOk({ ok: true })\n }\n\n return { save, status, remove }\n}\n\n/**\n * Stops everyone still active or paused in an archived sequence, a page at\n * a time, through the engine's own transition. Returns how many it stopped.\n */\nasync function stopOpenEnrollments(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n sequenceId: string,\n nowMs: number,\n): Promise<number> {\n const enrollments = outreachOrgCollection(firestore, caller.orgId, 'enrollments')\n let stopped = 0\n let after: FirebaseFirestore.QueryDocumentSnapshot | null = null\n for (;;) {\n let page = enrollments.where('sequenceId', '==', sequenceId).orderBy('__name__').limit(ARCHIVE_PAGE)\n if (after) page = page.startAfter(after)\n const snapshot = await page.get()\n if (snapshot.empty) break\n const batch = firestore.batch()\n let writes = 0\n for (const doc of snapshot.docs) {\n const enrollment = readStoredOutreachEnrollment(doc.id, doc.data())\n if (!enrollment || !OUTREACH_OPEN_ENROLLMENT_STATUSES.includes(enrollment.status)) continue\n const { patch } = applyOutreachEnrollmentEvent(enrollment, {\n type: 'stop',\n reason: 'sequence_archived',\n atMs: nowMs,\n byUid: caller.uid,\n })\n if (!patch) continue\n batch.update(doc.ref, { ...patch, updatedAtMs: nowMs })\n writes += 1\n }\n if (writes) await batch.commit()\n stopped += writes\n after = snapshot.docs[snapshot.docs.length - 1]\n if (snapshot.size < ARCHIVE_PAGE) break\n }\n return stopped\n}\n"],"names":["normalizeCampaignIds","createResourceUid","applyOutreachEnrollmentEvent","validateOutreachSequence","validateOutreachSequenceActivation","OUTREACH_OPEN_ENROLLMENT_STATUSES","outreachEnrolledSequenceIssues","outreachMailboxActivationIssue","readOutreachSequenceDraft","readOutreachComplianceSettingsDoc","outreachOrgCollection","readStoredOutreachEnrollment","readStoredOutreachMailbox","readStoredOutreachSequence","OUTREACH_SEQUENCE_ACTIVITY_TARGET","outreachRouteGate","outreachMethodNotAllowed","outreachOk","outreachRefusal","readOutreachDocumentId","readOutreachJsonBody","ARCHIVE_PAGE","OUTREACH_SEQUENCE_ACTIVITY","create","activate","pause","archive","delete","TRANSITIONS","from","to","ACTION_REFUSALS","loadSequence","firestore","orgId","sequenceId","snapshot","doc","get","exists","data","undefined","hasEnrollments","found","where","limit","empty","issue","path","code","message","severity","draftPlacementIssues","caller","draft","options","issues","campaignIds","hostId","host","collection","push","mailbox","mailboxId","loadMailbox","activation","activating","placement","mailboxIssues","filter","entry","some","settings","outside","allowedCountries","includes","length","join","containers","getAll","map","campaignId","unknown","index","Boolean","status","connectedByUid","uid","isOrgAdmin","distinctIssues","seen","Set","key","has","add","createOutreachSequenceRoutes","deps","activity","action","sequence","logOrgActivity","email","type","id","name","save","request","method","body","gate","Response","rawId","existing","stored","refused","find","judged","ownership","errors","nowMs","now","ref","stats","createdAtMs","updatedAtMs","set","ok","created","warnings","Object","prototype","hasOwnProperty","call","transition","stoppedEnrollments","orgSettings","next","update","stopOpenEnrollments","remove","enrollments","stopped","after","page","orderBy","startAfter","batch","writes","docs","enrollment","patch","reason","atMs","byUid","commit","size"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/sequence-routes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { normalizeCampaignIds } from '@aglyn/aglyn/app-utils/campaign-membership'\nimport { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport type { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport { applyOutreachEnrollmentEvent } from '../engine/enrollment-state'\nimport {\n validateOutreachSequence,\n validateOutreachSequenceActivation,\n} from '../engine/sequence-validation'\nimport type {\n OutreachSequenceAction,\n OutreachSequenceSaveResponse,\n OutreachSequenceStatusResponse,\n} from '../model/outreach-api'\nimport { OUTREACH_OPEN_ENROLLMENT_STATUSES } from '../engine/gates'\nimport type { OutreachMailbox, OutreachSequence, OutreachSequenceStatus } from '../model/outreach.types'\nimport {\n outreachEnrolledSequenceIssues,\n outreachMailboxActivationIssue,\n readOutreachSequenceDraft,\n type OutreachSequenceDraft,\n type OutreachSequenceIssue,\n} from '../model/sequence-draft'\nimport { readOutreachComplianceSettingsDoc } from '../storage/compliance-settings-store'\nimport {\n outreachOrgCollection,\n readStoredOutreachEnrollment,\n readStoredOutreachMailbox,\n readStoredOutreachSequence,\n} from '../storage/outreach-records'\nimport { OUTREACH_SEQUENCE_ACTIVITY_TARGET, type OutreachRouteDeps } from './route-deps'\nimport { outreachRouteGate, type OutreachRouteCaller } from './route-gate'\nimport {\n outreachMethodNotAllowed,\n outreachOk,\n outreachRefusal,\n readOutreachDocumentId,\n readOutreachJsonBody,\n} from './route-http'\n\n/**\n * THE SEQUENCE ROUTES (AGL-2980): save, activate, pause, archive, delete.\n *\n * ## Who may change a sequence\n *\n * A sequence sends as the member whose mailbox it names, so it is theirs to\n * change: the member who connected the mailbox, or an organization owner or\n * admin, may save it, activate it and send from it — the same two the\n * mailbox's own routes let manage the mailbox. Anyone with `outreach.use`\n * may pause or archive any sequence, because stopping mail is never the act\n * that needs guarding.\n *\n * ## Saving\n *\n * The draft is read with the editor's reader and judged by the engine's\n * validator, so the save refuses exactly what the editor showed beside a\n * field. On top of that, the site must be one of the organization's, the\n * mailbox one of its connected mailboxes, the countries ones the\n * organization allows, and — once anyone is enrolled — the steps, site and\n * mailbox fixed (`outreachEnrolledSequenceIssues`). A new sequence is a\n * draft; an edit keeps the status it had, and an active sequence stays\n * active only because the edit passed the same validator activation does.\n *\n * ## Activating\n *\n * `validateOutreachSequenceActivation`: everything a save refuses on, and an\n * organization whose footer cannot be written — no legal name, or no postal\n * address. And its mailbox must be sending (`outreachMailboxActivationIssue`):\n * chosen, still connected, not paused, and not waiting to be reconnected —\n * the same words the sequence page shows beside its disabled Activate.\n *\n * ## Archiving\n *\n * Final. Every person still active or paused in the sequence is stopped\n * with `sequence_archived`, in batches, so no archived sequence goes on\n * sending.\n */\n\n/** A batch holds 500 writes; a page of enrollments stops this many. */\nconst ARCHIVE_PAGE = 400\n\n/** The activity line each lifecycle act writes. */\nexport const OUTREACH_SEQUENCE_ACTIVITY = {\n create: 'Created a sequence',\n activate: 'Activated a sequence',\n pause: 'Paused a sequence',\n archive: 'Archived a sequence',\n delete: 'Deleted a sequence',\n} as const\n\n/** Where each action may start, and where it ends. */\nconst TRANSITIONS: Record<\n OutreachSequenceAction,\n { from: readonly OutreachSequenceStatus[]; to: OutreachSequenceStatus }\n> = {\n activate: { from: ['draft', 'paused'], to: 'active' },\n pause: { from: ['active'], to: 'paused' },\n archive: { from: ['draft', 'active', 'paused'], to: 'archived' },\n}\n\nconst ACTION_REFUSALS: Record<OutreachSequenceAction, string> = {\n activate: 'Only a draft or paused sequence can be activated.',\n pause: 'Only an active sequence can be paused.',\n archive: 'This sequence is already archived.',\n}\n\nexport interface OutreachSequenceRoutes {\n save: PluginWebApiHandler\n status: PluginWebApiHandler\n remove: PluginWebApiHandler\n}\n\ntype Firestore = FirebaseFirestore.Firestore\n\nasync function loadSequence(\n firestore: Firestore,\n orgId: string,\n sequenceId: string,\n): Promise<OutreachSequence | null> {\n const snapshot = await outreachOrgCollection(firestore, orgId, 'sequences').doc(sequenceId).get()\n return readStoredOutreachSequence(sequenceId, snapshot.exists ? snapshot.data() : undefined)\n}\n\n/** Whether anyone was ever enrolled in the sequence. */\nasync function hasEnrollments(firestore: Firestore, orgId: string, sequenceId: string): Promise<boolean> {\n const found = await outreachOrgCollection(firestore, orgId, 'enrollments')\n .where('sequenceId', '==', sequenceId)\n .limit(1)\n .get()\n return !found.empty\n}\n\nconst issue = (\n path: string,\n code: OutreachSequenceIssue['code'],\n message: string,\n): OutreachSequenceIssue => ({ path, code, message, severity: 'error' })\n\n/**\n * The organization-level facts a draft has to agree with: its site is the\n * organization's, its mailbox is connected and the caller may send from it,\n * and its countries are ones the organization allows. `activating` adds\n * that the mailbox is sending now — chosen, and neither paused nor waiting\n * to be reconnected.\n */\nasync function draftPlacementIssues(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n // A draft, or a stored sequence being activated, which may predate the\n // campaign field (AGL-3254).\n draft: Pick<OutreachSequenceDraft, 'hostId' | 'mailboxId' | 'settings'> & Pick<OutreachSequence, 'campaignIds'>,\n options: { activating?: boolean } = {},\n): Promise<OutreachSequenceIssue[]> {\n const issues: OutreachSequenceIssue[] = []\n const campaignIds = normalizeCampaignIds(draft.campaignIds)\n if (draft.hostId) {\n const host = readOutreachDocumentId(draft.hostId)\n ? await firestore.collection('hosts').doc(draft.hostId).get()\n : null\n if (!host?.exists || host.get('orgId') !== caller.orgId) {\n issues.push(issue('hostId', 'host_unknown', \"Choose one of this organization's sites.\"))\n }\n } else {\n issues.push(issue('hostId', 'host_unknown', \"Choose the site whose contacts this sequence emails.\"))\n }\n const mailbox = draft.mailboxId ? await loadMailbox(firestore, caller.orgId, draft.mailboxId) : null\n const activation = options.activating ? outreachMailboxActivationIssue(draft.mailboxId, mailbox) : null\n if (draft.mailboxId) {\n const placement = mailboxIssues(caller, mailbox)\n // A mailbox that is gone is refused once, in activation's words when\n // activating: they say what to do next.\n issues.push(\n ...(activation?.code === 'mailbox_unknown'\n ? placement.filter((entry) => entry.code !== 'mailbox_unknown')\n : placement),\n )\n }\n if (activation && !issues.some((entry) => entry.code === 'mailbox_not_yours')) issues.push(activation)\n const settings = await readOutreachComplianceSettingsDoc(firestore, caller.orgId)\n const outside = draft.settings.allowedCountries.filter(\n (code) => !settings.allowedCountries.includes(code),\n )\n if (outside.length) {\n issues.push(\n issue(\n 'settings.allowedCountries',\n 'country_not_in_org',\n `${outside.join(', ')} ${outside.length === 1 ? \"isn't\" : \"aren't\"} among the countries your organization allows. Add ${outside.length === 1 ? 'it' : 'them'} in Sequences compliance settings first.`,\n ),\n )\n }\n /*\n * The campaigns must be the ORGANIZATION's live containers\n * (`orgs/{orgId}/emailCampaigns`, AGL-3254): a request can claim any id,\n * and a sequence filed under another org's campaign, or under one the\n * console soft-deleted, would credit its outcomes to a page nobody here\n * can open. A sequence is an org record, so any live campaign of the org\n * qualifies, whichever sites it is placed on.\n */\n if (campaignIds.length) {\n const containers = firestore.collection('orgs').doc(caller.orgId).collection('emailCampaigns')\n const found = await firestore.getAll(\n ...campaignIds.map((campaignId) => containers.doc(readOutreachDocumentId(campaignId) ?? '-')),\n )\n const unknown = campaignIds.filter((campaignId, index) => {\n const snapshot = found[index]\n return !readOutreachDocumentId(campaignId) || !snapshot?.exists || Boolean(snapshot.get('deletedAt'))\n })\n if (unknown.length) {\n issues.push(\n issue(\n 'campaignIds',\n 'campaign_unknown',\n unknown.length === 1\n ? \"One of the campaigns picked isn't one of this organization's campaigns any more. Pick it again.\"\n : \"Some of the campaigns picked aren't this organization's campaigns any more. Pick them again.\",\n ),\n )\n }\n }\n return issues\n}\n\n/** A mailbox of this organization's, as stored; `null` when there is none by that id. */\nasync function loadMailbox(firestore: Firestore, orgId: string, mailboxId: string): Promise<OutreachMailbox | null> {\n const snapshot = readOutreachDocumentId(mailboxId)\n ? await outreachOrgCollection(firestore, orgId, 'mailboxes').doc(mailboxId).get()\n : null\n return readStoredOutreachMailbox(mailboxId, snapshot?.exists ? snapshot.data() : undefined)\n}\n\n/** Whether the mailbox is connected here, and the caller may send from it. */\nfunction mailboxIssues(caller: OutreachRouteCaller, mailbox: OutreachMailbox | null): OutreachSequenceIssue[] {\n if (!mailbox || mailbox.status === 'disconnected') {\n return [issue('mailboxId', 'mailbox_unknown', 'That mailbox is not connected to this organization.')]\n }\n if (mailbox.connectedByUid !== caller.uid && !caller.isOrgAdmin) {\n return [\n issue(\n 'mailboxId',\n 'mailbox_not_yours',\n 'Only the member who connected this mailbox, or an organization owner or admin, can send from it.',\n ),\n ]\n }\n return []\n}\n\n/** One issue per field and code: activation and the engine can both ask for a mailbox. */\nfunction distinctIssues(issues: readonly OutreachSequenceIssue[]): OutreachSequenceIssue[] {\n const seen = new Set<string>()\n return issues.filter((entry) => {\n const key = `${entry.path}|${entry.code}`\n if (seen.has(key)) return false\n seen.add(key)\n return true\n })\n}\n\nexport function createOutreachSequenceRoutes(deps: OutreachRouteDeps): OutreachSequenceRoutes {\n const activity = (caller: OutreachRouteCaller, action: string, sequence: Pick<OutreachSequence, 'id' | 'name'>) =>\n deps.logOrgActivity(caller.orgId, { uid: caller.uid, email: caller.email }, action, {\n type: OUTREACH_SEQUENCE_ACTIVITY_TARGET,\n id: sequence.id,\n name: sequence.name,\n })\n\n const save: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate)\n if (caller instanceof Response) return caller\n const firestore = deps.firestore()\n\n const rawId = body['sequenceId']\n const sequenceId = rawId === undefined || rawId === null || rawId === '' ? null : readOutreachDocumentId(rawId)\n if (rawId && !sequenceId) return outreachRefusal(400, 'invalid-request', 'Name the sequence to save.')\n const existing = sequenceId ? await loadSequence(firestore, caller.orgId, sequenceId) : null\n if (sequenceId && !existing) {\n return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n }\n if (existing?.status === 'archived') {\n return outreachRefusal(409, 'sequence-archived', \"An archived sequence can't be edited.\")\n }\n\n const draft = readOutreachSequenceDraft(body['sequence'])\n // A sequence sends as its mailbox's member, so it is theirs (or an\n // admin's) to change — the stored mailbox as well as a new one: moving\n // a colleague's sequence onto your own mailbox is still changing theirs.\n if (existing?.mailboxId && existing.mailboxId !== draft.mailboxId) {\n const stored = mailboxIssues(caller, await loadMailbox(firestore, caller.orgId, existing.mailboxId))\n const refused = stored.find((entry) => entry.code === 'mailbox_not_yours')\n if (refused) return outreachRefusal(403, 'permission', refused.message)\n }\n const judged: OutreachSequenceIssue[] = [\n ...validateOutreachSequence(draft),\n ...(await draftPlacementIssues(firestore, caller, draft)),\n ]\n const ownership = judged.find((entry) => entry.code === 'mailbox_not_yours')\n if (ownership) return outreachRefusal(403, 'permission', ownership.message)\n if (existing && (await hasEnrollments(firestore, caller.orgId, existing.id))) {\n judged.push(...outreachEnrolledSequenceIssues(existing, draft))\n }\n const errors = judged.filter((entry) => entry.severity === 'error')\n if (errors.length) {\n return outreachRefusal(400, 'invalid-sequence', errors[0].message, { issues: judged })\n }\n\n const nowMs = deps.now()\n const ref = existing\n ? outreachOrgCollection(firestore, caller.orgId, 'sequences').doc(existing.id)\n : outreachOrgCollection(firestore, caller.orgId, 'sequences').doc(createResourceUid())\n const sequence: OutreachSequence = {\n id: ref.id,\n ...draft,\n status: existing?.status ?? 'draft',\n /*\n * Carried through explicitly, because the write below REPLACES the\n * document (AGL-3239). The counters are the runtime's and the click\n * route's, never the editor's, and a member renaming a sequence must\n * not reset what it measured.\n */\n ...(existing?.stats ? { stats: existing.stats } : {}),\n createdAtMs: existing?.createdAtMs || nowMs,\n updatedAtMs: nowMs,\n }\n await ref.set(sequence)\n if (!existing) await activity(caller, OUTREACH_SEQUENCE_ACTIVITY.create, sequence)\n return outreachOk({\n ok: true,\n sequence,\n created: !existing,\n warnings: judged.filter((entry) => entry.severity === 'warning'),\n } satisfies OutreachSequenceSaveResponse)\n }\n\n const status: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate)\n if (caller instanceof Response) return caller\n const action = body['action'] as OutreachSequenceAction\n const sequenceId = readOutreachDocumentId(body['sequenceId'])\n if (!sequenceId || !Object.prototype.hasOwnProperty.call(TRANSITIONS, action)) {\n return outreachRefusal(400, 'invalid-request', 'Name the sequence, and activate, pause or archive.')\n }\n const firestore = deps.firestore()\n const sequence = await loadSequence(firestore, caller.orgId, sequenceId)\n if (!sequence) return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n const transition = TRANSITIONS[action]\n if (sequence.status === transition.to) {\n return outreachOk({ ok: true, sequence, stoppedEnrollments: 0 } satisfies OutreachSequenceStatusResponse)\n }\n if (!transition.from.includes(sequence.status)) {\n return outreachRefusal(409, 'transition-refused', ACTION_REFUSALS[action])\n }\n\n if (action === 'activate') {\n const orgSettings = await readOutreachComplianceSettingsDoc(firestore, caller.orgId)\n const judged: OutreachSequenceIssue[] = distinctIssues([\n ...validateOutreachSequenceActivation(sequence, orgSettings),\n ...(await draftPlacementIssues(firestore, caller, sequence, { activating: true })),\n ]).filter((entry) => entry.severity === 'error')\n if (judged.length) {\n const ownership = judged.find((entry) => entry.code === 'mailbox_not_yours')\n return ownership\n ? outreachRefusal(403, 'permission', ownership.message)\n : outreachRefusal(409, 'activation-refused', judged[0].message, { issues: judged })\n }\n }\n\n const nowMs = deps.now()\n const next: OutreachSequence = { ...sequence, status: transition.to, updatedAtMs: nowMs }\n await outreachOrgCollection(firestore, caller.orgId, 'sequences')\n .doc(sequence.id)\n .update({ status: next.status, updatedAtMs: nowMs })\n const stoppedEnrollments =\n action === 'archive' ? await stopOpenEnrollments(firestore, caller, sequence.id, nowMs) : 0\n await activity(caller, OUTREACH_SEQUENCE_ACTIVITY[action], sequence)\n return outreachOk({ ok: true, sequence: next, stoppedEnrollments } satisfies OutreachSequenceStatusResponse)\n }\n\n const remove: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate)\n if (caller instanceof Response) return caller\n const sequenceId = readOutreachDocumentId(body['sequenceId'])\n if (!sequenceId) return outreachRefusal(400, 'invalid-request', 'Name the sequence to delete.')\n const firestore = deps.firestore()\n const sequence = await loadSequence(firestore, caller.orgId, sequenceId)\n if (!sequence) return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n if (sequence.status !== 'draft' || (await hasEnrollments(firestore, caller.orgId, sequence.id))) {\n // What people were sent, and why they stopped, is the record of what\n // this organization did; only a draft nobody was enrolled in has none.\n return outreachRefusal(\n 409,\n 'delete-refused',\n 'Only a draft nobody was enrolled in can be deleted. Archive this sequence instead.',\n )\n }\n await outreachOrgCollection(firestore, caller.orgId, 'sequences').doc(sequence.id).delete()\n await activity(caller, OUTREACH_SEQUENCE_ACTIVITY.delete, sequence)\n return outreachOk({ ok: true })\n }\n\n return { save, status, remove }\n}\n\n/**\n * Stops everyone still active or paused in an archived sequence, a page at\n * a time, through the engine's own transition. Returns how many it stopped.\n */\nasync function stopOpenEnrollments(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n sequenceId: string,\n nowMs: number,\n): Promise<number> {\n const enrollments = outreachOrgCollection(firestore, caller.orgId, 'enrollments')\n let stopped = 0\n let after: FirebaseFirestore.QueryDocumentSnapshot | null = null\n for (;;) {\n let page = enrollments.where('sequenceId', '==', sequenceId).orderBy('__name__').limit(ARCHIVE_PAGE)\n if (after) page = page.startAfter(after)\n const snapshot = await page.get()\n if (snapshot.empty) break\n const batch = firestore.batch()\n let writes = 0\n for (const doc of snapshot.docs) {\n const enrollment = readStoredOutreachEnrollment(doc.id, doc.data())\n if (!enrollment || !OUTREACH_OPEN_ENROLLMENT_STATUSES.includes(enrollment.status)) continue\n const { patch } = applyOutreachEnrollmentEvent(enrollment, {\n type: 'stop',\n reason: 'sequence_archived',\n atMs: nowMs,\n byUid: caller.uid,\n })\n if (!patch) continue\n batch.update(doc.ref, { ...patch, updatedAtMs: nowMs })\n writes += 1\n }\n if (writes) await batch.commit()\n stopped += writes\n after = snapshot.docs[snapshot.docs.length - 1]\n if (snapshot.size < ARCHIVE_PAGE) break\n }\n return stopped\n}\n"],"names":["normalizeCampaignIds","createResourceUid","applyOutreachEnrollmentEvent","validateOutreachSequence","validateOutreachSequenceActivation","OUTREACH_OPEN_ENROLLMENT_STATUSES","outreachEnrolledSequenceIssues","outreachMailboxActivationIssue","readOutreachSequenceDraft","readOutreachComplianceSettingsDoc","outreachOrgCollection","readStoredOutreachEnrollment","readStoredOutreachMailbox","readStoredOutreachSequence","OUTREACH_SEQUENCE_ACTIVITY_TARGET","outreachRouteGate","outreachMethodNotAllowed","outreachOk","outreachRefusal","readOutreachDocumentId","readOutreachJsonBody","ARCHIVE_PAGE","OUTREACH_SEQUENCE_ACTIVITY","create","activate","pause","archive","delete","TRANSITIONS","from","to","ACTION_REFUSALS","loadSequence","firestore","orgId","sequenceId","snapshot","doc","get","exists","data","undefined","hasEnrollments","found","where","limit","empty","issue","path","code","message","severity","draftPlacementIssues","caller","draft","options","issues","campaignIds","hostId","host","collection","push","mailbox","mailboxId","loadMailbox","activation","activating","placement","mailboxIssues","filter","entry","some","settings","outside","allowedCountries","includes","length","join","containers","getAll","map","campaignId","unknown","index","Boolean","status","connectedByUid","uid","isOrgAdmin","distinctIssues","seen","Set","key","has","add","createOutreachSequenceRoutes","deps","activity","action","sequence","logOrgActivity","email","type","id","name","save","request","method","body","gate","Response","rawId","existing","stored","refused","find","judged","ownership","errors","nowMs","now","ref","stats","createdAtMs","updatedAtMs","set","ok","created","warnings","Object","prototype","hasOwnProperty","call","transition","stoppedEnrollments","orgSettings","next","update","stopOpenEnrollments","remove","enrollments","stopped","after","page","orderBy","startAfter","batch","writes","docs","enrollment","patch","reason","atMs","byUid","commit","size"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,oBAAoB,QAAQ,6CAA4C;AACjF,SAASC,iBAAiB,QAAQ,6CAA4C;AAE9E,SAASC,4BAA4B,QAAQ,gCAA4B;AACzE,SACEC,wBAAwB,EACxBC,kCAAkC,QAC7B,mCAA+B;AAMtC,SAASC,iCAAiC,QAAQ,qBAAiB;AAEnE,SACEC,8BAA8B,EAC9BC,8BAA8B,EAC9BC,yBAAyB,QAGpB,6BAAyB;AAChC,SAASC,iCAAiC,QAAQ,0CAAsC;AACxF,SACEC,qBAAqB,EACrBC,4BAA4B,EAC5BC,yBAAyB,EACzBC,0BAA0B,QACrB,iCAA6B;AACpC,SAASC,iCAAiC,QAAgC,kBAAc;AACxF,SAASC,iBAAiB,QAAkC,kBAAc;AAC1E,SACEC,wBAAwB,EACxBC,UAAU,EACVC,eAAe,EACfC,sBAAsB,EACtBC,oBAAoB,QACf,kBAAc;AAErB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoCC,GAED,qEAAqE,GACrE,MAAMC,eAAe;AAErB,iDAAiD,GACjD,OAAO,MAAMC,6BAA6B;IACxCC,QAAQ;IACRC,UAAU;IACVC,OAAO;IACPC,SAAS;IACTC,QAAQ;AACV,EAAU;AAEV,oDAAoD,GACpD,MAAMC,cAGF;IACFJ,UAAU;QAAEK,MAAM;YAAC;YAAS;SAAS;QAAEC,IAAI;IAAS;IACpDL,OAAO;QAAEI,MAAM;YAAC;SAAS;QAAEC,IAAI;IAAS;IACxCJ,SAAS;QAAEG,MAAM;YAAC;YAAS;YAAU;SAAS;QAAEC,IAAI;IAAW;AACjE;AAEA,MAAMC,kBAA0D;IAC9DP,UAAU;IACVC,OAAO;IACPC,SAAS;AACX;AAUA,eAAeM,aACbC,SAAoB,EACpBC,KAAa,EACbC,UAAkB;IAElB,MAAMC,WAAW,MAAM1B,sBAAsBuB,WAAWC,OAAO,aAAaG,GAAG,CAACF,YAAYG,GAAG;IAC/F,OAAOzB,2BAA2BsB,YAAYC,SAASG,MAAM,GAAGH,SAASI,IAAI,KAAKC;AACpF;AAEA,sDAAsD,GACtD,eAAeC,eAAeT,SAAoB,EAAEC,KAAa,EAAEC,UAAkB;IACnF,MAAMQ,QAAQ,MAAMjC,sBAAsBuB,WAAWC,OAAO,eACzDU,KAAK,CAAC,cAAc,MAAMT,YAC1BU,KAAK,CAAC,GACNP,GAAG;IACN,OAAO,CAACK,MAAMG,KAAK;AACrB;AAEA,MAAMC,QAAQ,CACZC,MACAC,MACAC,UAC2B,CAAA;QAAEF;QAAMC;QAAMC;QAASC,UAAU;IAAQ,CAAA;AAEtE;;;;;;CAMC,GACD,eAAeC,qBACbnB,SAAoB,EACpBoB,MAA2B,EAC3B,uEAAuE;AACvE,6BAA6B;AAC7BC,KAA+G,EAC/GC,UAAoC,CAAC,CAAC;IAEtC,MAAMC,SAAkC,EAAE;IAC1C,MAAMC,cAAczD,qBAAqBsD,MAAMG,WAAW;IAC1D,IAAIH,MAAMI,MAAM,EAAE;QAChB,MAAMC,OAAOxC,uBAAuBmC,MAAMI,MAAM,IAC5C,MAAMzB,UAAU2B,UAAU,CAAC,SAASvB,GAAG,CAACiB,MAAMI,MAAM,EAAEpB,GAAG,KACzD;QACJ,IAAI,EAACqB,wBAAAA,KAAMpB,MAAM,KAAIoB,KAAKrB,GAAG,CAAC,aAAae,OAAOnB,KAAK,EAAE;YACvDsB,OAAOK,IAAI,CAACd,MAAM,UAAU,gBAAgB;QAC9C;IACF,OAAO;QACLS,OAAOK,IAAI,CAACd,MAAM,UAAU,gBAAgB;IAC9C;IACA,MAAMe,UAAUR,MAAMS,SAAS,GAAG,MAAMC,YAAY/B,WAAWoB,OAAOnB,KAAK,EAAEoB,MAAMS,SAAS,IAAI;IAChG,MAAME,aAAaV,QAAQW,UAAU,GAAG3D,+BAA+B+C,MAAMS,SAAS,EAAED,WAAW;IACnG,IAAIR,MAAMS,SAAS,EAAE;QACnB,MAAMI,YAAYC,cAAcf,QAAQS;QACxC,qEAAqE;QACrE,wCAAwC;QACxCN,OAAOK,IAAI,IACLI,CAAAA,8BAAAA,WAAYhB,IAAI,MAAK,oBACrBkB,UAAUE,MAAM,CAAC,CAACC,QAAUA,MAAMrB,IAAI,KAAK,qBAC3CkB;IAER;IACA,IAAIF,cAAc,CAACT,OAAOe,IAAI,CAAC,CAACD,QAAUA,MAAMrB,IAAI,KAAK,sBAAsBO,OAAOK,IAAI,CAACI;IAC3F,MAAMO,WAAW,MAAM/D,kCAAkCwB,WAAWoB,OAAOnB,KAAK;IAChF,MAAMuC,UAAUnB,MAAMkB,QAAQ,CAACE,gBAAgB,CAACL,MAAM,CACpD,CAACpB,OAAS,CAACuB,SAASE,gBAAgB,CAACC,QAAQ,CAAC1B;IAEhD,IAAIwB,QAAQG,MAAM,EAAE;QAClBpB,OAAOK,IAAI,CACTd,MACE,6BACA,sBACA,GAAG0B,QAAQI,IAAI,CAAC,MAAM,CAAC,EAAEJ,QAAQG,MAAM,KAAK,IAAI,UAAU,SAAS,mDAAmD,EAAEH,QAAQG,MAAM,KAAK,IAAI,OAAO,OAAO,wCAAwC,CAAC;IAG5M;IACA;;;;;;;GAOC,GACD,IAAInB,YAAYmB,MAAM,EAAE;QACtB,MAAME,aAAa7C,UAAU2B,UAAU,CAAC,QAAQvB,GAAG,CAACgB,OAAOnB,KAAK,EAAE0B,UAAU,CAAC;QAC7E,MAAMjB,QAAQ,MAAMV,UAAU8C,MAAM,IAC/BtB,YAAYuB,GAAG,CAAC,CAACC;gBAA8B9D;mBAAf2D,WAAWzC,GAAG,EAAClB,0BAAAA,uBAAuB8D,uBAAvB9D,0BAAsC;;QAE1F,MAAM+D,UAAUzB,YAAYY,MAAM,CAAC,CAACY,YAAYE;YAC9C,MAAM/C,WAAWO,KAAK,CAACwC,MAAM;YAC7B,OAAO,CAAChE,uBAAuB8D,eAAe,EAAC7C,4BAAAA,SAAUG,MAAM,KAAI6C,QAAQhD,SAASE,GAAG,CAAC;QAC1F;QACA,IAAI4C,QAAQN,MAAM,EAAE;YAClBpB,OAAOK,IAAI,CACTd,MACE,eACA,oBACAmC,QAAQN,MAAM,KAAK,IACf,oGACA;QAGV;IACF;IACA,OAAOpB;AACT;AAEA,uFAAuF,GACvF,eAAeQ,YAAY/B,SAAoB,EAAEC,KAAa,EAAE6B,SAAiB;IAC/E,MAAM3B,WAAWjB,uBAAuB4C,aACpC,MAAMrD,sBAAsBuB,WAAWC,OAAO,aAAaG,GAAG,CAAC0B,WAAWzB,GAAG,KAC7E;IACJ,OAAO1B,0BAA0BmD,WAAW3B,CAAAA,4BAAAA,SAAUG,MAAM,IAAGH,SAASI,IAAI,KAAKC;AACnF;AAEA,4EAA4E,GAC5E,SAAS2B,cAAcf,MAA2B,EAAES,OAA+B;IACjF,IAAI,CAACA,WAAWA,QAAQuB,MAAM,KAAK,gBAAgB;QACjD,OAAO;YAACtC,MAAM,aAAa,mBAAmB;SAAuD;IACvG;IACA,IAAIe,QAAQwB,cAAc,KAAKjC,OAAOkC,GAAG,IAAI,CAAClC,OAAOmC,UAAU,EAAE;QAC/D,OAAO;YACLzC,MACE,aACA,qBACA;SAEH;IACH;IACA,OAAO,EAAE;AACX;AAEA,wFAAwF,GACxF,SAAS0C,eAAejC,MAAwC;IAC9D,MAAMkC,OAAO,IAAIC;IACjB,OAAOnC,OAAOa,MAAM,CAAC,CAACC;QACpB,MAAMsB,MAAM,GAAGtB,MAAMtB,IAAI,CAAC,CAAC,EAAEsB,MAAMrB,IAAI,EAAE;QACzC,IAAIyC,KAAKG,GAAG,CAACD,MAAM,OAAO;QAC1BF,KAAKI,GAAG,CAACF;QACT,OAAO;IACT;AACF;AAEA,OAAO,SAASG,6BAA6BC,IAAuB;IAClE,MAAMC,WAAW,CAAC5C,QAA6B6C,QAAgBC,WAC7DH,KAAKI,cAAc,CAAC/C,OAAOnB,KAAK,EAAE;YAAEqD,KAAKlC,OAAOkC,GAAG;YAAEc,OAAOhD,OAAOgD,KAAK;QAAC,GAAGH,QAAQ;YAClFI,MAAMxF;YACNyF,IAAIJ,SAASI,EAAE;YACfC,MAAML,SAASK,IAAI;QACrB;IAEF,MAAMC,OAA4B,OAAOC;;QACvC,IAAIA,QAAQC,MAAM,KAAK,QAAQ,OAAO3F,yBAAyB;QAC/D,MAAM4F,OAAO,MAAMxF,qBAAqBsF;QACxC,MAAMrD,SAAS,MAAMtC,kBAAkB2F,SAASE,IAAI,CAAC,QAAQ,EAAEZ,KAAKa,IAAI;QACxE,IAAIxD,kBAAkByD,UAAU,OAAOzD;QACvC,MAAMpB,YAAY+D,KAAK/D,SAAS;QAEhC,MAAM8E,QAAQH,IAAI,CAAC,aAAa;QAChC,MAAMzE,aAAa4E,UAAUtE,aAAasE,UAAU,QAAQA,UAAU,KAAK,OAAO5F,uBAAuB4F;QACzG,IAAIA,SAAS,CAAC5E,YAAY,OAAOjB,gBAAgB,KAAK,mBAAmB;QACzE,MAAM8F,WAAW7E,aAAa,MAAMH,aAAaC,WAAWoB,OAAOnB,KAAK,EAAEC,cAAc;QACxF,IAAIA,cAAc,CAAC6E,UAAU;YAC3B,OAAO9F,gBAAgB,KAAK,sBAAsB;QACpD;QACA,IAAI8F,CAAAA,4BAAAA,SAAU3B,MAAM,MAAK,YAAY;YACnC,OAAOnE,gBAAgB,KAAK,qBAAqB;QACnD;QAEA,MAAMoC,QAAQ9C,0BAA0BoG,IAAI,CAAC,WAAW;QACxD,mEAAmE;QACnE,uEAAuE;QACvE,yEAAyE;QACzE,IAAII,CAAAA,4BAAAA,SAAUjD,SAAS,KAAIiD,SAASjD,SAAS,KAAKT,MAAMS,SAAS,EAAE;YACjE,MAAMkD,SAAS7C,cAAcf,QAAQ,MAAMW,YAAY/B,WAAWoB,OAAOnB,KAAK,EAAE8E,SAASjD,SAAS;YAClG,MAAMmD,UAAUD,OAAOE,IAAI,CAAC,CAAC7C,QAAUA,MAAMrB,IAAI,KAAK;YACtD,IAAIiE,SAAS,OAAOhG,gBAAgB,KAAK,cAAcgG,QAAQhE,OAAO;QACxE;QACA,MAAMkE,SAAkC;eACnCjH,yBAAyBmD;eACxB,MAAMF,qBAAqBnB,WAAWoB,QAAQC;SACnD;QACD,MAAM+D,YAAYD,OAAOD,IAAI,CAAC,CAAC7C,QAAUA,MAAMrB,IAAI,KAAK;QACxD,IAAIoE,WAAW,OAAOnG,gBAAgB,KAAK,cAAcmG,UAAUnE,OAAO;QAC1E,IAAI8D,YAAa,MAAMtE,eAAeT,WAAWoB,OAAOnB,KAAK,EAAE8E,SAAST,EAAE,GAAI;YAC5Ea,OAAOvD,IAAI,IAAIvD,+BAA+B0G,UAAU1D;QAC1D;QACA,MAAMgE,SAASF,OAAO/C,MAAM,CAAC,CAACC,QAAUA,MAAMnB,QAAQ,KAAK;QAC3D,IAAImE,OAAO1C,MAAM,EAAE;YACjB,OAAO1D,gBAAgB,KAAK,oBAAoBoG,MAAM,CAAC,EAAE,CAACpE,OAAO,EAAE;gBAAEM,QAAQ4D;YAAO;QACtF;QAEA,MAAMG,QAAQvB,KAAKwB,GAAG;QACtB,MAAMC,MAAMT,WACRtG,sBAAsBuB,WAAWoB,OAAOnB,KAAK,EAAE,aAAaG,GAAG,CAAC2E,SAAST,EAAE,IAC3E7F,sBAAsBuB,WAAWoB,OAAOnB,KAAK,EAAE,aAAaG,GAAG,CAACpC;QACpE,MAAMkG,WAA6B;YACjCI,IAAIkB,IAAIlB,EAAE;WACPjD;YACH+B,MAAM,UAAE2B,4BAAAA,SAAU3B,MAAM,mBAAI;WAOxB2B,CAAAA,4BAAAA,SAAUU,KAAK,IAAG;YAAEA,OAAOV,SAASU,KAAK;QAAC,IAAI,CAAC;YACnDC,aAAaX,CAAAA,4BAAAA,SAAUW,WAAW,KAAIJ;YACtCK,aAAaL;;QAEf,MAAME,IAAII,GAAG,CAAC1B;QACd,IAAI,CAACa,UAAU,MAAMf,SAAS5C,QAAQ/B,2BAA2BC,MAAM,EAAE4E;QACzE,OAAOlF,WAAW;YAChB6G,IAAI;YACJ3B;YACA4B,SAAS,CAACf;YACVgB,UAAUZ,OAAO/C,MAAM,CAAC,CAACC,QAAUA,MAAMnB,QAAQ,KAAK;QACxD;IACF;IAEA,MAAMkC,SAA8B,OAAOqB;QACzC,IAAIA,QAAQC,MAAM,KAAK,QAAQ,OAAO3F,yBAAyB;QAC/D,MAAM4F,OAAO,MAAMxF,qBAAqBsF;QACxC,MAAMrD,SAAS,MAAMtC,kBAAkB2F,SAASE,IAAI,CAAC,QAAQ,EAAEZ,KAAKa,IAAI;QACxE,IAAIxD,kBAAkByD,UAAU,OAAOzD;QACvC,MAAM6C,SAASU,IAAI,CAAC,SAAS;QAC7B,MAAMzE,aAAahB,uBAAuByF,IAAI,CAAC,aAAa;QAC5D,IAAI,CAACzE,cAAc,CAAC8F,OAAOC,SAAS,CAACC,cAAc,CAACC,IAAI,CAACxG,aAAasE,SAAS;YAC7E,OAAOhF,gBAAgB,KAAK,mBAAmB;QACjD;QACA,MAAMe,YAAY+D,KAAK/D,SAAS;QAChC,MAAMkE,WAAW,MAAMnE,aAAaC,WAAWoB,OAAOnB,KAAK,EAAEC;QAC7D,IAAI,CAACgE,UAAU,OAAOjF,gBAAgB,KAAK,sBAAsB;QACjE,MAAMmH,aAAazG,WAAW,CAACsE,OAAO;QACtC,IAAIC,SAASd,MAAM,KAAKgD,WAAWvG,EAAE,EAAE;YACrC,OAAOb,WAAW;gBAAE6G,IAAI;gBAAM3B;gBAAUmC,oBAAoB;YAAE;QAChE;QACA,IAAI,CAACD,WAAWxG,IAAI,CAAC8C,QAAQ,CAACwB,SAASd,MAAM,GAAG;YAC9C,OAAOnE,gBAAgB,KAAK,sBAAsBa,eAAe,CAACmE,OAAO;QAC3E;QAEA,IAAIA,WAAW,YAAY;YACzB,MAAMqC,cAAc,MAAM9H,kCAAkCwB,WAAWoB,OAAOnB,KAAK;YACnF,MAAMkF,SAAkC3B,eAAe;mBAClDrF,mCAAmC+F,UAAUoC;mBAC5C,MAAMnF,qBAAqBnB,WAAWoB,QAAQ8C,UAAU;oBAAEjC,YAAY;gBAAK;aAChF,EAAEG,MAAM,CAAC,CAACC,QAAUA,MAAMnB,QAAQ,KAAK;YACxC,IAAIiE,OAAOxC,MAAM,EAAE;gBACjB,MAAMyC,YAAYD,OAAOD,IAAI,CAAC,CAAC7C,QAAUA,MAAMrB,IAAI,KAAK;gBACxD,OAAOoE,YACHnG,gBAAgB,KAAK,cAAcmG,UAAUnE,OAAO,IACpDhC,gBAAgB,KAAK,sBAAsBkG,MAAM,CAAC,EAAE,CAAClE,OAAO,EAAE;oBAAEM,QAAQ4D;gBAAO;YACrF;QACF;QAEA,MAAMG,QAAQvB,KAAKwB,GAAG;QACtB,MAAMgB,OAAyB,aAAKrC;YAAUd,QAAQgD,WAAWvG,EAAE;YAAE8F,aAAaL;;QAClF,MAAM7G,sBAAsBuB,WAAWoB,OAAOnB,KAAK,EAAE,aAClDG,GAAG,CAAC8D,SAASI,EAAE,EACfkC,MAAM,CAAC;YAAEpD,QAAQmD,KAAKnD,MAAM;YAAEuC,aAAaL;QAAM;QACpD,MAAMe,qBACJpC,WAAW,YAAY,MAAMwC,oBAAoBzG,WAAWoB,QAAQ8C,SAASI,EAAE,EAAEgB,SAAS;QAC5F,MAAMtB,SAAS5C,QAAQ/B,0BAA0B,CAAC4E,OAAO,EAAEC;QAC3D,OAAOlF,WAAW;YAAE6G,IAAI;YAAM3B,UAAUqC;YAAMF;QAAmB;IACnE;IAEA,MAAMK,SAA8B,OAAOjC;QACzC,IAAIA,QAAQC,MAAM,KAAK,QAAQ,OAAO3F,yBAAyB;QAC/D,MAAM4F,OAAO,MAAMxF,qBAAqBsF;QACxC,MAAMrD,SAAS,MAAMtC,kBAAkB2F,SAASE,IAAI,CAAC,QAAQ,EAAEZ,KAAKa,IAAI;QACxE,IAAIxD,kBAAkByD,UAAU,OAAOzD;QACvC,MAAMlB,aAAahB,uBAAuByF,IAAI,CAAC,aAAa;QAC5D,IAAI,CAACzE,YAAY,OAAOjB,gBAAgB,KAAK,mBAAmB;QAChE,MAAMe,YAAY+D,KAAK/D,SAAS;QAChC,MAAMkE,WAAW,MAAMnE,aAAaC,WAAWoB,OAAOnB,KAAK,EAAEC;QAC7D,IAAI,CAACgE,UAAU,OAAOjF,gBAAgB,KAAK,sBAAsB;QACjE,IAAIiF,SAASd,MAAM,KAAK,WAAY,MAAM3C,eAAeT,WAAWoB,OAAOnB,KAAK,EAAEiE,SAASI,EAAE,GAAI;YAC/F,qEAAqE;YACrE,uEAAuE;YACvE,OAAOrF,gBACL,KACA,kBACA;QAEJ;QACA,MAAMR,sBAAsBuB,WAAWoB,OAAOnB,KAAK,EAAE,aAAaG,GAAG,CAAC8D,SAASI,EAAE,EAAE5E,MAAM;QACzF,MAAMsE,SAAS5C,QAAQ/B,2BAA2BK,MAAM,EAAEwE;QAC1D,OAAOlF,WAAW;YAAE6G,IAAI;QAAK;IAC/B;IAEA,OAAO;QAAErB;QAAMpB;QAAQsD;IAAO;AAChC;AAEA;;;CAGC,GACD,eAAeD,oBACbzG,SAAoB,EACpBoB,MAA2B,EAC3BlB,UAAkB,EAClBoF,KAAa;IAEb,MAAMqB,cAAclI,sBAAsBuB,WAAWoB,OAAOnB,KAAK,EAAE;IACnE,IAAI2G,UAAU;IACd,IAAIC,QAAwD;IAC5D,OAAS;QACP,IAAIC,OAAOH,YAAYhG,KAAK,CAAC,cAAc,MAAMT,YAAY6G,OAAO,CAAC,YAAYnG,KAAK,CAACxB;QACvF,IAAIyH,OAAOC,OAAOA,KAAKE,UAAU,CAACH;QAClC,MAAM1G,WAAW,MAAM2G,KAAKzG,GAAG;QAC/B,IAAIF,SAASU,KAAK,EAAE;QACpB,MAAMoG,QAAQjH,UAAUiH,KAAK;QAC7B,IAAIC,SAAS;QACb,KAAK,MAAM9G,OAAOD,SAASgH,IAAI,CAAE;YAC/B,MAAMC,aAAa1I,6BAA6B0B,IAAIkE,EAAE,EAAElE,IAAIG,IAAI;YAChE,IAAI,CAAC6G,cAAc,CAAChJ,kCAAkCsE,QAAQ,CAAC0E,WAAWhE,MAAM,GAAG;YACnF,MAAM,EAAEiE,KAAK,EAAE,GAAGpJ,6BAA6BmJ,YAAY;gBACzD/C,MAAM;gBACNiD,QAAQ;gBACRC,MAAMjC;gBACNkC,OAAOpG,OAAOkC,GAAG;YACnB;YACA,IAAI,CAAC+D,OAAO;YACZJ,MAAMT,MAAM,CAACpG,IAAIoF,GAAG,EAAE,aAAK6B;gBAAO1B,aAAaL;;YAC/C4B,UAAU;QACZ;QACA,IAAIA,QAAQ,MAAMD,MAAMQ,MAAM;QAC9Bb,WAAWM;QACXL,QAAQ1G,SAASgH,IAAI,CAAChH,SAASgH,IAAI,CAACxE,MAAM,GAAG,EAAE;QAC/C,IAAIxC,SAASuH,IAAI,GAAGtI,cAAc;IACpC;IACA,OAAOwH;AACT"}
@@ -121,6 +121,7 @@ import { outreachOrgCollection, readStoredOutreachEnrollment } from "../storage/
121
121
  if (!campaignIds.length) continue;
122
122
  await deps.campaignCredit.credit({
123
123
  hostId: input.hostId,
124
+ orgId: input.orgId,
124
125
  campaignIds,
125
126
  outcome: 'converted',
126
127
  atMs
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/runtime/campaign-credit.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { normalizeCampaignIds } from '@aglyn/aglyn/app-utils/campaign-membership'\nimport type { OutreachEnrollment } from '../model/outreach.types'\nimport { outreachOrgCollection, readStoredOutreachEnrollment } from '../storage/outreach-records'\nimport type { OutreachRuntimeDeps } from './runtime-deps'\n\n/**\n * WHAT A SEQUENCE PRODUCED, CREDITED TO ITS CAMPAIGNS (AGL-3254).\n *\n * A sequence joins a campaign the way a form does, and every person\n * enrolled carries the sequence's campaigns as they stood at enrollment.\n * From then on each outcome of the enrollment is credited to those\n * campaigns through the platform's campaign attribution — the runtime's\n * `campaignCredit` seam — and never by writing the marketing plugin's\n * documents or importing it:\n *\n * - `enrolled`, by the enroll route, when the enrollment is created;\n * - `sent`, here, when the enrollment's FIRST email leaves — which is also\n * when the person's own record gains the sequence's campaign as \"where\n * this came from\", because a first email is the touch;\n * - `replied`, here, when the reply stops the sequence;\n * - `converted`, here, when a lead in a sequence becomes a contact;\n * - `meetings`, by the booking door, from the touch the click route stamps.\n *\n * Every credit happens once per enrollment because each is tied to a state\n * change that happens once: there is one first email, a reply moves the\n * status to `replied` and nothing sends after, a lead converts once. An\n * enrollment naming no campaign credits nothing, silently — a sequence\n * outside every campaign is the ordinary case, not an error.\n */\n\n/** The campaigns an enrollment credits: what it carries, cleaned. */\nexport function outreachEnrollmentCampaignIds(\n enrollment: Pick<OutreachEnrollment, 'campaignIds'>,\n): string[] {\n return normalizeCampaignIds(enrollment.campaignIds)\n}\n\n/** Whether the email about to be recorded is the enrollment's first. */\nexport function isOutreachFirstEmail(enrollment: Pick<OutreachEnrollment, 'stepRecords'>): boolean {\n return !(enrollment.stepRecords ?? []).some((record) => record.kind === 'email')\n}\n\n/**\n * The first email of an enrollment left: `sent` on every campaign, and the\n * person's record credited to the first of them. One campaign on the\n * record, because the record's \"where this came from\" names one touch; the\n * sequence's first campaign is the one the rep listed first.\n */\nexport async function creditOutreachFirstSend(\n deps: Pick<OutreachRuntimeDeps, 'campaignCredit'>,\n input: { enrollment: OutreachEnrollment; atMs: number },\n): Promise<void> {\n const { enrollment, atMs } = input\n const campaignIds = outreachEnrollmentCampaignIds(enrollment)\n if (!campaignIds.length || !isOutreachFirstEmail(enrollment)) return\n await deps.campaignCredit.credit({ hostId: enrollment.hostId, campaignIds, outcome: 'sent', atMs })\n const record =\n enrollment.target === 'lead' && enrollment.leadId\n ? { kind: 'lead' as const, refId: enrollment.leadId }\n : enrollment.contactId\n ? { kind: 'contact' as const, refId: enrollment.contactId }\n : null\n if (!record) return\n await deps.campaignCredit.attributeRecord({\n hostId: enrollment.hostId,\n ...record,\n campaignId: campaignIds[0],\n sequenceId: enrollment.sequenceId,\n enrollmentId: enrollment.id,\n atMs,\n })\n}\n\n/** A reply stopped the sequence: `replied` on every campaign. */\nexport async function creditOutreachReply(\n deps: Pick<OutreachRuntimeDeps, 'campaignCredit'>,\n input: { enrollment: OutreachEnrollment; atMs: number },\n): Promise<void> {\n const campaignIds = outreachEnrollmentCampaignIds(input.enrollment)\n if (!campaignIds.length) return\n await deps.campaignCredit.credit({\n hostId: input.enrollment.hostId,\n campaignIds,\n outcome: 'replied',\n atMs: input.atMs,\n })\n}\n\n/**\n * A person clicked a link in a sequence email: the sequence's campaign\n * becomes their last campaign touch on the site, so what they go on to do\n * there — book, submit a form — is credited to it by the platform's own\n * doors. The first campaign, for the record-side reason above.\n */\nexport async function recordOutreachSequenceTouch(\n deps: Pick<OutreachRuntimeDeps, 'campaignCredit'>,\n input: { enrollment: OutreachEnrollment; atMs: number },\n): Promise<void> {\n const { enrollment } = input\n const campaignIds = outreachEnrollmentCampaignIds(enrollment)\n if (!campaignIds.length || !enrollment.email) return\n await deps.campaignCredit.recordTouch({\n hostId: enrollment.hostId,\n email: enrollment.email,\n campaignId: campaignIds[0],\n sequenceId: enrollment.sequenceId,\n enrollmentId: enrollment.id,\n atMs: input.atMs,\n })\n}\n\n/**\n * A lead in a sequence became a contact: `converted` on every campaign of\n * every enrollment made on the lead. Run from the lead-conversion seam,\n * which fires once per conversion. Answers how many enrollments credited.\n */\nexport async function creditOutreachLeadConversion(\n deps: Pick<OutreachRuntimeDeps, 'firestore' | 'now' | 'campaignCredit'>,\n input: { orgId: string; hostId: string; leadId: string },\n): Promise<number> {\n const rows = await outreachOrgCollection(deps.firestore(), input.orgId, 'enrollments')\n .where('leadId', '==', input.leadId)\n .get()\n const atMs = deps.now()\n let credited = 0\n for (const row of rows.docs) {\n const enrollment = readStoredOutreachEnrollment(row.id, row.data())\n if (!enrollment || enrollment.hostId !== input.hostId) continue\n const campaignIds = outreachEnrollmentCampaignIds(enrollment)\n if (!campaignIds.length) continue\n await deps.campaignCredit.credit({ hostId: input.hostId, campaignIds, outcome: 'converted', atMs })\n credited += 1\n }\n return credited\n}\n"],"names":["normalizeCampaignIds","outreachOrgCollection","readStoredOutreachEnrollment","outreachEnrollmentCampaignIds","enrollment","campaignIds","isOutreachFirstEmail","stepRecords","some","record","kind","creditOutreachFirstSend","deps","input","atMs","length","campaignCredit","credit","hostId","outcome","target","leadId","refId","contactId","attributeRecord","campaignId","sequenceId","enrollmentId","id","creditOutreachReply","recordOutreachSequenceTouch","email","recordTouch","creditOutreachLeadConversion","rows","firestore","orgId","where","get","now","credited","row","docs","data"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/runtime/campaign-credit.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { normalizeCampaignIds } from '@aglyn/aglyn/app-utils/campaign-membership'\nimport type { OutreachEnrollment } from '../model/outreach.types'\nimport { outreachOrgCollection, readStoredOutreachEnrollment } from '../storage/outreach-records'\nimport type { OutreachRuntimeDeps } from './runtime-deps'\n\n/**\n * WHAT A SEQUENCE PRODUCED, CREDITED TO ITS CAMPAIGNS (AGL-3254).\n *\n * A sequence joins a campaign the way a form does, and every person\n * enrolled carries the sequence's campaigns as they stood at enrollment.\n * From then on each outcome of the enrollment is credited to those\n * campaigns through the platform's campaign attribution — the runtime's\n * `campaignCredit` seam — and never by writing the marketing plugin's\n * documents or importing it:\n *\n * - `enrolled`, by the enroll route, when the enrollment is created;\n * - `sent`, here, when the enrollment's FIRST email leaves — which is also\n * when the person's own record gains the sequence's campaign as \"where\n * this came from\", because a first email is the touch;\n * - `replied`, here, when the reply stops the sequence;\n * - `converted`, here, when a lead in a sequence becomes a contact;\n * - `meetings`, by the booking door, from the touch the click route stamps.\n *\n * Every credit happens once per enrollment because each is tied to a state\n * change that happens once: there is one first email, a reply moves the\n * status to `replied` and nothing sends after, a lead converts once. An\n * enrollment naming no campaign credits nothing, silently — a sequence\n * outside every campaign is the ordinary case, not an error.\n */\n\n/** The campaigns an enrollment credits: what it carries, cleaned. */\nexport function outreachEnrollmentCampaignIds(\n enrollment: Pick<OutreachEnrollment, 'campaignIds'>,\n): string[] {\n return normalizeCampaignIds(enrollment.campaignIds)\n}\n\n/** Whether the email about to be recorded is the enrollment's first. */\nexport function isOutreachFirstEmail(enrollment: Pick<OutreachEnrollment, 'stepRecords'>): boolean {\n return !(enrollment.stepRecords ?? []).some((record) => record.kind === 'email')\n}\n\n/**\n * The first email of an enrollment left: `sent` on every campaign, and the\n * person's record credited to the first of them. One campaign on the\n * record, because the record's \"where this came from\" names one touch; the\n * sequence's first campaign is the one the rep listed first.\n */\nexport async function creditOutreachFirstSend(\n deps: Pick<OutreachRuntimeDeps, 'campaignCredit'>,\n input: { enrollment: OutreachEnrollment; atMs: number },\n): Promise<void> {\n const { enrollment, atMs } = input\n const campaignIds = outreachEnrollmentCampaignIds(enrollment)\n if (!campaignIds.length || !isOutreachFirstEmail(enrollment)) return\n await deps.campaignCredit.credit({ hostId: enrollment.hostId, campaignIds, outcome: 'sent', atMs })\n const record =\n enrollment.target === 'lead' && enrollment.leadId\n ? { kind: 'lead' as const, refId: enrollment.leadId }\n : enrollment.contactId\n ? { kind: 'contact' as const, refId: enrollment.contactId }\n : null\n if (!record) return\n await deps.campaignCredit.attributeRecord({\n hostId: enrollment.hostId,\n ...record,\n campaignId: campaignIds[0],\n sequenceId: enrollment.sequenceId,\n enrollmentId: enrollment.id,\n atMs,\n })\n}\n\n/** A reply stopped the sequence: `replied` on every campaign. */\nexport async function creditOutreachReply(\n deps: Pick<OutreachRuntimeDeps, 'campaignCredit'>,\n input: { enrollment: OutreachEnrollment; atMs: number },\n): Promise<void> {\n const campaignIds = outreachEnrollmentCampaignIds(input.enrollment)\n if (!campaignIds.length) return\n await deps.campaignCredit.credit({\n hostId: input.enrollment.hostId,\n campaignIds,\n outcome: 'replied',\n atMs: input.atMs,\n })\n}\n\n/**\n * A person clicked a link in a sequence email: the sequence's campaign\n * becomes their last campaign touch on the site, so what they go on to do\n * there — book, submit a form — is credited to it by the platform's own\n * doors. The first campaign, for the record-side reason above.\n */\nexport async function recordOutreachSequenceTouch(\n deps: Pick<OutreachRuntimeDeps, 'campaignCredit'>,\n input: { enrollment: OutreachEnrollment; atMs: number },\n): Promise<void> {\n const { enrollment } = input\n const campaignIds = outreachEnrollmentCampaignIds(enrollment)\n if (!campaignIds.length || !enrollment.email) return\n await deps.campaignCredit.recordTouch({\n hostId: enrollment.hostId,\n email: enrollment.email,\n campaignId: campaignIds[0],\n sequenceId: enrollment.sequenceId,\n enrollmentId: enrollment.id,\n atMs: input.atMs,\n })\n}\n\n/**\n * A lead in a sequence became a contact: `converted` on every campaign of\n * every enrollment made on the lead. Run from the lead-conversion seam,\n * which fires once per conversion. Answers how many enrollments credited.\n */\nexport async function creditOutreachLeadConversion(\n deps: Pick<OutreachRuntimeDeps, 'firestore' | 'now' | 'campaignCredit'>,\n input: { orgId: string; hostId: string; leadId: string },\n): Promise<number> {\n const rows = await outreachOrgCollection(deps.firestore(), input.orgId, 'enrollments')\n .where('leadId', '==', input.leadId)\n .get()\n const atMs = deps.now()\n let credited = 0\n for (const row of rows.docs) {\n const enrollment = readStoredOutreachEnrollment(row.id, row.data())\n if (!enrollment || enrollment.hostId !== input.hostId) continue\n const campaignIds = outreachEnrollmentCampaignIds(enrollment)\n if (!campaignIds.length) continue\n await deps.campaignCredit.credit({\n hostId: input.hostId,\n orgId: input.orgId,\n campaignIds,\n outcome: 'converted',\n atMs,\n })\n credited += 1\n }\n return credited\n}\n"],"names":["normalizeCampaignIds","outreachOrgCollection","readStoredOutreachEnrollment","outreachEnrollmentCampaignIds","enrollment","campaignIds","isOutreachFirstEmail","stepRecords","some","record","kind","creditOutreachFirstSend","deps","input","atMs","length","campaignCredit","credit","hostId","outcome","target","leadId","refId","contactId","attributeRecord","campaignId","sequenceId","enrollmentId","id","creditOutreachReply","recordOutreachSequenceTouch","email","recordTouch","creditOutreachLeadConversion","rows","firestore","orgId","where","get","now","credited","row","docs","data"],"mappings":"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"}
@@ -43,9 +43,10 @@ import { createOutreachUnsubscribeRoute } from "./unsubscribe-route.js";
43
43
  * readings of one rule. Each writer already never throws.
44
44
  */ export function platformOutreachCampaignCredit(firestore) {
45
45
  return {
46
- async credit ({ hostId, campaignIds, outcome, atMs }) {
46
+ async credit ({ hostId, orgId, campaignIds, outcome, atMs }) {
47
47
  await creditCampaignSequenceOutcome({
48
48
  hostId,
49
+ orgId,
49
50
  campaignIds,
50
51
  outcome,
51
52
  atMs