@aglyn/plugins-crm 1.0.0-beta.221 → 1.0.0-beta.222

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 (514) hide show
  1. package/README.md +2 -1
  2. package/package.json +13 -13
  3. package/src/lib/components/activity-list.js +12 -5
  4. package/src/lib/components/activity-list.js.map +1 -1
  5. package/src/lib/components/companies-bulk-bar.d.ts +1 -4
  6. package/src/lib/components/companies-bulk-bar.js +7 -20
  7. package/src/lib/components/companies-bulk-bar.js.map +1 -1
  8. package/src/lib/components/companies-section.d.ts +3 -3
  9. package/src/lib/components/companies-section.js +149 -46
  10. package/src/lib/components/companies-section.js.map +1 -1
  11. package/src/lib/components/company-edit-drawer.js +281 -20
  12. package/src/lib/components/company-edit-drawer.js.map +1 -1
  13. package/src/lib/components/company-properties-card.js +99 -8
  14. package/src/lib/components/company-properties-card.js.map +1 -1
  15. package/src/lib/components/contact-detail-page.js +3 -1
  16. package/src/lib/components/contact-detail-page.js.map +1 -1
  17. package/src/lib/components/contact-merge-dialog.js +33 -10
  18. package/src/lib/components/contact-merge-dialog.js.map +1 -1
  19. package/src/lib/components/contact-properties-card.d.ts +0 -69
  20. package/src/lib/components/contact-properties-card.js +350 -60
  21. package/src/lib/components/contact-properties-card.js.map +1 -1
  22. package/src/lib/components/contact-reports-to-field.d.ts +46 -0
  23. package/src/lib/components/contact-reports-to-field.js +67 -0
  24. package/src/lib/components/contact-reports-to-field.js.map +1 -0
  25. package/src/lib/components/contacts-bulk-bar.d.ts +1 -8
  26. package/src/lib/components/contacts-bulk-bar.js +7 -19
  27. package/src/lib/components/contacts-bulk-bar.js.map +1 -1
  28. package/src/lib/components/contacts-section.js +46 -33
  29. package/src/lib/components/contacts-section.js.map +1 -1
  30. package/src/lib/components/crm-call-actions.d.ts +10 -0
  31. package/src/lib/components/crm-call-actions.js +26 -9
  32. package/src/lib/components/crm-call-actions.js.map +1 -1
  33. package/src/lib/components/crm-record-picker.d.ts +15 -0
  34. package/src/lib/components/crm-record-picker.js +16 -10
  35. package/src/lib/components/crm-record-picker.js.map +1 -1
  36. package/src/lib/components/crm-transfer-buttons.d.ts +54 -0
  37. package/src/lib/components/crm-transfer-buttons.js +167 -0
  38. package/src/lib/components/crm-transfer-buttons.js.map +1 -0
  39. package/src/lib/components/deal-contact-roles-card.d.ts +46 -0
  40. package/src/lib/components/deal-contact-roles-card.js +368 -0
  41. package/src/lib/components/deal-contact-roles-card.js.map +1 -0
  42. package/src/lib/components/deal-detail-page.d.ts +3 -2
  43. package/src/lib/components/deal-detail-page.js +25 -6
  44. package/src/lib/components/deal-detail-page.js.map +1 -1
  45. package/src/lib/components/deal-edit-drawer.d.ts +10 -0
  46. package/src/lib/components/deal-edit-drawer.js +219 -10
  47. package/src/lib/components/deal-edit-drawer.js.map +1 -1
  48. package/src/lib/components/deal-properties-card.d.ts +7 -0
  49. package/src/lib/components/deal-properties-card.js +34 -3
  50. package/src/lib/components/deal-properties-card.js.map +1 -1
  51. package/src/lib/components/deal-stage-card.d.ts +4 -0
  52. package/src/lib/components/deal-stage-card.js +10 -2
  53. package/src/lib/components/deal-stage-card.js.map +1 -1
  54. package/src/lib/components/deals-bulk-bar.d.ts +0 -3
  55. package/src/lib/components/deals-bulk-bar.js +7 -20
  56. package/src/lib/components/deals-bulk-bar.js.map +1 -1
  57. package/src/lib/components/deals-section.d.ts +3 -3
  58. package/src/lib/components/deals-section.js +123 -63
  59. package/src/lib/components/deals-section.js.map +1 -1
  60. package/src/lib/components/fields-section.js +31 -17
  61. package/src/lib/components/fields-section.js.map +1 -1
  62. package/src/lib/components/lead-convert-dialog.d.ts +4 -0
  63. package/src/lib/components/lead-convert-dialog.js +23 -1
  64. package/src/lib/components/lead-convert-dialog.js.map +1 -1
  65. package/src/lib/components/lead-detail-page.js +8 -2
  66. package/src/lib/components/lead-detail-page.js.map +1 -1
  67. package/src/lib/components/lead-profile-fields.d.ts +71 -0
  68. package/src/lib/components/lead-profile-fields.js +386 -0
  69. package/src/lib/components/lead-profile-fields.js.map +1 -0
  70. package/src/lib/components/lead-properties-card.d.ts +2 -1
  71. package/src/lib/components/lead-properties-card.js +85 -176
  72. package/src/lib/components/lead-properties-card.js.map +1 -1
  73. package/src/lib/components/lead-source-select.d.ts +4 -23
  74. package/src/lib/components/lead-source-select.js +9 -73
  75. package/src/lib/components/lead-source-select.js.map +1 -1
  76. package/src/lib/components/lead-status-chip.d.ts +9 -3
  77. package/src/lib/components/lead-status-chip.js +8 -4
  78. package/src/lib/components/lead-status-chip.js.map +1 -1
  79. package/src/lib/components/lead-status-options.d.ts +20 -0
  80. package/src/lib/components/lead-status-options.js +51 -0
  81. package/src/lib/components/lead-status-options.js.map +1 -0
  82. package/src/lib/components/lead-unqualify-dialog.d.ts +5 -0
  83. package/src/lib/components/lead-unqualify-dialog.js +50 -18
  84. package/src/lib/components/lead-unqualify-dialog.js.map +1 -1
  85. package/src/lib/components/leads-bulk-bar.d.ts +0 -3
  86. package/src/lib/components/leads-bulk-bar.js +29 -29
  87. package/src/lib/components/leads-bulk-bar.js.map +1 -1
  88. package/src/lib/components/leads-section.js +120 -68
  89. package/src/lib/components/leads-section.js.map +1 -1
  90. package/src/lib/components/linked-deals-card.d.ts +4 -0
  91. package/src/lib/components/linked-deals-card.js +47 -3
  92. package/src/lib/components/linked-deals-card.js.map +1 -1
  93. package/src/lib/components/log-activity-dialog.js +43 -3
  94. package/src/lib/components/log-activity-dialog.js.map +1 -1
  95. package/src/lib/components/new-contact-drawer.d.ts +8 -0
  96. package/src/lib/components/new-contact-drawer.js +108 -6
  97. package/src/lib/components/new-contact-drawer.js.map +1 -1
  98. package/src/lib/components/new-lead-drawer.d.ts +9 -2
  99. package/src/lib/components/new-lead-drawer.js +88 -121
  100. package/src/lib/components/new-lead-drawer.js.map +1 -1
  101. package/src/lib/components/picklist-select.d.ts +36 -0
  102. package/src/lib/components/picklist-select.js +114 -0
  103. package/src/lib/components/picklist-select.js.map +1 -0
  104. package/src/lib/components/picklist-values-card.d.ts +34 -0
  105. package/src/lib/components/{lead-source-values-card.js → picklist-values-card.js} +241 -58
  106. package/src/lib/components/picklist-values-card.js.map +1 -0
  107. package/src/lib/components/pipeline-stages-dialog.d.ts +7 -0
  108. package/src/lib/components/pipeline-stages-dialog.js +26 -4
  109. package/src/lib/components/pipeline-stages-dialog.js.map +1 -1
  110. package/src/lib/components/pipelines-dialog.js +15 -6
  111. package/src/lib/components/pipelines-dialog.js.map +1 -1
  112. package/src/lib/components/recent-activity-feed.js +20 -5
  113. package/src/lib/components/recent-activity-feed.js.map +1 -1
  114. package/src/lib/components/recipes-card.js +4 -2
  115. package/src/lib/components/recipes-card.js.map +1 -1
  116. package/src/lib/components/record-tasks-card.js +15 -8
  117. package/src/lib/components/record-tasks-card.js.map +1 -1
  118. package/src/lib/components/reports/forecast-card.d.ts +6 -0
  119. package/src/lib/components/reports/forecast-card.js +101 -3
  120. package/src/lib/components/reports/forecast-card.js.map +1 -1
  121. package/src/lib/components/reports/lead-funnel-card.d.ts +12 -0
  122. package/src/lib/components/reports/lead-funnel-card.js +9 -5
  123. package/src/lib/components/reports/lead-funnel-card.js.map +1 -1
  124. package/src/lib/components/reports/lead-source-card.d.ts +23 -0
  125. package/src/lib/components/reports/lead-source-card.js +318 -0
  126. package/src/lib/components/reports/lead-source-card.js.map +1 -0
  127. package/src/lib/components/reports/report-export.js +1 -1
  128. package/src/lib/components/reports/report-export.js.map +1 -1
  129. package/src/lib/components/reports-section.js +8 -0
  130. package/src/lib/components/reports-section.js.map +1 -1
  131. package/src/lib/components/sharing-rule-drawer.d.ts +5 -0
  132. package/src/lib/components/sharing-rule-drawer.js +108 -19
  133. package/src/lib/components/sharing-rule-drawer.js.map +1 -1
  134. package/src/lib/components/sharing-rules-card.js +11 -3
  135. package/src/lib/components/sharing-rules-card.js.map +1 -1
  136. package/src/lib/components/task-cells.d.ts +9 -1
  137. package/src/lib/components/task-cells.js +9 -4
  138. package/src/lib/components/task-cells.js.map +1 -1
  139. package/src/lib/components/task-edit-drawer.js +111 -43
  140. package/src/lib/components/task-edit-drawer.js.map +1 -1
  141. package/src/lib/components/tasks-bulk-bar.d.ts +0 -3
  142. package/src/lib/components/tasks-bulk-bar.js +7 -20
  143. package/src/lib/components/tasks-bulk-bar.js.map +1 -1
  144. package/src/lib/components/tasks-calendar.d.ts +3 -0
  145. package/src/lib/components/tasks-calendar.js +8 -1
  146. package/src/lib/components/tasks-calendar.js.map +1 -1
  147. package/src/lib/components/tasks-section.d.ts +3 -3
  148. package/src/lib/components/tasks-section.js +58 -43
  149. package/src/lib/components/tasks-section.js.map +1 -1
  150. package/src/lib/constants/api-routes.d.ts +2 -9
  151. package/src/lib/constants/api-routes.js +1 -7
  152. package/src/lib/constants/api-routes.js.map +1 -1
  153. package/src/lib/constants/bundle-common.d.ts +6 -0
  154. package/src/lib/constants/bundle-common.js +11 -0
  155. package/src/lib/constants/bundle-common.js.map +1 -1
  156. package/src/lib/constants/company-filters.d.ts +14 -0
  157. package/src/lib/constants/company-filters.js +51 -1
  158. package/src/lib/constants/company-filters.js.map +1 -1
  159. package/src/lib/constants/contact-filters.d.ts +2 -0
  160. package/src/lib/constants/contact-filters.js +18 -1
  161. package/src/lib/constants/contact-filters.js.map +1 -1
  162. package/src/lib/constants/deal-filters.d.ts +22 -1
  163. package/src/lib/constants/deal-filters.js +67 -4
  164. package/src/lib/constants/deal-filters.js.map +1 -1
  165. package/src/lib/declarations.console-server.d.ts +3 -0
  166. package/src/lib/declarations.console-server.js +35 -0
  167. package/src/lib/declarations.console-server.js.map +1 -1
  168. package/src/lib/declarations.d.ts +26 -0
  169. package/src/lib/declarations.js +35 -0
  170. package/src/lib/declarations.js.map +1 -0
  171. package/src/lib/declarations.server.d.ts +11 -0
  172. package/src/lib/declarations.server.js +92 -1
  173. package/src/lib/declarations.server.js.map +1 -1
  174. package/src/lib/hooks/use-company-picklists.d.ts +15 -0
  175. package/src/lib/hooks/use-company-picklists.js +55 -0
  176. package/src/lib/hooks/use-company-picklists.js.map +1 -0
  177. package/src/lib/hooks/use-crm-picklist.d.ts +25 -0
  178. package/src/lib/hooks/use-crm-picklist.js +53 -0
  179. package/src/lib/hooks/use-crm-picklist.js.map +1 -0
  180. package/src/lib/hooks/use-crm-record-names.d.ts +16 -0
  181. package/src/lib/hooks/use-crm-record-names.js +45 -19
  182. package/src/lib/hooks/use-crm-record-names.js.map +1 -1
  183. package/src/lib/hooks/use-crm-task-picklists.d.ts +11 -0
  184. package/src/lib/hooks/use-crm-task-picklists.js +45 -0
  185. package/src/lib/hooks/use-crm-task-picklists.js.map +1 -0
  186. package/src/lib/hooks/use-deals.d.ts +14 -0
  187. package/src/lib/hooks/use-deals.js +29 -0
  188. package/src/lib/hooks/use-deals.js.map +1 -1
  189. package/src/lib/hooks/use-lead-picklists.d.ts +14 -0
  190. package/src/lib/hooks/use-lead-picklists.js +46 -0
  191. package/src/lib/hooks/use-lead-picklists.js.map +1 -0
  192. package/src/lib/hooks/use-lead-source-picklist.d.ts +5 -19
  193. package/src/lib/hooks/use-lead-source-picklist.js +12 -22
  194. package/src/lib/hooks/use-lead-source-picklist.js.map +1 -1
  195. package/src/lib/hooks/use-lead-status-picklist.d.ts +9 -0
  196. package/src/lib/hooks/use-lead-status-picklist.js +40 -0
  197. package/src/lib/hooks/use-lead-status-picklist.js.map +1 -0
  198. package/src/lib/model/companies.d.ts +32 -3
  199. package/src/lib/model/companies.js +133 -16
  200. package/src/lib/model/companies.js.map +1 -1
  201. package/src/lib/model/contact-merge.d.ts +27 -3
  202. package/src/lib/model/contact-merge.js +81 -7
  203. package/src/lib/model/contact-merge.js.map +1 -1
  204. package/src/lib/model/contact-record.d.ts +17 -0
  205. package/src/lib/model/contact-record.js +15 -1
  206. package/src/lib/model/contact-record.js.map +1 -1
  207. package/src/lib/model/contact-update.d.ts +31 -2
  208. package/src/lib/model/contact-update.js +14 -0
  209. package/src/lib/model/contact-update.js.map +1 -1
  210. package/src/lib/model/crm-list-query.d.ts +5 -0
  211. package/src/lib/model/crm-list-query.js +8 -0
  212. package/src/lib/model/crm-list-query.js.map +1 -1
  213. package/src/lib/model/crm-recipes.d.ts +40 -0
  214. package/src/lib/model/crm-recipes.js +244 -0
  215. package/src/lib/model/crm-recipes.js.map +1 -0
  216. package/src/lib/model/crm-record-counts.d.ts +36 -0
  217. package/src/lib/model/crm-record-counts.js +54 -0
  218. package/src/lib/model/crm-record-counts.js.map +1 -0
  219. package/src/lib/model/crm-record-lists.d.ts +23 -0
  220. package/src/lib/model/crm-record-lists.js +161 -0
  221. package/src/lib/model/crm-record-lists.js.map +1 -0
  222. package/src/lib/model/crm-reports.d.ts +73 -3
  223. package/src/lib/model/crm-reports.js +120 -1
  224. package/src/lib/model/crm-reports.js.map +1 -1
  225. package/src/lib/model/crm-sharing.d.ts +21 -3
  226. package/src/lib/model/crm-sharing.js +27 -6
  227. package/src/lib/model/crm-sharing.js.map +1 -1
  228. package/src/lib/model/deal-board-model.d.ts +8 -2
  229. package/src/lib/model/deal-board-model.js +15 -2
  230. package/src/lib/model/deal-board-model.js.map +1 -1
  231. package/src/lib/model/deal-contact-roles.d.ts +45 -0
  232. package/src/lib/model/deal-contact-roles.js +73 -0
  233. package/src/lib/model/deal-contact-roles.js.map +1 -0
  234. package/src/lib/model/deal-form-model.d.ts +56 -0
  235. package/src/lib/model/deal-form-model.js +79 -7
  236. package/src/lib/model/deal-form-model.js.map +1 -1
  237. package/src/lib/model/download-text-file.d.ts +23 -0
  238. package/src/lib/model/download-text-file.js +34 -0
  239. package/src/lib/model/download-text-file.js.map +1 -0
  240. package/src/lib/model/lead-filters.d.ts +36 -2
  241. package/src/lib/model/lead-filters.js +123 -2
  242. package/src/lib/model/lead-filters.js.map +1 -1
  243. package/src/lib/model/lead-source-direction.d.ts +38 -0
  244. package/src/lib/model/lead-source-direction.js +52 -0
  245. package/src/lib/model/lead-source-direction.js.map +1 -0
  246. package/src/lib/model/picklist-values.d.ts +119 -0
  247. package/src/lib/model/picklist-values.js +256 -0
  248. package/src/lib/model/picklist-values.js.map +1 -0
  249. package/src/lib/model/record-facts-disclosed.d.ts +81 -0
  250. package/src/lib/model/record-facts-disclosed.js +281 -0
  251. package/src/lib/model/record-facts-disclosed.js.map +1 -0
  252. package/src/lib/model/record-facts.d.ts +215 -34
  253. package/src/lib/model/record-facts.js +307 -128
  254. package/src/lib/model/record-facts.js.map +1 -1
  255. package/src/lib/model/task-picklists.d.ts +52 -0
  256. package/src/lib/model/task-picklists.js +83 -0
  257. package/src/lib/model/task-picklists.js.map +1 -0
  258. package/src/lib/model/task-routes.d.ts +13 -0
  259. package/src/lib/model/task-routes.js +23 -4
  260. package/src/lib/model/task-routes.js.map +1 -1
  261. package/src/lib/plan-entitlements.js +6 -4
  262. package/src/lib/plan-entitlements.js.map +1 -1
  263. package/src/lib/plugin.js +17 -0
  264. package/src/lib/plugin.js.map +1 -1
  265. package/src/lib/server/api-v1/contacts.js +220 -14
  266. package/src/lib/server/api-v1/contacts.js.map +1 -1
  267. package/src/lib/server/api-v1/crm-activities.js +14 -1
  268. package/src/lib/server/api-v1/crm-activities.js.map +1 -1
  269. package/src/lib/server/api-v1/crm-companies.js +123 -14
  270. package/src/lib/server/api-v1/crm-companies.js.map +1 -1
  271. package/src/lib/server/api-v1/crm-deals.js +291 -26
  272. package/src/lib/server/api-v1/crm-deals.js.map +1 -1
  273. package/src/lib/server/api-v1/crm-leads.js +144 -38
  274. package/src/lib/server/api-v1/crm-leads.js.map +1 -1
  275. package/src/lib/server/api-v1/crm-pipelines.d.ts +6 -0
  276. package/src/lib/server/api-v1/crm-pipelines.js +8 -2
  277. package/src/lib/server/api-v1/crm-pipelines.js.map +1 -1
  278. package/src/lib/server/api-v1/crm-tasks.js +75 -33
  279. package/src/lib/server/api-v1/crm-tasks.js.map +1 -1
  280. package/src/lib/server/api-v1/openapi.d.ts +0 -11
  281. package/src/lib/server/api-v1/openapi.js +171 -16
  282. package/src/lib/server/api-v1/openapi.js.map +1 -1
  283. package/src/lib/server/assign-contact-owner.d.ts +162 -0
  284. package/src/lib/server/assign-contact-owner.js +340 -0
  285. package/src/lib/server/assign-contact-owner.js.map +1 -0
  286. package/src/lib/server/associate-company-by-domain.d.ts +70 -0
  287. package/src/lib/server/associate-company-by-domain.js +153 -0
  288. package/src/lib/server/associate-company-by-domain.js.map +1 -0
  289. package/src/lib/server/automation-steps.d.ts +139 -0
  290. package/src/lib/server/automation-steps.js +472 -0
  291. package/src/lib/server/automation-steps.js.map +1 -0
  292. package/src/lib/server/capture-contact.js +65 -21
  293. package/src/lib/server/capture-contact.js.map +1 -1
  294. package/src/lib/server/capture-host-contact.d.ts +104 -0
  295. package/src/lib/server/capture-host-contact.js +151 -0
  296. package/src/lib/server/capture-host-contact.js.map +1 -0
  297. package/src/lib/server/company-children.d.ts +25 -0
  298. package/src/lib/server/company-children.js +42 -0
  299. package/src/lib/server/company-children.js.map +1 -0
  300. package/src/lib/server/company-delete.d.ts +18 -0
  301. package/src/lib/server/company-delete.js +44 -21
  302. package/src/lib/server/company-delete.js.map +1 -1
  303. package/src/lib/server/contact-email-history.js +21 -19
  304. package/src/lib/server/contact-email-history.js.map +1 -1
  305. package/src/lib/server/contact-remove.js +27 -2
  306. package/src/lib/server/contact-remove.js.map +1 -1
  307. package/src/lib/server/contact-reports-to.d.ts +61 -0
  308. package/src/lib/server/contact-reports-to.js +114 -0
  309. package/src/lib/server/contact-reports-to.js.map +1 -0
  310. package/src/lib/server/contact-update.d.ts +42 -1
  311. package/src/lib/server/contact-update.js +204 -23
  312. package/src/lib/server/contact-update.js.map +1 -1
  313. package/src/lib/server/convert-host-lead.d.ts +46 -1
  314. package/src/lib/server/convert-host-lead.js +173 -15
  315. package/src/lib/server/convert-host-lead.js.map +1 -1
  316. package/src/lib/server/convert-open-lead.js +12 -5
  317. package/src/lib/server/convert-open-lead.js.map +1 -1
  318. package/src/lib/server/crm-record-indexes.d.ts +45 -0
  319. package/src/lib/server/crm-record-indexes.js +78 -0
  320. package/src/lib/server/crm-record-indexes.js.map +1 -0
  321. package/src/lib/server/crm-sharing.d.ts +9 -1
  322. package/src/lib/server/crm-sharing.js +23 -3
  323. package/src/lib/server/crm-sharing.js.map +1 -1
  324. package/src/lib/server/deal-contact-roles.d.ts +27 -0
  325. package/src/lib/server/deal-contact-roles.js +89 -0
  326. package/src/lib/server/deal-contact-roles.js.map +1 -0
  327. package/src/lib/server/lead-convert.d.ts +5 -0
  328. package/src/lib/server/lead-convert.js +19 -3
  329. package/src/lib/server/lead-convert.js.map +1 -1
  330. package/src/lib/server/lead-create.d.ts +17 -2
  331. package/src/lib/server/lead-create.js +46 -14
  332. package/src/lib/server/lead-create.js.map +1 -1
  333. package/src/lib/server/lead-nurturing.d.ts +6 -0
  334. package/src/lib/server/lead-nurturing.js +57 -20
  335. package/src/lib/server/lead-nurturing.js.map +1 -1
  336. package/src/lib/server/lead-picklists.d.ts +35 -0
  337. package/src/lib/server/lead-picklists.js +52 -0
  338. package/src/lib/server/lead-picklists.js.map +1 -0
  339. package/src/lib/server/lead-source-picklist.d.ts +1 -13
  340. package/src/lib/server/lead-source-picklist.js +6 -33
  341. package/src/lib/server/lead-source-picklist.js.map +1 -1
  342. package/src/lib/server/merge-contacts.js +22 -4
  343. package/src/lib/server/merge-contacts.js.map +1 -1
  344. package/src/lib/server/person-eraser.d.ts +53 -0
  345. package/src/lib/server/person-eraser.js +149 -0
  346. package/src/lib/server/person-eraser.js.map +1 -0
  347. package/src/lib/server/person-records.d.ts +69 -0
  348. package/src/lib/server/person-records.js +317 -0
  349. package/src/lib/server/person-records.js.map +1 -0
  350. package/src/lib/server/person-refund.d.ts +97 -0
  351. package/src/lib/server/person-refund.js +221 -0
  352. package/src/lib/server/person-refund.js.map +1 -0
  353. package/src/lib/server/picklist-values.d.ts +21 -0
  354. package/src/lib/server/picklist-values.js +308 -0
  355. package/src/lib/server/picklist-values.js.map +1 -0
  356. package/src/lib/server/read-picklist.d.ts +44 -0
  357. package/src/lib/server/read-picklist.js +65 -0
  358. package/src/lib/server/read-picklist.js.map +1 -0
  359. package/src/lib/server/recipe-routes.js +15 -7
  360. package/src/lib/server/recipe-routes.js.map +1 -1
  361. package/src/lib/server/record-email-state.js +7 -1
  362. package/src/lib/server/record-email-state.js.map +1 -1
  363. package/src/lib/server/record-facts.d.ts +3 -0
  364. package/src/lib/server/record-facts.js +233 -31
  365. package/src/lib/server/record-facts.js.map +1 -1
  366. package/src/lib/server/record-origin.d.ts +27 -0
  367. package/src/lib/server/record-origin.js +104 -0
  368. package/src/lib/server/record-origin.js.map +1 -0
  369. package/src/lib/server/record-timeline.js +15 -2
  370. package/src/lib/server/record-timeline.js.map +1 -1
  371. package/src/lib/server/suite-gate.js +4 -3
  372. package/src/lib/server/suite-gate.js.map +1 -1
  373. package/src/lib/server/task-routes.js +30 -16
  374. package/src/lib/server/task-routes.js.map +1 -1
  375. package/src/lib/server-deal-stage.d.ts +5 -1
  376. package/src/lib/server-deal-stage.js +10 -5
  377. package/src/lib/server-deal-stage.js.map +1 -1
  378. package/src/lib/server.d.ts +8 -3
  379. package/src/lib/server.js +47 -43
  380. package/src/lib/server.js.map +1 -1
  381. package/src/lib/testing/memory-firestore.d.ts +55 -0
  382. package/src/lib/testing/memory-firestore.js +335 -0
  383. package/src/lib/testing/memory-firestore.js.map +1 -0
  384. package/src/lib/testing/stand-in-email-send-index.d.ts +25 -0
  385. package/src/lib/testing/stand-in-email-send-index.js +49 -0
  386. package/src/lib/testing/stand-in-email-send-index.js.map +1 -0
  387. package/src/lib/transfer/activities.d.ts +30 -0
  388. package/src/lib/transfer/activities.js +132 -0
  389. package/src/lib/transfer/activities.js.map +1 -0
  390. package/src/lib/transfer/aliases.d.ts +22 -0
  391. package/src/lib/transfer/aliases.js +1342 -0
  392. package/src/lib/transfer/aliases.js.map +1 -0
  393. package/src/lib/transfer/common.d.ts +222 -0
  394. package/src/lib/transfer/common.js +832 -0
  395. package/src/lib/transfer/common.js.map +1 -0
  396. package/src/lib/transfer/companies.d.ts +43 -0
  397. package/src/lib/transfer/companies.js +549 -0
  398. package/src/lib/transfer/companies.js.map +1 -0
  399. package/src/lib/transfer/contacts.d.ts +31 -0
  400. package/src/lib/transfer/contacts.js +727 -0
  401. package/src/lib/transfer/contacts.js.map +1 -0
  402. package/src/lib/transfer/deals.d.ts +66 -0
  403. package/src/lib/transfer/deals.js +698 -0
  404. package/src/lib/transfer/deals.js.map +1 -0
  405. package/src/lib/transfer/email-templates-package.d.ts +40 -0
  406. package/src/lib/transfer/email-templates-package.js +76 -0
  407. package/src/lib/transfer/email-templates-package.js.map +1 -0
  408. package/src/lib/transfer/email-templates-package.server.d.ts +32 -0
  409. package/src/lib/transfer/email-templates-package.server.js +176 -0
  410. package/src/lib/transfer/email-templates-package.server.js.map +1 -0
  411. package/src/lib/transfer/fields.d.ts +102 -0
  412. package/src/lib/transfer/fields.js +1755 -0
  413. package/src/lib/transfer/fields.js.map +1 -0
  414. package/src/lib/transfer/leads.d.ts +30 -0
  415. package/src/lib/transfer/leads.js +582 -0
  416. package/src/lib/transfer/leads.js.map +1 -0
  417. package/src/lib/transfer/pipelines.d.ts +21 -0
  418. package/src/lib/transfer/pipelines.js +172 -0
  419. package/src/lib/transfer/pipelines.js.map +1 -0
  420. package/src/lib/transfer/register.d.ts +47 -0
  421. package/src/lib/transfer/register.js +277 -0
  422. package/src/lib/transfer/register.js.map +1 -0
  423. package/src/lib/transfer/tasks.d.ts +30 -0
  424. package/src/lib/transfer/tasks.js +418 -0
  425. package/src/lib/transfer/tasks.js.map +1 -0
  426. package/src/lib/transfer/ui.d.ts +20 -0
  427. package/src/lib/transfer/ui.js +42 -0
  428. package/src/lib/transfer/ui.js.map +1 -0
  429. package/src/lib/components/company-import-drawer.d.ts +0 -42
  430. package/src/lib/components/company-import-drawer.js +0 -99
  431. package/src/lib/components/company-import-drawer.js.map +0 -1
  432. package/src/lib/components/contact-import-drawer.d.ts +0 -46
  433. package/src/lib/components/contact-import-drawer.js +0 -149
  434. package/src/lib/components/contact-import-drawer.js.map +0 -1
  435. package/src/lib/components/crm-export-all-button.d.ts +0 -39
  436. package/src/lib/components/crm-export-all-button.js +0 -98
  437. package/src/lib/components/crm-export-all-button.js.map +0 -1
  438. package/src/lib/components/csv-import-drawer.d.ts +0 -139
  439. package/src/lib/components/csv-import-drawer.js +0 -757
  440. package/src/lib/components/csv-import-drawer.js.map +0 -1
  441. package/src/lib/components/deal-import-drawer.d.ts +0 -24
  442. package/src/lib/components/deal-import-drawer.js +0 -83
  443. package/src/lib/components/deal-import-drawer.js.map +0 -1
  444. package/src/lib/components/lead-import-drawer.d.ts +0 -27
  445. package/src/lib/components/lead-import-drawer.js +0 -118
  446. package/src/lib/components/lead-import-drawer.js.map +0 -1
  447. package/src/lib/components/lead-source-values-card.d.ts +0 -27
  448. package/src/lib/components/lead-source-values-card.js.map +0 -1
  449. package/src/lib/components/task-import-drawer.d.ts +0 -24
  450. package/src/lib/components/task-import-drawer.js +0 -82
  451. package/src/lib/components/task-import-drawer.js.map +0 -1
  452. package/src/lib/model/companies-csv.d.ts +0 -36
  453. package/src/lib/model/companies-csv.js +0 -38
  454. package/src/lib/model/companies-csv.js.map +0 -1
  455. package/src/lib/model/contacts-csv.d.ts +0 -54
  456. package/src/lib/model/contacts-csv.js +0 -67
  457. package/src/lib/model/contacts-csv.js.map +0 -1
  458. package/src/lib/model/crm-company-import.d.ts +0 -154
  459. package/src/lib/model/crm-company-import.js +0 -331
  460. package/src/lib/model/crm-company-import.js.map +0 -1
  461. package/src/lib/model/crm-csv.d.ts +0 -239
  462. package/src/lib/model/crm-csv.js +0 -467
  463. package/src/lib/model/crm-csv.js.map +0 -1
  464. package/src/lib/model/crm-deal-import.d.ts +0 -112
  465. package/src/lib/model/crm-deal-import.js +0 -263
  466. package/src/lib/model/crm-deal-import.js.map +0 -1
  467. package/src/lib/model/crm-import.d.ts +0 -250
  468. package/src/lib/model/crm-import.js +0 -467
  469. package/src/lib/model/crm-import.js.map +0 -1
  470. package/src/lib/model/crm-lead-import.d.ts +0 -138
  471. package/src/lib/model/crm-lead-import.js +0 -446
  472. package/src/lib/model/crm-lead-import.js.map +0 -1
  473. package/src/lib/model/crm-task-import.d.ts +0 -94
  474. package/src/lib/model/crm-task-import.js +0 -275
  475. package/src/lib/model/crm-task-import.js.map +0 -1
  476. package/src/lib/model/deals-csv.d.ts +0 -32
  477. package/src/lib/model/deals-csv.js +0 -36
  478. package/src/lib/model/deals-csv.js.map +0 -1
  479. package/src/lib/model/import-column-shapes.d.ts +0 -36
  480. package/src/lib/model/import-column-shapes.js +0 -76
  481. package/src/lib/model/import-column-shapes.js.map +0 -1
  482. package/src/lib/model/lead-source-values.d.ts +0 -82
  483. package/src/lib/model/lead-source-values.js +0 -149
  484. package/src/lib/model/lead-source-values.js.map +0 -1
  485. package/src/lib/model/leads-csv.d.ts +0 -40
  486. package/src/lib/model/leads-csv.js +0 -40
  487. package/src/lib/model/leads-csv.js.map +0 -1
  488. package/src/lib/model/tasks-csv.d.ts +0 -28
  489. package/src/lib/model/tasks-csv.js +0 -28
  490. package/src/lib/model/tasks-csv.js.map +0 -1
  491. package/src/lib/server/companies-import.d.ts +0 -57
  492. package/src/lib/server/companies-import.js +0 -242
  493. package/src/lib/server/companies-import.js.map +0 -1
  494. package/src/lib/server/contacts-import.d.ts +0 -109
  495. package/src/lib/server/contacts-import.js +0 -393
  496. package/src/lib/server/contacts-import.js.map +0 -1
  497. package/src/lib/server/deals-import.d.ts +0 -76
  498. package/src/lib/server/deals-import.js +0 -248
  499. package/src/lib/server/deals-import.js.map +0 -1
  500. package/src/lib/server/export-route.d.ts +0 -60
  501. package/src/lib/server/export-route.js +0 -447
  502. package/src/lib/server/export-route.js.map +0 -1
  503. package/src/lib/server/import-context.d.ts +0 -87
  504. package/src/lib/server/import-context.js +0 -182
  505. package/src/lib/server/import-context.js.map +0 -1
  506. package/src/lib/server/lead-source-values.d.ts +0 -50
  507. package/src/lib/server/lead-source-values.js +0 -191
  508. package/src/lib/server/lead-source-values.js.map +0 -1
  509. package/src/lib/server/leads-import.d.ts +0 -84
  510. package/src/lib/server/leads-import.js +0 -367
  511. package/src/lib/server/leads-import.js.map +0 -1
  512. package/src/lib/server/tasks-import.d.ts +0 -54
  513. package/src/lib/server/tasks-import.js +0 -160
  514. package/src/lib/server/tasks-import.js.map +0 -1
@@ -16,12 +16,15 @@
16
16
  */ import { _ as _extends } from "@swc/helpers/_/_extends";
17
17
  import { CONTACT_SOURCE_LABELS } from "@aglyn/aglyn/app-utils/contacts";
18
18
  import { formLeadSource } from "@aglyn/aglyn/app-utils/forms";
19
+ import { normalizePhone } from "@aglyn/aglyn/foundation/definitions/contact.types";
19
20
  import { isCrmLeadOpen, normalizeContactEmail, personKey } from "@aglyn/aglyn/server";
20
- import { captureHostContact } from "@aglyn/tenant-runtime/capture-host-contact";
21
+ import { captureHostContact } from "./capture-host-contact.js";
21
22
  import { contactCaptureActor } from "@aglyn/tenant-runtime/capture-actor";
22
23
  import { emitHostEvent } from "@aglyn/tenant-runtime/emit-host-event";
23
24
  import { addHostLeadOutcome, findContactByEmail, firebaseAdmin, orgDataCollectionForHost, readLeadForHost } from "@aglyn/tenant-data-admin";
25
+ import { CONVERSION_TOUCH_DETAIL } from "@aglyn/aglyn/plugin-manager/plugin-conversion-credit";
24
26
  import { convertOpenLeadOntoContact } from "./convert-open-lead.js";
27
+ import { crmRecordOriginWriter } from "./record-origin.js";
25
28
  /**
26
29
  * The CRM answering the platform's contact-capture contract (AGL-3080).
27
30
  *
@@ -78,11 +81,12 @@ import { convertOpenLeadOntoContact } from "./convert-open-lead.js";
78
81
  if (!normalizeContactEmail(request.identity.email)) return refusedEmail();
79
82
  try {
80
83
  if (surface === 'lead') {
81
- if (!await heldAsContact(request)) return await fileLead(request);
84
+ if (!await heldAsContact(request)) return await stampedAfter(request, fileLead(request));
82
85
  } else if (surface === 'touch') {
83
- if (await openLeadFor(request)) return await fileLead(request);
86
+ if (await openLeadFor(request)) return await stampedAfter(request, fileLead(request));
84
87
  }
85
88
  const verdict = await captureOnContact(request);
89
+ if (verdict.ok) await stampOrigin(request);
86
90
  if (verdict.ok && verdict.record === 'contact' && surface === 'relationship') {
87
91
  var _request_purchaseCents;
88
92
  await convertOpenLeadOntoContact({
@@ -104,6 +108,26 @@ import { convertOpenLeadOntoContact } from "./convert-open-lead.js";
104
108
  };
105
109
  }
106
110
  }
111
+ /**
112
+ * WHERE THE PERSON CAME FROM (AGL-3519): the door's word — a form, a
113
+ * booking, a newsletter, a member, an order — as the built-in Lead source
114
+ * it names, on a record the capture just started and that names none. A
115
+ * person met before keeps whatever lead source they hold, or the lack of one.
116
+ * Never fails the capture: the writer swallows its own errors.
117
+ */ async function stampOrigin(request) {
118
+ var _normalizeContactEmail;
119
+ await crmRecordOriginWriter.stamp({
120
+ hostId: request.hostId,
121
+ email: (_normalizeContactEmail = normalizeContactEmail(request.identity.email)) != null ? _normalizeContactEmail : '',
122
+ origin: request.interaction.source,
123
+ firstTouchOnly: true
124
+ });
125
+ }
126
+ /** A filed lead's verdict, after its origin is stamped when the filing took. */ async function stampedAfter(request, filed) {
127
+ const verdict = await filed;
128
+ if (verdict.ok) await stampOrigin(request);
129
+ return verdict;
130
+ }
107
131
  /** The refusal every door gets for an address nothing can key. */ function refusedEmail() {
108
132
  return {
109
133
  ok: false,
@@ -159,9 +183,9 @@ import { convertOpenLeadOntoContact } from "./convert-open-lead.js";
159
183
  source
160
184
  }, request.marketingConsent ? {
161
185
  marketingConsent: true
162
- } : {}, disclosureOf(request))
163
- }, campaignTouchOf(request.detail).campaignTouch ? {
164
- touch: campaignTouchOf(request.detail).campaignTouch
186
+ } : {}, disclosureOf(request), phoneFillOf(request))
187
+ }, conversionTouchOf(request.detail).conversionTouch ? {
188
+ touch: conversionTouchOf(request.detail).conversionTouch
165
189
  } : {}));
166
190
  if (!outcome.stored) {
167
191
  return {
@@ -209,7 +233,7 @@ import { convertOpenLeadOntoContact } from "./convert-open-lead.js";
209
233
  } : {}, request.interaction.summary ? {
210
234
  summary: request.interaction.summary
211
235
  } : {}, entryPointOf(request.detail))
212
- }, campaignTouchOf(request.detail), request.marketingConsent === undefined ? {} : {
236
+ }, conversionTouchOf(request.detail), request.marketingConsent === undefined ? {} : {
213
237
  marketingConsent: request.marketingConsent
214
238
  }, disclosureOf(request), ((_request_tags = request.tags) == null ? void 0 : _request_tags.length) ? {
215
239
  tags: request.tags
@@ -221,6 +245,10 @@ import { convertOpenLeadOntoContact } from "./convert-open-lead.js";
221
245
  initialLifecycleStage: request.lifecycleFloor
222
246
  } : {}, request.profile ? {
223
247
  facet: request.profile
248
+ } : {}, phoneFillOf(request).phoneFill ? {
249
+ facetFill: {
250
+ phone: phoneFillOf(request).phoneFill
251
+ }
224
252
  } : {}));
225
253
  if ('refused' in verdict) {
226
254
  return {
@@ -263,6 +291,22 @@ import { convertOpenLeadOntoContact } from "./convert-open-lead.js";
263
291
  disclosedConsentGroup: key
264
292
  } : {};
265
293
  }
294
+ /**
295
+ * THE PHONE A CAPTURE LEARNED IN PASSING (AGL-3493), off `profileFill`.
296
+ *
297
+ * Normalized here so a lead and a contact written by one capture keep the
298
+ * same E.164 string, and dropped when it will not normalize: a fill is an
299
+ * enrichment, and a number in some other shape on the record is the
300
+ * "stored three ways" failure `normalizePhone` exists to end. Both writers
301
+ * put it only where the record holds no phone.
302
+ */ function phoneFillOf(request) {
303
+ var _request_profileFill;
304
+ const raw = (_request_profileFill = request.profileFill) == null ? void 0 : _request_profileFill['phone'];
305
+ const phone = typeof raw === 'string' ? normalizePhone(raw) : null;
306
+ return phone ? {
307
+ phoneFill: phone
308
+ } : {};
309
+ }
266
310
  /**
267
311
  * THE ENTRY POINT, off the silo's own `detail` bag (AGL-3080).
268
312
  *
@@ -305,21 +349,21 @@ import { convertOpenLeadOntoContact } from "./convert-open-lead.js";
305
349
  *
306
350
  * ⚠️ A different fact from `campaignIds` and the two must never be folded
307
351
  * together — `upsert-contact.ts` says so at the field itself. A touch is the
308
- * ad or the link the visitor arrived by, already resolved through the
309
- * allowlist by the silo; `campaignIds` is which campaigns the merchant filed
310
- * the capture SURFACE under, which is true of everybody who fills that form
311
- * in. Folding them would credit a campaign for a visitor who typed the
312
- * address.
352
+ * ad or the link the visitor arrived by, already resolved by the plugin that
353
+ * credits outcomes (`plugin-conversion-credit`) at the silo's door;
354
+ * `campaignIds` is which campaigns the merchant filed the capture SURFACE
355
+ * under, which is true of everybody who fills that form in. Folding them
356
+ * would credit a campaign for a visitor who typed the address.
313
357
  *
314
- * Passed through as the silo resolved it, unread: the touch's shape belongs
315
- * to whatever resolves it, and re-validating it here would be a second copy
316
- * of a rule that has already run. Only its presence is decided here, because
317
- * `null` and absent mean the same thing to the writer and a caller should
318
- * not have to know which one it sends.
319
- */ function campaignTouchOf(detail) {
320
- const touch = detail == null ? void 0 : detail['campaignTouch'];
321
- return touch ? {
322
- campaignTouch: touch
358
+ * Passed through as the silo handed it, unread: the touch's shape belongs
359
+ * to the creditor that resolved it, and the creditor checks it again when
360
+ * the contact or the lead is credited. Only its presence is decided here,
361
+ * because `null` and absent mean the same thing to the writer and a caller
362
+ * should not have to know which one it sends.
363
+ */ function conversionTouchOf(detail) {
364
+ const touch = detail == null ? void 0 : detail[CONVERSION_TOUCH_DETAIL];
365
+ return touch && typeof touch === 'object' ? {
366
+ conversionTouch: touch
323
367
  } : {};
324
368
  }
325
369
  /**
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/capture-contact.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type {\n PluginContactCaptureRequest,\n PluginContactCaptured,\n} from '@aglyn/aglyn/plugin-manager/plugin-contact-capture'\nimport {\n CONTACT_SOURCE_LABELS,\n type ContactSource,\n} from '@aglyn/aglyn/app-utils/contacts'\nimport { formLeadSource } from '@aglyn/aglyn/app-utils/forms'\nimport {\n type CrmLeadFields,\n isCrmLeadOpen,\n normalizeContactEmail,\n personKey,\n} from '@aglyn/aglyn/server'\nimport { captureHostContact } from '@aglyn/tenant-runtime/capture-host-contact'\nimport { contactCaptureActor } from '@aglyn/tenant-runtime/capture-actor'\nimport { emitHostEvent } from '@aglyn/tenant-runtime/emit-host-event'\nimport {\n addHostLeadOutcome,\n findContactByEmail,\n firebaseAdmin,\n orgDataCollectionForHost,\n readLeadForHost,\n type UpsertHostContactOptions,\n} from '@aglyn/tenant-data-admin'\nimport type { ResolvedCampaignTouch } from '@aglyn/tenant-data-admin/server/campaign-conversion-attribution'\nimport { convertOpenLeadOntoContact } from './convert-open-lead'\n\n/**\n * The CRM answering the platform's contact-capture contract (AGL-3080).\n *\n * Four silos meet the same person — a form submission, a member signing up,\n * an order, a booking — and none of them is the record system. Each holds an\n * address, a name and the fact that something happened, and each wants the\n * workspace's person record to know. Today each imports `captureHostContact`\n * directly, which is the highest fan-in edge in the repo and makes the CRM\n * something a storefront cannot take a payment without.\n *\n * So this is the CRM's side of the seam: the silo reports what it saw, and\n * the plugin that keeps people decides everything a record system decides —\n * keying the address, whether this is somebody new, the audience band, the\n * erasure rows, the company, the owner, and what a new person sets off.\n * `captureHostContact` already does all of that; this translates the\n * contract's vocabulary into its options and its verdict back.\n *\n * ⚠️ IT NEVER THROWS. Every caller has already done the thing it is\n * recording — the submission was accepted, the order was paid — so a refusal\n * is RETURNED and costs the silo nothing. A throw here would lose an order\n * for a contact that could not be filed.\n */\n/**\n * WHICH RECORD A CAPTURE LANDS ON (AGL-3232) — the Salesforce rule, decided\n * here because the CRM is the plugin that models both records.\n *\n * One person is one record: a LEAD until somebody qualifies them, a CONTACT\n * after. Every door used to write both — a lead-routed form filed a lead\n * AND a contact at stage Lead — so one person sat in two lists and the Leads\n * queue was never the whole story. Now a door says what kind of surface it\n * is (`request.surface`) and this decides:\n *\n * - a LEAD surface files a lead, unless the workspace already holds the\n * address as a contact — a customer who books a demo is a customer's\n * interaction, not a new lead — in which case the capture lands on the\n * contact and no lead is filed;\n * - a RELATIONSHIP (a member account, a purchase) makes the contact, and\n * an open lead the site held for the address is stamped converted onto\n * it, so nobody keeps working a lead who already joined or bought;\n * - a TOUCH (an unrouted form, a newsletter opt-in) lands on the open lead\n * when the site holds one — its consent and its history stay on the one\n * record the rep is working — and on the contact otherwise.\n *\n * A lead is filed through `addHostLead`, the one writer of the leads silo,\n * so it is keyed on the person, counted against the ceiling and carries the\n * campaign touch the door resolved. A NEW lead announces itself with the\n * `lead` host event, which is what a \"new lead\" automation listens for; a\n * repeat capture on a lead the site already held announces nothing, the\n * way a repeat visit by a contact is an interaction and not a creation.\n */\nexport async function captureContactForCrm(\n request: PluginContactCaptureRequest,\n): Promise<PluginContactCaptured> {\n const surface = request.surface ?? 'touch'\n // The one refusal every surface shares, answered before any read: an\n // address nothing can key is a person nothing can record.\n if (!normalizeContactEmail(request.identity.email)) return refusedEmail()\n try {\n if (surface === 'lead') {\n if (!(await heldAsContact(request))) return await fileLead(request)\n } else if (surface === 'touch') {\n if (await openLeadFor(request)) return await fileLead(request)\n }\n const verdict = await captureOnContact(request)\n if (verdict.ok && verdict.record === 'contact' && surface === 'relationship') {\n await convertOpenLeadOntoContact({\n hostId: request.hostId,\n email: request.identity.email,\n contactId: verdict.contactId,\n // Money changing hands is a purchase whichever door took it — a paid\n // booking or a stay is one as much as an order is.\n by:\n request.interaction.source === 'order' || (request.purchaseCents ?? 0) > 0\n ? 'purchase'\n : 'signup',\n })\n }\n return verdict\n } catch (error) {\n console.error('crm contact capture failed', error)\n return {\n ok: false,\n reason: 'error',\n error: 'The contact could not be recorded. Nothing else was affected.',\n }\n }\n}\n\n/** The refusal every door gets for an address nothing can key. */\nfunction refusedEmail(): PluginContactCaptured {\n return { ok: false, reason: 'invalid-email', error: refusalText('invalid-email') }\n}\n\n/**\n * Whether the workspace already holds the address as a contact — the\n * address index first, then the query, the same lookup every dedupe uses.\n * ORG-wide, not scoped to the capturing site: the contact door dedupes\n * across every site in the org, so a person any site holds as a contact\n * is a contact, whichever site met them this time.\n */\nasync function heldAsContact(request: PluginContactCaptureRequest): Promise<boolean> {\n const contacts = await orgDataCollectionForHost(request.hostId, 'contacts')\n return (await findContactByEmail(contacts, request.identity.email)) !== null\n}\n\n/** Whether the site holds an OPEN lead for the address — neither converted nor closed. */\nasync function openLeadFor(request: PluginContactCaptureRequest): Promise<boolean> {\n const key = personKey(request.identity.email) as string\n /*\n * Through the seam, which reads the org row and falls back to a lead the\n * backfill has not reached (AGL-3275). UNSCOPED on purpose: a sibling brand\n * in the same consent group has to find this person, or its door concludes\n * there is no lead and files the duplicate this migration exists to end.\n */\n const snapshot = await readLeadForHost(request.hostId, key)\n if (!snapshot) return false\n const lead = (snapshot.data() ?? {}) as Record<string, unknown> & CrmLeadFields\n return !lead.convertedContactId && isCrmLeadOpen(lead)\n}\n\n/**\n * The capture as a LEAD. The source string is the lead silo's own\n * vocabulary — `form:{formId}` for a form, the door's word otherwise — so\n * a lead's provenance survives the form being renamed, as the submission's\n * does.\n */\nasync function fileLead(request: PluginContactCaptureRequest): Promise<PluginContactCaptured> {\n const email = normalizeContactEmail(request.identity.email) as string\n const key = personKey(email) as string\n const hostRef = firebaseAdmin.app().firestore().collection('hosts').doc(request.hostId)\n // `addHostLead` resolves and writes the org collection itself (AGL-3275);\n // this only needs to know whether the person is new, which decides the event.\n const created = !(await readLeadForHost(request.hostId, key))\n const { formId } = entryPointOf(request.detail)\n const source =\n request.interaction.source === 'form' && formId\n ? formLeadSource(formId)\n : request.interaction.source\n const outcome = await addHostLeadOutcome({\n hostRef,\n hostId: request.hostId,\n lead: {\n email,\n ...(request.identity.name ? { name: request.identity.name } : {}),\n source,\n ...(request.marketingConsent ? { marketingConsent: true } : {}),\n ...disclosureOf(request),\n },\n ...(campaignTouchOf(request.detail).campaignTouch\n ? { touch: campaignTouchOf(request.detail).campaignTouch }\n : {}),\n })\n if (!outcome.stored) {\n return {\n ok: false,\n reason: 'band',\n error: 'This site is at the number of leads it may hold, so the lead was not recorded.',\n }\n }\n if (created) {\n const cause = contactCaptureActor(\n request.interaction.source,\n email,\n request.actor,\n )\n await emitHostEvent(\n request.hostId,\n 'lead',\n {\n email,\n source,\n leadId: key,\n ...(request.identity.name ? { name: request.identity.name } : {}),\n },\n cause ? { actor: cause } : {},\n )\n }\n return { ok: true, record: 'lead', leadId: key, created, sourceAdded: outcome.sourceAdded }\n}\n\n/** The capture as a CONTACT — what every capture was before the rule above. */\nasync function captureOnContact(\n request: PluginContactCaptureRequest,\n): Promise<PluginContactCaptured> {\n try {\n const verdict = await captureHostContact({\n ...(request.actor ? { actor: request.actor } : {}),\n hostId: request.hostId,\n email: request.identity.email,\n ...(request.identity.name ? { name: request.identity.name } : {}),\n source: contactSourceOf(request.interaction.source),\n interaction: {\n ...(request.interaction.atMs === undefined\n ? {}\n : { atMs: request.interaction.atMs }),\n ...(request.interaction.refId ? { refId: request.interaction.refId } : {}),\n ...(request.interaction.summary\n ? { summary: request.interaction.summary }\n : {}),\n ...entryPointOf(request.detail),\n },\n ...campaignTouchOf(request.detail),\n ...(request.marketingConsent === undefined\n ? {}\n : { marketingConsent: request.marketingConsent }),\n ...disclosureOf(request),\n ...(request.tags?.length ? { tags: request.tags } : {}),\n ...campaignsFiledUnder(request),\n ...(request.purchaseCents === undefined\n ? {}\n : { purchaseCents: request.purchaseCents }),\n ...(request.purchaseCurrency\n ? { purchaseCurrency: request.purchaseCurrency }\n : {}),\n ...(request.lifecycleFloor\n ? { initialLifecycleStage: request.lifecycleFloor as never }\n : {}),\n ...(request.profile ? { facet: request.profile as never } : {}),\n })\n if ('refused' in verdict) {\n return { ok: false, reason: verdict.refused, error: refusalText(verdict.refused) }\n }\n return {\n ok: true,\n record: 'contact',\n contactId: verdict.contactId,\n created: verdict.created,\n }\n } catch (error) {\n /*\n * The contract's `error` is the last state, not a channel for a stack:\n * a silo may show or log it as it stands, so it says what happened and\n * names nothing internal. The detail goes to the log, where whoever is\n * debugging a missing contact will look.\n */\n console.error('crm contact capture failed', error)\n return {\n ok: false,\n reason: 'error',\n error: 'The contact could not be recorded. Nothing else was affected.',\n }\n }\n}\n\n/**\n * THE DISCLOSURE the capture surface rendered, handed to whichever record the\n * capture lands on (AGL-3320).\n *\n * Passed through unread: both writers resolve the site's group themselves and\n * pool the opt-in only when this is its current key, so a lead and a contact\n * written by one capture record the same sites. A string or nothing, since\n * the writers compare it exactly and anything else could only fail to match.\n */\nfunction disclosureOf(\n request: PluginContactCaptureRequest,\n): { disclosedConsentGroup?: string } {\n const key = request.disclosedConsentGroup\n return typeof key === 'string' && key ? { disclosedConsentGroup: key } : {}\n}\n\n/**\n * THE ENTRY POINT, off the silo's own `detail` bag (AGL-3080).\n *\n * The contract carries a capture's silo-side facts opaquely, so the two the\n * CRM models are picked out here rather than typed into the platform. Both\n * ride the INTERACTION, which is where `upsertHostContact` already keeps\n * them: which form a person came in through routes the owner-assignment\n * rules and rides the `contactCreated` payload, and the page is what a\n * timeline row says about where they were.\n *\n * Absent, misspelled or the wrong type is the same answer as a door that\n * never had one — a capture without an entry point, which is every capture\n * that did not come through a form. It is never a reason to refuse: the\n * person is the part that matters.\n */\nfunction entryPointOf(\n detail: Readonly<Record<string, unknown>> | undefined,\n): { formId?: string; path?: string } {\n const text = (value: unknown): string | undefined =>\n typeof value === 'string' && value.trim() ? value : undefined\n const formId = text(detail?.['formId'])\n const path = text(detail?.['path'])\n return {\n ...(formId ? { formId } : {}),\n ...(path ? { path } : {}),\n }\n}\n\n/**\n * The campaigns the capture surface is filed under, as the contact writer\n * records them. A capture names what its surface is filed under by container\n * kind; the CRM keeps the `campaign` kind on the person's site facet, and a\n * kind it does not keep is the capture's to name and the CRM's to ignore.\n */\nfunction campaignsFiledUnder(\n request: PluginContactCaptureRequest,\n): Pick<UpsertHostContactOptions, 'campaignIds'> {\n const ids = request.containers?.['campaign'] ?? []\n return ids.length ? { campaignIds: ids } : {}\n}\n\n/**\n * WHERE THE VISITOR CAME FROM, off the same bag.\n *\n * ⚠️ A different fact from `campaignIds` and the two must never be folded\n * together — `upsert-contact.ts` says so at the field itself. A touch is the\n * ad or the link the visitor arrived by, already resolved through the\n * allowlist by the silo; `campaignIds` is which campaigns the merchant filed\n * the capture SURFACE under, which is true of everybody who fills that form\n * in. Folding them would credit a campaign for a visitor who typed the\n * address.\n *\n * Passed through as the silo resolved it, unread: the touch's shape belongs\n * to whatever resolves it, and re-validating it here would be a second copy\n * of a rule that has already run. Only its presence is decided here, because\n * `null` and absent mean the same thing to the writer and a caller should\n * not have to know which one it sends.\n */\nfunction campaignTouchOf(\n detail: Readonly<Record<string, unknown>> | undefined,\n): Pick<UpsertHostContactOptions, 'campaignTouch'> {\n const touch = detail?.['campaignTouch']\n return touch ? { campaignTouch: touch as ResolvedCampaignTouch } : {}\n}\n\n/**\n * The silo's word for its door, as a source the CRM stores.\n *\n * ⚠️ `ContactSource` is a CLOSED union and the contract's `source` is an open\n * string, deliberately: a silo declares its door with\n * `registerPluginContactSource` rather than core enumerating every plugin's.\n * The two meet here, and a word outside the union is passed through rather\n * than rejected — refusing it would lose a third-party plugin's capture over\n * a label, and the capture is the part that matters.\n *\n * ⛔ What it costs, until the union is widened: the console's source filter\n * and `SOURCE_LABELS` key on these values, so an undeclared word renders raw\n * and matches no filter. Every first-party silo uses a word in the union, so\n * nothing does that today.\n */\nexport function contactSourceOf(source: string): ContactSource {\n const word = String(source ?? '').trim()\n if (!Object.hasOwn(CONTACT_SOURCE_LABELS, word)) {\n // Said once, where somebody debugging an unlabelled row will find it.\n // Not a refusal: the capture is worth more than the label.\n console.warn(\n `crm contact capture: source \"${word}\" has no label, so it will render ` +\n 'raw and match no filter in the console.',\n )\n }\n return word as ContactSource\n}\n\n/** What a refused caller is told — customer-safe, and never a key. */\nfunction refusalText(reason: 'invalid-email' | 'band' | 'erased' | 'error'): string {\n switch (reason) {\n case 'invalid-email':\n return 'That address could not be read, so no contact was recorded.'\n case 'band':\n return 'This workspace is at the number of contacts its plan holds.'\n case 'erased':\n return 'This person was erased from this workspace and was not recreated.'\n default:\n return 'The contact could not be recorded. Nothing else was affected.'\n }\n}\n"],"names":["CONTACT_SOURCE_LABELS","formLeadSource","isCrmLeadOpen","normalizeContactEmail","personKey","captureHostContact","contactCaptureActor","emitHostEvent","addHostLeadOutcome","findContactByEmail","firebaseAdmin","orgDataCollectionForHost","readLeadForHost","convertOpenLeadOntoContact","captureContactForCrm","request","surface","identity","email","refusedEmail","heldAsContact","fileLead","openLeadFor","verdict","captureOnContact","ok","record","hostId","contactId","by","interaction","source","purchaseCents","error","console","reason","refusalText","contacts","snapshot","key","lead","data","convertedContactId","hostRef","app","firestore","collection","doc","created","formId","entryPointOf","detail","outcome","name","marketingConsent","disclosureOf","campaignTouchOf","campaignTouch","touch","stored","cause","actor","leadId","sourceAdded","contactSourceOf","atMs","undefined","refId","summary","tags","length","campaignsFiledUnder","purchaseCurrency","lifecycleFloor","initialLifecycleStage","profile","facet","refused","disclosedConsentGroup","text","value","trim","path","ids","containers","campaignIds","word","String","Object","hasOwn","warn"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/capture-contact.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type {\n PluginContactCaptureRequest,\n PluginContactCaptured,\n} from '@aglyn/aglyn/plugin-manager/plugin-contact-capture'\nimport {\n CONTACT_SOURCE_LABELS,\n type ContactSource,\n} from '@aglyn/aglyn/app-utils/contacts'\nimport { formLeadSource } from '@aglyn/aglyn/app-utils/forms'\nimport { normalizePhone } from '@aglyn/aglyn/foundation/definitions/contact.types'\nimport {\n type CrmLeadFields,\n isCrmLeadOpen,\n normalizeContactEmail,\n personKey,\n} from '@aglyn/aglyn/server'\nimport { captureHostContact } from './capture-host-contact'\nimport { contactCaptureActor } from '@aglyn/tenant-runtime/capture-actor'\nimport { emitHostEvent } from '@aglyn/tenant-runtime/emit-host-event'\nimport {\n addHostLeadOutcome,\n findContactByEmail,\n firebaseAdmin,\n orgDataCollectionForHost,\n readLeadForHost,\n type UpsertHostContactOptions,\n} from '@aglyn/tenant-data-admin'\nimport {\n CONVERSION_TOUCH_DETAIL,\n type PluginConversionTouch,\n} from '@aglyn/aglyn/plugin-manager/plugin-conversion-credit'\nimport { convertOpenLeadOntoContact } from './convert-open-lead'\nimport { crmRecordOriginWriter } from './record-origin'\n\n/**\n * The CRM answering the platform's contact-capture contract (AGL-3080).\n *\n * Four silos meet the same person — a form submission, a member signing up,\n * an order, a booking — and none of them is the record system. Each holds an\n * address, a name and the fact that something happened, and each wants the\n * workspace's person record to know. Today each imports `captureHostContact`\n * directly, which is the highest fan-in edge in the repo and makes the CRM\n * something a storefront cannot take a payment without.\n *\n * So this is the CRM's side of the seam: the silo reports what it saw, and\n * the plugin that keeps people decides everything a record system decides —\n * keying the address, whether this is somebody new, the audience band, the\n * erasure rows, the company, the owner, and what a new person sets off.\n * `captureHostContact` already does all of that; this translates the\n * contract's vocabulary into its options and its verdict back.\n *\n * ⚠️ IT NEVER THROWS. Every caller has already done the thing it is\n * recording — the submission was accepted, the order was paid — so a refusal\n * is RETURNED and costs the silo nothing. A throw here would lose an order\n * for a contact that could not be filed.\n */\n/**\n * WHICH RECORD A CAPTURE LANDS ON (AGL-3232) — the Salesforce rule, decided\n * here because the CRM is the plugin that models both records.\n *\n * One person is one record: a LEAD until somebody qualifies them, a CONTACT\n * after. Every door used to write both — a lead-routed form filed a lead\n * AND a contact at stage Lead — so one person sat in two lists and the Leads\n * queue was never the whole story. Now a door says what kind of surface it\n * is (`request.surface`) and this decides:\n *\n * - a LEAD surface files a lead, unless the workspace already holds the\n * address as a contact — a customer who books a demo is a customer's\n * interaction, not a new lead — in which case the capture lands on the\n * contact and no lead is filed;\n * - a RELATIONSHIP (a member account, a purchase) makes the contact, and\n * an open lead the site held for the address is stamped converted onto\n * it, so nobody keeps working a lead who already joined or bought;\n * - a TOUCH (an unrouted form, a newsletter opt-in) lands on the open lead\n * when the site holds one — its consent and its history stay on the one\n * record the rep is working — and on the contact otherwise.\n *\n * A lead is filed through `addHostLead`, the one writer of the leads silo,\n * so it is keyed on the person, counted against the ceiling and carries the\n * campaign touch the door resolved. A NEW lead announces itself with the\n * `lead` host event, which is what a \"new lead\" automation listens for; a\n * repeat capture on a lead the site already held announces nothing, the\n * way a repeat visit by a contact is an interaction and not a creation.\n */\nexport async function captureContactForCrm(\n request: PluginContactCaptureRequest,\n): Promise<PluginContactCaptured> {\n const surface = request.surface ?? 'touch'\n // The one refusal every surface shares, answered before any read: an\n // address nothing can key is a person nothing can record.\n if (!normalizeContactEmail(request.identity.email)) return refusedEmail()\n try {\n if (surface === 'lead') {\n if (!(await heldAsContact(request))) return await stampedAfter(request, fileLead(request))\n } else if (surface === 'touch') {\n if (await openLeadFor(request)) return await stampedAfter(request, fileLead(request))\n }\n const verdict = await captureOnContact(request)\n if (verdict.ok) await stampOrigin(request)\n if (verdict.ok && verdict.record === 'contact' && surface === 'relationship') {\n await convertOpenLeadOntoContact({\n hostId: request.hostId,\n email: request.identity.email,\n contactId: verdict.contactId,\n // Money changing hands is a purchase whichever door took it — a paid\n // booking or a stay is one as much as an order is.\n by:\n request.interaction.source === 'order' || (request.purchaseCents ?? 0) > 0\n ? 'purchase'\n : 'signup',\n })\n }\n return verdict\n } catch (error) {\n console.error('crm contact capture failed', error)\n return {\n ok: false,\n reason: 'error',\n error: 'The contact could not be recorded. Nothing else was affected.',\n }\n }\n}\n\n/**\n * WHERE THE PERSON CAME FROM (AGL-3519): the door's word — a form, a\n * booking, a newsletter, a member, an order — as the built-in Lead source\n * it names, on a record the capture just started and that names none. A\n * person met before keeps whatever lead source they hold, or the lack of one.\n * Never fails the capture: the writer swallows its own errors.\n */\nasync function stampOrigin(request: PluginContactCaptureRequest): Promise<void> {\n await crmRecordOriginWriter.stamp({\n hostId: request.hostId,\n email: normalizeContactEmail(request.identity.email) ?? '',\n origin: request.interaction.source,\n firstTouchOnly: true,\n })\n}\n\n/** A filed lead's verdict, after its origin is stamped when the filing took. */\nasync function stampedAfter(\n request: PluginContactCaptureRequest,\n filed: Promise<PluginContactCaptured>,\n): Promise<PluginContactCaptured> {\n const verdict = await filed\n if (verdict.ok) await stampOrigin(request)\n return verdict\n}\n\n/** The refusal every door gets for an address nothing can key. */\nfunction refusedEmail(): PluginContactCaptured {\n return { ok: false, reason: 'invalid-email', error: refusalText('invalid-email') }\n}\n\n/**\n * Whether the workspace already holds the address as a contact — the\n * address index first, then the query, the same lookup every dedupe uses.\n * ORG-wide, not scoped to the capturing site: the contact door dedupes\n * across every site in the org, so a person any site holds as a contact\n * is a contact, whichever site met them this time.\n */\nasync function heldAsContact(request: PluginContactCaptureRequest): Promise<boolean> {\n const contacts = await orgDataCollectionForHost(request.hostId, 'contacts')\n return (await findContactByEmail(contacts, request.identity.email)) !== null\n}\n\n/** Whether the site holds an OPEN lead for the address — neither converted nor closed. */\nasync function openLeadFor(request: PluginContactCaptureRequest): Promise<boolean> {\n const key = personKey(request.identity.email) as string\n /*\n * Through the seam, which reads the org row and falls back to a lead the\n * backfill has not reached (AGL-3275). UNSCOPED on purpose: a sibling brand\n * in the same consent group has to find this person, or its door concludes\n * there is no lead and files the duplicate this migration exists to end.\n */\n const snapshot = await readLeadForHost(request.hostId, key)\n if (!snapshot) return false\n const lead = (snapshot.data() ?? {}) as Record<string, unknown> & CrmLeadFields\n return !lead.convertedContactId && isCrmLeadOpen(lead)\n}\n\n/**\n * The capture as a LEAD. The source string is the lead silo's own\n * vocabulary — `form:{formId}` for a form, the door's word otherwise — so\n * a lead's provenance survives the form being renamed, as the submission's\n * does.\n */\nasync function fileLead(request: PluginContactCaptureRequest): Promise<PluginContactCaptured> {\n const email = normalizeContactEmail(request.identity.email) as string\n const key = personKey(email) as string\n const hostRef = firebaseAdmin.app().firestore().collection('hosts').doc(request.hostId)\n // `addHostLead` resolves and writes the org collection itself (AGL-3275);\n // this only needs to know whether the person is new, which decides the event.\n const created = !(await readLeadForHost(request.hostId, key))\n const { formId } = entryPointOf(request.detail)\n const source =\n request.interaction.source === 'form' && formId\n ? formLeadSource(formId)\n : request.interaction.source\n const outcome = await addHostLeadOutcome({\n hostRef,\n hostId: request.hostId,\n lead: {\n email,\n ...(request.identity.name ? { name: request.identity.name } : {}),\n source,\n ...(request.marketingConsent ? { marketingConsent: true } : {}),\n ...disclosureOf(request),\n ...phoneFillOf(request),\n },\n ...(conversionTouchOf(request.detail).conversionTouch\n ? { touch: conversionTouchOf(request.detail).conversionTouch }\n : {}),\n })\n if (!outcome.stored) {\n return {\n ok: false,\n reason: 'band',\n error: 'This site is at the number of leads it may hold, so the lead was not recorded.',\n }\n }\n if (created) {\n const cause = contactCaptureActor(\n request.interaction.source,\n email,\n request.actor,\n )\n await emitHostEvent(\n request.hostId,\n 'lead',\n {\n email,\n source,\n leadId: key,\n ...(request.identity.name ? { name: request.identity.name } : {}),\n },\n cause ? { actor: cause } : {},\n )\n }\n return { ok: true, record: 'lead', leadId: key, created, sourceAdded: outcome.sourceAdded }\n}\n\n/** The capture as a CONTACT — what every capture was before the rule above. */\nasync function captureOnContact(\n request: PluginContactCaptureRequest,\n): Promise<PluginContactCaptured> {\n try {\n const verdict = await captureHostContact({\n ...(request.actor ? { actor: request.actor } : {}),\n hostId: request.hostId,\n email: request.identity.email,\n ...(request.identity.name ? { name: request.identity.name } : {}),\n source: contactSourceOf(request.interaction.source),\n interaction: {\n ...(request.interaction.atMs === undefined\n ? {}\n : { atMs: request.interaction.atMs }),\n ...(request.interaction.refId ? { refId: request.interaction.refId } : {}),\n ...(request.interaction.summary\n ? { summary: request.interaction.summary }\n : {}),\n ...entryPointOf(request.detail),\n },\n ...conversionTouchOf(request.detail),\n ...(request.marketingConsent === undefined\n ? {}\n : { marketingConsent: request.marketingConsent }),\n ...disclosureOf(request),\n ...(request.tags?.length ? { tags: request.tags } : {}),\n ...campaignsFiledUnder(request),\n ...(request.purchaseCents === undefined\n ? {}\n : { purchaseCents: request.purchaseCents }),\n ...(request.purchaseCurrency\n ? { purchaseCurrency: request.purchaseCurrency }\n : {}),\n ...(request.lifecycleFloor\n ? { initialLifecycleStage: request.lifecycleFloor as never }\n : {}),\n ...(request.profile ? { facet: request.profile as never } : {}),\n ...(phoneFillOf(request).phoneFill\n ? { facetFill: { phone: phoneFillOf(request).phoneFill } }\n : {}),\n })\n if ('refused' in verdict) {\n return { ok: false, reason: verdict.refused, error: refusalText(verdict.refused) }\n }\n return {\n ok: true,\n record: 'contact',\n contactId: verdict.contactId,\n created: verdict.created,\n }\n } catch (error) {\n /*\n * The contract's `error` is the last state, not a channel for a stack:\n * a silo may show or log it as it stands, so it says what happened and\n * names nothing internal. The detail goes to the log, where whoever is\n * debugging a missing contact will look.\n */\n console.error('crm contact capture failed', error)\n return {\n ok: false,\n reason: 'error',\n error: 'The contact could not be recorded. Nothing else was affected.',\n }\n }\n}\n\n/**\n * THE DISCLOSURE the capture surface rendered, handed to whichever record the\n * capture lands on (AGL-3320).\n *\n * Passed through unread: both writers resolve the site's group themselves and\n * pool the opt-in only when this is its current key, so a lead and a contact\n * written by one capture record the same sites. A string or nothing, since\n * the writers compare it exactly and anything else could only fail to match.\n */\nfunction disclosureOf(\n request: PluginContactCaptureRequest,\n): { disclosedConsentGroup?: string } {\n const key = request.disclosedConsentGroup\n return typeof key === 'string' && key ? { disclosedConsentGroup: key } : {}\n}\n\n/**\n * THE PHONE A CAPTURE LEARNED IN PASSING (AGL-3493), off `profileFill`.\n *\n * Normalized here so a lead and a contact written by one capture keep the\n * same E.164 string, and dropped when it will not normalize: a fill is an\n * enrichment, and a number in some other shape on the record is the\n * \"stored three ways\" failure `normalizePhone` exists to end. Both writers\n * put it only where the record holds no phone.\n */\nfunction phoneFillOf(request: PluginContactCaptureRequest): { phoneFill?: string } {\n const raw = request.profileFill?.['phone']\n const phone = typeof raw === 'string' ? normalizePhone(raw) : null\n return phone ? { phoneFill: phone } : {}\n}\n\n/**\n * THE ENTRY POINT, off the silo's own `detail` bag (AGL-3080).\n *\n * The contract carries a capture's silo-side facts opaquely, so the two the\n * CRM models are picked out here rather than typed into the platform. Both\n * ride the INTERACTION, which is where `upsertHostContact` already keeps\n * them: which form a person came in through routes the owner-assignment\n * rules and rides the `contactCreated` payload, and the page is what a\n * timeline row says about where they were.\n *\n * Absent, misspelled or the wrong type is the same answer as a door that\n * never had one — a capture without an entry point, which is every capture\n * that did not come through a form. It is never a reason to refuse: the\n * person is the part that matters.\n */\nfunction entryPointOf(\n detail: Readonly<Record<string, unknown>> | undefined,\n): { formId?: string; path?: string } {\n const text = (value: unknown): string | undefined =>\n typeof value === 'string' && value.trim() ? value : undefined\n const formId = text(detail?.['formId'])\n const path = text(detail?.['path'])\n return {\n ...(formId ? { formId } : {}),\n ...(path ? { path } : {}),\n }\n}\n\n/**\n * The campaigns the capture surface is filed under, as the contact writer\n * records them. A capture names what its surface is filed under by container\n * kind; the CRM keeps the `campaign` kind on the person's site facet, and a\n * kind it does not keep is the capture's to name and the CRM's to ignore.\n */\nfunction campaignsFiledUnder(\n request: PluginContactCaptureRequest,\n): Pick<UpsertHostContactOptions, 'campaignIds'> {\n const ids = request.containers?.['campaign'] ?? []\n return ids.length ? { campaignIds: ids } : {}\n}\n\n/**\n * WHERE THE VISITOR CAME FROM, off the same bag.\n *\n * ⚠️ A different fact from `campaignIds` and the two must never be folded\n * together — `upsert-contact.ts` says so at the field itself. A touch is the\n * ad or the link the visitor arrived by, already resolved by the plugin that\n * credits outcomes (`plugin-conversion-credit`) at the silo's door;\n * `campaignIds` is which campaigns the merchant filed the capture SURFACE\n * under, which is true of everybody who fills that form in. Folding them\n * would credit a campaign for a visitor who typed the address.\n *\n * Passed through as the silo handed it, unread: the touch's shape belongs\n * to the creditor that resolved it, and the creditor checks it again when\n * the contact or the lead is credited. Only its presence is decided here,\n * because `null` and absent mean the same thing to the writer and a caller\n * should not have to know which one it sends.\n */\nfunction conversionTouchOf(\n detail: Readonly<Record<string, unknown>> | undefined,\n): Pick<UpsertHostContactOptions, 'conversionTouch'> {\n const touch = detail?.[CONVERSION_TOUCH_DETAIL]\n return touch && typeof touch === 'object'\n ? { conversionTouch: touch as PluginConversionTouch }\n : {}\n}\n\n/**\n * The silo's word for its door, as a source the CRM stores.\n *\n * ⚠️ `ContactSource` is a CLOSED union and the contract's `source` is an open\n * string, deliberately: a silo declares its door with\n * `registerPluginContactSource` rather than core enumerating every plugin's.\n * The two meet here, and a word outside the union is passed through rather\n * than rejected — refusing it would lose a third-party plugin's capture over\n * a label, and the capture is the part that matters.\n *\n * ⛔ What it costs, until the union is widened: the console's source filter\n * and `SOURCE_LABELS` key on these values, so an undeclared word renders raw\n * and matches no filter. Every first-party silo uses a word in the union, so\n * nothing does that today.\n */\nexport function contactSourceOf(source: string): ContactSource {\n const word = String(source ?? '').trim()\n if (!Object.hasOwn(CONTACT_SOURCE_LABELS, word)) {\n // Said once, where somebody debugging an unlabelled row will find it.\n // Not a refusal: the capture is worth more than the label.\n console.warn(\n `crm contact capture: source \"${word}\" has no label, so it will render ` +\n 'raw and match no filter in the console.',\n )\n }\n return word as ContactSource\n}\n\n/** What a refused caller is told — customer-safe, and never a key. */\nfunction refusalText(reason: 'invalid-email' | 'band' | 'erased' | 'error'): string {\n switch (reason) {\n case 'invalid-email':\n return 'That address could not be read, so no contact was recorded.'\n case 'band':\n return 'This workspace is at the number of contacts its plan holds.'\n case 'erased':\n return 'This person was erased from this workspace and was not recreated.'\n default:\n return 'The contact could not be recorded. Nothing else was affected.'\n }\n}\n"],"names":["CONTACT_SOURCE_LABELS","formLeadSource","normalizePhone","isCrmLeadOpen","normalizeContactEmail","personKey","captureHostContact","contactCaptureActor","emitHostEvent","addHostLeadOutcome","findContactByEmail","firebaseAdmin","orgDataCollectionForHost","readLeadForHost","CONVERSION_TOUCH_DETAIL","convertOpenLeadOntoContact","crmRecordOriginWriter","captureContactForCrm","request","surface","identity","email","refusedEmail","heldAsContact","stampedAfter","fileLead","openLeadFor","verdict","captureOnContact","ok","stampOrigin","record","hostId","contactId","by","interaction","source","purchaseCents","error","console","reason","stamp","origin","firstTouchOnly","filed","refusalText","contacts","snapshot","key","lead","data","convertedContactId","hostRef","app","firestore","collection","doc","created","formId","entryPointOf","detail","outcome","name","marketingConsent","disclosureOf","phoneFillOf","conversionTouchOf","conversionTouch","touch","stored","cause","actor","leadId","sourceAdded","contactSourceOf","atMs","undefined","refId","summary","tags","length","campaignsFiledUnder","purchaseCurrency","lifecycleFloor","initialLifecycleStage","profile","facet","phoneFill","facetFill","phone","refused","disclosedConsentGroup","raw","profileFill","text","value","trim","path","ids","containers","campaignIds","word","String","Object","hasOwn","warn"],"mappings":"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"}
@@ -0,0 +1,104 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { type HostContactCreated, type UpsertHostContactOptions, type UpsertHostContactVerdict } from '@aglyn/tenant-data-admin';
18
+ import type { HostEventActor, HostEventPayload } from '@aglyn/tenant-runtime/host-event-listeners';
19
+ /**
20
+ * THE contact capture door for a server path (AGL-2605).
21
+ *
22
+ * `upsertHostContact` writes the contact and reports, through its
23
+ * `onCreated` hook, when the write made a NEW person. This wrapper is the
24
+ * one place that hook is bound to `contactCreated`, and it is the function
25
+ * every server door calls — the forms route, the membership and newsletter
26
+ * handlers, the order and booking webhooks. A door that calls
27
+ * `upsertHostContact` directly still captures the contact and fires nothing,
28
+ * which is a contact no automation can welcome; `capture-host-contact.spec`
29
+ * scans for exactly that call.
30
+ *
31
+ * ## Why the binding lives here and not in the data library
32
+ *
33
+ * What listens for the event — the automation engine among them — imports
34
+ * the data library for its Firestore handle, its org helpers and its
35
+ * senders. The data library emitting an event would import the listeners
36
+ * back, which is a cycle, and the module boundaries (`scope:data` depends on
37
+ * data and util only) refuse it in any case. So the lower layer reports a
38
+ * fact and this layer, which already knows how to announce one, announces
39
+ * it. The alternative — a sink the runtime registers into when its module
40
+ * happens to be imported — would fire only in a process that had loaded the
41
+ * runtime, and a Stripe webhook that created a contact in a process that had
42
+ * not would announce nothing with no error anywhere. The listeners themselves
43
+ * are registered by a call made at boot (`host-event-listeners.ts`), so every
44
+ * process has them before its first request.
45
+ *
46
+ * Fire-and-forget in the same sense the capture itself is: the hook's
47
+ * failure is caught inside `upsertHostContact`, so a runner that throws
48
+ * costs the door nothing, and this function never rejects.
49
+ *
50
+ * ## The company, before the announcement (AGL-2613)
51
+ *
52
+ * A new person with a work email address is linked to the company at that
53
+ * domain here, in the same hook, BEFORE `contactCreated` goes out — so an
54
+ * automation that reads the new contact finds them already filed, rather
55
+ * than racing a link that lands a moment later. Only when the door did not
56
+ * name a company itself: the console's drawer and an import row that
57
+ * carried one have written it into the facet, and the capture must not
58
+ * second-guess a person's choice with a domain match. The association never
59
+ * rejects, and its own catch keeps a failed lookup from costing the event.
60
+ *
61
+ * ## The owner, on the same terms (AGL-2618)
62
+ *
63
+ * Then the org's assignment rules and the site's default owner decide who
64
+ * follows the new person up — again before `contactCreated`, so an
65
+ * automation's "create a task for the owner" finds one — and again only
66
+ * when the door named none: a drawer, an import column or a conversion
67
+ * that picked an owner has said whose the record is. The pass reads the
68
+ * form and the tags off the same options the data library was handed,
69
+ * because the created-report carries the identity and not the routing.
70
+ * It never rejects either; a record it could not assign is one somebody
71
+ * assigns by hand, which is what every record was before this existed.
72
+ *
73
+ * What this does NOT do is close a lead (AGL-3232): a lead is the CRM's
74
+ * record, and which relationship closes one is the CRM's rule, answered in
75
+ * its capture writer for the doors that report through the capture seam.
76
+ * The order doors call this directly and close nothing — a buyer with an
77
+ * open lead is a lead the rep converts, as in Salesforce.
78
+ */
79
+ export declare function captureHostContact(captureOptions: Omit<UpsertHostContactOptions, 'onCreated'> & {
80
+ /** Who caused the capture, when the door knows (AGL-3376). */
81
+ actor?: HostEventActor;
82
+ }): Promise<UpsertHostContactVerdict>;
83
+ /**
84
+ * The `contactCreated` payload, as the scalars an event may carry.
85
+ *
86
+ * `HostEventPayload` holds strings, numbers and booleans and nothing
87
+ * nested, because the payload seeds an expression scope and a condition
88
+ * editor whose operators compare strings. So `campaignIds` rides as one
89
+ * comma-joined string — `contains` still finds a campaign in it — and is
90
+ * present only when the capture had campaigns, so `notEmpty` on it reads
91
+ * as "came in through a campaign form". `name` and `lifecycleStage` are
92
+ * always present, empty when the door had none, so a condition on either
93
+ * never sees a missing key: `lifecycleStage == "lead"` is the filter that
94
+ * picks the form captures out of the sign-ups (AGL-2612).
95
+ *
96
+ * `formId` rides the same way `campaignIds` does — present only when the
97
+ * capture came through a form — because the created-report carries the
98
+ * person's identity and not the routing, and the form is routing: it is
99
+ * read off the interaction the door was handed. Present, it is what lets a
100
+ * condition say `formId` equals this form and no other (AGL-2626); absent,
101
+ * `notEmpty` on it reads as "came in through a form".
102
+ */
103
+ export declare function contactCreatedPayload(created: HostContactCreated, formId?: string | null): HostEventPayload;
104
+ export default captureHostContact;
@@ -0,0 +1,151 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
3
+ /**
4
+ * @license
5
+ * Copyright 2026 Aglyn LLC
6
+ *
7
+ * Licensed under the Apache License, Version 2.0 (the "License");
8
+ * you may not use this file except in compliance with the License.
9
+ * You may obtain a copy of the License at
10
+ *
11
+ * http://www.apache.org/licenses/LICENSE-2.0
12
+ *
13
+ * Unless required by applicable law or agreed to in writing, software
14
+ * distributed under the License is distributed on an "AS IS" BASIS,
15
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ * See the License for the specific language governing permissions and
17
+ * limitations under the License.
18
+ */ import { upsertHostContact } from "@aglyn/tenant-data-admin";
19
+ import { assignOwnerForCapture } from "./assign-contact-owner.js";
20
+ import { associateCompanyByDomain } from "./associate-company-by-domain.js";
21
+ import { emitHostEvent } from "@aglyn/tenant-runtime/emit-host-event";
22
+ import { contactCaptureActor } from "@aglyn/tenant-runtime/capture-actor";
23
+ /**
24
+ * THE contact capture door for a server path (AGL-2605).
25
+ *
26
+ * `upsertHostContact` writes the contact and reports, through its
27
+ * `onCreated` hook, when the write made a NEW person. This wrapper is the
28
+ * one place that hook is bound to `contactCreated`, and it is the function
29
+ * every server door calls — the forms route, the membership and newsletter
30
+ * handlers, the order and booking webhooks. A door that calls
31
+ * `upsertHostContact` directly still captures the contact and fires nothing,
32
+ * which is a contact no automation can welcome; `capture-host-contact.spec`
33
+ * scans for exactly that call.
34
+ *
35
+ * ## Why the binding lives here and not in the data library
36
+ *
37
+ * What listens for the event — the automation engine among them — imports
38
+ * the data library for its Firestore handle, its org helpers and its
39
+ * senders. The data library emitting an event would import the listeners
40
+ * back, which is a cycle, and the module boundaries (`scope:data` depends on
41
+ * data and util only) refuse it in any case. So the lower layer reports a
42
+ * fact and this layer, which already knows how to announce one, announces
43
+ * it. The alternative — a sink the runtime registers into when its module
44
+ * happens to be imported — would fire only in a process that had loaded the
45
+ * runtime, and a Stripe webhook that created a contact in a process that had
46
+ * not would announce nothing with no error anywhere. The listeners themselves
47
+ * are registered by a call made at boot (`host-event-listeners.ts`), so every
48
+ * process has them before its first request.
49
+ *
50
+ * Fire-and-forget in the same sense the capture itself is: the hook's
51
+ * failure is caught inside `upsertHostContact`, so a runner that throws
52
+ * costs the door nothing, and this function never rejects.
53
+ *
54
+ * ## The company, before the announcement (AGL-2613)
55
+ *
56
+ * A new person with a work email address is linked to the company at that
57
+ * domain here, in the same hook, BEFORE `contactCreated` goes out — so an
58
+ * automation that reads the new contact finds them already filed, rather
59
+ * than racing a link that lands a moment later. Only when the door did not
60
+ * name a company itself: the console's drawer and an import row that
61
+ * carried one have written it into the facet, and the capture must not
62
+ * second-guess a person's choice with a domain match. The association never
63
+ * rejects, and its own catch keeps a failed lookup from costing the event.
64
+ *
65
+ * ## The owner, on the same terms (AGL-2618)
66
+ *
67
+ * Then the org's assignment rules and the site's default owner decide who
68
+ * follows the new person up — again before `contactCreated`, so an
69
+ * automation's "create a task for the owner" finds one — and again only
70
+ * when the door named none: a drawer, an import column or a conversion
71
+ * that picked an owner has said whose the record is. The pass reads the
72
+ * form and the tags off the same options the data library was handed,
73
+ * because the created-report carries the identity and not the routing.
74
+ * It never rejects either; a record it could not assign is one somebody
75
+ * assigns by hand, which is what every record was before this existed.
76
+ *
77
+ * What this does NOT do is close a lead (AGL-3232): a lead is the CRM's
78
+ * record, and which relationship closes one is the CRM's rule, answered in
79
+ * its capture writer for the doors that report through the capture seam.
80
+ * The order doors call this directly and close nothing — a buyer with an
81
+ * open lead is a lead the rep converts, as in Salesforce.
82
+ */ export async function captureHostContact(captureOptions) {
83
+ const { actor } = captureOptions, options = _object_without_properties_loose(captureOptions, [
84
+ "actor"
85
+ ]);
86
+ return upsertHostContact(_extends({}, options, {
87
+ onCreated: async (created)=>{
88
+ var _options_facet, _options_facet1;
89
+ if (!((_options_facet = options.facet) == null ? void 0 : _options_facet.companyId)) {
90
+ await associateCompanyByDomain(created).catch((error)=>{
91
+ console.error('captureHostContact company association failed', error);
92
+ });
93
+ }
94
+ if (!((_options_facet1 = options.facet) == null ? void 0 : _options_facet1.ownerUid)) {
95
+ var _options_interaction_formId;
96
+ await assignOwnerForCapture({
97
+ hostId: created.hostId,
98
+ contactId: created.contactId,
99
+ email: created.email,
100
+ source: created.source,
101
+ formId: (_options_interaction_formId = options.interaction.formId) != null ? _options_interaction_formId : null,
102
+ tags: options.tags
103
+ }).catch((error)=>{
104
+ console.error('captureHostContact owner assignment failed', error);
105
+ });
106
+ }
107
+ const cause = contactCaptureActor(created.source, created.email, actor);
108
+ await emitHostEvent(created.hostId, 'contactCreated', contactCreatedPayload(created, options.interaction.formId), cause ? {
109
+ actor: cause
110
+ } : {});
111
+ }
112
+ }));
113
+ }
114
+ /**
115
+ * The `contactCreated` payload, as the scalars an event may carry.
116
+ *
117
+ * `HostEventPayload` holds strings, numbers and booleans and nothing
118
+ * nested, because the payload seeds an expression scope and a condition
119
+ * editor whose operators compare strings. So `campaignIds` rides as one
120
+ * comma-joined string — `contains` still finds a campaign in it — and is
121
+ * present only when the capture had campaigns, so `notEmpty` on it reads
122
+ * as "came in through a campaign form". `name` and `lifecycleStage` are
123
+ * always present, empty when the door had none, so a condition on either
124
+ * never sees a missing key: `lifecycleStage == "lead"` is the filter that
125
+ * picks the form captures out of the sign-ups (AGL-2612).
126
+ *
127
+ * `formId` rides the same way `campaignIds` does — present only when the
128
+ * capture came through a form — because the created-report carries the
129
+ * person's identity and not the routing, and the form is routing: it is
130
+ * read off the interaction the door was handed. Present, it is what lets a
131
+ * condition say `formId` equals this form and no other (AGL-2626); absent,
132
+ * `notEmpty` on it reads as "came in through a form".
133
+ */ export function contactCreatedPayload(created, formId) {
134
+ var _created_name, _created_lifecycleStage;
135
+ const form = String(formId != null ? formId : '').trim();
136
+ return _extends({
137
+ contactId: created.contactId,
138
+ email: created.email,
139
+ name: (_created_name = created.name) != null ? _created_name : '',
140
+ source: created.source,
141
+ hostId: created.hostId,
142
+ lifecycleStage: (_created_lifecycleStage = created.lifecycleStage) != null ? _created_lifecycleStage : ''
143
+ }, created.campaignIds.length ? {
144
+ campaignIds: created.campaignIds.join(',')
145
+ } : {}, form ? {
146
+ formId: form
147
+ } : {});
148
+ }
149
+ export default captureHostContact;
150
+
151
+ //# sourceMappingURL=capture-host-contact.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/capture-host-contact.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n type HostContactCreated,\n upsertHostContact,\n type UpsertHostContactOptions,\n type UpsertHostContactVerdict,\n} from '@aglyn/tenant-data-admin'\nimport { assignOwnerForCapture } from './assign-contact-owner'\nimport { associateCompanyByDomain } from './associate-company-by-domain'\nimport { emitHostEvent } from '@aglyn/tenant-runtime/emit-host-event'\nimport { contactCaptureActor } from '@aglyn/tenant-runtime/capture-actor'\nimport type { HostEventActor, HostEventPayload } from '@aglyn/tenant-runtime/host-event-listeners'\n\n\n/**\n * THE contact capture door for a server path (AGL-2605).\n *\n * `upsertHostContact` writes the contact and reports, through its\n * `onCreated` hook, when the write made a NEW person. This wrapper is the\n * one place that hook is bound to `contactCreated`, and it is the function\n * every server door calls — the forms route, the membership and newsletter\n * handlers, the order and booking webhooks. A door that calls\n * `upsertHostContact` directly still captures the contact and fires nothing,\n * which is a contact no automation can welcome; `capture-host-contact.spec`\n * scans for exactly that call.\n *\n * ## Why the binding lives here and not in the data library\n *\n * What listens for the event — the automation engine among them — imports\n * the data library for its Firestore handle, its org helpers and its\n * senders. The data library emitting an event would import the listeners\n * back, which is a cycle, and the module boundaries (`scope:data` depends on\n * data and util only) refuse it in any case. So the lower layer reports a\n * fact and this layer, which already knows how to announce one, announces\n * it. The alternative — a sink the runtime registers into when its module\n * happens to be imported — would fire only in a process that had loaded the\n * runtime, and a Stripe webhook that created a contact in a process that had\n * not would announce nothing with no error anywhere. The listeners themselves\n * are registered by a call made at boot (`host-event-listeners.ts`), so every\n * process has them before its first request.\n *\n * Fire-and-forget in the same sense the capture itself is: the hook's\n * failure is caught inside `upsertHostContact`, so a runner that throws\n * costs the door nothing, and this function never rejects.\n *\n * ## The company, before the announcement (AGL-2613)\n *\n * A new person with a work email address is linked to the company at that\n * domain here, in the same hook, BEFORE `contactCreated` goes out — so an\n * automation that reads the new contact finds them already filed, rather\n * than racing a link that lands a moment later. Only when the door did not\n * name a company itself: the console's drawer and an import row that\n * carried one have written it into the facet, and the capture must not\n * second-guess a person's choice with a domain match. The association never\n * rejects, and its own catch keeps a failed lookup from costing the event.\n *\n * ## The owner, on the same terms (AGL-2618)\n *\n * Then the org's assignment rules and the site's default owner decide who\n * follows the new person up — again before `contactCreated`, so an\n * automation's \"create a task for the owner\" finds one — and again only\n * when the door named none: a drawer, an import column or a conversion\n * that picked an owner has said whose the record is. The pass reads the\n * form and the tags off the same options the data library was handed,\n * because the created-report carries the identity and not the routing.\n * It never rejects either; a record it could not assign is one somebody\n * assigns by hand, which is what every record was before this existed.\n *\n * What this does NOT do is close a lead (AGL-3232): a lead is the CRM's\n * record, and which relationship closes one is the CRM's rule, answered in\n * its capture writer for the doors that report through the capture seam.\n * The order doors call this directly and close nothing — a buyer with an\n * open lead is a lead the rep converts, as in Salesforce.\n */\nexport async function captureHostContact(\n captureOptions: Omit<UpsertHostContactOptions, 'onCreated'> & {\n /** Who caused the capture, when the door knows (AGL-3376). */\n actor?: HostEventActor\n },\n): Promise<UpsertHostContactVerdict> {\n const { actor, ...options } = captureOptions\n return upsertHostContact({\n ...options,\n onCreated: async (created) => {\n if (!options.facet?.companyId) {\n await associateCompanyByDomain(created).catch((error: unknown) => {\n console.error('captureHostContact company association failed', error)\n })\n }\n if (!options.facet?.ownerUid) {\n await assignOwnerForCapture({\n hostId: created.hostId,\n contactId: created.contactId,\n email: created.email,\n source: created.source,\n formId: options.interaction.formId ?? null,\n tags: options.tags,\n }).catch((error: unknown) => {\n console.error('captureHostContact owner assignment failed', error)\n })\n }\n const cause = contactCaptureActor(created.source, created.email, actor)\n await emitHostEvent(\n created.hostId,\n 'contactCreated',\n contactCreatedPayload(created, options.interaction.formId),\n cause ? { actor: cause } : {},\n )\n },\n })\n}\n\n/**\n * The `contactCreated` payload, as the scalars an event may carry.\n *\n * `HostEventPayload` holds strings, numbers and booleans and nothing\n * nested, because the payload seeds an expression scope and a condition\n * editor whose operators compare strings. So `campaignIds` rides as one\n * comma-joined string — `contains` still finds a campaign in it — and is\n * present only when the capture had campaigns, so `notEmpty` on it reads\n * as \"came in through a campaign form\". `name` and `lifecycleStage` are\n * always present, empty when the door had none, so a condition on either\n * never sees a missing key: `lifecycleStage == \"lead\"` is the filter that\n * picks the form captures out of the sign-ups (AGL-2612).\n *\n * `formId` rides the same way `campaignIds` does — present only when the\n * capture came through a form — because the created-report carries the\n * person's identity and not the routing, and the form is routing: it is\n * read off the interaction the door was handed. Present, it is what lets a\n * condition say `formId` equals this form and no other (AGL-2626); absent,\n * `notEmpty` on it reads as \"came in through a form\".\n */\nexport function contactCreatedPayload(\n created: HostContactCreated,\n formId?: string | null,\n): HostEventPayload {\n const form = String(formId ?? '').trim()\n return {\n contactId: created.contactId,\n email: created.email,\n name: created.name ?? '',\n source: created.source,\n hostId: created.hostId,\n lifecycleStage: created.lifecycleStage ?? '',\n ...(created.campaignIds.length\n ? { campaignIds: created.campaignIds.join(',') }\n : {}),\n ...(form ? { formId: form } : {}),\n }\n}\n\nexport default captureHostContact\n"],"names":["upsertHostContact","assignOwnerForCapture","associateCompanyByDomain","emitHostEvent","contactCaptureActor","captureHostContact","captureOptions","actor","options","onCreated","created","facet","companyId","catch","error","console","ownerUid","hostId","contactId","email","source","formId","interaction","tags","cause","contactCreatedPayload","form","String","trim","name","lifecycleStage","campaignIds","length","join"],"mappings":";;AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAEEA,iBAAiB,QAGZ,2BAA0B;AACjC,SAASC,qBAAqB,QAAQ,4BAAwB;AAC9D,SAASC,wBAAwB,QAAQ,mCAA+B;AACxE,SAASC,aAAa,QAAQ,wCAAuC;AACrE,SAASC,mBAAmB,QAAQ,sCAAqC;AAIzE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2DC,GACD,OAAO,eAAeC,mBACpBC,cAGC;IAED,MAAM,EAAEC,KAAK,EAAc,GAAGD,gBAAZE,2CAAYF;;;IAC9B,OAAON,kBAAkB,aACpBQ;QACHC,WAAW,OAAOC;gBACXF,gBAKAA;YALL,IAAI,GAACA,iBAAAA,QAAQG,KAAK,qBAAbH,eAAeI,SAAS,GAAE;gBAC7B,MAAMV,yBAAyBQ,SAASG,KAAK,CAAC,CAACC;oBAC7CC,QAAQD,KAAK,CAAC,iDAAiDA;gBACjE;YACF;YACA,IAAI,GAACN,kBAAAA,QAAQG,KAAK,qBAAbH,gBAAeQ,QAAQ,GAAE;oBAMlBR;gBALV,MAAMP,sBAAsB;oBAC1BgB,QAAQP,QAAQO,MAAM;oBACtBC,WAAWR,QAAQQ,SAAS;oBAC5BC,OAAOT,QAAQS,KAAK;oBACpBC,QAAQV,QAAQU,MAAM;oBACtBC,MAAM,GAAEb,8BAAAA,QAAQc,WAAW,CAACD,MAAM,YAA1Bb,8BAA8B;oBACtCe,MAAMf,QAAQe,IAAI;gBACpB,GAAGV,KAAK,CAAC,CAACC;oBACRC,QAAQD,KAAK,CAAC,8CAA8CA;gBAC9D;YACF;YACA,MAAMU,QAAQpB,oBAAoBM,QAAQU,MAAM,EAAEV,QAAQS,KAAK,EAAEZ;YACjE,MAAMJ,cACJO,QAAQO,MAAM,EACd,kBACAQ,sBAAsBf,SAASF,QAAQc,WAAW,CAACD,MAAM,GACzDG,QAAQ;gBAAEjB,OAAOiB;YAAM,IAAI,CAAC;QAEhC;;AAEJ;AAEA;;;;;;;;;;;;;;;;;;;CAmBC,GACD,OAAO,SAASC,sBACdf,OAA2B,EAC3BW,MAAsB;QAMdX,eAGUA;IAPlB,MAAMgB,OAAOC,OAAON,iBAAAA,SAAU,IAAIO,IAAI;IACtC,OAAO;QACLV,WAAWR,QAAQQ,SAAS;QAC5BC,OAAOT,QAAQS,KAAK;QACpBU,IAAI,GAAEnB,gBAAAA,QAAQmB,IAAI,YAAZnB,gBAAgB;QACtBU,QAAQV,QAAQU,MAAM;QACtBH,QAAQP,QAAQO,MAAM;QACtBa,cAAc,GAAEpB,0BAAAA,QAAQoB,cAAc,YAAtBpB,0BAA0B;OACtCA,QAAQqB,WAAW,CAACC,MAAM,GAC1B;QAAED,aAAarB,QAAQqB,WAAW,CAACE,IAAI,CAAC;IAAK,IAC7C,CAAC,GACDP,OAAO;QAAEL,QAAQK;IAAK,IAAI,CAAC;AAEnC;AAEA,eAAerB,mBAAkB"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * Clear `parentCompanyId` on at most `limit` companies under `companyId`
19
+ * (AGL-3514), and say whether more remain. Shared by `crm/company-delete`
20
+ * and the REST resource's delete, which calls it until none remain.
21
+ */
22
+ export declare function detachChildCompanies(firestore: FirebaseFirestore.Firestore, companies: FirebaseFirestore.CollectionReference, companyId: string, limit?: number): Promise<{
23
+ detached: number;
24
+ moreRemain: boolean;
25
+ }>;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { FieldValue } from "firebase-admin/firestore";
17
+ import { COMPANY_DETACH_LIMIT } from "../model/company-delete-route.js";
18
+ /**
19
+ * Clear `parentCompanyId` on at most `limit` companies under `companyId`
20
+ * (AGL-3514), and say whether more remain. Shared by `crm/company-delete`
21
+ * and the REST resource's delete, which calls it until none remain.
22
+ */ export async function detachChildCompanies(firestore, companies, companyId, limit = COMPANY_DETACH_LIMIT) {
23
+ // One past the bound, so "more remain" is a fact from the probe row.
24
+ const probe = await companies.where('parentCompanyId', '==', companyId).limit(limit + 1).get();
25
+ const children = probe.docs.slice(0, limit);
26
+ if (children.length) {
27
+ const batch = firestore.batch();
28
+ for (const child of children){
29
+ batch.update(child.ref, {
30
+ parentCompanyId: FieldValue.delete(),
31
+ updatedAt: FieldValue.serverTimestamp()
32
+ });
33
+ }
34
+ await batch.commit();
35
+ }
36
+ return {
37
+ detached: children.length,
38
+ moreRemain: probe.docs.length > limit
39
+ };
40
+ }
41
+
42
+ //# sourceMappingURL=company-children.js.map