@aglyn/plugins-crm 1.0.0-beta.230 → 1.0.0-beta.231

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.
Files changed (46) hide show
  1. package/package.json +14 -13
  2. package/src/lib/components/crm-next-activity-column.d.ts +1 -13
  3. package/src/lib/components/crm-next-activity-column.js +1 -19
  4. package/src/lib/components/crm-next-activity-column.js.map +1 -1
  5. package/src/lib/components/lead-status-options.d.ts +2 -14
  6. package/src/lib/components/lead-status-options.js +1 -13
  7. package/src/lib/components/lead-status-options.js.map +1 -1
  8. package/src/lib/components/lead-unqualify-dialog.d.ts +1 -2
  9. package/src/lib/components/lead-unqualify-dialog.js +2 -1
  10. package/src/lib/components/lead-unqualify-dialog.js.map +1 -1
  11. package/src/lib/components/settings-section.d.ts +18 -0
  12. package/src/lib/components/settings-section.js +97 -1
  13. package/src/lib/components/settings-section.js.map +1 -1
  14. package/src/lib/constants/company-filters.d.ts +2 -2
  15. package/src/lib/constants/company-filters.js +1 -1
  16. package/src/lib/constants/company-filters.js.map +1 -1
  17. package/src/lib/constants/contact-filters.d.ts +3 -3
  18. package/src/lib/constants/contact-filters.js +2 -1
  19. package/src/lib/constants/contact-filters.js.map +1 -1
  20. package/src/lib/constants/deal-filters.d.ts +3 -3
  21. package/src/lib/constants/deal-filters.js +1 -1
  22. package/src/lib/constants/deal-filters.js.map +1 -1
  23. package/src/lib/constants/field-list-query.d.ts +1 -1
  24. package/src/lib/constants/field-list-query.js +1 -1
  25. package/src/lib/constants/field-list-query.js.map +1 -1
  26. package/src/lib/declarations.server.js +9 -0
  27. package/src/lib/declarations.server.js.map +1 -1
  28. package/src/lib/model/contacts-list-seed.d.ts +1 -1
  29. package/src/lib/model/contacts-list-seed.js.map +1 -1
  30. package/src/lib/model/crm-list-query.d.ts +4 -4
  31. package/src/lib/model/crm-list-query.js +2 -2
  32. package/src/lib/model/crm-list-query.js.map +1 -1
  33. package/src/lib/model/crm-next-activity.d.ts +13 -0
  34. package/src/lib/model/crm-next-activity.js +19 -0
  35. package/src/lib/model/crm-next-activity.js.map +1 -1
  36. package/src/lib/model/lead-filters.d.ts +6 -5
  37. package/src/lib/model/lead-filters.js +3 -2
  38. package/src/lib/model/lead-filters.js.map +1 -1
  39. package/src/lib/model/lead-status-choices.d.ts +32 -0
  40. package/src/lib/model/lead-status-choices.js +31 -0
  41. package/src/lib/model/lead-status-choices.js.map +1 -0
  42. package/src/lib/model/task-views.d.ts +2 -2
  43. package/src/lib/model/task-views.js.map +1 -1
  44. package/src/lib/server/person-records.d.ts +27 -0
  45. package/src/lib/server/person-records.js +127 -4
  46. package/src/lib/server/person-records.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/person-records.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n containerMembershipField,\n contactContainerFieldPath,\n normalizeContainerIds,\n} from '@aglyn/aglyn/app-utils/container-membership'\nimport { normalizeContactEmail } from '@aglyn/aglyn/app-utils/contacts'\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 { personKey } from '@aglyn/aglyn/app-utils/person-key'\nimport {\n scopeTokensForHost,\n seenOnlyThroughGrant,\n visibleToHost,\n} from '@aglyn/aglyn/app-utils/scope-tokens'\nimport type {\n PluginPersonFileRequest,\n PluginPersonFindRequest,\n PluginPersonReadRequest,\n PluginPersonRecord,\n PluginPersonRecordRef,\n PluginPersonRecords,\n PluginPersonViewPeople,\n PluginPersonViewRequest,\n PluginPersonWroteInRequest,\n} from '@aglyn/aglyn/plugin-manager/plugin-person-records'\nimport {\n consentGroupForSite,\n firebaseAdmin,\n resolveOrgIdForHost,\n} from '@aglyn/tenant-data-admin'\n// The leaves, not the barrel: this plugin's specs substitute the barrel\n// wholesale, and the lookup and the restamp must reach the real logic.\nimport { findContactByEmail } from '@aglyn/tenant-data-admin/server/contact-email-index'\nimport { restampCrmListFieldsAt } from '@aglyn/tenant-data-admin/server/crm-records'\nimport { collectDynamicListCandidates } from '@aglyn/tenant-data-admin/server/dynamic-list-materialize'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { recordPersonRefund } from './person-refund'\n\n/**\n * THE CRM AS THE PLUGIN THAT KEEPS PEOPLE, asked by the plugins that do not\n * (AGL-3080) — the CRM's side of `plugin-person-records`.\n *\n * Two kinds of record are a person here, in the words `plugin-record-routes`\n * addresses them by: a `contact`, the person once they are known, filed at\n * `orgs/{orgId}/contacts/{id}`; and a `lead`, the person before anybody has\n * qualified them, at `orgs/{orgId}/leads/{personKey}` (AGL-3275). One person\n * is one record (AGL-3232), so a lookup by address answers the contact when\n * there is one and the lead only for a caller that asked for any kind.\n *\n * The record's `data` is the stored document. Beside the fields the platform\n * defines (`marketingConsent…`, `emailState`, `visibleTo`), a contact keeps\n * what each consent group knows of the person in `facets.{groupId}` — the\n * sources, stage, owner, company, address and tags — and a lead keeps the\n * same facts at its top level.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** The person kinds, as `plugin-record-routes` addresses them. */\nexport const CRM_PERSON_KINDS = { contact: 'contact', lead: 'lead' } as const\n\n/** The collection each person kind is stored in, under the organization. */\nconst STORED_IN: Readonly<Record<string, string>> = {\n [CRM_PERSON_KINDS.contact]: 'contacts',\n [CRM_PERSON_KINDS.lead]: 'leads',\n}\n\n/** `getAll` takes this many references per call. */\nconst GET_ALL_CHUNK = 300\n\n/**\n * The most leads a saved Leads view is taken from: the site's most recently\n * seen, the Leads list's own window (AGL-3234).\n */\nexport const CRM_LEADS_VIEW_WINDOW = 200\n\nexport interface CrmPersonRecordsDeps {\n firestore(): Firestore\n /** The organization a site belongs to, or `null` for one with none. */\n orgIdForHost(hostId: string): Promise<string | null>\n /** The consent group a site files its facet under. */\n groupIdForHost(hostId: string): Promise<string>\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 contactViewEmails(input: { hostId: string; viewId: string }): Promise<{ emails: string[]; complete: boolean }>\n}\n\nexport function defaultCrmPersonRecordsDeps(): CrmPersonRecordsDeps {\n return {\n firestore: () => firebaseAdmin.app().firestore(),\n orgIdForHost: async (hostId) => (await resolveOrgIdForHost(hostId)) ?? null,\n groupIdForHost: async (hostId) => (await consentGroupForSite(hostId)).groupId,\n contactViewEmails: async ({ hostId, viewId }) => {\n const scan = await collectDynamicListCandidates({\n hostId,\n rule: { sources: ['contacts'], viewId },\n })\n return { emails: scan.candidates.map((candidate) => candidate.email), complete: scan.complete }\n },\n }\n}\n\nfunction personOf(kind: string, snapshot: FirebaseFirestore.DocumentSnapshot): PluginPersonRecord {\n const data = (snapshot.data() ?? {}) as Record<string, unknown>\n return { kind, id: snapshot.id, email: normalizeContactEmail(data['email']), data }\n}\n\n/**\n * The organization a request names, or the one its site belongs to. A site\n * with no organization THROWS, as the org-data lookup every caller used to\n * make threw: each caller has already decided which way a failed read falls.\n */\nasync function orgOf(\n deps: CrmPersonRecordsDeps,\n request: { orgId?: string | null; hostId?: string | null },\n): Promise<string> {\n const named = String(request.orgId ?? '').trim()\n if (named) return named\n const hostId = String(request.hostId ?? '').trim()\n const resolved = hostId ? await deps.orgIdForHost(hostId) : null\n if (!resolved) throw new Error(`[crm] no organization to find a person in for host ${hostId || '(none)'}`)\n return resolved\n}\n\nexport function createCrmPersonRecords(deps: CrmPersonRecordsDeps): PluginPersonRecords {\n return {\n /**\n * The contact first, through the org's address index (AGL-2633) so an\n * address a merge folded into another record still names the person who\n * now holds it; then, for a caller that asked for any kind, the lead the\n * address is keyed as. Narrowed to the site when asked, on the document\n * the address names: a record the site cannot see is `null`.\n */\n async find(request: PluginPersonFindRequest) {\n const email = normalizeContactEmail(request.email)\n if (!email) return null\n const orgId = await orgOf(deps, request)\n const org = deps.firestore().collection('orgs').doc(orgId)\n const hostId = String(request.hostId ?? '').trim()\n const narrowTo = request.onlyVisibleToSite && hostId ? hostId : null\n const contact = await findContactByEmail(\n org.collection(STORED_IN[CRM_PERSON_KINDS.contact]),\n email,\n narrowTo ? { hostId: narrowTo } : {},\n )\n if (contact) return personOf(CRM_PERSON_KINDS.contact, contact)\n if (!request.anyKind) return null\n const key = personKey(email)\n if (!key) return null\n const lead = await org.collection(STORED_IN[CRM_PERSON_KINDS.lead]).doc(key).get()\n if (!lead.exists) return null\n if (narrowTo && !visibleToHost(lead.get('visibleTo') as string[] | undefined, narrowTo)) {\n return null\n }\n return personOf(CRM_PERSON_KINDS.lead, lead)\n },\n\n /** Every record named, read at once; a kind the CRM does not keep is `null`. */\n async read(request: PluginPersonReadRequest) {\n const firestore = deps.firestore()\n const org = firestore.collection('orgs').doc(request.orgId)\n const refs = request.records.map((record) => {\n const collection = STORED_IN[record.kind]\n const id = String(record.id ?? '').trim()\n return collection && id ? org.collection(collection).doc(id) : null\n })\n const wanted = refs.filter((ref): ref is FirebaseFirestore.DocumentReference => ref !== null)\n const snapshots: FirebaseFirestore.DocumentSnapshot[] = []\n for (let start = 0; start < wanted.length; start += GET_ALL_CHUNK) {\n snapshots.push(...(await firestore.getAll(...wanted.slice(start, start + GET_ALL_CHUNK))))\n }\n let at = 0\n return request.records.map((record, index) => {\n if (!refs[index]) return null\n const snapshot = snapshots[at++]\n return snapshot?.exists ? personOf(record.kind, snapshot) : null\n })\n },\n\n /**\n * Files the person under the containers as the site holds them: on a\n * contact, inside the site's consent-group facet — which campaigns a\n * merchant filed somebody under is that merchant's record, and written at\n * the top it would be readable by every site of an agency's account; on a\n * lead, in the lead's own membership field, whose list fields are then\n * restamped so the Leads list's filter finds it (AGL-3321).\n *\n * `update`, never a merge-set: a dotted path in `set` is a literal field\n * name, and the record must exist. A record the site cannot see is not\n * filed.\n */\n async fileUnder(request: PluginPersonFileRequest) {\n const ids = normalizeContainerIds(request.ids)\n const collection = STORED_IN[request.record.kind]\n const id = String(request.record.id ?? '').trim()\n if (!ids.length || !collection || !id) return { filed: false }\n const orgId = await orgOf(deps, request)\n const ref = deps.firestore().collection('orgs').doc(orgId).collection(collection).doc(id)\n const snapshot = await ref.get()\n if (!snapshot.exists) return { filed: false }\n if (!visibleToHost(snapshot.get('visibleTo') as string[] | undefined, request.hostId)) {\n return { filed: false }\n }\n if (request.record.kind === CRM_PERSON_KINDS.lead) {\n await ref.update({\n [containerMembershipField(request.containerKind)]: FieldValue.arrayUnion(...ids),\n updatedAt: FieldValue.serverTimestamp(),\n })\n await restampCrmListFieldsAt(ref, 'leads')\n return { filed: true }\n }\n const groupId = await deps.groupIdForHost(request.hostId)\n await ref.update({\n [contactContainerFieldPath(groupId, request.containerKind)]: FieldValue.arrayUnion(...ids),\n updatedAt: FieldValue.serverTimestamp(),\n })\n return { filed: true }\n },\n\n recordRefund: (request) => recordPersonRefund(request),\n\n /**\n * The people a saved view selects for a site (AGL-3234). A colleague's\n * private view is theirs: not listed for this member, so it does not\n * exist for them here either.\n *\n * A Leads view takes the site's most recently seen leads —\n * {@link CRM_LEADS_VIEW_WINDOW}, the Leads list's own window — narrowed by\n * the view's status clause the way the list narrows it: open leads when\n * the view names no status, one status when it does, everything for\n * `all`. Held, not merely seen: a lead another site only SHARED with this\n * one (AGL-3336) is not this site's to take.\n *\n * A Contacts view is read the way the dynamic-list sweep reads one, and\n * each address it selects is resolved to the contact the site may see. A\n * view filtering on something that reading cannot apply is refused whole:\n * dropping the filter would select more people than the view shows.\n */\n async peopleInView(request: PluginPersonViewRequest): Promise<PluginPersonViewPeople> {\n const firestore = deps.firestore()\n const org = firestore.collection('orgs').doc(request.orgId)\n const view = await org.collection(CRM_COLLECTIONS.views).doc(request.viewId).get()\n const data = view.exists ? ((view.data() ?? {}) as Record<string, unknown>) : null\n if (\n !data ||\n !crmViewIsListed(\n { shared: data['shared'] === true, ownerUid: String(data['ownerUid'] ?? '') },\n request.viewerUid,\n )\n ) {\n return { ok: false, reason: 'not-found' }\n }\n const filters = normalizeCrmViewFilters(data['filters'])\n if (data['section'] === 'leads') {\n const statusClause = filters.find((clause) => clause.field === 'status' && clause.op === 'equals')\n const wanted = String(statusClause?.value ?? 'open')\n // Narrowed to what the site may see before it is ordered (AGL-3275):\n // the collection is org-wide, and an agency's sequence must not be\n // offered another client's people.\n const window = await org\n .collection(STORED_IN[CRM_PERSON_KINDS.lead])\n .where('visibleTo', 'array-contains-any', scopeTokensForHost(request.hostId))\n .orderBy('lastSeenAtMs', 'desc')\n .limit(CRM_LEADS_VIEW_WINDOW + 1)\n .get()\n const matching = window.docs.slice(0, CRM_LEADS_VIEW_WINDOW).filter((doc) => {\n const lead = (doc.data() ?? {}) as Record<string, unknown>\n if (seenOnlyThroughGrant(lead, request.hostId)) return false\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 ok: true,\n people: matching.slice(0, request.limit).map((doc) => ({ kind: CRM_PERSON_KINDS.lead, id: doc.id })),\n total: matching.length,\n truncated: matching.length > request.limit || window.docs.length > CRM_LEADS_VIEW_WINDOW,\n }\n }\n if (data['section'] !== 'contacts') return { ok: false, reason: 'not-people' }\n const { unsupported } = dynamicListDimensionsForCrmView(filters)\n if (unsupported.length) {\n return {\n ok: false,\n reason: 'unsupported',\n unsupported: unsupported.map((clause) => clause.label || clause.field),\n }\n }\n const { emails, complete } = await deps.contactViewEmails({\n hostId: request.hostId,\n viewId: request.viewId,\n })\n const ordered = [...new Set(emails)].sort()\n const contacts = org.collection(STORED_IN[CRM_PERSON_KINDS.contact])\n const found = await Promise.all(\n ordered\n .slice(0, request.limit)\n .map((email) => findContactByEmail(contacts, email, { hostId: request.hostId })),\n )\n const ids = [...new Set(found.filter((snapshot) => snapshot).map((snapshot) => String(snapshot?.id)))]\n return {\n ok: true,\n people: ids.map((id): PluginPersonRecordRef => ({ kind: CRM_PERSON_KINDS.contact, id })),\n total: ordered.length,\n truncated: ordered.length > request.limit || !complete,\n }\n },\n\n /**\n * Whether each person has ever written in: an inbound email on their\n * timeline, filed on the contact, or on the lead while they were one\n * (AGL-3234). One keyed query each; a failed one is unknown.\n */\n async wroteIn(request: PluginPersonWroteInRequest) {\n const activities = deps\n .firestore()\n .collection('orgs')\n .doc(request.orgId)\n .collection(CRM_COLLECTIONS.activities)\n return Promise.all(\n request.records.map(async (record) => {\n const field =\n record.kind === CRM_PERSON_KINDS.contact\n ? 'contactId'\n : record.kind === CRM_PERSON_KINDS.lead\n ? 'leadId'\n : null\n const id = String(record.id ?? '').trim()\n if (!field || !id) return null\n try {\n const snapshot = await activities\n .where(field, '==', id)\n .where('direction', '==', 'inbound')\n .limit(1)\n .get()\n return !snapshot.empty\n } catch (error) {\n console.error('[crm] inbound email lookup failed; reading as unknown', error)\n return null\n }\n }),\n )\n },\n }\n}\n\n/** The service as the CRM registers it, its storage the admin app's. */\nexport const crmPersonRecords: Required<PluginPersonRecords> = {\n find: (request) => createCrmPersonRecords(defaultCrmPersonRecordsDeps()).find(request),\n read: (request) => createCrmPersonRecords(defaultCrmPersonRecordsDeps()).read(request),\n fileUnder: (request) => createCrmPersonRecords(defaultCrmPersonRecordsDeps()).fileUnder!(request),\n recordRefund: (request) => recordPersonRefund(request),\n peopleInView: (request) => createCrmPersonRecords(defaultCrmPersonRecordsDeps()).peopleInView!(request),\n wroteIn: (request) => createCrmPersonRecords(defaultCrmPersonRecordsDeps()).wroteIn!(request),\n}\n"],"names":["containerMembershipField","contactContainerFieldPath","normalizeContainerIds","normalizeContactEmail","CRM_COLLECTIONS","crmLeadStatus","crmViewIsListed","isCrmLeadOpen","normalizeCrmViewFilters","dynamicListDimensionsForCrmView","personKey","scopeTokensForHost","seenOnlyThroughGrant","visibleToHost","consentGroupForSite","firebaseAdmin","resolveOrgIdForHost","findContactByEmail","restampCrmListFieldsAt","collectDynamicListCandidates","FieldValue","recordPersonRefund","CRM_PERSON_KINDS","contact","lead","STORED_IN","GET_ALL_CHUNK","CRM_LEADS_VIEW_WINDOW","defaultCrmPersonRecordsDeps","firestore","app","orgIdForHost","hostId","groupIdForHost","groupId","contactViewEmails","viewId","scan","rule","sources","emails","candidates","map","candidate","email","complete","personOf","kind","snapshot","data","id","orgOf","deps","request","named","String","orgId","trim","resolved","Error","createCrmPersonRecords","find","org","collection","doc","narrowTo","onlyVisibleToSite","anyKind","key","get","exists","read","refs","records","record","wanted","filter","ref","snapshots","start","length","push","getAll","slice","at","index","fileUnder","ids","filed","update","containerKind","arrayUnion","updatedAt","serverTimestamp","recordRefund","peopleInView","view","views","shared","ownerUid","viewerUid","ok","reason","filters","statusClause","clause","field","op","value","window","where","orderBy","limit","matching","docs","people","total","truncated","unsupported","label","ordered","Set","sort","contacts","found","Promise","all","wroteIn","activities","empty","error","console","crmPersonRecords"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/person-records.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n containerMembershipField,\n contactContainerFieldPath,\n normalizeContainerIds,\n} from '@aglyn/aglyn/app-utils/container-membership'\nimport { normalizeContactEmail, readContactFacet } from '@aglyn/aglyn/app-utils/contacts'\nimport {\n CRM_COLLECTIONS,\n CRM_SCOPED_SEARCH_JOIN,\n CRM_SCOPED_SEARCH_TOKENS_FIELD,\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 { NAME_TOKEN_MAX_PREFIX, nameSearchKey } from '@aglyn/aglyn/app-utils/name-search'\nimport { personKey } from '@aglyn/aglyn/app-utils/person-key'\nimport {\n scopeTokensForHost,\n seenOnlyThroughGrant,\n visibleToHost,\n} from '@aglyn/aglyn/app-utils/scope-tokens'\nimport type {\n PluginPersonChange,\n PluginPersonChangesPage,\n PluginPersonChangesRequest,\n PluginPersonFileRequest,\n PluginPersonFindRequest,\n PluginPersonReadRequest,\n PluginPersonRecord,\n PluginPersonRecordRef,\n PluginPersonRecords,\n PluginPersonSearchRequest,\n PluginPersonViewPeople,\n PluginPersonViewRequest,\n PluginPersonWroteInRequest,\n} from '@aglyn/aglyn/plugin-manager/plugin-person-records'\nimport {\n consentGroupForSite,\n firebaseAdmin,\n resolveOrgIdForHost,\n} from '@aglyn/tenant-data-admin'\n// The leaves, not the barrel: this plugin's specs substitute the barrel\n// wholesale, and the lookup and the restamp must reach the real logic.\nimport { findContactByEmail } from '@aglyn/tenant-data-admin/server/contact-email-index'\nimport { restampCrmListFieldsAt } from '@aglyn/tenant-data-admin/server/crm-records'\nimport { collectDynamicListCandidates } from '@aglyn/tenant-data-admin/server/dynamic-list-materialize'\nimport { FieldPath, FieldValue, Timestamp } from 'firebase-admin/firestore'\nimport { recordPersonRefund } from './person-refund'\n\n/**\n * THE CRM AS THE PLUGIN THAT KEEPS PEOPLE, asked by the plugins that do not\n * (AGL-3080) — the CRM's side of `plugin-person-records`.\n *\n * Two kinds of record are a person here, in the words `plugin-record-routes`\n * addresses them by: a `contact`, the person once they are known, filed at\n * `orgs/{orgId}/contacts/{id}`; and a `lead`, the person before anybody has\n * qualified them, at `orgs/{orgId}/leads/{personKey}` (AGL-3275). One person\n * is one record (AGL-3232), so a lookup by address answers the contact when\n * there is one and the lead only for a caller that asked for any kind.\n *\n * The record's `data` is the stored document. Beside the fields the platform\n * defines (`marketingConsent…`, `emailState`, `visibleTo`), a contact keeps\n * what each consent group knows of the person in `facets.{groupId}` — the\n * sources, stage, owner, company, address and tags — and a lead keeps the\n * same facts at its top level.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** The person kinds, as `plugin-record-routes` addresses them. */\nexport const CRM_PERSON_KINDS = { contact: 'contact', lead: 'lead' } as const\n\n/** The collection each person kind is stored in, under the organization. */\nconst STORED_IN: Readonly<Record<string, string>> = {\n [CRM_PERSON_KINDS.contact]: 'contacts',\n [CRM_PERSON_KINDS.lead]: 'leads',\n}\n\n/** `getAll` takes this many references per call. */\nconst GET_ALL_CHUNK = 300\n\n/**\n * The most leads a saved Leads view is taken from: the site's most recently\n * seen, the Leads list's own window (AGL-3234).\n */\nexport const CRM_LEADS_VIEW_WINDOW = 200\n\nexport interface CrmPersonRecordsDeps {\n firestore(): Firestore\n /** The organization a site belongs to, or `null` for one with none. */\n orgIdForHost(hostId: string): Promise<string | null>\n /** The consent group a site files its facet under. */\n groupIdForHost(hostId: string): Promise<string>\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 contactViewEmails(input: { hostId: string; viewId: string }): Promise<{ emails: string[]; complete: boolean }>\n}\n\nexport function defaultCrmPersonRecordsDeps(): CrmPersonRecordsDeps {\n return {\n firestore: () => firebaseAdmin.app().firestore(),\n orgIdForHost: async (hostId) => (await resolveOrgIdForHost(hostId)) ?? null,\n groupIdForHost: async (hostId) => (await consentGroupForSite(hostId)).groupId,\n contactViewEmails: async ({ hostId, viewId }) => {\n const scan = await collectDynamicListCandidates({\n hostId,\n rule: { sources: ['contacts'], viewId },\n })\n return { emails: scan.candidates.map((candidate) => candidate.email), complete: scan.complete }\n },\n }\n}\n\nfunction personOf(kind: string, snapshot: FirebaseFirestore.DocumentSnapshot): PluginPersonRecord {\n const data = (snapshot.data() ?? {}) as Record<string, unknown>\n return { kind, id: snapshot.id, email: normalizeContactEmail(data['email']), data }\n}\n\n/**\n * The organization a request names, or the one its site belongs to. A site\n * with no organization THROWS, as the org-data lookup every caller used to\n * make threw: each caller has already decided which way a failed read falls.\n */\nasync function orgOf(\n deps: CrmPersonRecordsDeps,\n request: { orgId?: string | null; hostId?: string | null },\n): Promise<string> {\n const named = String(request.orgId ?? '').trim()\n if (named) return named\n const hostId = String(request.hostId ?? '').trim()\n const resolved = hostId ? await deps.orgIdForHost(hostId) : null\n if (!resolved) throw new Error(`[crm] no organization to find a person in for host ${hostId || '(none)'}`)\n return resolved\n}\n\n/** The most people one search answers, whatever the caller asks for. */\nexport const CRM_PERSON_SEARCH_MAX = 25\n\n/**\n * The one token a typed search asks the contacts' search index for\n * (AGL-3609), or `''` for nothing searchable.\n *\n * A run of digits and phone punctuation is a phone number, read as its\n * digits: `crmPhoneSearchWords` stores the whole number, the number without\n * a country code, its last seven and its last four, so `(555) 123-4567`,\n * `5551234567` and `4567` all find it. Anything else is a name or an\n * address, and the LONGEST word asks — it is the most selective one, and a\n * stored token is a prefix of a word, so `dana@acme.com` and `dana` both\n * reach the contact. Capped at the twelve characters a token keeps.\n */\nexport function crmPersonSearchWord(text: unknown): string {\n const raw = typeof text === 'string' ? text.trim() : ''\n if (!raw) return ''\n if (/^[\\d\\s()+.-]+$/.test(raw)) {\n const digits = raw.replace(/\\D/g, '')\n return digits.length >= 4 ? digits.slice(0, NAME_TOKEN_MAX_PREFIX) : ''\n }\n const words = nameSearchKey(raw).split(' ').filter(Boolean)\n const longest = words.reduce((best, word) => (word.length > best.length ? word : best), '')\n return longest.slice(0, NAME_TOKEN_MAX_PREFIX)\n}\n\n/**\n * A contacts walk's cursor (AGL-3639): the last contact's `updatedAt` to the\n * nanosecond, and its id for the tie. Opaque to every caller; a millisecond\n * alone would answer a contact written later in the same millisecond twice.\n */\nexport function encodeContactChangesCursor(updatedAt: unknown, id: string): string | null {\n const stamp = updatedAt as { seconds?: unknown; nanoseconds?: unknown } | null\n const seconds = Number(stamp?.seconds)\n const nanoseconds = Number(stamp?.nanoseconds ?? 0)\n if (!Number.isFinite(seconds) || !Number.isFinite(nanoseconds) || !id) return null\n return `${Math.trunc(seconds)}.${Math.trunc(nanoseconds)}.${id}`\n}\n\n/** Reads a cursor this module handed out, or `null` for anything else. */\nexport function decodeContactChangesCursor(\n cursor: string | null | undefined,\n): { seconds: number; nanoseconds: number; id: string } | null {\n const match = /^(\\d{1,12})\\.(\\d{1,9})\\.([A-Za-z0-9_-]{1,200})$/.exec(String(cursor ?? ''))\n if (!match) return null\n return { seconds: Number(match[1]), nanoseconds: Number(match[2]), id: match[3] }\n}\n\nconst millisOf = (value: unknown): number => {\n const stamp = value as { toMillis?: () => number } | null\n return typeof stamp?.toMillis === 'function' ? stamp.toMillis() : 0\n}\n\nconst integerOrNull = (value: unknown): number | null =>\n typeof value === 'number' && Number.isFinite(value) ? Math.round(value) : null\n\nexport function createCrmPersonRecords(deps: CrmPersonRecordsDeps): PluginPersonRecords {\n return {\n /**\n * The contact first, through the org's address index (AGL-2633) so an\n * address a merge folded into another record still names the person who\n * now holds it; then, for a caller that asked for any kind, the lead the\n * address is keyed as. Narrowed to the site when asked, on the document\n * the address names: a record the site cannot see is `null`.\n */\n async find(request: PluginPersonFindRequest) {\n const email = normalizeContactEmail(request.email)\n if (!email) return null\n const orgId = await orgOf(deps, request)\n const org = deps.firestore().collection('orgs').doc(orgId)\n const hostId = String(request.hostId ?? '').trim()\n const narrowTo = request.onlyVisibleToSite && hostId ? hostId : null\n const contact = await findContactByEmail(\n org.collection(STORED_IN[CRM_PERSON_KINDS.contact]),\n email,\n narrowTo ? { hostId: narrowTo } : {},\n )\n if (contact) return personOf(CRM_PERSON_KINDS.contact, contact)\n if (!request.anyKind) return null\n const key = personKey(email)\n if (!key) return null\n const lead = await org.collection(STORED_IN[CRM_PERSON_KINDS.lead]).doc(key).get()\n if (!lead.exists) return null\n if (narrowTo && !visibleToHost(lead.get('visibleTo') as string[] | undefined, narrowTo)) {\n return null\n }\n return personOf(CRM_PERSON_KINDS.lead, lead)\n },\n\n /** Every record named, read at once; a kind the CRM does not keep is `null`. */\n async read(request: PluginPersonReadRequest) {\n const firestore = deps.firestore()\n const org = firestore.collection('orgs').doc(request.orgId)\n const refs = request.records.map((record) => {\n const collection = STORED_IN[record.kind]\n const id = String(record.id ?? '').trim()\n return collection && id ? org.collection(collection).doc(id) : null\n })\n const wanted = refs.filter((ref): ref is FirebaseFirestore.DocumentReference => ref !== null)\n const snapshots: FirebaseFirestore.DocumentSnapshot[] = []\n for (let start = 0; start < wanted.length; start += GET_ALL_CHUNK) {\n snapshots.push(...(await firestore.getAll(...wanted.slice(start, start + GET_ALL_CHUNK))))\n }\n let at = 0\n return request.records.map((record, index) => {\n if (!refs[index]) return null\n const snapshot = snapshots[at++]\n return snapshot?.exists ? personOf(record.kind, snapshot) : null\n })\n },\n\n /**\n * Files the person under the containers as the site holds them: on a\n * contact, inside the site's consent-group facet — which campaigns a\n * merchant filed somebody under is that merchant's record, and written at\n * the top it would be readable by every site of an agency's account; on a\n * lead, in the lead's own membership field, whose list fields are then\n * restamped so the Leads list's filter finds it (AGL-3321).\n *\n * `update`, never a merge-set: a dotted path in `set` is a literal field\n * name, and the record must exist. A record the site cannot see is not\n * filed.\n */\n async fileUnder(request: PluginPersonFileRequest) {\n const ids = normalizeContainerIds(request.ids)\n const collection = STORED_IN[request.record.kind]\n const id = String(request.record.id ?? '').trim()\n if (!ids.length || !collection || !id) return { filed: false }\n const orgId = await orgOf(deps, request)\n const ref = deps.firestore().collection('orgs').doc(orgId).collection(collection).doc(id)\n const snapshot = await ref.get()\n if (!snapshot.exists) return { filed: false }\n if (!visibleToHost(snapshot.get('visibleTo') as string[] | undefined, request.hostId)) {\n return { filed: false }\n }\n if (request.record.kind === CRM_PERSON_KINDS.lead) {\n await ref.update({\n [containerMembershipField(request.containerKind)]: FieldValue.arrayUnion(...ids),\n updatedAt: FieldValue.serverTimestamp(),\n })\n await restampCrmListFieldsAt(ref, 'leads')\n return { filed: true }\n }\n const groupId = await deps.groupIdForHost(request.hostId)\n await ref.update({\n [contactContainerFieldPath(groupId, request.containerKind)]: FieldValue.arrayUnion(...ids),\n updatedAt: FieldValue.serverTimestamp(),\n })\n return { filed: true }\n },\n\n recordRefund: (request) => recordPersonRefund(request),\n\n /**\n * The people a saved view selects for a site (AGL-3234). A colleague's\n * private view is theirs: not listed for this member, so it does not\n * exist for them here either.\n *\n * A Leads view takes the site's most recently seen leads —\n * {@link CRM_LEADS_VIEW_WINDOW}, the Leads list's own window — narrowed by\n * the view's status clause the way the list narrows it: open leads when\n * the view names no status, one status when it does, everything for\n * `all`. Held, not merely seen: a lead another site only SHARED with this\n * one (AGL-3336) is not this site's to take.\n *\n * A Contacts view is read the way the dynamic-list sweep reads one, and\n * each address it selects is resolved to the contact the site may see. A\n * view filtering on something that reading cannot apply is refused whole:\n * dropping the filter would select more people than the view shows.\n */\n async peopleInView(request: PluginPersonViewRequest): Promise<PluginPersonViewPeople> {\n const firestore = deps.firestore()\n const org = firestore.collection('orgs').doc(request.orgId)\n const view = await org.collection(CRM_COLLECTIONS.views).doc(request.viewId).get()\n const data = view.exists ? ((view.data() ?? {}) as Record<string, unknown>) : null\n if (\n !data ||\n !crmViewIsListed(\n { shared: data['shared'] === true, ownerUid: String(data['ownerUid'] ?? '') },\n request.viewerUid,\n )\n ) {\n return { ok: false, reason: 'not-found' }\n }\n const filters = normalizeCrmViewFilters(data['filters'])\n if (data['section'] === 'leads') {\n const statusClause = filters.find((clause) => clause.field === 'status' && clause.op === 'equals')\n const wanted = String(statusClause?.value ?? 'open')\n // Narrowed to what the site may see before it is ordered (AGL-3275):\n // the collection is org-wide, and an agency's sequence must not be\n // offered another client's people.\n const window = await org\n .collection(STORED_IN[CRM_PERSON_KINDS.lead])\n .where('visibleTo', 'array-contains-any', scopeTokensForHost(request.hostId))\n .orderBy('lastSeenAtMs', 'desc')\n .limit(CRM_LEADS_VIEW_WINDOW + 1)\n .get()\n const matching = window.docs.slice(0, CRM_LEADS_VIEW_WINDOW).filter((doc) => {\n const lead = (doc.data() ?? {}) as Record<string, unknown>\n if (seenOnlyThroughGrant(lead, request.hostId)) return false\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 ok: true,\n people: matching.slice(0, request.limit).map((doc) => ({ kind: CRM_PERSON_KINDS.lead, id: doc.id })),\n total: matching.length,\n truncated: matching.length > request.limit || window.docs.length > CRM_LEADS_VIEW_WINDOW,\n }\n }\n if (data['section'] !== 'contacts') return { ok: false, reason: 'not-people' }\n const { unsupported } = dynamicListDimensionsForCrmView(filters)\n if (unsupported.length) {\n return {\n ok: false,\n reason: 'unsupported',\n unsupported: unsupported.map((clause) => clause.label || clause.field),\n }\n }\n const { emails, complete } = await deps.contactViewEmails({\n hostId: request.hostId,\n viewId: request.viewId,\n })\n const ordered = [...new Set(emails)].sort()\n const contacts = org.collection(STORED_IN[CRM_PERSON_KINDS.contact])\n const found = await Promise.all(\n ordered\n .slice(0, request.limit)\n .map((email) => findContactByEmail(contacts, email, { hostId: request.hostId })),\n )\n const ids = [...new Set(found.filter((snapshot) => snapshot).map((snapshot) => String(snapshot?.id)))]\n return {\n ok: true,\n people: ids.map((id): PluginPersonRecordRef => ({ kind: CRM_PERSON_KINDS.contact, id })),\n total: ordered.length,\n truncated: ordered.length > request.limit || !complete,\n }\n },\n\n /**\n * The contacts a site may see, oldest change first (AGL-3639): the walk a\n * connector copies people out by. On the Contacts list's own index\n * (`visibleTo` array-contains-any, `updatedAt` ascending), with the id as\n * the tie-break. A contact the site sees only through a sharing grant is\n * read and stepped over — another holder's person is not this site's to\n * export — and the cursor still moves past it. The profile is the site's\n * consent group's facet, never another holder's.\n */\n async changedSince(request: PluginPersonChangesRequest): Promise<PluginPersonChangesPage> {\n const hostId = String(request.hostId ?? '').trim()\n if (!hostId) return { people: [], next: null }\n const orgId = await orgOf(deps, request)\n const groupId = await deps.groupIdForHost(hostId)\n const after = decodeContactChangesCursor(request.after)\n let query = deps\n .firestore()\n .collection('orgs')\n .doc(orgId)\n .collection(STORED_IN[CRM_PERSON_KINDS.contact])\n .where('visibleTo', 'array-contains-any', scopeTokensForHost(hostId))\n .orderBy('updatedAt', 'asc')\n .orderBy(FieldPath.documentId(), 'asc')\n if (after) {\n query = query.startAfter(new Timestamp(after.seconds, after.nanoseconds), after.id)\n }\n const snapshot = await query.limit(Math.max(1, Math.min(500, request.limit))).get()\n const people: PluginPersonChange[] = []\n let next: string | null = null\n for (const doc of snapshot.docs) {\n const data = (doc.data() ?? {}) as Record<string, unknown>\n next = encodeContactChangesCursor(data['updatedAt'], doc.id) ?? next\n if (seenOnlyThroughGrant(data, hostId)) continue\n const email = normalizeContactEmail(data['email'])\n if (!email) continue\n const facet = readContactFacet(data, groupId)\n people.push({\n kind: CRM_PERSON_KINDS.contact,\n id: doc.id,\n email,\n data,\n changedAtMs: millisOf(data['updatedAt']),\n profile: {\n name: typeof facet.name === 'string' && facet.name.trim() ? facet.name.trim() : null,\n phone: typeof facet.phone === 'string' && facet.phone.trim() ? facet.phone.trim() : null,\n tags: Array.isArray(facet.tags) ? facet.tags.filter((tag) => typeof tag === 'string') : [],\n lifetimeValueCents: integerOrNull(facet.ltvCents),\n ordersCount: integerOrNull(facet.ordersCount),\n },\n })\n }\n return { people, next }\n },\n\n /**\n * Whether each person has ever written in: an inbound email on their\n * timeline, filed on the contact, or on the lead while they were one\n * (AGL-3234). One keyed query each; a failed one is unknown.\n */\n async wroteIn(request: PluginPersonWroteInRequest) {\n const activities = deps\n .firestore()\n .collection('orgs')\n .doc(request.orgId)\n .collection(CRM_COLLECTIONS.activities)\n return Promise.all(\n request.records.map(async (record) => {\n const field =\n record.kind === CRM_PERSON_KINDS.contact\n ? 'contactId'\n : record.kind === CRM_PERSON_KINDS.lead\n ? 'leadId'\n : null\n const id = String(record.id ?? '').trim()\n if (!field || !id) return null\n try {\n const snapshot = await activities\n .where(field, '==', id)\n .where('direction', '==', 'inbound')\n .limit(1)\n .get()\n return !snapshot.empty\n } catch (error) {\n console.error('[crm] inbound email lookup failed; reading as unknown', error)\n return null\n }\n }),\n )\n },\n\n /**\n * Contacts the site may see whose name, address, company or phone\n * starts with the typed word (AGL-3609): ONE query on the scoped search\n * tokens every contact writer stamps, `array-contains-any` over the\n * site's scope tokens joined to the word — the same clause the Contacts\n * list runs under a site, so a record the site cannot see is never\n * answered and no index beyond the field's own is needed.\n */\n async search(request: PluginPersonSearchRequest) {\n const word = crmPersonSearchWord(request.text)\n const hostId = String(request.hostId ?? '').trim()\n const limit = Math.min(CRM_PERSON_SEARCH_MAX, Math.max(0, Math.floor(Number(request.limit) || 0)))\n if (!word || !hostId || !limit) return []\n const orgId = await orgOf(deps, request)\n const tokens = scopeTokensForHost(hostId).map((scope) => `${scope}${CRM_SCOPED_SEARCH_JOIN}${word}`)\n const snapshot = await deps\n .firestore()\n .collection('orgs')\n .doc(orgId)\n .collection(STORED_IN[CRM_PERSON_KINDS.contact])\n .where(CRM_SCOPED_SEARCH_TOKENS_FIELD, 'array-contains-any', tokens)\n .limit(limit)\n .get()\n return snapshot.docs.map((doc) => personOf(CRM_PERSON_KINDS.contact, doc))\n },\n }\n}\n\n/** The service as the CRM registers it, its storage the admin app's. */\nexport const crmPersonRecords: Required<PluginPersonRecords> = {\n find: (request) => createCrmPersonRecords(defaultCrmPersonRecordsDeps()).find(request),\n read: (request) => createCrmPersonRecords(defaultCrmPersonRecordsDeps()).read(request),\n fileUnder: (request) => createCrmPersonRecords(defaultCrmPersonRecordsDeps()).fileUnder!(request),\n recordRefund: (request) => recordPersonRefund(request),\n peopleInView: (request) => createCrmPersonRecords(defaultCrmPersonRecordsDeps()).peopleInView!(request),\n wroteIn: (request) => createCrmPersonRecords(defaultCrmPersonRecordsDeps()).wroteIn!(request),\n search: (request) => createCrmPersonRecords(defaultCrmPersonRecordsDeps()).search!(request),\n changedSince: (request) => createCrmPersonRecords(defaultCrmPersonRecordsDeps()).changedSince!(request),\n}\n"],"names":["containerMembershipField","contactContainerFieldPath","normalizeContainerIds","normalizeContactEmail","readContactFacet","CRM_COLLECTIONS","CRM_SCOPED_SEARCH_JOIN","CRM_SCOPED_SEARCH_TOKENS_FIELD","crmLeadStatus","crmViewIsListed","isCrmLeadOpen","normalizeCrmViewFilters","dynamicListDimensionsForCrmView","NAME_TOKEN_MAX_PREFIX","nameSearchKey","personKey","scopeTokensForHost","seenOnlyThroughGrant","visibleToHost","consentGroupForSite","firebaseAdmin","resolveOrgIdForHost","findContactByEmail","restampCrmListFieldsAt","collectDynamicListCandidates","FieldPath","FieldValue","Timestamp","recordPersonRefund","CRM_PERSON_KINDS","contact","lead","STORED_IN","GET_ALL_CHUNK","CRM_LEADS_VIEW_WINDOW","defaultCrmPersonRecordsDeps","firestore","app","orgIdForHost","hostId","groupIdForHost","groupId","contactViewEmails","viewId","scan","rule","sources","emails","candidates","map","candidate","email","complete","personOf","kind","snapshot","data","id","orgOf","deps","request","named","String","orgId","trim","resolved","Error","CRM_PERSON_SEARCH_MAX","crmPersonSearchWord","text","raw","test","digits","replace","length","slice","words","split","filter","Boolean","longest","reduce","best","word","encodeContactChangesCursor","updatedAt","stamp","seconds","Number","nanoseconds","isFinite","Math","trunc","decodeContactChangesCursor","cursor","match","exec","millisOf","value","toMillis","integerOrNull","round","createCrmPersonRecords","find","org","collection","doc","narrowTo","onlyVisibleToSite","anyKind","key","get","exists","read","refs","records","record","wanted","ref","snapshots","start","push","getAll","at","index","fileUnder","ids","filed","update","containerKind","arrayUnion","serverTimestamp","recordRefund","peopleInView","view","views","shared","ownerUid","viewerUid","ok","reason","filters","statusClause","clause","field","op","window","where","orderBy","limit","matching","docs","people","total","truncated","unsupported","label","ordered","Set","sort","contacts","found","Promise","all","changedSince","next","after","query","documentId","startAfter","max","min","facet","changedAtMs","profile","name","phone","tags","Array","isArray","tag","lifetimeValueCents","ltvCents","ordersCount","wroteIn","activities","empty","error","console","search","floor","tokens","scope","crmPersonRecords"],"mappings":"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"}