@aglyn/plugins-crm 1.0.0-beta.199 → 1.0.0-beta.201

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 (169) hide show
  1. package/package.json +13 -13
  2. package/src/lib/components/activity-list.d.ts +36 -5
  3. package/src/lib/components/activity-list.js +159 -137
  4. package/src/lib/components/activity-list.js.map +1 -1
  5. package/src/lib/components/activity-timeline.d.ts +84 -0
  6. package/src/lib/components/activity-timeline.js +224 -0
  7. package/src/lib/components/activity-timeline.js.map +1 -0
  8. package/src/lib/components/companies-section.d.ts +6 -6
  9. package/src/lib/components/companies-section.js +54 -84
  10. package/src/lib/components/companies-section.js.map +1 -1
  11. package/src/lib/components/company-edit-drawer.js +17 -4
  12. package/src/lib/components/company-edit-drawer.js.map +1 -1
  13. package/src/lib/components/company-picker.js +6 -2
  14. package/src/lib/components/company-picker.js.map +1 -1
  15. package/src/lib/components/company-properties-card.js +53 -36
  16. package/src/lib/components/company-properties-card.js.map +1 -1
  17. package/src/lib/components/contact-associations-card.d.ts +3 -2
  18. package/src/lib/components/contact-associations-card.js +12 -12
  19. package/src/lib/components/contact-associations-card.js.map +1 -1
  20. package/src/lib/components/contact-detail-page.d.ts +4 -3
  21. package/src/lib/components/contact-detail-page.js +18 -12
  22. package/src/lib/components/contact-detail-page.js.map +1 -1
  23. package/src/lib/components/contact-list-columns.d.ts +3 -5
  24. package/src/lib/components/contact-list-columns.js +6 -8
  25. package/src/lib/components/contact-list-columns.js.map +1 -1
  26. package/src/lib/components/contact-properties-card.d.ts +21 -4
  27. package/src/lib/components/contact-properties-card.js +129 -29
  28. package/src/lib/components/contact-properties-card.js.map +1 -1
  29. package/src/lib/components/contact-timeline-card.d.ts +12 -6
  30. package/src/lib/components/contact-timeline-card.js +132 -174
  31. package/src/lib/components/contact-timeline-card.js.map +1 -1
  32. package/src/lib/components/contacts-bulk-bar.js +10 -1
  33. package/src/lib/components/contacts-bulk-bar.js.map +1 -1
  34. package/src/lib/components/contacts-section.js +282 -331
  35. package/src/lib/components/contacts-section.js.map +1 -1
  36. package/src/lib/components/crm-email-state-chip.js +4 -1
  37. package/src/lib/components/crm-email-state-chip.js.map +1 -1
  38. package/src/lib/components/crm-next-activity-column.d.ts +4 -2
  39. package/src/lib/components/crm-next-activity-column.js +6 -3
  40. package/src/lib/components/crm-next-activity-column.js.map +1 -1
  41. package/src/lib/components/deal-edit-drawer.js +8 -4
  42. package/src/lib/components/deal-edit-drawer.js.map +1 -1
  43. package/src/lib/components/deal-products-card.js +15 -13
  44. package/src/lib/components/deal-products-card.js.map +1 -1
  45. package/src/lib/components/deal-properties-card.js +7 -0
  46. package/src/lib/components/deal-properties-card.js.map +1 -1
  47. package/src/lib/components/deal-stage-card.js +31 -29
  48. package/src/lib/components/deal-stage-card.js.map +1 -1
  49. package/src/lib/components/deals-section.d.ts +2 -2
  50. package/src/lib/components/deals-section.js +67 -97
  51. package/src/lib/components/deals-section.js.map +1 -1
  52. package/src/lib/components/fields-section.d.ts +4 -0
  53. package/src/lib/components/fields-section.js +368 -154
  54. package/src/lib/components/fields-section.js.map +1 -1
  55. package/src/lib/components/form-contact-fields-card.js +24 -20
  56. package/src/lib/components/form-contact-fields-card.js.map +1 -1
  57. package/src/lib/components/lead-campaigns-card.d.ts +1 -1
  58. package/src/lib/components/lead-campaigns-card.js +43 -38
  59. package/src/lib/components/lead-campaigns-card.js.map +1 -1
  60. package/src/lib/components/lead-properties-card.d.ts +14 -6
  61. package/src/lib/components/lead-properties-card.js +323 -311
  62. package/src/lib/components/lead-properties-card.js.map +1 -1
  63. package/src/lib/components/lead-surfaces-note.js +13 -2
  64. package/src/lib/components/lead-surfaces-note.js.map +1 -1
  65. package/src/lib/components/leads-bulk-bar.js +16 -3
  66. package/src/lib/components/leads-bulk-bar.js.map +1 -1
  67. package/src/lib/components/leads-section.d.ts +2 -1
  68. package/src/lib/components/leads-section.js +110 -148
  69. package/src/lib/components/leads-section.js.map +1 -1
  70. package/src/lib/components/linked-deals-card.js +12 -10
  71. package/src/lib/components/linked-deals-card.js.map +1 -1
  72. package/src/lib/components/recent-activity-feed.d.ts +12 -8
  73. package/src/lib/components/recent-activity-feed.js +54 -29
  74. package/src/lib/components/recent-activity-feed.js.map +1 -1
  75. package/src/lib/components/record-activity-card.d.ts +4 -2
  76. package/src/lib/components/record-activity-card.js +29 -11
  77. package/src/lib/components/record-activity-card.js.map +1 -1
  78. package/src/lib/components/reports/activity-card.js +18 -12
  79. package/src/lib/components/reports/activity-card.js.map +1 -1
  80. package/src/lib/components/reports/lead-funnel-card.js +47 -37
  81. package/src/lib/components/reports/lead-funnel-card.js.map +1 -1
  82. package/src/lib/components/reports/pipeline-card.js +22 -16
  83. package/src/lib/components/reports/pipeline-card.js.map +1 -1
  84. package/src/lib/components/reports/report-export.d.ts +4 -11
  85. package/src/lib/components/reports/report-export.js +16 -33
  86. package/src/lib/components/reports/report-export.js.map +1 -1
  87. package/src/lib/components/reports/source-conversion-card.js +18 -12
  88. package/src/lib/components/reports/source-conversion-card.js.map +1 -1
  89. package/src/lib/components/reports/tasks-card.js +35 -25
  90. package/src/lib/components/reports/tasks-card.js.map +1 -1
  91. package/src/lib/components/tasks-section.js +151 -144
  92. package/src/lib/components/tasks-section.js.map +1 -1
  93. package/src/lib/constants/company-filters.d.ts +13 -0
  94. package/src/lib/constants/company-filters.js +47 -17
  95. package/src/lib/constants/company-filters.js.map +1 -1
  96. package/src/lib/constants/contact-filters.d.ts +61 -10
  97. package/src/lib/constants/contact-filters.js +373 -141
  98. package/src/lib/constants/contact-filters.js.map +1 -1
  99. package/src/lib/constants/deal-filters.d.ts +44 -0
  100. package/src/lib/constants/deal-filters.js +78 -0
  101. package/src/lib/constants/deal-filters.js.map +1 -0
  102. package/src/lib/constants/field-list-query.d.ts +23 -0
  103. package/src/lib/constants/field-list-query.js +80 -0
  104. package/src/lib/constants/field-list-query.js.map +1 -0
  105. package/src/lib/hooks/use-crm-list-query.d.ts +68 -0
  106. package/src/lib/hooks/use-crm-list-query.js +182 -0
  107. package/src/lib/hooks/use-crm-list-query.js.map +1 -0
  108. package/src/lib/hooks/use-crm-tasks.d.ts +23 -6
  109. package/src/lib/hooks/use-crm-tasks.js +44 -42
  110. package/src/lib/hooks/use-crm-tasks.js.map +1 -1
  111. package/src/lib/hooks/use-deals.d.ts +2 -34
  112. package/src/lib/hooks/use-deals.js +3 -62
  113. package/src/lib/hooks/use-deals.js.map +1 -1
  114. package/src/lib/model/crm-list-query.d.ts +91 -0
  115. package/src/lib/model/crm-list-query.js +182 -0
  116. package/src/lib/model/crm-list-query.js.map +1 -0
  117. package/src/lib/model/lead-filters.d.ts +50 -44
  118. package/src/lib/model/lead-filters.js +194 -88
  119. package/src/lib/model/lead-filters.js.map +1 -1
  120. package/src/lib/model/list-fields-api.d.ts +26 -0
  121. package/src/lib/model/list-fields-api.js +47 -0
  122. package/src/lib/model/list-fields-api.js.map +1 -0
  123. package/src/lib/model/list-fields.d.ts +47 -0
  124. package/src/lib/model/list-fields.js +39 -0
  125. package/src/lib/model/list-fields.js.map +1 -0
  126. package/src/lib/model/task-views.d.ts +15 -10
  127. package/src/lib/model/task-views.js +68 -9
  128. package/src/lib/model/task-views.js.map +1 -1
  129. package/src/lib/server/capture-contact.js +7 -5
  130. package/src/lib/server/capture-contact.js.map +1 -1
  131. package/src/lib/server/companies-import.js +3 -3
  132. package/src/lib/server/companies-import.js.map +1 -1
  133. package/src/lib/server/company-delete.js +3 -1
  134. package/src/lib/server/company-delete.js.map +1 -1
  135. package/src/lib/server/consent-group-participant.js +4 -0
  136. package/src/lib/server/consent-group-participant.js.map +1 -1
  137. package/src/lib/server/contact-update.js +14 -8
  138. package/src/lib/server/contact-update.js.map +1 -1
  139. package/src/lib/server/contacts-import.js +4 -2
  140. package/src/lib/server/contacts-import.js.map +1 -1
  141. package/src/lib/server/deals-import.js +4 -2
  142. package/src/lib/server/deals-import.js.map +1 -1
  143. package/src/lib/server/erase-person.js +2 -1
  144. package/src/lib/server/erase-person.js.map +1 -1
  145. package/src/lib/server/lead-create.js +4 -1
  146. package/src/lib/server/lead-create.js.map +1 -1
  147. package/src/lib/server/lead-source-values.js +9 -5
  148. package/src/lib/server/lead-source-values.js.map +1 -1
  149. package/src/lib/server/leads-import.js +4 -1
  150. package/src/lib/server/leads-import.js.map +1 -1
  151. package/src/lib/server/list-fields-routes.d.ts +29 -0
  152. package/src/lib/server/list-fields-routes.js +85 -0
  153. package/src/lib/server/list-fields-routes.js.map +1 -0
  154. package/src/lib/server/record-email-state.js +5 -2
  155. package/src/lib/server/record-email-state.js.map +1 -1
  156. package/src/lib/server/record-timeline.js +3 -2
  157. package/src/lib/server/record-timeline.js.map +1 -1
  158. package/src/lib/server/task-routes.js +6 -3
  159. package/src/lib/server/task-routes.js.map +1 -1
  160. package/src/lib/server/tasks-import.js +5 -2
  161. package/src/lib/server/tasks-import.js.map +1 -1
  162. package/src/lib/server.js +10 -1
  163. package/src/lib/server.js.map +1 -1
  164. package/src/lib/components/contact-custom-fields-card.d.ts +0 -39
  165. package/src/lib/components/contact-custom-fields-card.js +0 -204
  166. package/src/lib/components/contact-custom-fields-card.js.map +0 -1
  167. package/src/lib/hooks/use-org-leads.d.ts +0 -50
  168. package/src/lib/hooks/use-org-leads.js +0 -104
  169. package/src/lib/hooks/use-org-leads.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/consent-group-participant.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * THE CRM'S SHARE OF A CONSENT GROUP CHANGE (AGL-3320).\n *\n * Two things the CRM keeps are read through the CURRENT consent group, so a\n * change to the declaration moves under them unless they move too:\n *\n * - **C4 — a contact's per-site refusal.** `marketingConsentByHost.{site}`\n * holding `marketingConsent: false` refuses mail from every site of that\n * site's group (`readMarketingBasis`). When two sites stop being one\n * sender, each keeps the refusals the other held: the refusal is written\n * onto the receiving site's entry by dotted path, with the one it\n * replaced kept as `supersededEntry`. A refusal is carried whenever it was\n * given — over-carrying is the safe direction — and one the receiving site\n * already holds is left alone. Only contacts carry per-site refusals;\n * leads and list memberships carry grants alone.\n * - **The holder records**, `facets.{groupId}` — a profile, an owner, a\n * stage, notes, order figures and a timeline, keyed by the group that\n * keeps them. Every writer keys by the declaration in force, so after the\n * flip a site's records must be under its new key:\n * - a MOVE folds every old key's facet into the new key's, with the\n * existing target first and then the sources in the order the person\n * met their sites, through `mergeContactFacet` — the survivor's\n * values win, lists union, figures sum — and deletes each source in\n * the same write, so a figure is summed exactly once;\n * - a SPLIT copies the facet, without its figures and with only the\n * timeline entries of the successor's own sites, to each successor\n * that met the person itself, merged under whatever the successor\n * already holds. The figures follow the capture only when every site\n * that met the person lands under one key; otherwise they stay with the\n * surviving key, or — when the key dissolves — are dropped and counted.\n *\n * ## Run twice, write once\n *\n * The executor runs `carry` before the flip and again as the catch-up, and\n * the sweep runs everything once more. A carried refusal is skipped once the\n * receiving entry refuses; a MOVE or a dissolving SPLIT deletes its source in\n * the write that folds it, so a second pass finds nothing — except a\n * straggler a long job wrote under the old key after the first pass, which is\n * exactly what the sweep exists to fold in. A SPLIT whose key survives stays\n * a live key, so each copy is stamped with the change that made it\n * (`copiedByChange`), and a pass that finds the stamp does not copy again.\n */\n\nimport { mergeContactFacet } from '@aglyn/aglyn/app-utils/contact-merge'\nimport type {\n ConsentGroupChangePlan,\n ConsentGroupChangePreviewLine,\n ConsentGroupHolderMove,\n ConsentGroupHolderSplit,\n} from '@aglyn/aglyn/app-utils/consent-group-change'\nimport {\n CONTACT_FACETS_FIELD,\n type ContactInteraction,\n interactionsForGroup,\n} from '@aglyn/aglyn/app-utils/contacts'\nimport {\n COMPANY_CONTACTS_COUNT_FIELD,\n CONTACT_COMPANY_IDS_FIELD,\n CRM_COLLECTIONS,\n} from '@aglyn/aglyn/app-utils/crm'\nimport {\n CAPTURED_BY_HOST_FIELD,\n CONSENT_GROUP_ID_FIELD,\n MARKETING_CONSENT_BY_HOST_FIELD,\n MARKETING_CONSENT_FIELD,\n MARKETING_CONSENT_SOURCE_FIELD,\n} from '@aglyn/aglyn/app-utils/marketing-consent'\nimport type {\n ConsentGroupChangeParticipant,\n ConsentGroupChangeRunRequest,\n ConsentGroupChangeRunResult,\n} from '@aglyn/aglyn/plugin-manager/plugin-consent-group-change'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { FieldPath, FieldValue } from 'firebase-admin/firestore'\nimport { summarizeConsentGroupChange } from '../model/consent-group-summary'\n\n/** Contacts one page of the refusal carry reads. */\nexport const REFUSAL_PAGE = 300\n\n/** Contacts one page of the holder pass reads. */\nexport const HOLDER_PAGE = 200\n\n/**\n * The most records a preview reads to answer a count no aggregation can:\n * past it, the line says so with a `null` count rather than guess.\n */\nexport const PREVIEW_SCAN_LIMIT = 5_000\n\n/**\n * The figures a facet sums, and the instants that go with them — what\n * `mergeContactFacet` adds up or takes the earliest or latest of. A split\n * cannot divide one person's purchases between two holders, so a copy\n * leaves them behind.\n */\nexport const HOLDER_FIGURE_FIELDS = [\n 'ltvCents',\n 'ordersCount',\n 'refundedCents',\n 'refundedOrdersCount',\n 'firstPurchaseAtMs',\n 'lastPurchaseAtMs',\n 'lastRefundAtMs',\n 'lastEmailEngagementAtMs',\n] as const\n\n/** The stamp a split's copy carries: `{ [sourceKey]: changeId }`. */\nexport const COPIED_BY_CHANGE_FIELD = 'copiedByChange'\n\ntype Doc = Record<string, unknown>\n\n/** The error codes that mean the contact was written since it was read. */\nconst RACED = new Set([5, 9])\nconst codeOf = (error: unknown): number | undefined => (error as { code?: number } | null)?.code\n\nfunction mapOf(value: unknown): Doc {\n return value && typeof value === 'object' && !Array.isArray(value) ? (value as Doc) : {}\n}\n\n/** The sites that met this person, in the order they did — then the legacy `hostId`. */\nfunction captureOrder(contact: Doc): string[] {\n const raw = contact[CAPTURED_BY_HOST_FIELD]\n const ordered = Array.isArray(raw)\n ? raw.map((id) => String(id ?? '').trim()).filter(Boolean)\n : []\n const legacy = typeof contact['hostId'] === 'string' ? (contact['hostId'] as string) : ''\n return legacy && !ordered.includes(legacy) ? [...ordered, legacy] : ordered\n}\n\nfunction withoutFigures(facet: Doc): Doc {\n const out: Doc = { ...facet }\n for (const field of HOLDER_FIGURE_FIELDS) delete out[field]\n return out\n}\n\nfunction figuresOf(facet: Doc): Doc {\n const out: Doc = {}\n for (const field of HOLDER_FIGURE_FIELDS) {\n if (field in facet) out[field] = facet[field]\n }\n return out\n}\n\n/** What one contact's holder records become, as a dotted `update()` patch. */\nexport interface HolderPatch {\n patch: Doc\n combined: number\n copied: number\n figuresDropped: number\n /** Each company whose contacts count moves, by how much. */\n companyCounts: Array<{ companyId: string; delta: 1 | -1 }>\n}\n\n/**\n * The holder records of one contact after the plan's flows — or `null` when\n * nothing moves.\n *\n * `stragglersOnly` is the sweep: only the flows that delete their source\n * (MOVE, and a SPLIT whose key dissolves) run, because only those can have\n * left anything behind that a later writer recreated. A key that survived a\n * split is a live key after the flip, and its new writes belong to it.\n */\nexport function planHolderRecords(\n contact: Doc,\n plan: Pick<ConsentGroupChangePlan, 'flows'>,\n changeId: string,\n options: { stragglersOnly?: boolean } = {},\n): HolderPatch | null {\n const facets = mapOf(contact[CONTACT_FACETS_FIELD]) as Record<string, Doc>\n const captured = captureOrder(contact)\n const next: Record<string, Doc | null> = {}\n const current = (key: string): Doc | undefined =>\n key in next ? (next[key] ?? undefined) : facets[key]\n let combined = 0\n let copied = 0\n let figuresDropped = 0\n\n const splits = plan.flows.filter(\n (flow): flow is ConsentGroupHolderSplit =>\n flow.kind === 'split' && !(options.stragglersOnly && flow.survives),\n )\n const copies: Array<{ key: string; facet: Doc }> = []\n for (const split of splits) {\n const source = facets[split.from]\n if (!source || typeof source !== 'object') continue\n if (\n split.survives &&\n split.successors.some(\n (successor) => mapOf(facets[successor.key]?.[COPIED_BY_CHANGE_FIELD])[split.from] === changeId,\n )\n ) {\n continue\n }\n const keyOf = (hostId: string) =>\n split.stayHostIds.includes(hostId)\n ? split.from\n : (split.successors.find((successor) => successor.hostIds.includes(hostId))?.key ?? null)\n let eligible = split.successors.filter((successor) =>\n successor.hostIds.some((hostId) => captured.includes(hostId)),\n )\n if (!split.survives && !eligible.length) eligible = split.successors\n const landing = [\n ...new Set(\n captured\n .filter((hostId) => split.fromHostIds.includes(hostId))\n .map(keyOf)\n .filter((key): key is string => key !== null),\n ),\n ]\n const figuresTo =\n landing.length === 1 && landing[0] !== split.from && eligible.some((s) => s.key === landing[0])\n ? landing[0]\n : null\n for (const successor of eligible) {\n const copy: Doc = {\n ...withoutFigures(source),\n interactions: interactionsForGroup(\n (source['interactions'] ?? []) as ContactInteraction[],\n successor.hostIds,\n ),\n ...(successor.key === figuresTo ? figuresOf(source) : {}),\n }\n if (split.survives) copy[COPIED_BY_CHANGE_FIELD] = { [split.from]: changeId }\n copies.push({ key: successor.key, facet: copy })\n }\n if (split.survives) {\n if (figuresTo) next[split.from] = withoutFigures(current(split.from) ?? source)\n } else {\n next[split.from] = null\n const figures = figuresOf(source)\n if (!figuresTo && Object.keys(figures).some((field) => field !== 'lastEmailEngagementAtMs')) {\n figuresDropped += 1\n }\n }\n }\n\n const moves = new Map<string, ConsentGroupHolderMove[]>()\n for (const flow of plan.flows) {\n if (flow.kind !== 'move' || !facets[flow.from]) continue\n moves.set(flow.to, [...(moves.get(flow.to) ?? []), flow])\n }\n const metAt = (hostIds: readonly string[]) => {\n const indexes = hostIds.map((hostId) => captured.indexOf(hostId)).filter((index) => index >= 0)\n return indexes.length ? Math.min(...indexes) : Number.POSITIVE_INFINITY\n }\n for (const [target, sources] of moves) {\n const ordered = [...sources].sort(\n (a, b) => metAt(a.fromHostIds) - metAt(b.fromHostIds) || (a.from < b.from ? -1 : a.from > b.from ? 1 : 0),\n )\n let folded = current(target)\n // A person is COMBINED when two or more records become one; a lone\n // record under a new key is only moved.\n if (ordered.length + (folded ? 1 : 0) >= 2) combined += 1\n for (const source of ordered) {\n const facet = facets[source.from]\n folded = folded ? mergeContactFacet(folded, facet) : { ...facet }\n next[source.from] = null\n }\n if (folded) next[target] = folded\n }\n\n for (const { key, facet } of copies) {\n const existing = current(key)\n const merged = existing ? mergeContactFacet(existing, facet) : facet\n // The stamps are a union, never the survivor's alone: the merge keeps a\n // map the survivor already has, and a lost stamp would copy again.\n const stamps = {\n ...mapOf(existing?.[COPIED_BY_CHANGE_FIELD]),\n ...mapOf(facet[COPIED_BY_CHANGE_FIELD]),\n }\n if (Object.keys(stamps).length) merged[COPIED_BY_CHANGE_FIELD] = stamps\n next[key] = merged\n copied += 1\n }\n\n const keys = Object.keys(next)\n if (!keys.length) return null\n\n /*\n * The company mirror follows the facets: an id that no facet names any\n * more leaves it, and one a facet newly names joins it. Ids the mirror held\n * without a facet naming them — links older than the facets — are left.\n */\n const named = (source: Record<string, Doc | null | undefined>) =>\n new Set(\n Object.values(source)\n .map((facet) => (facet ? facet['companyId'] : null))\n .filter((id): id is string => typeof id === 'string' && id !== ''),\n )\n const after: Record<string, Doc | null> = { ...facets, ...next }\n const before = named(facets)\n const now = named(after)\n const mirror = Array.isArray(contact[CONTACT_COMPANY_IDS_FIELD])\n ? (contact[CONTACT_COMPANY_IDS_FIELD] as unknown[]).filter(\n (id): id is string => typeof id === 'string' && id !== '',\n )\n : []\n const companyCounts: HolderPatch['companyCounts'] = []\n const leaving = [...before].filter((id) => !now.has(id))\n const joining = [...now].filter((id) => !before.has(id))\n for (const id of leaving) if (mirror.includes(id)) companyCounts.push({ companyId: id, delta: -1 })\n for (const id of joining) if (!mirror.includes(id)) companyCounts.push({ companyId: id, delta: 1 })\n\n const patch: Doc = {}\n for (const key of keys) {\n patch[`${CONTACT_FACETS_FIELD}.${key}`] = next[key] === null ? FieldValue.delete() : next[key]\n }\n if (companyCounts.length) {\n patch[CONTACT_COMPANY_IDS_FIELD] = [\n ...mirror.filter((id) => !leaving.includes(id)),\n ...joining.filter((id) => !mirror.includes(id)),\n ]\n }\n patch['updatedAt'] = FieldValue.serverTimestamp()\n return { patch, combined, copied, figuresDropped, companyCounts }\n}\n\n/**\n * The refusal `from` holds, carried onto `to` — or `null` when `to` already\n * refuses or `from` does not.\n */\nexport function planRefusalCarry(\n contact: Doc,\n carry: { toHostId: string; fromHostId: string },\n changeId: string,\n): Doc | null {\n const byHost = mapOf(contact[MARKETING_CONSENT_BY_HOST_FIELD])\n const source = mapOf(byHost[carry.fromHostId])\n if (source[MARKETING_CONSENT_FIELD] !== false) return null\n const target = byHost[carry.toHostId]\n if (mapOf(target)[MARKETING_CONSENT_FIELD] === false) return null\n const entry: Doc = {\n [MARKETING_CONSENT_FIELD]: false,\n ...(source['marketingConsentAtMs'] !== undefined\n ? { marketingConsentAtMs: source['marketingConsentAtMs'] }\n : {}),\n ...(source[MARKETING_CONSENT_SOURCE_FIELD] !== undefined\n ? { [MARKETING_CONSENT_SOURCE_FIELD]: source[MARKETING_CONSENT_SOURCE_FIELD] }\n : {}),\n carriedFromHostId: carry.fromHostId,\n carriedByChangeId: changeId,\n ...(target && typeof target === 'object' ? { supersededEntry: target } : {}),\n }\n return {\n [`${MARKETING_CONSENT_BY_HOST_FIELD}.${carry.toHostId}`]: entry,\n updatedAt: FieldValue.serverTimestamp(),\n }\n}\n\n/*==========================================\n * THE PASSES\n *=========================================*/\n\ninterface Cursor {\n /** `refusals` walks the carries; `holders` walks every contact. */\n stage: 'refusals' | 'holders'\n /** The carry being walked, in `refusals`. */\n carry: number\n after: string | null\n}\n\nfunction readCursor(raw: string | null, first: Cursor['stage']): Cursor {\n if (raw) {\n try {\n const parsed = JSON.parse(raw) as Cursor\n if (parsed && (parsed.stage === 'refusals' || parsed.stage === 'holders')) return parsed\n } catch {\n // A cursor this participant did not write starts the phase over, which\n // every pass is safe to do.\n }\n }\n return { stage: first, carry: 0, after: null }\n}\n\nexport interface ConsentGroupParticipantOptions {\n firestore?: () => FirebaseFirestore.Firestore\n now?: () => number\n}\n\n/** One contact write, guarded by the read it was computed from. */\ninterface Pending {\n snapshot: FirebaseFirestore.QueryDocumentSnapshot | FirebaseFirestore.DocumentSnapshot\n patch: Doc\n after?: () => Promise<void>\n}\n\n/**\n * Writes a page's patches, each with a `lastUpdateTime` precondition, and\n * recomputes the ones that raced once from a fresh read. Answers how many\n * were written.\n */\nasync function writeGuarded(\n firestore: FirebaseFirestore.Firestore,\n pending: Pending[],\n recompute: (snapshot: FirebaseFirestore.DocumentSnapshot) => Pending | null,\n): Promise<number> {\n let written = 0\n let due = pending\n for (let round = 0; round < 2 && due.length; round += 1) {\n const writer = firestore.bulkWriter()\n const outcomes = due.map((entry) =>\n writer\n .update(entry.snapshot.ref, entry.patch, {\n lastUpdateTime: entry.snapshot.updateTime as FirebaseFirestore.Timestamp,\n })\n .then(\n () => ({ entry, error: null as unknown }),\n (error: unknown) => ({ entry, error: error ?? new Error('The write failed') }),\n ),\n )\n await writer.close()\n const raced: Pending[] = []\n for (const { entry, error } of await Promise.all(outcomes)) {\n if (error === null) {\n written += 1\n await entry.after?.()\n continue\n }\n const code = codeOf(error)\n if (code === undefined || !RACED.has(code)) throw error\n raced.push(entry)\n }\n if (!raced.length) return written\n const fresh = await firestore.getAll(...raced.map((entry) => entry.snapshot.ref))\n due = fresh\n .filter((snapshot) => snapshot.exists)\n .map(recompute)\n .filter((entry): entry is Pending => entry !== null)\n }\n if (due.length) {\n throw new Error(`[crm] ${due.length} contact(s) kept changing under the consent group change`)\n }\n return written\n}\n\n/**\n * The CRM's participant. `firestore` and `now` are injectable for the specs;\n * the defaults are the Admin SDK's and the wall clock.\n */\nexport function createConsentGroupParticipant(\n options: ConsentGroupParticipantOptions = {},\n): ConsentGroupChangeParticipant {\n const db = () => (options.firestore ?? (() => firebaseAdmin.app().firestore()))()\n const now = options.now ?? Date.now\n const contactsOf = (orgId: string) => db().collection('orgs').doc(orgId).collection('contacts')\n\n async function countOf(query: FirebaseFirestore.Query): Promise<number> {\n return Number((await query.count().get()).data().count) || 0\n }\n\n /** One page of one carry's refusals. */\n async function refusalPage(\n request: ConsentGroupChangeRunRequest,\n cursor: Cursor,\n counts: Record<string, number>,\n ): Promise<Cursor | null> {\n const carries = request.plan.carries\n if (cursor.carry >= carries.length) return null\n const carry = carries[cursor.carry]\n let query = contactsOf(request.orgId)\n .where(`${MARKETING_CONSENT_BY_HOST_FIELD}.${carry.fromHostId}.${MARKETING_CONSENT_FIELD}`, '==', false)\n .orderBy(FieldPath.documentId())\n .limit(REFUSAL_PAGE)\n if (cursor.after) query = query.startAfter(cursor.after)\n const page = await query.get()\n const plan = (snapshot: FirebaseFirestore.DocumentSnapshot): Pending | null => {\n const patch = planRefusalCarry((snapshot.data() ?? {}) as Doc, carry, request.changeId)\n return patch ? { snapshot, patch } : null\n }\n const pending = page.docs.map(plan).filter((entry): entry is Pending => entry !== null)\n counts['refusals'] = (counts['refusals'] ?? 0) +\n (request.dryRun ? pending.length : await writeGuarded(db(), pending, plan))\n if (page.size < REFUSAL_PAGE) return { stage: 'refusals', carry: cursor.carry + 1, after: null }\n return { stage: 'refusals', carry: cursor.carry, after: page.docs[page.docs.length - 1].id }\n }\n\n /** One page of the holder pass over every contact. */\n async function holderPage(\n request: ConsentGroupChangeRunRequest,\n cursor: Cursor,\n counts: Record<string, number>,\n ): Promise<Cursor | null> {\n let query = contactsOf(request.orgId).orderBy(FieldPath.documentId()).limit(HOLDER_PAGE)\n if (cursor.after) query = query.startAfter(cursor.after)\n const page = await query.get()\n const companies = db().collection('orgs').doc(request.orgId).collection(CRM_COLLECTIONS.companies)\n const stragglersOnly = request.phase === 'sweep'\n const tally = { combined: 0, copied: 0, figuresDropped: 0 }\n const count = (planned: HolderPatch) => {\n tally.combined += planned.combined\n tally.copied += planned.copied\n tally.figuresDropped += planned.figuresDropped\n }\n const plan = (snapshot: FirebaseFirestore.DocumentSnapshot): (Pending & { planned: HolderPatch }) | null => {\n const planned = planHolderRecords((snapshot.data() ?? {}) as Doc, request.plan, request.changeId, {\n stragglersOnly,\n })\n if (!planned) return null\n return {\n snapshot,\n planned,\n patch: planned.patch,\n after: async () => {\n count(planned)\n // A count that could not move is logged and left: the company's own\n // page takes the live aggregate, which corrects it.\n await Promise.all(\n planned.companyCounts.map((change) =>\n companies\n .doc(change.companyId)\n .update({ [COMPANY_CONTACTS_COUNT_FIELD]: FieldValue.increment(change.delta) })\n .catch((error: unknown) => {\n console.error('[crm] company contacts count could not move', change.companyId, error)\n }),\n ),\n )\n },\n }\n }\n const pending = page.docs\n .map(plan)\n .filter((entry): entry is Pending & { planned: HolderPatch } => entry !== null)\n if (request.dryRun) for (const entry of pending) count(entry.planned)\n else await writeGuarded(db(), pending, plan)\n counts['combined'] = (counts['combined'] ?? 0) + tally.combined\n counts['copied'] = (counts['copied'] ?? 0) + tally.copied\n counts['figuresDropped'] = (counts['figuresDropped'] ?? 0) + tally.figuresDropped\n if (page.size < HOLDER_PAGE) return null\n return { stage: 'holders', carry: 0, after: page.docs[page.docs.length - 1].id }\n }\n\n return {\n async run(request): Promise<ConsentGroupChangeRunResult> {\n const counts: Record<string, number> = {}\n const hasHolderFlows = request.plan.flows.length > 0\n // `carry` walks the refusals; `rehome` the holders; `sweep` both.\n let cursor: Cursor | null = readCursor(\n request.cursor,\n request.phase === 'rehome' ? 'holders' : 'refusals',\n )\n while (cursor && now() < request.deadlineMs) {\n if (cursor.stage === 'refusals') {\n const next: Cursor | null = await refusalPage(request, cursor, counts)\n cursor =\n next ??\n (request.phase === 'sweep' && hasHolderFlows\n ? { stage: 'holders', carry: 0, after: null }\n : null)\n continue\n }\n cursor = hasHolderFlows ? await holderPage(request, cursor, counts) : null\n }\n return { done: cursor === null, cursor: cursor ? JSON.stringify(cursor) : null, counts }\n },\n\n async preview({ orgId, plan }): Promise<ConsentGroupChangePreviewLine[]> {\n const contacts = contactsOf(orgId)\n const lines: ConsentGroupChangePreviewLine[] = []\n const moves = plan.flows.filter((flow): flow is ConsentGroupHolderMove => flow.kind === 'move')\n const splits = plan.flows.filter((flow): flow is ConsentGroupHolderSplit => flow.kind === 'split')\n const people = (count: number | null) => (count === 1 ? 'person' : 'people')\n const figure = (count: number | null) => (count === null ? 'some' : String(count))\n\n /*\n * `crm.combine`: the people whose records under two or more of the\n * folding keys become one. No aggregation can ask \"two or more\", so the\n * records under each source key are read — bounded, and `null` past\n * the bound — and put through the same fold the re-home runs.\n */\n if (moves.length) {\n const seen = new Map<string, Doc>()\n let bounded = true\n for (const move of moves) {\n const page = await contacts\n .where(`${CONTACT_FACETS_FIELD}.${move.from}`, '!=', null)\n .limit(PREVIEW_SCAN_LIMIT + 1)\n .get()\n if (page.size > PREVIEW_SCAN_LIMIT) bounded = false\n for (const doc of page.docs) seen.set(doc.id, (doc.data() ?? {}) as Doc)\n }\n const combine = bounded\n ? [...seen.values()].reduce(\n (total, contact) =>\n total + (planHolderRecords(contact, { flows: moves }, '')?.combined ?? 0),\n 0,\n )\n : null\n lines.push({\n id: 'crm.combine',\n text: `The CRM records these sites keep about the same person will be combined for ${figure(combine)} ${people(combine)}.`,\n count: combine,\n severity: 'info',\n })\n }\n\n if (splits.length) {\n /*\n * `crm.copy`: the people a separating site takes a copy for. When the\n * key survives, those are the people the separating sites met\n * themselves; when it dissolves, everybody it holds, since a record\n * nobody met goes to every successor.\n */\n let copy = 0\n for (const split of splits) {\n if (!split.survives) {\n copy += await countOf(contacts.where(`${CONTACT_FACETS_FIELD}.${split.from}`, '!=', null))\n continue\n }\n const leavers = split.successors.flatMap((successor) => successor.hostIds)\n for (let index = 0; index < leavers.length; index += 30) {\n copy += await countOf(\n contacts.where(CAPTURED_BY_HOST_FIELD, 'array-contains-any', leavers.slice(index, index + 30)),\n )\n }\n }\n lines.push({\n id: 'crm.copy',\n text: `The separating sites keep a copy of the CRM record for the ${copy} ${people(copy)} they met themselves.`,\n count: copy,\n severity: 'info',\n })\n\n /*\n * `crm.grants` and `crm.visibility`, per separating site: the people\n * who signed up to the group through it and still hold that grant,\n * and the people it can see without having met them.\n */\n let grants = 0\n let visible = 0\n for (const split of splits) {\n for (const hostId of split.successors.flatMap((successor) => successor.hostIds)) {\n grants += await countOf(\n contacts\n .where(`${MARKETING_CONSENT_BY_HOST_FIELD}.${hostId}.${CONSENT_GROUP_ID_FIELD}`, '==', split.from)\n .where(`${MARKETING_CONSENT_BY_HOST_FIELD}.${hostId}.${MARKETING_CONSENT_FIELD}`, '==', true),\n )\n const seen = await countOf(contacts.where('visibleTo', 'array-contains', `host:${hostId}`))\n const met = await countOf(contacts.where(CAPTURED_BY_HOST_FIELD, 'array-contains', hostId))\n visible += Math.max(0, seen - met)\n }\n }\n lines.push({\n id: 'crm.grants',\n text: `The separating sites can still email the ${grants} ${people(grants)} who signed up to the group while they were part of it.`,\n count: grants,\n severity: 'info',\n })\n lines.push({\n id: 'crm.visibility',\n text: `The separating sites will still see ${visible} ${people(visible)} they met only through the group's other sites, without their CRM details.`,\n count: visible,\n severity: 'info',\n })\n\n /*\n * `crm.figures`: on a dissolve, the people whose order totals cannot\n * follow one site and are dropped. Read like the combine — the people\n * with orders under the key, bounded — and decided by the same split.\n */\n const dissolving = splits.filter((split) => !split.survives)\n if (dissolving.length) {\n let figures: number | null = 0\n for (const split of dissolving) {\n const page = await contacts\n .where(`${CONTACT_FACETS_FIELD}.${split.from}.ordersCount`, '>', 0)\n .limit(PREVIEW_SCAN_LIMIT + 1)\n .get()\n if (page.size > PREVIEW_SCAN_LIMIT || figures === null) {\n figures = null\n continue\n }\n for (const doc of page.docs) {\n figures +=\n planHolderRecords((doc.data() ?? {}) as Doc, { flows: [split] }, '')?.figuresDropped ?? 0\n }\n }\n lines.push({\n id: 'crm.figures',\n text: `Order totals for ${figure(figures)} ${people(figures)} who bought on more than one of these sites can't be split and will be removed.`,\n count: figures,\n severity: 'warning',\n })\n }\n }\n\n /*\n * `crm.refusals`: the refusals C4 would write — each separating pair's\n * declined consents, less the ones the receiving site already holds.\n */\n if (plan.carries.length) {\n let refusals = 0\n for (const carry of plan.carries) {\n const declined = (hostId: string) =>\n `${MARKETING_CONSENT_BY_HOST_FIELD}.${hostId}.${MARKETING_CONSENT_FIELD}`\n const from = contacts.where(declined(carry.fromHostId), '==', false)\n refusals += Math.max(\n 0,\n (await countOf(from)) - (await countOf(from.where(declined(carry.toHostId), '==', false))),\n )\n }\n lines.push({\n id: 'crm.refusals',\n text: `${refusals} declined ${refusals === 1 ? 'consent' : 'consents'} will be copied to the sites that separate.`,\n count: refusals,\n severity: 'info',\n })\n }\n return lines\n },\n\n summarize: summarizeConsentGroupChange,\n }\n}\n\n/** The participant the CRM registers, on the Admin SDK and the wall clock. */\nexport const consentGroupParticipant = createConsentGroupParticipant()\n"],"names":["mergeContactFacet","CONTACT_FACETS_FIELD","interactionsForGroup","COMPANY_CONTACTS_COUNT_FIELD","CONTACT_COMPANY_IDS_FIELD","CRM_COLLECTIONS","CAPTURED_BY_HOST_FIELD","CONSENT_GROUP_ID_FIELD","MARKETING_CONSENT_BY_HOST_FIELD","MARKETING_CONSENT_FIELD","MARKETING_CONSENT_SOURCE_FIELD","firebaseAdmin","FieldPath","FieldValue","summarizeConsentGroupChange","REFUSAL_PAGE","HOLDER_PAGE","PREVIEW_SCAN_LIMIT","HOLDER_FIGURE_FIELDS","COPIED_BY_CHANGE_FIELD","RACED","Set","codeOf","error","code","mapOf","value","Array","isArray","captureOrder","contact","raw","ordered","map","id","String","trim","filter","Boolean","legacy","includes","withoutFigures","facet","out","field","figuresOf","planHolderRecords","plan","changeId","options","facets","captured","next","current","key","undefined","combined","copied","figuresDropped","splits","flows","flow","kind","stragglersOnly","survives","copies","split","source","from","successors","some","successor","keyOf","hostId","stayHostIds","find","hostIds","eligible","length","landing","fromHostIds","figuresTo","s","copy","interactions","push","figures","Object","keys","moves","Map","set","to","get","metAt","indexes","indexOf","index","Math","min","Number","POSITIVE_INFINITY","target","sources","sort","a","b","folded","existing","merged","stamps","named","values","after","before","now","mirror","companyCounts","leaving","has","joining","companyId","delta","patch","delete","serverTimestamp","planRefusalCarry","carry","byHost","fromHostId","toHostId","entry","marketingConsentAtMs","carriedFromHostId","carriedByChangeId","supersededEntry","updatedAt","readCursor","first","parsed","JSON","parse","stage","writeGuarded","firestore","pending","recompute","written","due","round","writer","bulkWriter","outcomes","update","snapshot","ref","lastUpdateTime","updateTime","then","Error","close","raced","Promise","all","fresh","getAll","exists","createConsentGroupParticipant","db","app","Date","contactsOf","orgId","collection","doc","countOf","query","count","data","refusalPage","request","cursor","counts","carries","where","orderBy","documentId","limit","startAfter","page","docs","dryRun","size","holderPage","companies","phase","tally","planned","change","increment","catch","console","run","hasHolderFlows","deadlineMs","done","stringify","preview","contacts","lines","people","figure","seen","bounded","move","combine","reduce","total","text","severity","leavers","flatMap","slice","grants","visible","met","max","dissolving","refusals","declined","summarize","consentGroupParticipant"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/consent-group-participant.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * THE CRM'S SHARE OF A CONSENT GROUP CHANGE (AGL-3320).\n *\n * Two things the CRM keeps are read through the CURRENT consent group, so a\n * change to the declaration moves under them unless they move too:\n *\n * - **C4 — a contact's per-site refusal.** `marketingConsentByHost.{site}`\n * holding `marketingConsent: false` refuses mail from every site of that\n * site's group (`readMarketingBasis`). When two sites stop being one\n * sender, each keeps the refusals the other held: the refusal is written\n * onto the receiving site's entry by dotted path, with the one it\n * replaced kept as `supersededEntry`. A refusal is carried whenever it was\n * given — over-carrying is the safe direction — and one the receiving site\n * already holds is left alone. Only contacts carry per-site refusals;\n * leads and list memberships carry grants alone.\n * - **The holder records**, `facets.{groupId}` — a profile, an owner, a\n * stage, notes, order figures and a timeline, keyed by the group that\n * keeps them. Every writer keys by the declaration in force, so after the\n * flip a site's records must be under its new key:\n * - a MOVE folds every old key's facet into the new key's, with the\n * existing target first and then the sources in the order the person\n * met their sites, through `mergeContactFacet` — the survivor's\n * values win, lists union, figures sum — and deletes each source in\n * the same write, so a figure is summed exactly once;\n * - a SPLIT copies the facet, without its figures and with only the\n * timeline entries of the successor's own sites, to each successor\n * that met the person itself, merged under whatever the successor\n * already holds. The figures follow the capture only when every site\n * that met the person lands under one key; otherwise they stay with the\n * surviving key, or — when the key dissolves — are dropped and counted.\n *\n * ## Run twice, write once\n *\n * The executor runs `carry` before the flip and again as the catch-up, and\n * the sweep runs everything once more. A carried refusal is skipped once the\n * receiving entry refuses; a MOVE or a dissolving SPLIT deletes its source in\n * the write that folds it, so a second pass finds nothing — except a\n * straggler a long job wrote under the old key after the first pass, which is\n * exactly what the sweep exists to fold in. A SPLIT whose key survives stays\n * a live key, so each copy is stamped with the change that made it\n * (`copiedByChange`), and a pass that finds the stamp does not copy again.\n */\n\nimport { mergeContactFacet } from '@aglyn/aglyn/app-utils/contact-merge'\nimport type {\n ConsentGroupChangePlan,\n ConsentGroupChangePreviewLine,\n ConsentGroupHolderMove,\n ConsentGroupHolderSplit,\n} from '@aglyn/aglyn/app-utils/consent-group-change'\nimport {\n CONTACT_FACETS_FIELD,\n type ContactInteraction,\n interactionsForGroup,\n} from '@aglyn/aglyn/app-utils/contacts'\nimport {\n COMPANY_CONTACTS_COUNT_FIELD,\n CONTACT_COMPANY_IDS_FIELD,\n CRM_COLLECTIONS,\n} from '@aglyn/aglyn/app-utils/crm'\nimport {\n CAPTURED_BY_HOST_FIELD,\n CONSENT_GROUP_ID_FIELD,\n MARKETING_CONSENT_BY_HOST_FIELD,\n MARKETING_CONSENT_FIELD,\n MARKETING_CONSENT_SOURCE_FIELD,\n} from '@aglyn/aglyn/app-utils/marketing-consent'\nimport type {\n ConsentGroupChangeParticipant,\n ConsentGroupChangeRunRequest,\n ConsentGroupChangeRunResult,\n} from '@aglyn/aglyn/plugin-manager/plugin-consent-group-change'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { restampCrmListFieldsAt } from '@aglyn/tenant-data-admin/server/crm-records'\nimport { FieldPath, FieldValue } from 'firebase-admin/firestore'\nimport { summarizeConsentGroupChange } from '../model/consent-group-summary'\n\n/** Contacts one page of the refusal carry reads. */\nexport const REFUSAL_PAGE = 300\n\n/** Contacts one page of the holder pass reads. */\nexport const HOLDER_PAGE = 200\n\n/**\n * The most records a preview reads to answer a count no aggregation can:\n * past it, the line says so with a `null` count rather than guess.\n */\nexport const PREVIEW_SCAN_LIMIT = 5_000\n\n/**\n * The figures a facet sums, and the instants that go with them — what\n * `mergeContactFacet` adds up or takes the earliest or latest of. A split\n * cannot divide one person's purchases between two holders, so a copy\n * leaves them behind.\n */\nexport const HOLDER_FIGURE_FIELDS = [\n 'ltvCents',\n 'ordersCount',\n 'refundedCents',\n 'refundedOrdersCount',\n 'firstPurchaseAtMs',\n 'lastPurchaseAtMs',\n 'lastRefundAtMs',\n 'lastEmailEngagementAtMs',\n] as const\n\n/** The stamp a split's copy carries: `{ [sourceKey]: changeId }`. */\nexport const COPIED_BY_CHANGE_FIELD = 'copiedByChange'\n\ntype Doc = Record<string, unknown>\n\n/** The error codes that mean the contact was written since it was read. */\nconst RACED = new Set([5, 9])\nconst codeOf = (error: unknown): number | undefined => (error as { code?: number } | null)?.code\n\nfunction mapOf(value: unknown): Doc {\n return value && typeof value === 'object' && !Array.isArray(value) ? (value as Doc) : {}\n}\n\n/** The sites that met this person, in the order they did — then the legacy `hostId`. */\nfunction captureOrder(contact: Doc): string[] {\n const raw = contact[CAPTURED_BY_HOST_FIELD]\n const ordered = Array.isArray(raw)\n ? raw.map((id) => String(id ?? '').trim()).filter(Boolean)\n : []\n const legacy = typeof contact['hostId'] === 'string' ? (contact['hostId'] as string) : ''\n return legacy && !ordered.includes(legacy) ? [...ordered, legacy] : ordered\n}\n\nfunction withoutFigures(facet: Doc): Doc {\n const out: Doc = { ...facet }\n for (const field of HOLDER_FIGURE_FIELDS) delete out[field]\n return out\n}\n\nfunction figuresOf(facet: Doc): Doc {\n const out: Doc = {}\n for (const field of HOLDER_FIGURE_FIELDS) {\n if (field in facet) out[field] = facet[field]\n }\n return out\n}\n\n/** What one contact's holder records become, as a dotted `update()` patch. */\nexport interface HolderPatch {\n patch: Doc\n combined: number\n copied: number\n figuresDropped: number\n /** Each company whose contacts count moves, by how much. */\n companyCounts: Array<{ companyId: string; delta: 1 | -1 }>\n}\n\n/**\n * The holder records of one contact after the plan's flows — or `null` when\n * nothing moves.\n *\n * `stragglersOnly` is the sweep: only the flows that delete their source\n * (MOVE, and a SPLIT whose key dissolves) run, because only those can have\n * left anything behind that a later writer recreated. A key that survived a\n * split is a live key after the flip, and its new writes belong to it.\n */\nexport function planHolderRecords(\n contact: Doc,\n plan: Pick<ConsentGroupChangePlan, 'flows'>,\n changeId: string,\n options: { stragglersOnly?: boolean } = {},\n): HolderPatch | null {\n const facets = mapOf(contact[CONTACT_FACETS_FIELD]) as Record<string, Doc>\n const captured = captureOrder(contact)\n const next: Record<string, Doc | null> = {}\n const current = (key: string): Doc | undefined =>\n key in next ? (next[key] ?? undefined) : facets[key]\n let combined = 0\n let copied = 0\n let figuresDropped = 0\n\n const splits = plan.flows.filter(\n (flow): flow is ConsentGroupHolderSplit =>\n flow.kind === 'split' && !(options.stragglersOnly && flow.survives),\n )\n const copies: Array<{ key: string; facet: Doc }> = []\n for (const split of splits) {\n const source = facets[split.from]\n if (!source || typeof source !== 'object') continue\n if (\n split.survives &&\n split.successors.some(\n (successor) => mapOf(facets[successor.key]?.[COPIED_BY_CHANGE_FIELD])[split.from] === changeId,\n )\n ) {\n continue\n }\n const keyOf = (hostId: string) =>\n split.stayHostIds.includes(hostId)\n ? split.from\n : (split.successors.find((successor) => successor.hostIds.includes(hostId))?.key ?? null)\n let eligible = split.successors.filter((successor) =>\n successor.hostIds.some((hostId) => captured.includes(hostId)),\n )\n if (!split.survives && !eligible.length) eligible = split.successors\n const landing = [\n ...new Set(\n captured\n .filter((hostId) => split.fromHostIds.includes(hostId))\n .map(keyOf)\n .filter((key): key is string => key !== null),\n ),\n ]\n const figuresTo =\n landing.length === 1 && landing[0] !== split.from && eligible.some((s) => s.key === landing[0])\n ? landing[0]\n : null\n for (const successor of eligible) {\n const copy: Doc = {\n ...withoutFigures(source),\n interactions: interactionsForGroup(\n (source['interactions'] ?? []) as ContactInteraction[],\n successor.hostIds,\n ),\n ...(successor.key === figuresTo ? figuresOf(source) : {}),\n }\n if (split.survives) copy[COPIED_BY_CHANGE_FIELD] = { [split.from]: changeId }\n copies.push({ key: successor.key, facet: copy })\n }\n if (split.survives) {\n if (figuresTo) next[split.from] = withoutFigures(current(split.from) ?? source)\n } else {\n next[split.from] = null\n const figures = figuresOf(source)\n if (!figuresTo && Object.keys(figures).some((field) => field !== 'lastEmailEngagementAtMs')) {\n figuresDropped += 1\n }\n }\n }\n\n const moves = new Map<string, ConsentGroupHolderMove[]>()\n for (const flow of plan.flows) {\n if (flow.kind !== 'move' || !facets[flow.from]) continue\n moves.set(flow.to, [...(moves.get(flow.to) ?? []), flow])\n }\n const metAt = (hostIds: readonly string[]) => {\n const indexes = hostIds.map((hostId) => captured.indexOf(hostId)).filter((index) => index >= 0)\n return indexes.length ? Math.min(...indexes) : Number.POSITIVE_INFINITY\n }\n for (const [target, sources] of moves) {\n const ordered = [...sources].sort(\n (a, b) => metAt(a.fromHostIds) - metAt(b.fromHostIds) || (a.from < b.from ? -1 : a.from > b.from ? 1 : 0),\n )\n let folded = current(target)\n // A person is COMBINED when two or more records become one; a lone\n // record under a new key is only moved.\n if (ordered.length + (folded ? 1 : 0) >= 2) combined += 1\n for (const source of ordered) {\n const facet = facets[source.from]\n folded = folded ? mergeContactFacet(folded, facet) : { ...facet }\n next[source.from] = null\n }\n if (folded) next[target] = folded\n }\n\n for (const { key, facet } of copies) {\n const existing = current(key)\n const merged = existing ? mergeContactFacet(existing, facet) : facet\n // The stamps are a union, never the survivor's alone: the merge keeps a\n // map the survivor already has, and a lost stamp would copy again.\n const stamps = {\n ...mapOf(existing?.[COPIED_BY_CHANGE_FIELD]),\n ...mapOf(facet[COPIED_BY_CHANGE_FIELD]),\n }\n if (Object.keys(stamps).length) merged[COPIED_BY_CHANGE_FIELD] = stamps\n next[key] = merged\n copied += 1\n }\n\n const keys = Object.keys(next)\n if (!keys.length) return null\n\n /*\n * The company mirror follows the facets: an id that no facet names any\n * more leaves it, and one a facet newly names joins it. Ids the mirror held\n * without a facet naming them — links older than the facets — are left.\n */\n const named = (source: Record<string, Doc | null | undefined>) =>\n new Set(\n Object.values(source)\n .map((facet) => (facet ? facet['companyId'] : null))\n .filter((id): id is string => typeof id === 'string' && id !== ''),\n )\n const after: Record<string, Doc | null> = { ...facets, ...next }\n const before = named(facets)\n const now = named(after)\n const mirror = Array.isArray(contact[CONTACT_COMPANY_IDS_FIELD])\n ? (contact[CONTACT_COMPANY_IDS_FIELD] as unknown[]).filter(\n (id): id is string => typeof id === 'string' && id !== '',\n )\n : []\n const companyCounts: HolderPatch['companyCounts'] = []\n const leaving = [...before].filter((id) => !now.has(id))\n const joining = [...now].filter((id) => !before.has(id))\n for (const id of leaving) if (mirror.includes(id)) companyCounts.push({ companyId: id, delta: -1 })\n for (const id of joining) if (!mirror.includes(id)) companyCounts.push({ companyId: id, delta: 1 })\n\n const patch: Doc = {}\n for (const key of keys) {\n patch[`${CONTACT_FACETS_FIELD}.${key}`] = next[key] === null ? FieldValue.delete() : next[key]\n }\n if (companyCounts.length) {\n patch[CONTACT_COMPANY_IDS_FIELD] = [\n ...mirror.filter((id) => !leaving.includes(id)),\n ...joining.filter((id) => !mirror.includes(id)),\n ]\n }\n patch['updatedAt'] = FieldValue.serverTimestamp()\n return { patch, combined, copied, figuresDropped, companyCounts }\n}\n\n/**\n * The refusal `from` holds, carried onto `to` — or `null` when `to` already\n * refuses or `from` does not.\n */\nexport function planRefusalCarry(\n contact: Doc,\n carry: { toHostId: string; fromHostId: string },\n changeId: string,\n): Doc | null {\n const byHost = mapOf(contact[MARKETING_CONSENT_BY_HOST_FIELD])\n const source = mapOf(byHost[carry.fromHostId])\n if (source[MARKETING_CONSENT_FIELD] !== false) return null\n const target = byHost[carry.toHostId]\n if (mapOf(target)[MARKETING_CONSENT_FIELD] === false) return null\n const entry: Doc = {\n [MARKETING_CONSENT_FIELD]: false,\n ...(source['marketingConsentAtMs'] !== undefined\n ? { marketingConsentAtMs: source['marketingConsentAtMs'] }\n : {}),\n ...(source[MARKETING_CONSENT_SOURCE_FIELD] !== undefined\n ? { [MARKETING_CONSENT_SOURCE_FIELD]: source[MARKETING_CONSENT_SOURCE_FIELD] }\n : {}),\n carriedFromHostId: carry.fromHostId,\n carriedByChangeId: changeId,\n ...(target && typeof target === 'object' ? { supersededEntry: target } : {}),\n }\n return {\n [`${MARKETING_CONSENT_BY_HOST_FIELD}.${carry.toHostId}`]: entry,\n updatedAt: FieldValue.serverTimestamp(),\n }\n}\n\n/*==========================================\n * THE PASSES\n *=========================================*/\n\ninterface Cursor {\n /** `refusals` walks the carries; `holders` walks every contact. */\n stage: 'refusals' | 'holders'\n /** The carry being walked, in `refusals`. */\n carry: number\n after: string | null\n}\n\nfunction readCursor(raw: string | null, first: Cursor['stage']): Cursor {\n if (raw) {\n try {\n const parsed = JSON.parse(raw) as Cursor\n if (parsed && (parsed.stage === 'refusals' || parsed.stage === 'holders')) return parsed\n } catch {\n // A cursor this participant did not write starts the phase over, which\n // every pass is safe to do.\n }\n }\n return { stage: first, carry: 0, after: null }\n}\n\nexport interface ConsentGroupParticipantOptions {\n firestore?: () => FirebaseFirestore.Firestore\n now?: () => number\n}\n\n/** One contact write, guarded by the read it was computed from. */\ninterface Pending {\n snapshot: FirebaseFirestore.QueryDocumentSnapshot | FirebaseFirestore.DocumentSnapshot\n patch: Doc\n after?: () => Promise<void>\n}\n\n/**\n * Writes a page's patches, each with a `lastUpdateTime` precondition, and\n * recomputes the ones that raced once from a fresh read. Answers how many\n * were written.\n */\nasync function writeGuarded(\n firestore: FirebaseFirestore.Firestore,\n pending: Pending[],\n recompute: (snapshot: FirebaseFirestore.DocumentSnapshot) => Pending | null,\n): Promise<number> {\n let written = 0\n let due = pending\n for (let round = 0; round < 2 && due.length; round += 1) {\n const writer = firestore.bulkWriter()\n const outcomes = due.map((entry) =>\n writer\n .update(entry.snapshot.ref, entry.patch, {\n lastUpdateTime: entry.snapshot.updateTime as FirebaseFirestore.Timestamp,\n })\n .then(\n () => ({ entry, error: null as unknown }),\n (error: unknown) => ({ entry, error: error ?? new Error('The write failed') }),\n ),\n )\n await writer.close()\n const raced: Pending[] = []\n for (const { entry, error } of await Promise.all(outcomes)) {\n if (error === null) {\n written += 1\n await entry.after?.()\n continue\n }\n const code = codeOf(error)\n if (code === undefined || !RACED.has(code)) throw error\n raced.push(entry)\n }\n if (!raced.length) return written\n const fresh = await firestore.getAll(...raced.map((entry) => entry.snapshot.ref))\n due = fresh\n .filter((snapshot) => snapshot.exists)\n .map(recompute)\n .filter((entry): entry is Pending => entry !== null)\n }\n if (due.length) {\n throw new Error(`[crm] ${due.length} contact(s) kept changing under the consent group change`)\n }\n return written\n}\n\n/**\n * The CRM's participant. `firestore` and `now` are injectable for the specs;\n * the defaults are the Admin SDK's and the wall clock.\n */\nexport function createConsentGroupParticipant(\n options: ConsentGroupParticipantOptions = {},\n): ConsentGroupChangeParticipant {\n const db = () => (options.firestore ?? (() => firebaseAdmin.app().firestore()))()\n const now = options.now ?? Date.now\n const contactsOf = (orgId: string) => db().collection('orgs').doc(orgId).collection('contacts')\n\n async function countOf(query: FirebaseFirestore.Query): Promise<number> {\n return Number((await query.count().get()).data().count) || 0\n }\n\n /** One page of one carry's refusals. */\n async function refusalPage(\n request: ConsentGroupChangeRunRequest,\n cursor: Cursor,\n counts: Record<string, number>,\n ): Promise<Cursor | null> {\n const carries = request.plan.carries\n if (cursor.carry >= carries.length) return null\n const carry = carries[cursor.carry]\n let query = contactsOf(request.orgId)\n .where(`${MARKETING_CONSENT_BY_HOST_FIELD}.${carry.fromHostId}.${MARKETING_CONSENT_FIELD}`, '==', false)\n .orderBy(FieldPath.documentId())\n .limit(REFUSAL_PAGE)\n if (cursor.after) query = query.startAfter(cursor.after)\n const page = await query.get()\n const plan = (snapshot: FirebaseFirestore.DocumentSnapshot): Pending | null => {\n const patch = planRefusalCarry((snapshot.data() ?? {}) as Doc, carry, request.changeId)\n return patch ? { snapshot, patch } : null\n }\n const pending = page.docs.map(plan).filter((entry): entry is Pending => entry !== null)\n counts['refusals'] = (counts['refusals'] ?? 0) +\n (request.dryRun ? pending.length : await writeGuarded(db(), pending, plan))\n if (page.size < REFUSAL_PAGE) return { stage: 'refusals', carry: cursor.carry + 1, after: null }\n return { stage: 'refusals', carry: cursor.carry, after: page.docs[page.docs.length - 1].id }\n }\n\n /** One page of the holder pass over every contact. */\n async function holderPage(\n request: ConsentGroupChangeRunRequest,\n cursor: Cursor,\n counts: Record<string, number>,\n ): Promise<Cursor | null> {\n let query = contactsOf(request.orgId).orderBy(FieldPath.documentId()).limit(HOLDER_PAGE)\n if (cursor.after) query = query.startAfter(cursor.after)\n const page = await query.get()\n const companies = db().collection('orgs').doc(request.orgId).collection(CRM_COLLECTIONS.companies)\n const stragglersOnly = request.phase === 'sweep'\n const tally = { combined: 0, copied: 0, figuresDropped: 0 }\n const count = (planned: HolderPatch) => {\n tally.combined += planned.combined\n tally.copied += planned.copied\n tally.figuresDropped += planned.figuresDropped\n }\n const plan = (snapshot: FirebaseFirestore.DocumentSnapshot): (Pending & { planned: HolderPatch }) | null => {\n const planned = planHolderRecords((snapshot.data() ?? {}) as Doc, request.plan, request.changeId, {\n stragglersOnly,\n })\n if (!planned) return null\n return {\n snapshot,\n planned,\n patch: planned.patch,\n after: async () => {\n count(planned)\n // The holders moved, and with them the facet keys the Contacts\n // list filters by (AGL-3321).\n await restampCrmListFieldsAt(snapshot.ref, 'contacts')\n // A count that could not move is logged and left: the company's own\n // page takes the live aggregate, which corrects it.\n await Promise.all(\n planned.companyCounts.map((change) =>\n companies\n .doc(change.companyId)\n .update({ [COMPANY_CONTACTS_COUNT_FIELD]: FieldValue.increment(change.delta) })\n .catch((error: unknown) => {\n console.error('[crm] company contacts count could not move', change.companyId, error)\n }),\n ),\n )\n },\n }\n }\n const pending = page.docs\n .map(plan)\n .filter((entry): entry is Pending & { planned: HolderPatch } => entry !== null)\n if (request.dryRun) for (const entry of pending) count(entry.planned)\n else await writeGuarded(db(), pending, plan)\n counts['combined'] = (counts['combined'] ?? 0) + tally.combined\n counts['copied'] = (counts['copied'] ?? 0) + tally.copied\n counts['figuresDropped'] = (counts['figuresDropped'] ?? 0) + tally.figuresDropped\n if (page.size < HOLDER_PAGE) return null\n return { stage: 'holders', carry: 0, after: page.docs[page.docs.length - 1].id }\n }\n\n return {\n async run(request): Promise<ConsentGroupChangeRunResult> {\n const counts: Record<string, number> = {}\n const hasHolderFlows = request.plan.flows.length > 0\n // `carry` walks the refusals; `rehome` the holders; `sweep` both.\n let cursor: Cursor | null = readCursor(\n request.cursor,\n request.phase === 'rehome' ? 'holders' : 'refusals',\n )\n while (cursor && now() < request.deadlineMs) {\n if (cursor.stage === 'refusals') {\n const next: Cursor | null = await refusalPage(request, cursor, counts)\n cursor =\n next ??\n (request.phase === 'sweep' && hasHolderFlows\n ? { stage: 'holders', carry: 0, after: null }\n : null)\n continue\n }\n cursor = hasHolderFlows ? await holderPage(request, cursor, counts) : null\n }\n return { done: cursor === null, cursor: cursor ? JSON.stringify(cursor) : null, counts }\n },\n\n async preview({ orgId, plan }): Promise<ConsentGroupChangePreviewLine[]> {\n const contacts = contactsOf(orgId)\n const lines: ConsentGroupChangePreviewLine[] = []\n const moves = plan.flows.filter((flow): flow is ConsentGroupHolderMove => flow.kind === 'move')\n const splits = plan.flows.filter((flow): flow is ConsentGroupHolderSplit => flow.kind === 'split')\n const people = (count: number | null) => (count === 1 ? 'person' : 'people')\n const figure = (count: number | null) => (count === null ? 'some' : String(count))\n\n /*\n * `crm.combine`: the people whose records under two or more of the\n * folding keys become one. No aggregation can ask \"two or more\", so the\n * records under each source key are read — bounded, and `null` past\n * the bound — and put through the same fold the re-home runs.\n */\n if (moves.length) {\n const seen = new Map<string, Doc>()\n let bounded = true\n for (const move of moves) {\n const page = await contacts\n .where(`${CONTACT_FACETS_FIELD}.${move.from}`, '!=', null)\n .limit(PREVIEW_SCAN_LIMIT + 1)\n .get()\n if (page.size > PREVIEW_SCAN_LIMIT) bounded = false\n for (const doc of page.docs) seen.set(doc.id, (doc.data() ?? {}) as Doc)\n }\n const combine = bounded\n ? [...seen.values()].reduce(\n (total, contact) =>\n total + (planHolderRecords(contact, { flows: moves }, '')?.combined ?? 0),\n 0,\n )\n : null\n lines.push({\n id: 'crm.combine',\n text: `The CRM records these sites keep about the same person will be combined for ${figure(combine)} ${people(combine)}.`,\n count: combine,\n severity: 'info',\n })\n }\n\n if (splits.length) {\n /*\n * `crm.copy`: the people a separating site takes a copy for. When the\n * key survives, those are the people the separating sites met\n * themselves; when it dissolves, everybody it holds, since a record\n * nobody met goes to every successor.\n */\n let copy = 0\n for (const split of splits) {\n if (!split.survives) {\n copy += await countOf(contacts.where(`${CONTACT_FACETS_FIELD}.${split.from}`, '!=', null))\n continue\n }\n const leavers = split.successors.flatMap((successor) => successor.hostIds)\n for (let index = 0; index < leavers.length; index += 30) {\n copy += await countOf(\n contacts.where(CAPTURED_BY_HOST_FIELD, 'array-contains-any', leavers.slice(index, index + 30)),\n )\n }\n }\n lines.push({\n id: 'crm.copy',\n text: `The separating sites keep a copy of the CRM record for the ${copy} ${people(copy)} they met themselves.`,\n count: copy,\n severity: 'info',\n })\n\n /*\n * `crm.grants` and `crm.visibility`, per separating site: the people\n * who signed up to the group through it and still hold that grant,\n * and the people it can see without having met them.\n */\n let grants = 0\n let visible = 0\n for (const split of splits) {\n for (const hostId of split.successors.flatMap((successor) => successor.hostIds)) {\n grants += await countOf(\n contacts\n .where(`${MARKETING_CONSENT_BY_HOST_FIELD}.${hostId}.${CONSENT_GROUP_ID_FIELD}`, '==', split.from)\n .where(`${MARKETING_CONSENT_BY_HOST_FIELD}.${hostId}.${MARKETING_CONSENT_FIELD}`, '==', true),\n )\n const seen = await countOf(contacts.where('visibleTo', 'array-contains', `host:${hostId}`))\n const met = await countOf(contacts.where(CAPTURED_BY_HOST_FIELD, 'array-contains', hostId))\n visible += Math.max(0, seen - met)\n }\n }\n lines.push({\n id: 'crm.grants',\n text: `The separating sites can still email the ${grants} ${people(grants)} who signed up to the group while they were part of it.`,\n count: grants,\n severity: 'info',\n })\n lines.push({\n id: 'crm.visibility',\n text: `The separating sites will still see ${visible} ${people(visible)} they met only through the group's other sites, without their CRM details.`,\n count: visible,\n severity: 'info',\n })\n\n /*\n * `crm.figures`: on a dissolve, the people whose order totals cannot\n * follow one site and are dropped. Read like the combine — the people\n * with orders under the key, bounded — and decided by the same split.\n */\n const dissolving = splits.filter((split) => !split.survives)\n if (dissolving.length) {\n let figures: number | null = 0\n for (const split of dissolving) {\n const page = await contacts\n .where(`${CONTACT_FACETS_FIELD}.${split.from}.ordersCount`, '>', 0)\n .limit(PREVIEW_SCAN_LIMIT + 1)\n .get()\n if (page.size > PREVIEW_SCAN_LIMIT || figures === null) {\n figures = null\n continue\n }\n for (const doc of page.docs) {\n figures +=\n planHolderRecords((doc.data() ?? {}) as Doc, { flows: [split] }, '')?.figuresDropped ?? 0\n }\n }\n lines.push({\n id: 'crm.figures',\n text: `Order totals for ${figure(figures)} ${people(figures)} who bought on more than one of these sites can't be split and will be removed.`,\n count: figures,\n severity: 'warning',\n })\n }\n }\n\n /*\n * `crm.refusals`: the refusals C4 would write — each separating pair's\n * declined consents, less the ones the receiving site already holds.\n */\n if (plan.carries.length) {\n let refusals = 0\n for (const carry of plan.carries) {\n const declined = (hostId: string) =>\n `${MARKETING_CONSENT_BY_HOST_FIELD}.${hostId}.${MARKETING_CONSENT_FIELD}`\n const from = contacts.where(declined(carry.fromHostId), '==', false)\n refusals += Math.max(\n 0,\n (await countOf(from)) - (await countOf(from.where(declined(carry.toHostId), '==', false))),\n )\n }\n lines.push({\n id: 'crm.refusals',\n text: `${refusals} declined ${refusals === 1 ? 'consent' : 'consents'} will be copied to the sites that separate.`,\n count: refusals,\n severity: 'info',\n })\n }\n return lines\n },\n\n summarize: summarizeConsentGroupChange,\n }\n}\n\n/** The participant the CRM registers, on the Admin SDK and the wall clock. */\nexport const consentGroupParticipant = createConsentGroupParticipant()\n"],"names":["mergeContactFacet","CONTACT_FACETS_FIELD","interactionsForGroup","COMPANY_CONTACTS_COUNT_FIELD","CONTACT_COMPANY_IDS_FIELD","CRM_COLLECTIONS","CAPTURED_BY_HOST_FIELD","CONSENT_GROUP_ID_FIELD","MARKETING_CONSENT_BY_HOST_FIELD","MARKETING_CONSENT_FIELD","MARKETING_CONSENT_SOURCE_FIELD","firebaseAdmin","restampCrmListFieldsAt","FieldPath","FieldValue","summarizeConsentGroupChange","REFUSAL_PAGE","HOLDER_PAGE","PREVIEW_SCAN_LIMIT","HOLDER_FIGURE_FIELDS","COPIED_BY_CHANGE_FIELD","RACED","Set","codeOf","error","code","mapOf","value","Array","isArray","captureOrder","contact","raw","ordered","map","id","String","trim","filter","Boolean","legacy","includes","withoutFigures","facet","out","field","figuresOf","planHolderRecords","plan","changeId","options","facets","captured","next","current","key","undefined","combined","copied","figuresDropped","splits","flows","flow","kind","stragglersOnly","survives","copies","split","source","from","successors","some","successor","keyOf","hostId","stayHostIds","find","hostIds","eligible","length","landing","fromHostIds","figuresTo","s","copy","interactions","push","figures","Object","keys","moves","Map","set","to","get","metAt","indexes","indexOf","index","Math","min","Number","POSITIVE_INFINITY","target","sources","sort","a","b","folded","existing","merged","stamps","named","values","after","before","now","mirror","companyCounts","leaving","has","joining","companyId","delta","patch","delete","serverTimestamp","planRefusalCarry","carry","byHost","fromHostId","toHostId","entry","marketingConsentAtMs","carriedFromHostId","carriedByChangeId","supersededEntry","updatedAt","readCursor","first","parsed","JSON","parse","stage","writeGuarded","firestore","pending","recompute","written","due","round","writer","bulkWriter","outcomes","update","snapshot","ref","lastUpdateTime","updateTime","then","Error","close","raced","Promise","all","fresh","getAll","exists","createConsentGroupParticipant","db","app","Date","contactsOf","orgId","collection","doc","countOf","query","count","data","refusalPage","request","cursor","counts","carries","where","orderBy","documentId","limit","startAfter","page","docs","dryRun","size","holderPage","companies","phase","tally","planned","change","increment","catch","console","run","hasHolderFlows","deadlineMs","done","stringify","preview","contacts","lines","people","figure","seen","bounded","move","combine","reduce","total","text","severity","leavers","flatMap","slice","grants","visible","met","max","dissolving","refusals","declined","summarize","consentGroupParticipant"],"mappings":"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"}
@@ -69,7 +69,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
69
69
  * company deleted in between cannot fail the save of the people at it — the
70
70
  * company page's live aggregate is what corrects a count that did not move.
71
71
  */ import { consentGroupForHost, CONTACT_FIELDS_MAX_PER_ORG, campaignMembershipValue, contactCampaignFieldPath, contactFacetPath, contactPrimaryGroup, CRM_COLLECTIONS, CRM_LEAD_SOURCE_PICKLIST, CRM_MEDIA_IDS_FIELD, crmReadTokens, effectiveCrmLeadSourcePicklist, isOrgWideMember, judgeCrmLeadSource, normalizeAddress, normalizeCrmMediaIds, normalizeCrmPicklistLabel, normalizePhone, planContactCompanyLink, readContactCompanyLink, readContactFacet, readCrmCustomInput, visibleToTokens } from "@aglyn/aglyn/server";
72
- import { companyContactsCountFields, contactCompanyLinkFields, firebaseAdmin, resolveOrgMembership } from "@aglyn/tenant-data-admin";
72
+ import { companyContactsCountFields, contactCompanyLinkFields, firebaseAdmin, resolveOrgMembership, restampCrmListFieldsOf } from "@aglyn/tenant-data-admin";
73
73
  import { FieldValue } from "firebase-admin/firestore";
74
74
  import { CONTACT_UPDATE_FIELDS, CRM_CONTACT_UPDATE_MAX } from "../model/contact-update.js";
75
75
  import { CONTACT_NOTES_MAX, CONTACT_PHONE_REFUSAL, CONTACT_TAGS_MAX, normalizeTags, readBulkTag, typed } from "./contact-profile.js";
@@ -327,20 +327,26 @@ const refused = (contactId, error)=>({
327
327
  if (!writes.length) return failed;
328
328
  const batch = firestore.batch();
329
329
  for (const write of writes)batch.update(write.ref, write.update);
330
+ let committed = false;
330
331
  try {
331
332
  await batch.commit();
332
- return failed;
333
+ committed = true;
333
334
  } catch (unused) {
334
335
  // A batch names no document; the pass below finds the one it was.
335
336
  }
336
- for (const write of writes){
337
- try {
338
- await write.ref.update(write.update);
339
- } catch (error) {
340
- console.error('[crm] contact-update could not write a contact', write.contactId, error);
341
- failed.add(write.contactId);
337
+ if (!committed) {
338
+ for (const write of writes){
339
+ try {
340
+ await write.ref.update(write.update);
341
+ } catch (error) {
342
+ console.error('[crm] contact-update could not write a contact', write.contactId, error);
343
+ failed.add(write.contactId);
344
+ }
342
345
  }
343
346
  }
347
+ // The owner, tags, company and custom values are what the Contacts list
348
+ // filters by through `facetKeys` (AGL-3321): restamped from each row.
349
+ await restampCrmListFieldsOf(writes.filter((write)=>!failed.has(write.contactId)).map((write)=>write.ref), 'contacts');
344
350
  return failed;
345
351
  }
346
352
  /** Each company's contacts count moved once, by the sum of what the written links moved. */ async function settleCompanyCounts(companies, writes) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/contact-update.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * `POST /api/crm/contact-update` — one holder's profile fields on one or\n * more contacts (AGL-2804).\n *\n * Body: `{ hostId, contactIds, set }` under a site, or `{ orgId, hostId?,\n * contactIds, set }` at the organization level, where `set` is\n * `ContactUpdateFields`. Answers `{ ok, results }`: one outcome per contact,\n * in the order the ids were sent.\n *\n * ## Why every facet write comes here\n *\n * A contact's fields live in the facet of the holder that keeps them —\n * `facets.{groupId}.*` — and the Firestore rules cannot tell one field of a\n * facet from another: the holder is a map key a rule cannot address, and a\n * write's affected keys name only `facets`. So the rules let a client update\n * change nothing in a facet beyond letting a holder go, and the console's\n * edits arrive here, where the plan is asked about the fields the request\n * actually carries.\n *\n * ## Who may call it\n *\n * The CRM writer the task routes admit (`authorizeCrmWriter`): under a site,\n * a member whose role writes org data, who holds `data.manage` and whose\n * access reaches the site; at the organization level, an org-wide member\n * with the permission. Each contact must then be one the caller may read —\n * the rules' `canReadScoped()` — and, under a site, one the site's hub lists.\n *\n * ## The plan\n *\n * Every field is the CRM's (AGL-2790), and a plan without the CRM has none\n * of it (AGL-2851), so it edits no contact: not the owner, company, custom\n * values or files, and not the profile a capture writes (name, phone, job\n * title, address), the tags, the notes or the campaign filing either. A\n * request carrying any field is refused to such a plan once the caller is\n * known, and before a contact is read — staff included, because the plan is\n * a fact about the workspace. Erasing a person is another route, open on\n * every plan.\n *\n * ## One holder, by dotted path\n *\n * Under a site the facet is the site's consent group's; at the organization\n * level it is each contact's own primary holder, the facet the record page\n * shows there. Every field is one dotted path — a nested `facets` object\n * would replace every other holder's map — and a text field sent empty is\n * deleted rather than stored blank, so \"no phone\" has one shape.\n *\n * ## Many contacts, one commit\n *\n * The contacts are read together and written in one batch. A batch that\n * fails is retried contact by contact, so the one that could not be written\n * is named and the rest still land. A company's contacts count follows the\n * link: summed per company and settled once the contacts are written, so a\n * company deleted in between cannot fail the save of the people at it — the\n * company page's live aggregate is what corrects a count that did not move.\n */\n\nimport {\n type AglynPostalAddress,\n consentGroupForHost,\n CONTACT_FIELDS_MAX_PER_ORG,\n type ContactCompanyLinkPlan,\n type ContactCustomValue,\n type ContactFieldDefinition,\n campaignMembershipValue,\n contactCampaignFieldPath,\n contactFacetPath,\n contactPrimaryGroup,\n CRM_COLLECTIONS,\n CRM_LEAD_SOURCE_PICKLIST,\n CRM_MEDIA_IDS_FIELD,\n crmReadTokens,\n effectiveCrmLeadSourcePicklist,\n isOrgWideMember,\n judgeCrmLeadSource,\n normalizeAddress,\n normalizeCrmMediaIds,\n normalizeCrmPicklistLabel,\n normalizePhone,\n type PluginApiHandler,\n planContactCompanyLink,\n readContactCompanyLink,\n readContactFacet,\n readCrmCustomInput,\n visibleToTokens,\n} from '@aglyn/aglyn/server'\nimport {\n companyContactsCountFields,\n contactCompanyLinkFields,\n firebaseAdmin,\n resolveOrgMembership,\n} from '@aglyn/tenant-data-admin'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport {\n CONTACT_UPDATE_FIELDS,\n type ContactUpdateFields,\n type ContactUpdateOutcome,\n type ContactUpdateResponse,\n CRM_CONTACT_UPDATE_MAX,\n} from '../model/contact-update'\nimport {\n CONTACT_NOTES_MAX,\n CONTACT_PHONE_REFUSAL,\n CONTACT_TAGS_MAX,\n normalizeTags,\n readBulkTag,\n typed,\n} from './contact-profile'\nimport { readCrmRouteScope } from './org-caller'\nimport { crmSuiteRefusal } from './suite-gate'\nimport { authorizeCrmWriter, canReach, type Writer } from './task-routes'\n\n/** What the suite gate names for each field a request can carry. */\nconst SUITE_ACTS: Record<keyof ContactUpdateFields, string> = {\n name: \"Editing a contact's profile\",\n phone: \"Editing a contact's profile\",\n jobTitle: \"Editing a contact's profile\",\n leadSource: \"Editing a contact's profile\",\n address: \"Editing a contact's profile\",\n notes: \"Editing a contact's notes\",\n tags: 'Tagging a contact',\n addTag: 'Tagging a contact',\n removeTag: 'Tagging a contact',\n campaignIds: 'Filing a contact under a campaign',\n ownerUid: \"Assigning a contact's owner\",\n companyId: 'Filing a contact under a company',\n companyName: 'Filing a contact under a company',\n custom: \"Editing a contact's custom fields\",\n mediaIds: 'Attaching files to a contact',\n}\n\n/** A contact read at the organization level that no site has captured. */\nconst NO_HOLDER_REFUSAL = 'No site holds this contact yet, so it has no profile to edit.'\n\n/**\n * The act a request is refused for on a plan without the CRM: the first\n * field it carries, in the order the route reads them. Every field is the\n * CRM's, so whichever field a request names is the act its refusal names.\n */\nexport function contactUpdateSuiteAct(fields: ContactUpdateFields): string {\n const field = CONTACT_UPDATE_FIELDS.find((key) => fields[key] !== undefined)\n return field ? SUITE_ACTS[field] : \"Editing a contact's profile\"\n}\n\n/**\n * A body's `set`, read into the fields it names, or the sentence refusing\n * it. A key the route does not save is refused rather than dropped: a save\n * that silently lost a field would read as a save that kept it.\n */\nexport function readContactUpdateFields(\n input: unknown,\n): { ok: true; fields: ContactUpdateFields } | { ok: false; error: string } {\n if (!input || typeof input !== 'object' || Array.isArray(input)) {\n return { ok: false, error: 'Name the fields to save.' }\n }\n const raw = input as Record<string, unknown>\n const unknownKey = Object.keys(raw).find(\n (key) => !(CONTACT_UPDATE_FIELDS as readonly string[]).includes(key),\n )\n if (unknownKey) {\n return { ok: false, error: `\"${unknownKey}\" is not a field a contact's profile saves.` }\n }\n const fields: ContactUpdateFields = {}\n if ('name' in raw) fields.name = typed(raw['name'], 120)\n if ('phone' in raw) {\n const text = typed(raw['phone'], 40)\n const phone = text ? normalizePhone(text) : ''\n if (phone === null) return { ok: false, error: CONTACT_PHONE_REFUSAL }\n fields.phone = phone\n }\n if ('jobTitle' in raw) fields.jobTitle = typed(raw['jobTitle'], 120)\n // Held to the label's shape here; judged against the org's list by the\n // handler, which alone can read it (AGL-3298).\n if ('leadSource' in raw) fields.leadSource = normalizeCrmPicklistLabel(raw['leadSource'])\n if ('address' in raw) {\n const address = raw['address']\n fields.address =\n address && typeof address === 'object' && !Array.isArray(address)\n ? normalizeAddress(address as AglynPostalAddress)\n : null\n }\n if ('notes' in raw) fields.notes = String(raw['notes'] ?? '').slice(0, CONTACT_NOTES_MAX)\n // One write may carry one transform per field, so the tags are replaced,\n // added to or taken from — never two of those at once.\n if (['tags', 'addTag', 'removeTag'].filter((key) => key in raw).length > 1) {\n return { ok: false, error: 'Send the tags, a tag to add or a tag to remove — one of them.' }\n }\n if ('tags' in raw) {\n if (!Array.isArray(raw['tags'])) return { ok: false, error: 'Tags must be a list.' }\n fields.tags = normalizeTags(raw['tags'])\n }\n for (const key of ['addTag', 'removeTag'] as const) {\n if (!(key in raw)) continue\n const tag = readBulkTag(raw[key])\n if (!tag) return { ok: false, error: 'Name the tag.' }\n fields[key] = tag\n }\n if ('campaignIds' in raw) {\n const ids = raw['campaignIds']\n if (!Array.isArray(ids)) return { ok: false, error: 'Campaigns must be a list.' }\n fields.campaignIds = campaignMembershipValue(ids.map((id) => String(id ?? '')))\n }\n if ('ownerUid' in raw) fields.ownerUid = typed(raw['ownerUid'], 128)\n if ('companyId' in raw) {\n const companyId = raw['companyId'] === null ? null : typed(raw['companyId'], 128)\n if (companyId !== null && (!companyId || companyId.includes('/'))) {\n return { ok: false, error: 'The company could not be read.' }\n }\n fields.companyId = companyId\n }\n if ('companyName' in raw) fields.companyName = typed(raw['companyName'], 120)\n if ('custom' in raw) {\n const custom = raw['custom']\n if (!custom || typeof custom !== 'object' || Array.isArray(custom)) {\n return { ok: false, error: 'Custom fields must be values keyed by field key.' }\n }\n fields.custom = custom as Record<string, ContactCustomValue>\n }\n if ('mediaIds' in raw) {\n if (!Array.isArray(raw['mediaIds'])) {\n return { ok: false, error: 'Files must be a list of ids.' }\n }\n fields.mediaIds = normalizeCrmMediaIds(raw['mediaIds'])\n }\n if (!Object.keys(fields).length) return { ok: false, error: 'Name the fields to save.' }\n return { ok: true, fields }\n}\n\n/** The ids a body names, deduplicated — or `null` when any is unreadable or there are too many. */\nfunction readContactIds(value: unknown): string[] | null {\n if (!Array.isArray(value) || !value.length || value.length > CRM_CONTACT_UPDATE_MAX) {\n return null\n }\n const ids = value.map((id) => typed(id, 200))\n if (ids.some((id) => !id || id.includes('/'))) return null\n return [...new Set(ids)]\n}\n\n/**\n * Under a site, whether the caller's access reaches it. The rules could not\n * ask this of a facet write — they cannot see which holder a write is for —\n * and a collaborator on one site must not write another site's notes on a\n * person both of them hold.\n */\nfunction reachesSite(writer: Writer, hostId: string): boolean {\n return (\n writer.staff ||\n isOrgWideMember(writer.member) ||\n Boolean((writer.member?.hostAccess as Record<string, unknown> | undefined)?.[hostId])\n )\n}\n\nconst refused = (contactId: string, error: string): ContactUpdateOutcome => ({\n contactId,\n ok: false,\n error,\n})\n\ninterface ContactWrite {\n contactId: string\n ref: FirebaseFirestore.DocumentReference\n update: Record<string, unknown>\n counts: ContactCompanyLinkPlan['counts']\n}\n\n/**\n * One contact's update for one holder, or the sentence refusing the row.\n * `update` is `null` when the document already says everything asked — a tag\n * the row already carries, a link already made.\n */\nfunction contactPatch(\n contact: Record<string, unknown>,\n groupId: string,\n fields: ContactUpdateFields,\n custom: Record<string, ContactCustomValue> | undefined,\n linkedCompanyName: string,\n):\n | { update: Record<string, unknown> | null; counts: ContactCompanyLinkPlan['counts'] }\n | { refused: string } {\n const path = (field: string) => contactFacetPath(groupId, field)\n /** A text field as stored: the value, or the field's absence. */\n const text = (value: string) => value || FieldValue.delete()\n const update: Record<string, unknown> = {}\n let counts: ContactCompanyLinkPlan['counts'] = []\n\n if (fields.name !== undefined) update[path('name')] = text(fields.name)\n if (fields.phone !== undefined) {\n update[path('phone')] = text(fields.phone)\n // The search echo — see `HostContact.phone`.\n update['phone'] = text(fields.phone)\n }\n if (fields.jobTitle !== undefined) update[path('jobTitle')] = text(fields.jobTitle)\n if (fields.leadSource !== undefined) update[path('leadSource')] = text(fields.leadSource)\n if (fields.address !== undefined) {\n update[path('address')] = fields.address ?? FieldValue.delete()\n }\n if (fields.notes !== undefined) update[path('notes')] = fields.notes\n if (fields.tags !== undefined) update[path('tags')] = fields.tags\n\n const held = (readContactFacet(contact, groupId).tags ?? []).map((tag) =>\n String(tag).toLowerCase(),\n )\n if (fields.addTag && !held.includes(fields.addTag)) {\n // The record page's cap, which a selection must not slip past.\n if (held.length >= CONTACT_TAGS_MAX) {\n return { refused: `already has ${CONTACT_TAGS_MAX} tags` }\n }\n update[path('tags')] = FieldValue.arrayUnion(fields.addTag)\n }\n if (fields.removeTag && held.includes(fields.removeTag)) {\n update[path('tags')] = FieldValue.arrayRemove(fields.removeTag)\n }\n if (fields.campaignIds !== undefined) {\n update[contactCampaignFieldPath(groupId)] = fields.campaignIds\n }\n if (fields.ownerUid !== undefined) update[path('ownerUid')] = text(fields.ownerUid)\n\n if (fields.companyId !== undefined) {\n const plan = planContactCompanyLink(\n readContactCompanyLink(contact, groupId),\n fields.companyId,\n )\n if (plan) {\n Object.assign(update, contactCompanyLinkFields(plan, groupId))\n counts = plan.counts\n if (fields.companyName === undefined) {\n // The label follows the link: the company's own name, or nothing.\n const label = plan.companyId ? linkedCompanyName : ''\n update[path('companyName')] = text(label)\n // The search echo — see `HostContact.companyName`.\n update['companyName'] = text(label)\n }\n }\n }\n if (fields.companyName !== undefined) {\n update[path('companyName')] = text(fields.companyName)\n update['companyName'] = text(fields.companyName)\n }\n for (const [key, value] of Object.entries(custom ?? {})) {\n update[path(`custom.${key}`)] = value\n }\n if (fields.mediaIds !== undefined) update[path(CRM_MEDIA_IDS_FIELD)] = fields.mediaIds\n\n if (!Object.keys(update).length) return { update: null, counts }\n update['updatedAt'] = FieldValue.serverTimestamp()\n return { update, counts }\n}\n\n/**\n * Every write in one batch, and — when the batch is refused — each on its\n * own, so the contact that could not be written is the only one reported.\n * Answers the ids that were not written.\n */\nasync function commitContactWrites(\n firestore: FirebaseFirestore.Firestore,\n writes: readonly ContactWrite[],\n): Promise<Set<string>> {\n const failed = new Set<string>()\n if (!writes.length) return failed\n const batch = firestore.batch()\n for (const write of writes) batch.update(write.ref, write.update)\n try {\n await batch.commit()\n return failed\n } catch {\n // A batch names no document; the pass below finds the one it was.\n }\n for (const write of writes) {\n try {\n await write.ref.update(write.update)\n } catch (error) {\n console.error('[crm] contact-update could not write a contact', write.contactId, error)\n failed.add(write.contactId)\n }\n }\n return failed\n}\n\n/** Each company's contacts count moved once, by the sum of what the written links moved. */\nasync function settleCompanyCounts(\n companies: FirebaseFirestore.CollectionReference,\n writes: readonly ContactWrite[],\n): Promise<void> {\n const deltas = new Map<string, number>()\n for (const write of writes) {\n for (const count of write.counts) {\n deltas.set(count.companyId, (deltas.get(count.companyId) ?? 0) + count.delta)\n }\n }\n await Promise.all(\n [...deltas]\n .filter(([, delta]) => delta !== 0)\n .map(([companyId, delta]) =>\n companies\n .doc(companyId)\n .update(companyContactsCountFields(delta))\n .catch((error: unknown) => {\n console.error('[crm] company contacts count could not move', companyId, delta, error)\n }),\n ),\n )\n}\n\nexport const crmContactUpdateHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n res.setHeader('Allow', 'POST')\n res.status(405).json({ error: 'Method not allowed' })\n return\n }\n const body = (req.body ?? {}) as Record<string, unknown>\n const scope = readCrmRouteScope(body)\n if (!scope) {\n res.status(400).json({ error: 'Missing hostId' })\n return\n }\n const contactIds = readContactIds(body['contactIds'])\n if (!contactIds) {\n res.status(400).json({ error: `Name between 1 and ${CRM_CONTACT_UPDATE_MAX} contacts.` })\n return\n }\n const read = readContactUpdateFields(body['set'])\n if (read.ok === false) {\n res.status(400).json({ error: read.error })\n return\n }\n const { fields } = read\n\n try {\n const writer = await authorizeCrmWriter(req, scope, { suiteAct: null })\n if (writer.ok === false) {\n res.status(writer.status).json(writer.body)\n return\n }\n if (scope.level === 'site' && !reachesSite(writer, scope.hostId)) {\n res.status(403).json({ error: 'Your access to this organization does not reach this site.' })\n return\n }\n // The plan after the person, and before any contact is read (AGL-2787).\n const suite = crmSuiteRefusal(writer.org, contactUpdateSuiteAct(fields))\n if (suite) {\n res.status(suite.status).json(suite.body)\n return\n }\n\n const firestore = firebaseAdmin.app().firestore()\n const orgRef = firestore.collection('orgs').doc(writer.orgId)\n const siteGroup =\n scope.level === 'site' ? consentGroupForHost(writer.org, scope.hostId) : null\n const siteTokens = siteGroup ? crmReadTokens(siteGroup) : null\n\n let custom: Record<string, ContactCustomValue> | undefined\n if (fields.custom) {\n const definitions = await orgRef\n .collection(CRM_COLLECTIONS.contactFields)\n .limit(CONTACT_FIELDS_MAX_PER_ORG)\n .get()\n const judged = readCrmCustomInput(\n fields.custom,\n definitions.docs.map((doc) => doc.data() as ContactFieldDefinition),\n 'contact',\n )\n if ('errors' in judged) {\n res.status(400).json({\n error: Object.values(judged.errors)[0] ?? 'A custom value could not be saved.',\n })\n return\n }\n custom = judged.values\n }\n\n /*\n * THE ORG'S LEAD SOURCES (AGL-3298), read only when the body names one.\n * Judged per contact below, because a contact keeps the value it\n * already holds even when the list has since deactivated it.\n */\n const leadSources = fields.leadSource\n ? effectiveCrmLeadSourcePicklist(\n (\n await orgRef.collection(CRM_COLLECTIONS.picklists).doc(CRM_LEAD_SOURCE_PICKLIST).get()\n ).data(),\n )\n : null\n\n if (fields.ownerUid) {\n const owner = await resolveOrgMembership(fields.ownerUid, writer.orgId).catch(() => null)\n if (!owner?.member) {\n res.status(400).json({ error: 'The owner is not a member of this organization.' })\n return\n }\n }\n\n // The company a link names has to exist and be visible to the caller and\n // the site, or a person could be filed under a record nobody here can open.\n let linkedCompanyName = ''\n if (fields.companyId) {\n const company = await orgRef\n .collection(CRM_COLLECTIONS.companies)\n .doc(fields.companyId)\n .get()\n const tokens = company.exists ? (company.get('visibleTo') as string[] | undefined) : undefined\n const visible =\n company.exists &&\n canReach(writer, tokens) &&\n (!siteTokens || visibleToTokens(tokens, siteTokens))\n if (!visible) {\n res.status(404).json({ error: 'Unknown company' })\n return\n }\n linkedCompanyName = typed(company.get('name'), 120)\n }\n\n const contacts = orgRef.collection('contacts')\n const snapshots = await firestore.getAll(...contactIds.map((id) => contacts.doc(id)))\n const outcomes = new Map<string, ContactUpdateOutcome>()\n const writes: ContactWrite[] = []\n snapshots.forEach((snapshot, index) => {\n const contactId = contactIds[index]\n if (!snapshot.exists) {\n outcomes.set(contactId, refused(contactId, 'That contact no longer exists.'))\n return\n }\n const data = (snapshot.data() ?? {}) as Record<string, unknown>\n const visibleTo = data['visibleTo'] as string[] | undefined\n if (!canReach(writer, visibleTo)) {\n outcomes.set(contactId, refused(contactId, 'That contact is not visible to you.'))\n return\n }\n if (siteTokens && !visibleToTokens(visibleTo, siteTokens)) {\n outcomes.set(contactId, refused(contactId, 'That contact is not visible to this site.'))\n return\n }\n const group = siteGroup ?? contactPrimaryGroup(data, writer.org)\n if (!group.groupId) {\n outcomes.set(contactId, refused(contactId, NO_HOLDER_REFUSAL))\n return\n }\n let rowFields = fields\n if (leadSources && fields.leadSource) {\n const judged = judgeCrmLeadSource(\n leadSources,\n fields.leadSource,\n readContactFacet(data, group.groupId).leadSource,\n )\n if (judged.ok === false) {\n outcomes.set(contactId, refused(contactId, judged.error))\n return\n }\n rowFields = { ...fields, leadSource: judged.value ?? '' }\n }\n const patch = contactPatch(data, group.groupId, rowFields, custom, linkedCompanyName)\n if ('refused' in patch) {\n outcomes.set(contactId, refused(contactId, patch.refused))\n return\n }\n outcomes.set(contactId, { contactId, ok: true })\n if (patch.update) {\n writes.push({ contactId, ref: snapshot.ref, update: patch.update, counts: patch.counts })\n }\n })\n\n const failed = await commitContactWrites(firestore, writes)\n for (const contactId of failed) {\n outcomes.set(contactId, refused(contactId, 'The contact could not be saved.'))\n }\n await settleCompanyCounts(\n orgRef.collection(CRM_COLLECTIONS.companies),\n writes.filter((write) => !failed.has(write.contactId)),\n )\n\n const answer: ContactUpdateResponse = {\n ok: true,\n results: contactIds.map(\n (contactId) => outcomes.get(contactId) ?? refused(contactId, 'The contact could not be saved.'),\n ),\n }\n res.status(200).json(answer)\n } catch (error) {\n console.error('[crm] contact-update failed', error)\n res.status(500).json({ error: 'The contact could not be saved.' })\n }\n}\n"],"names":["consentGroupForHost","CONTACT_FIELDS_MAX_PER_ORG","campaignMembershipValue","contactCampaignFieldPath","contactFacetPath","contactPrimaryGroup","CRM_COLLECTIONS","CRM_LEAD_SOURCE_PICKLIST","CRM_MEDIA_IDS_FIELD","crmReadTokens","effectiveCrmLeadSourcePicklist","isOrgWideMember","judgeCrmLeadSource","normalizeAddress","normalizeCrmMediaIds","normalizeCrmPicklistLabel","normalizePhone","planContactCompanyLink","readContactCompanyLink","readContactFacet","readCrmCustomInput","visibleToTokens","companyContactsCountFields","contactCompanyLinkFields","firebaseAdmin","resolveOrgMembership","FieldValue","CONTACT_UPDATE_FIELDS","CRM_CONTACT_UPDATE_MAX","CONTACT_NOTES_MAX","CONTACT_PHONE_REFUSAL","CONTACT_TAGS_MAX","normalizeTags","readBulkTag","typed","readCrmRouteScope","crmSuiteRefusal","authorizeCrmWriter","canReach","SUITE_ACTS","name","phone","jobTitle","leadSource","address","notes","tags","addTag","removeTag","campaignIds","ownerUid","companyId","companyName","custom","mediaIds","NO_HOLDER_REFUSAL","contactUpdateSuiteAct","fields","field","find","key","undefined","readContactUpdateFields","input","raw","Array","isArray","ok","error","unknownKey","Object","keys","includes","text","String","slice","filter","length","tag","ids","map","id","readContactIds","value","some","Set","reachesSite","writer","hostId","staff","member","Boolean","hostAccess","refused","contactId","contactPatch","contact","groupId","linkedCompanyName","path","delete","update","counts","held","toLowerCase","arrayUnion","arrayRemove","plan","assign","label","entries","serverTimestamp","commitContactWrites","firestore","writes","failed","batch","write","ref","commit","console","add","settleCompanyCounts","companies","deltas","Map","count","set","get","delta","Promise","all","doc","catch","crmContactUpdateHandler","req","res","method","setHeader","status","json","body","scope","contactIds","read","suiteAct","level","suite","org","app","orgRef","collection","orgId","siteGroup","siteTokens","definitions","contactFields","limit","judged","docs","data","values","errors","leadSources","picklists","owner","company","tokens","exists","visible","contacts","snapshots","getAll","outcomes","forEach","snapshot","index","visibleTo","group","rowFields","patch","push","has","answer","results"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/contact-update.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * `POST /api/crm/contact-update` — one holder's profile fields on one or\n * more contacts (AGL-2804).\n *\n * Body: `{ hostId, contactIds, set }` under a site, or `{ orgId, hostId?,\n * contactIds, set }` at the organization level, where `set` is\n * `ContactUpdateFields`. Answers `{ ok, results }`: one outcome per contact,\n * in the order the ids were sent.\n *\n * ## Why every facet write comes here\n *\n * A contact's fields live in the facet of the holder that keeps them —\n * `facets.{groupId}.*` — and the Firestore rules cannot tell one field of a\n * facet from another: the holder is a map key a rule cannot address, and a\n * write's affected keys name only `facets`. So the rules let a client update\n * change nothing in a facet beyond letting a holder go, and the console's\n * edits arrive here, where the plan is asked about the fields the request\n * actually carries.\n *\n * ## Who may call it\n *\n * The CRM writer the task routes admit (`authorizeCrmWriter`): under a site,\n * a member whose role writes org data, who holds `data.manage` and whose\n * access reaches the site; at the organization level, an org-wide member\n * with the permission. Each contact must then be one the caller may read —\n * the rules' `canReadScoped()` — and, under a site, one the site's hub lists.\n *\n * ## The plan\n *\n * Every field is the CRM's (AGL-2790), and a plan without the CRM has none\n * of it (AGL-2851), so it edits no contact: not the owner, company, custom\n * values or files, and not the profile a capture writes (name, phone, job\n * title, address), the tags, the notes or the campaign filing either. A\n * request carrying any field is refused to such a plan once the caller is\n * known, and before a contact is read — staff included, because the plan is\n * a fact about the workspace. Erasing a person is another route, open on\n * every plan.\n *\n * ## One holder, by dotted path\n *\n * Under a site the facet is the site's consent group's; at the organization\n * level it is each contact's own primary holder, the facet the record page\n * shows there. Every field is one dotted path — a nested `facets` object\n * would replace every other holder's map — and a text field sent empty is\n * deleted rather than stored blank, so \"no phone\" has one shape.\n *\n * ## Many contacts, one commit\n *\n * The contacts are read together and written in one batch. A batch that\n * fails is retried contact by contact, so the one that could not be written\n * is named and the rest still land. A company's contacts count follows the\n * link: summed per company and settled once the contacts are written, so a\n * company deleted in between cannot fail the save of the people at it — the\n * company page's live aggregate is what corrects a count that did not move.\n */\n\nimport {\n type AglynPostalAddress,\n consentGroupForHost,\n CONTACT_FIELDS_MAX_PER_ORG,\n type ContactCompanyLinkPlan,\n type ContactCustomValue,\n type ContactFieldDefinition,\n campaignMembershipValue,\n contactCampaignFieldPath,\n contactFacetPath,\n contactPrimaryGroup,\n CRM_COLLECTIONS,\n CRM_LEAD_SOURCE_PICKLIST,\n CRM_MEDIA_IDS_FIELD,\n crmReadTokens,\n effectiveCrmLeadSourcePicklist,\n isOrgWideMember,\n judgeCrmLeadSource,\n normalizeAddress,\n normalizeCrmMediaIds,\n normalizeCrmPicklistLabel,\n normalizePhone,\n type PluginApiHandler,\n planContactCompanyLink,\n readContactCompanyLink,\n readContactFacet,\n readCrmCustomInput,\n visibleToTokens,\n} from '@aglyn/aglyn/server'\nimport {\n companyContactsCountFields,\n contactCompanyLinkFields,\n firebaseAdmin,\n resolveOrgMembership,\n restampCrmListFieldsOf,\n} from '@aglyn/tenant-data-admin'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport {\n CONTACT_UPDATE_FIELDS,\n type ContactUpdateFields,\n type ContactUpdateOutcome,\n type ContactUpdateResponse,\n CRM_CONTACT_UPDATE_MAX,\n} from '../model/contact-update'\nimport {\n CONTACT_NOTES_MAX,\n CONTACT_PHONE_REFUSAL,\n CONTACT_TAGS_MAX,\n normalizeTags,\n readBulkTag,\n typed,\n} from './contact-profile'\nimport { readCrmRouteScope } from './org-caller'\nimport { crmSuiteRefusal } from './suite-gate'\nimport { authorizeCrmWriter, canReach, type Writer } from './task-routes'\n\n/** What the suite gate names for each field a request can carry. */\nconst SUITE_ACTS: Record<keyof ContactUpdateFields, string> = {\n name: \"Editing a contact's profile\",\n phone: \"Editing a contact's profile\",\n jobTitle: \"Editing a contact's profile\",\n leadSource: \"Editing a contact's profile\",\n address: \"Editing a contact's profile\",\n notes: \"Editing a contact's notes\",\n tags: 'Tagging a contact',\n addTag: 'Tagging a contact',\n removeTag: 'Tagging a contact',\n campaignIds: 'Filing a contact under a campaign',\n ownerUid: \"Assigning a contact's owner\",\n companyId: 'Filing a contact under a company',\n companyName: 'Filing a contact under a company',\n custom: \"Editing a contact's custom fields\",\n mediaIds: 'Attaching files to a contact',\n}\n\n/** A contact read at the organization level that no site has captured. */\nconst NO_HOLDER_REFUSAL = 'No site holds this contact yet, so it has no profile to edit.'\n\n/**\n * The act a request is refused for on a plan without the CRM: the first\n * field it carries, in the order the route reads them. Every field is the\n * CRM's, so whichever field a request names is the act its refusal names.\n */\nexport function contactUpdateSuiteAct(fields: ContactUpdateFields): string {\n const field = CONTACT_UPDATE_FIELDS.find((key) => fields[key] !== undefined)\n return field ? SUITE_ACTS[field] : \"Editing a contact's profile\"\n}\n\n/**\n * A body's `set`, read into the fields it names, or the sentence refusing\n * it. A key the route does not save is refused rather than dropped: a save\n * that silently lost a field would read as a save that kept it.\n */\nexport function readContactUpdateFields(\n input: unknown,\n): { ok: true; fields: ContactUpdateFields } | { ok: false; error: string } {\n if (!input || typeof input !== 'object' || Array.isArray(input)) {\n return { ok: false, error: 'Name the fields to save.' }\n }\n const raw = input as Record<string, unknown>\n const unknownKey = Object.keys(raw).find(\n (key) => !(CONTACT_UPDATE_FIELDS as readonly string[]).includes(key),\n )\n if (unknownKey) {\n return { ok: false, error: `\"${unknownKey}\" is not a field a contact's profile saves.` }\n }\n const fields: ContactUpdateFields = {}\n if ('name' in raw) fields.name = typed(raw['name'], 120)\n if ('phone' in raw) {\n const text = typed(raw['phone'], 40)\n const phone = text ? normalizePhone(text) : ''\n if (phone === null) return { ok: false, error: CONTACT_PHONE_REFUSAL }\n fields.phone = phone\n }\n if ('jobTitle' in raw) fields.jobTitle = typed(raw['jobTitle'], 120)\n // Held to the label's shape here; judged against the org's list by the\n // handler, which alone can read it (AGL-3298).\n if ('leadSource' in raw) fields.leadSource = normalizeCrmPicklistLabel(raw['leadSource'])\n if ('address' in raw) {\n const address = raw['address']\n fields.address =\n address && typeof address === 'object' && !Array.isArray(address)\n ? normalizeAddress(address as AglynPostalAddress)\n : null\n }\n if ('notes' in raw) fields.notes = String(raw['notes'] ?? '').slice(0, CONTACT_NOTES_MAX)\n // One write may carry one transform per field, so the tags are replaced,\n // added to or taken from — never two of those at once.\n if (['tags', 'addTag', 'removeTag'].filter((key) => key in raw).length > 1) {\n return { ok: false, error: 'Send the tags, a tag to add or a tag to remove — one of them.' }\n }\n if ('tags' in raw) {\n if (!Array.isArray(raw['tags'])) return { ok: false, error: 'Tags must be a list.' }\n fields.tags = normalizeTags(raw['tags'])\n }\n for (const key of ['addTag', 'removeTag'] as const) {\n if (!(key in raw)) continue\n const tag = readBulkTag(raw[key])\n if (!tag) return { ok: false, error: 'Name the tag.' }\n fields[key] = tag\n }\n if ('campaignIds' in raw) {\n const ids = raw['campaignIds']\n if (!Array.isArray(ids)) return { ok: false, error: 'Campaigns must be a list.' }\n fields.campaignIds = campaignMembershipValue(ids.map((id) => String(id ?? '')))\n }\n if ('ownerUid' in raw) fields.ownerUid = typed(raw['ownerUid'], 128)\n if ('companyId' in raw) {\n const companyId = raw['companyId'] === null ? null : typed(raw['companyId'], 128)\n if (companyId !== null && (!companyId || companyId.includes('/'))) {\n return { ok: false, error: 'The company could not be read.' }\n }\n fields.companyId = companyId\n }\n if ('companyName' in raw) fields.companyName = typed(raw['companyName'], 120)\n if ('custom' in raw) {\n const custom = raw['custom']\n if (!custom || typeof custom !== 'object' || Array.isArray(custom)) {\n return { ok: false, error: 'Custom fields must be values keyed by field key.' }\n }\n fields.custom = custom as Record<string, ContactCustomValue>\n }\n if ('mediaIds' in raw) {\n if (!Array.isArray(raw['mediaIds'])) {\n return { ok: false, error: 'Files must be a list of ids.' }\n }\n fields.mediaIds = normalizeCrmMediaIds(raw['mediaIds'])\n }\n if (!Object.keys(fields).length) return { ok: false, error: 'Name the fields to save.' }\n return { ok: true, fields }\n}\n\n/** The ids a body names, deduplicated — or `null` when any is unreadable or there are too many. */\nfunction readContactIds(value: unknown): string[] | null {\n if (!Array.isArray(value) || !value.length || value.length > CRM_CONTACT_UPDATE_MAX) {\n return null\n }\n const ids = value.map((id) => typed(id, 200))\n if (ids.some((id) => !id || id.includes('/'))) return null\n return [...new Set(ids)]\n}\n\n/**\n * Under a site, whether the caller's access reaches it. The rules could not\n * ask this of a facet write — they cannot see which holder a write is for —\n * and a collaborator on one site must not write another site's notes on a\n * person both of them hold.\n */\nfunction reachesSite(writer: Writer, hostId: string): boolean {\n return (\n writer.staff ||\n isOrgWideMember(writer.member) ||\n Boolean((writer.member?.hostAccess as Record<string, unknown> | undefined)?.[hostId])\n )\n}\n\nconst refused = (contactId: string, error: string): ContactUpdateOutcome => ({\n contactId,\n ok: false,\n error,\n})\n\ninterface ContactWrite {\n contactId: string\n ref: FirebaseFirestore.DocumentReference\n update: Record<string, unknown>\n counts: ContactCompanyLinkPlan['counts']\n}\n\n/**\n * One contact's update for one holder, or the sentence refusing the row.\n * `update` is `null` when the document already says everything asked — a tag\n * the row already carries, a link already made.\n */\nfunction contactPatch(\n contact: Record<string, unknown>,\n groupId: string,\n fields: ContactUpdateFields,\n custom: Record<string, ContactCustomValue> | undefined,\n linkedCompanyName: string,\n):\n | { update: Record<string, unknown> | null; counts: ContactCompanyLinkPlan['counts'] }\n | { refused: string } {\n const path = (field: string) => contactFacetPath(groupId, field)\n /** A text field as stored: the value, or the field's absence. */\n const text = (value: string) => value || FieldValue.delete()\n const update: Record<string, unknown> = {}\n let counts: ContactCompanyLinkPlan['counts'] = []\n\n if (fields.name !== undefined) update[path('name')] = text(fields.name)\n if (fields.phone !== undefined) {\n update[path('phone')] = text(fields.phone)\n // The search echo — see `HostContact.phone`.\n update['phone'] = text(fields.phone)\n }\n if (fields.jobTitle !== undefined) update[path('jobTitle')] = text(fields.jobTitle)\n if (fields.leadSource !== undefined) update[path('leadSource')] = text(fields.leadSource)\n if (fields.address !== undefined) {\n update[path('address')] = fields.address ?? FieldValue.delete()\n }\n if (fields.notes !== undefined) update[path('notes')] = fields.notes\n if (fields.tags !== undefined) update[path('tags')] = fields.tags\n\n const held = (readContactFacet(contact, groupId).tags ?? []).map((tag) =>\n String(tag).toLowerCase(),\n )\n if (fields.addTag && !held.includes(fields.addTag)) {\n // The record page's cap, which a selection must not slip past.\n if (held.length >= CONTACT_TAGS_MAX) {\n return { refused: `already has ${CONTACT_TAGS_MAX} tags` }\n }\n update[path('tags')] = FieldValue.arrayUnion(fields.addTag)\n }\n if (fields.removeTag && held.includes(fields.removeTag)) {\n update[path('tags')] = FieldValue.arrayRemove(fields.removeTag)\n }\n if (fields.campaignIds !== undefined) {\n update[contactCampaignFieldPath(groupId)] = fields.campaignIds\n }\n if (fields.ownerUid !== undefined) update[path('ownerUid')] = text(fields.ownerUid)\n\n if (fields.companyId !== undefined) {\n const plan = planContactCompanyLink(\n readContactCompanyLink(contact, groupId),\n fields.companyId,\n )\n if (plan) {\n Object.assign(update, contactCompanyLinkFields(plan, groupId))\n counts = plan.counts\n if (fields.companyName === undefined) {\n // The label follows the link: the company's own name, or nothing.\n const label = plan.companyId ? linkedCompanyName : ''\n update[path('companyName')] = text(label)\n // The search echo — see `HostContact.companyName`.\n update['companyName'] = text(label)\n }\n }\n }\n if (fields.companyName !== undefined) {\n update[path('companyName')] = text(fields.companyName)\n update['companyName'] = text(fields.companyName)\n }\n for (const [key, value] of Object.entries(custom ?? {})) {\n update[path(`custom.${key}`)] = value\n }\n if (fields.mediaIds !== undefined) update[path(CRM_MEDIA_IDS_FIELD)] = fields.mediaIds\n\n if (!Object.keys(update).length) return { update: null, counts }\n update['updatedAt'] = FieldValue.serverTimestamp()\n return { update, counts }\n}\n\n/**\n * Every write in one batch, and — when the batch is refused — each on its\n * own, so the contact that could not be written is the only one reported.\n * Answers the ids that were not written.\n */\nasync function commitContactWrites(\n firestore: FirebaseFirestore.Firestore,\n writes: readonly ContactWrite[],\n): Promise<Set<string>> {\n const failed = new Set<string>()\n if (!writes.length) return failed\n const batch = firestore.batch()\n for (const write of writes) batch.update(write.ref, write.update)\n let committed = false\n try {\n await batch.commit()\n committed = true\n } catch {\n // A batch names no document; the pass below finds the one it was.\n }\n if (!committed) {\n for (const write of writes) {\n try {\n await write.ref.update(write.update)\n } catch (error) {\n console.error('[crm] contact-update could not write a contact', write.contactId, error)\n failed.add(write.contactId)\n }\n }\n }\n // The owner, tags, company and custom values are what the Contacts list\n // filters by through `facetKeys` (AGL-3321): restamped from each row.\n await restampCrmListFieldsOf(\n writes.filter((write) => !failed.has(write.contactId)).map((write) => write.ref),\n 'contacts',\n )\n return failed\n}\n\n/** Each company's contacts count moved once, by the sum of what the written links moved. */\nasync function settleCompanyCounts(\n companies: FirebaseFirestore.CollectionReference,\n writes: readonly ContactWrite[],\n): Promise<void> {\n const deltas = new Map<string, number>()\n for (const write of writes) {\n for (const count of write.counts) {\n deltas.set(count.companyId, (deltas.get(count.companyId) ?? 0) + count.delta)\n }\n }\n await Promise.all(\n [...deltas]\n .filter(([, delta]) => delta !== 0)\n .map(([companyId, delta]) =>\n companies\n .doc(companyId)\n .update(companyContactsCountFields(delta))\n .catch((error: unknown) => {\n console.error('[crm] company contacts count could not move', companyId, delta, error)\n }),\n ),\n )\n}\n\nexport const crmContactUpdateHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n res.setHeader('Allow', 'POST')\n res.status(405).json({ error: 'Method not allowed' })\n return\n }\n const body = (req.body ?? {}) as Record<string, unknown>\n const scope = readCrmRouteScope(body)\n if (!scope) {\n res.status(400).json({ error: 'Missing hostId' })\n return\n }\n const contactIds = readContactIds(body['contactIds'])\n if (!contactIds) {\n res.status(400).json({ error: `Name between 1 and ${CRM_CONTACT_UPDATE_MAX} contacts.` })\n return\n }\n const read = readContactUpdateFields(body['set'])\n if (read.ok === false) {\n res.status(400).json({ error: read.error })\n return\n }\n const { fields } = read\n\n try {\n const writer = await authorizeCrmWriter(req, scope, { suiteAct: null })\n if (writer.ok === false) {\n res.status(writer.status).json(writer.body)\n return\n }\n if (scope.level === 'site' && !reachesSite(writer, scope.hostId)) {\n res.status(403).json({ error: 'Your access to this organization does not reach this site.' })\n return\n }\n // The plan after the person, and before any contact is read (AGL-2787).\n const suite = crmSuiteRefusal(writer.org, contactUpdateSuiteAct(fields))\n if (suite) {\n res.status(suite.status).json(suite.body)\n return\n }\n\n const firestore = firebaseAdmin.app().firestore()\n const orgRef = firestore.collection('orgs').doc(writer.orgId)\n const siteGroup =\n scope.level === 'site' ? consentGroupForHost(writer.org, scope.hostId) : null\n const siteTokens = siteGroup ? crmReadTokens(siteGroup) : null\n\n let custom: Record<string, ContactCustomValue> | undefined\n if (fields.custom) {\n const definitions = await orgRef\n .collection(CRM_COLLECTIONS.contactFields)\n .limit(CONTACT_FIELDS_MAX_PER_ORG)\n .get()\n const judged = readCrmCustomInput(\n fields.custom,\n definitions.docs.map((doc) => doc.data() as ContactFieldDefinition),\n 'contact',\n )\n if ('errors' in judged) {\n res.status(400).json({\n error: Object.values(judged.errors)[0] ?? 'A custom value could not be saved.',\n })\n return\n }\n custom = judged.values\n }\n\n /*\n * THE ORG'S LEAD SOURCES (AGL-3298), read only when the body names one.\n * Judged per contact below, because a contact keeps the value it\n * already holds even when the list has since deactivated it.\n */\n const leadSources = fields.leadSource\n ? effectiveCrmLeadSourcePicklist(\n (\n await orgRef.collection(CRM_COLLECTIONS.picklists).doc(CRM_LEAD_SOURCE_PICKLIST).get()\n ).data(),\n )\n : null\n\n if (fields.ownerUid) {\n const owner = await resolveOrgMembership(fields.ownerUid, writer.orgId).catch(() => null)\n if (!owner?.member) {\n res.status(400).json({ error: 'The owner is not a member of this organization.' })\n return\n }\n }\n\n // The company a link names has to exist and be visible to the caller and\n // the site, or a person could be filed under a record nobody here can open.\n let linkedCompanyName = ''\n if (fields.companyId) {\n const company = await orgRef\n .collection(CRM_COLLECTIONS.companies)\n .doc(fields.companyId)\n .get()\n const tokens = company.exists ? (company.get('visibleTo') as string[] | undefined) : undefined\n const visible =\n company.exists &&\n canReach(writer, tokens) &&\n (!siteTokens || visibleToTokens(tokens, siteTokens))\n if (!visible) {\n res.status(404).json({ error: 'Unknown company' })\n return\n }\n linkedCompanyName = typed(company.get('name'), 120)\n }\n\n const contacts = orgRef.collection('contacts')\n const snapshots = await firestore.getAll(...contactIds.map((id) => contacts.doc(id)))\n const outcomes = new Map<string, ContactUpdateOutcome>()\n const writes: ContactWrite[] = []\n snapshots.forEach((snapshot, index) => {\n const contactId = contactIds[index]\n if (!snapshot.exists) {\n outcomes.set(contactId, refused(contactId, 'That contact no longer exists.'))\n return\n }\n const data = (snapshot.data() ?? {}) as Record<string, unknown>\n const visibleTo = data['visibleTo'] as string[] | undefined\n if (!canReach(writer, visibleTo)) {\n outcomes.set(contactId, refused(contactId, 'That contact is not visible to you.'))\n return\n }\n if (siteTokens && !visibleToTokens(visibleTo, siteTokens)) {\n outcomes.set(contactId, refused(contactId, 'That contact is not visible to this site.'))\n return\n }\n const group = siteGroup ?? contactPrimaryGroup(data, writer.org)\n if (!group.groupId) {\n outcomes.set(contactId, refused(contactId, NO_HOLDER_REFUSAL))\n return\n }\n let rowFields = fields\n if (leadSources && fields.leadSource) {\n const judged = judgeCrmLeadSource(\n leadSources,\n fields.leadSource,\n readContactFacet(data, group.groupId).leadSource,\n )\n if (judged.ok === false) {\n outcomes.set(contactId, refused(contactId, judged.error))\n return\n }\n rowFields = { ...fields, leadSource: judged.value ?? '' }\n }\n const patch = contactPatch(data, group.groupId, rowFields, custom, linkedCompanyName)\n if ('refused' in patch) {\n outcomes.set(contactId, refused(contactId, patch.refused))\n return\n }\n outcomes.set(contactId, { contactId, ok: true })\n if (patch.update) {\n writes.push({ contactId, ref: snapshot.ref, update: patch.update, counts: patch.counts })\n }\n })\n\n const failed = await commitContactWrites(firestore, writes)\n for (const contactId of failed) {\n outcomes.set(contactId, refused(contactId, 'The contact could not be saved.'))\n }\n await settleCompanyCounts(\n orgRef.collection(CRM_COLLECTIONS.companies),\n writes.filter((write) => !failed.has(write.contactId)),\n )\n\n const answer: ContactUpdateResponse = {\n ok: true,\n results: contactIds.map(\n (contactId) => outcomes.get(contactId) ?? refused(contactId, 'The contact could not be saved.'),\n ),\n }\n res.status(200).json(answer)\n } catch (error) {\n console.error('[crm] contact-update failed', error)\n res.status(500).json({ error: 'The contact could not be saved.' })\n }\n}\n"],"names":["consentGroupForHost","CONTACT_FIELDS_MAX_PER_ORG","campaignMembershipValue","contactCampaignFieldPath","contactFacetPath","contactPrimaryGroup","CRM_COLLECTIONS","CRM_LEAD_SOURCE_PICKLIST","CRM_MEDIA_IDS_FIELD","crmReadTokens","effectiveCrmLeadSourcePicklist","isOrgWideMember","judgeCrmLeadSource","normalizeAddress","normalizeCrmMediaIds","normalizeCrmPicklistLabel","normalizePhone","planContactCompanyLink","readContactCompanyLink","readContactFacet","readCrmCustomInput","visibleToTokens","companyContactsCountFields","contactCompanyLinkFields","firebaseAdmin","resolveOrgMembership","restampCrmListFieldsOf","FieldValue","CONTACT_UPDATE_FIELDS","CRM_CONTACT_UPDATE_MAX","CONTACT_NOTES_MAX","CONTACT_PHONE_REFUSAL","CONTACT_TAGS_MAX","normalizeTags","readBulkTag","typed","readCrmRouteScope","crmSuiteRefusal","authorizeCrmWriter","canReach","SUITE_ACTS","name","phone","jobTitle","leadSource","address","notes","tags","addTag","removeTag","campaignIds","ownerUid","companyId","companyName","custom","mediaIds","NO_HOLDER_REFUSAL","contactUpdateSuiteAct","fields","field","find","key","undefined","readContactUpdateFields","input","raw","Array","isArray","ok","error","unknownKey","Object","keys","includes","text","String","slice","filter","length","tag","ids","map","id","readContactIds","value","some","Set","reachesSite","writer","hostId","staff","member","Boolean","hostAccess","refused","contactId","contactPatch","contact","groupId","linkedCompanyName","path","delete","update","counts","held","toLowerCase","arrayUnion","arrayRemove","plan","assign","label","entries","serverTimestamp","commitContactWrites","firestore","writes","failed","batch","write","ref","committed","commit","console","add","has","settleCompanyCounts","companies","deltas","Map","count","set","get","delta","Promise","all","doc","catch","crmContactUpdateHandler","req","res","method","setHeader","status","json","body","scope","contactIds","read","suiteAct","level","suite","org","app","orgRef","collection","orgId","siteGroup","siteTokens","definitions","contactFields","limit","judged","docs","data","values","errors","leadSources","picklists","owner","company","tokens","exists","visible","contacts","snapshots","getAll","outcomes","forEach","snapshot","index","visibleTo","group","rowFields","patch","push","answer","results"],"mappings":"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"}
@@ -58,7 +58,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
58
58
  * `resolveImportContext` (`import-context.ts`) — the two gates every CSV
59
59
  * import route asks, shared with the companies import so one permission
60
60
  * has one spelling.
61
- */ import { CRM_COLLECTIONS, nameSearchFields, visibleToTokens } from "@aglyn/aglyn/server";
61
+ */ import { CRM_COLLECTIONS, crmNewRecordListFields, nameSearchFields, visibleToTokens } from "@aglyn/aglyn/server";
62
62
  import { CONTACT_IMPORT_CHUNK_SIZE, CONTACT_IMPORT_MAX_BODY_BYTES, normalizeContactImportRow } from "../model/crm-import.js";
63
63
  import { crmRecordsQuotaForOrg, firebaseAdmin } from "@aglyn/tenant-data-admin";
64
64
  import { captureHostContact } from "@aglyn/tenant-runtime";
@@ -122,7 +122,9 @@ import { ownerDirectory, readImportRows, resolveImportContext } from "./import-c
122
122
  createdByUid: context.uid,
123
123
  createdAt: FieldValue.serverTimestamp(),
124
124
  updatedAt: FieldValue.serverTimestamp()
125
- }));
125
+ }, crmNewRecordListFields('companies', _extends({}, fields, {
126
+ visibleTo: context.scopeTokens
127
+ }))));
126
128
  tally.created += 1;
127
129
  const made = {
128
130
  id: created.id,