@aglyn/plugins-crm 1.0.0-beta.220 → 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 +69 -22
  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
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/contact-update.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * `POST /api/crm/contact-update` — one holder's profile fields on one or\n * more contacts (AGL-2804).\n *\n * Body: `{ hostId, contactIds, set }` under a site, or `{ orgId, hostId?,\n * contactIds, set }` at the organization level, where `set` is\n * `ContactUpdateFields`. Answers `{ ok, results }`: one outcome per contact,\n * in the order the ids were sent.\n *\n * ## Why every facet write comes here\n *\n * A contact's fields live in the facet of the holder that keeps them —\n * `facets.{groupId}.*` — and the Firestore rules cannot tell one field of a\n * facet from another: the holder is a map key a rule cannot address, and a\n * write's affected keys name only `facets`. So the rules let a client update\n * change nothing in a facet beyond letting a holder go, and the console's\n * edits arrive here, where the plan is asked about the fields the request\n * actually carries.\n *\n * ## Who may call it\n *\n * The CRM writer the task routes admit (`authorizeCrmWriter`): under a site,\n * a member whose role writes org data, who holds `data.manage` and whose\n * access reaches the site; at the organization level, an org-wide member\n * with the permission. Each contact must then be one the caller may read —\n * the rules' `canReadScoped()` — and, under a site, one the site's hub lists.\n *\n * ## The plan\n *\n * Every field is the CRM's (AGL-2790), and a plan without the CRM has none\n * of it (AGL-2851), so it edits no contact: not the owner, company, custom\n * values or files, and not the profile a capture writes (name, phone, job\n * title, address), the tags, the notes or the campaign filing either. A\n * request carrying any field is refused to such a plan once the caller is\n * known, and before a contact is read — staff included, because the plan is\n * a fact about the workspace. Erasing a person is another route, open on\n * every plan.\n *\n * ## One holder, by dotted path\n *\n * Under a site the facet is the site's consent group's; at the organization\n * level it is each contact's own primary holder, the facet the record page\n * shows there. Every field is one dotted path — a nested `facets` object\n * would replace every other holder's map — and a text field sent empty is\n * deleted rather than stored blank, so \"no phone\" has one shape.\n *\n * ## Many contacts, one commit\n *\n * The contacts are read together and written in one batch. A batch that\n * fails is retried contact by contact, so the one that could not be written\n * is named and the rest still land. A company's contacts count follows the\n * link: summed per company and settled once the contacts are written, so a\n * company deleted in between cannot fail the save of the people at it — the\n * company page's live aggregate is what corrects a count that did not move.\n */\n\nimport {\n type AglynPostalAddress,\n consentGroupForHost,\n CONTACT_FIELDS_MAX_PER_ORG,\n type ContactCompanyLinkPlan,\n type ContactCustomValue,\n type ContactFieldDefinition,\n containerMembershipValue,\n contactContainerFieldPath,\n contactFacetPath,\n CRM_COLLECTIONS,\n CRM_LEAD_SOURCE_PICKLIST,\n CRM_MEDIA_IDS_FIELD,\n crmReadTokens,\n effectiveCrmLeadSourcePicklist,\n isOrgWideMember,\n judgeCrmLeadSource,\n normalizeAddress,\n normalizeCrmMediaIds,\n normalizeCrmPicklistLabel,\n normalizePhone,\n type PluginApiHandler,\n planContactCompanyLink,\n readContactCompanyLink,\n readContactFacet,\n readCrmCustomInput,\n visibleToTokens,\n} from '@aglyn/aglyn/server'\nimport {\n companyContactsCountFields,\n contactCompanyLinkFields,\n firebaseAdmin,\n resolveOrgMembership,\n restampCrmListFieldsOf,\n} from '@aglyn/tenant-data-admin'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport {\n CONTACT_UPDATE_FIELDS,\n type ContactUpdateFields,\n type ContactUpdateOutcome,\n type ContactUpdateResponse,\n CRM_CONTACT_UPDATE_MAX,\n} from '../model/contact-update'\nimport {\n CONTACT_NOTES_MAX,\n CONTACT_PHONE_REFUSAL,\n CONTACT_TAGS_MAX,\n normalizeTags,\n readBulkTag,\n typed,\n} from './contact-profile'\nimport { readCrmRouteScope } from './org-caller'\nimport { crmSuiteRefusal } from './suite-gate'\nimport { authorizeCrmWriter, canReach, type Writer } from './task-routes'\nimport { contactPrimaryGroup } from '../model/contact-holder'\n\n/** What the suite gate names for each field a request can carry. */\nconst SUITE_ACTS: Record<keyof ContactUpdateFields, string> = {\n name: \"Editing a contact's profile\",\n phone: \"Editing a contact's profile\",\n jobTitle: \"Editing a contact's profile\",\n leadSource: \"Editing a contact's profile\",\n address: \"Editing a contact's profile\",\n notes: \"Editing a contact's notes\",\n tags: 'Tagging a contact',\n addTag: 'Tagging a contact',\n removeTag: 'Tagging a contact',\n campaignIds: 'Filing a contact under a campaign',\n ownerUid: \"Assigning a contact's owner\",\n companyId: 'Filing a contact under a company',\n companyName: 'Filing a contact under a company',\n custom: \"Editing a contact's custom fields\",\n mediaIds: 'Attaching files to a contact',\n}\n\n/** A contact read at the organization level that no site has captured. */\nconst NO_HOLDER_REFUSAL = 'No site holds this contact yet, so it has no profile to edit.'\n\n/**\n * The act a request is refused for on a plan without the CRM: the first\n * field it carries, in the order the route reads them. Every field is the\n * CRM's, so whichever field a request names is the act its refusal names.\n */\nexport function contactUpdateSuiteAct(fields: ContactUpdateFields): string {\n const field = CONTACT_UPDATE_FIELDS.find((key) => fields[key] !== undefined)\n return field ? SUITE_ACTS[field] : \"Editing a contact's profile\"\n}\n\n/**\n * A body's `set`, read into the fields it names, or the sentence refusing\n * it. A key the route does not save is refused rather than dropped: a save\n * that silently lost a field would read as a save that kept it.\n */\nexport function readContactUpdateFields(\n input: unknown,\n): { ok: true; fields: ContactUpdateFields } | { ok: false; error: string } {\n if (!input || typeof input !== 'object' || Array.isArray(input)) {\n return { ok: false, error: 'Name the fields to save.' }\n }\n const raw = input as Record<string, unknown>\n const unknownKey = Object.keys(raw).find(\n (key) => !(CONTACT_UPDATE_FIELDS as readonly string[]).includes(key),\n )\n if (unknownKey) {\n return { ok: false, error: `\"${unknownKey}\" is not a field a contact's profile saves.` }\n }\n const fields: ContactUpdateFields = {}\n if ('name' in raw) fields.name = typed(raw['name'], 120)\n if ('phone' in raw) {\n const text = typed(raw['phone'], 40)\n const phone = text ? normalizePhone(text) : ''\n if (phone === null) return { ok: false, error: CONTACT_PHONE_REFUSAL }\n fields.phone = phone\n }\n if ('jobTitle' in raw) fields.jobTitle = typed(raw['jobTitle'], 120)\n // Held to the label's shape here; judged against the org's list by the\n // handler, which alone can read it (AGL-3298).\n if ('leadSource' in raw) fields.leadSource = normalizeCrmPicklistLabel(raw['leadSource'])\n if ('address' in raw) {\n const address = raw['address']\n fields.address =\n address && typeof address === 'object' && !Array.isArray(address)\n ? normalizeAddress(address as AglynPostalAddress)\n : null\n }\n if ('notes' in raw) fields.notes = String(raw['notes'] ?? '').slice(0, CONTACT_NOTES_MAX)\n // One write may carry one transform per field, so the tags are replaced,\n // added to or taken from — never two of those at once.\n if (['tags', 'addTag', 'removeTag'].filter((key) => key in raw).length > 1) {\n return { ok: false, error: 'Send the tags, a tag to add or a tag to remove — one of them.' }\n }\n if ('tags' in raw) {\n if (!Array.isArray(raw['tags'])) return { ok: false, error: 'Tags must be a list.' }\n fields.tags = normalizeTags(raw['tags'])\n }\n for (const key of ['addTag', 'removeTag'] as const) {\n if (!(key in raw)) continue\n const tag = readBulkTag(raw[key])\n if (!tag) return { ok: false, error: 'Name the tag.' }\n fields[key] = tag\n }\n if ('campaignIds' in raw) {\n const ids = raw['campaignIds']\n if (!Array.isArray(ids)) return { ok: false, error: 'Campaigns must be a list.' }\n fields.campaignIds = containerMembershipValue(ids.map((id) => String(id ?? '')))\n }\n if ('ownerUid' in raw) fields.ownerUid = typed(raw['ownerUid'], 128)\n if ('companyId' in raw) {\n const companyId = raw['companyId'] === null ? null : typed(raw['companyId'], 128)\n if (companyId !== null && (!companyId || companyId.includes('/'))) {\n return { ok: false, error: 'The company could not be read.' }\n }\n fields.companyId = companyId\n }\n if ('companyName' in raw) fields.companyName = typed(raw['companyName'], 120)\n if ('custom' in raw) {\n const custom = raw['custom']\n if (!custom || typeof custom !== 'object' || Array.isArray(custom)) {\n return { ok: false, error: 'Custom fields must be values keyed by field key.' }\n }\n fields.custom = custom as Record<string, ContactCustomValue>\n }\n if ('mediaIds' in raw) {\n if (!Array.isArray(raw['mediaIds'])) {\n return { ok: false, error: 'Files must be a list of ids.' }\n }\n fields.mediaIds = normalizeCrmMediaIds(raw['mediaIds'])\n }\n if (!Object.keys(fields).length) return { ok: false, error: 'Name the fields to save.' }\n return { ok: true, fields }\n}\n\n/** The ids a body names, deduplicated — or `null` when any is unreadable or there are too many. */\nfunction readContactIds(value: unknown): string[] | null {\n if (!Array.isArray(value) || !value.length || value.length > CRM_CONTACT_UPDATE_MAX) {\n return null\n }\n const ids = value.map((id) => typed(id, 200))\n if (ids.some((id) => !id || id.includes('/'))) return null\n return [...new Set(ids)]\n}\n\n/**\n * Under a site, whether the caller's access reaches it. The rules could not\n * ask this of a facet write — they cannot see which holder a write is for —\n * and a collaborator on one site must not write another site's notes on a\n * person both of them hold.\n */\nfunction reachesSite(writer: Writer, hostId: string): boolean {\n return (\n writer.staff ||\n isOrgWideMember(writer.member) ||\n Boolean((writer.member?.hostAccess as Record<string, unknown> | undefined)?.[hostId])\n )\n}\n\nconst refused = (contactId: string, error: string): ContactUpdateOutcome => ({\n contactId,\n ok: false,\n error,\n})\n\ninterface ContactWrite {\n contactId: string\n ref: FirebaseFirestore.DocumentReference\n update: Record<string, unknown>\n counts: ContactCompanyLinkPlan['counts']\n}\n\n/**\n * One contact's update for one holder, or the sentence refusing the row.\n * `update` is `null` when the document already says everything asked — a tag\n * the row already carries, a link already made.\n */\nfunction contactPatch(\n contact: Record<string, unknown>,\n groupId: string,\n fields: ContactUpdateFields,\n custom: Record<string, ContactCustomValue> | undefined,\n linkedCompanyName: string,\n):\n | { update: Record<string, unknown> | null; counts: ContactCompanyLinkPlan['counts'] }\n | { refused: string } {\n const path = (field: string) => contactFacetPath(groupId, field)\n /** A text field as stored: the value, or the field's absence. */\n const text = (value: string) => value || FieldValue.delete()\n const update: Record<string, unknown> = {}\n let counts: ContactCompanyLinkPlan['counts'] = []\n\n if (fields.name !== undefined) update[path('name')] = text(fields.name)\n if (fields.phone !== undefined) {\n update[path('phone')] = text(fields.phone)\n // The search echo — see `HostContact.phone`.\n update['phone'] = text(fields.phone)\n }\n if (fields.jobTitle !== undefined) update[path('jobTitle')] = text(fields.jobTitle)\n if (fields.leadSource !== undefined) update[path('leadSource')] = text(fields.leadSource)\n if (fields.address !== undefined) {\n update[path('address')] = fields.address ?? FieldValue.delete()\n }\n if (fields.notes !== undefined) update[path('notes')] = fields.notes\n if (fields.tags !== undefined) update[path('tags')] = fields.tags\n\n const held = (readContactFacet(contact, groupId).tags ?? []).map((tag) =>\n String(tag).toLowerCase(),\n )\n if (fields.addTag && !held.includes(fields.addTag)) {\n // The record page's cap, which a selection must not slip past.\n if (held.length >= CONTACT_TAGS_MAX) {\n return { refused: `already has ${CONTACT_TAGS_MAX} tags` }\n }\n update[path('tags')] = FieldValue.arrayUnion(fields.addTag)\n }\n if (fields.removeTag && held.includes(fields.removeTag)) {\n update[path('tags')] = FieldValue.arrayRemove(fields.removeTag)\n }\n if (fields.campaignIds !== undefined) {\n update[contactContainerFieldPath(groupId, 'campaign')] = fields.campaignIds\n }\n if (fields.ownerUid !== undefined) update[path('ownerUid')] = text(fields.ownerUid)\n\n if (fields.companyId !== undefined) {\n const plan = planContactCompanyLink(\n readContactCompanyLink(contact, groupId),\n fields.companyId,\n )\n if (plan) {\n Object.assign(update, contactCompanyLinkFields(plan, groupId))\n counts = plan.counts\n if (fields.companyName === undefined) {\n // The label follows the link: the company's own name, or nothing.\n const label = plan.companyId ? linkedCompanyName : ''\n update[path('companyName')] = text(label)\n // The search echo — see `HostContact.companyName`.\n update['companyName'] = text(label)\n }\n }\n }\n if (fields.companyName !== undefined) {\n update[path('companyName')] = text(fields.companyName)\n update['companyName'] = text(fields.companyName)\n }\n for (const [key, value] of Object.entries(custom ?? {})) {\n update[path(`custom.${key}`)] = value\n }\n if (fields.mediaIds !== undefined) update[path(CRM_MEDIA_IDS_FIELD)] = fields.mediaIds\n\n if (!Object.keys(update).length) return { update: null, counts }\n update['updatedAt'] = FieldValue.serverTimestamp()\n return { update, counts }\n}\n\n/**\n * Every write in one batch, and — when the batch is refused — each on its\n * own, so the contact that could not be written is the only one reported.\n * Answers the ids that were not written.\n */\nasync function commitContactWrites(\n firestore: FirebaseFirestore.Firestore,\n writes: readonly ContactWrite[],\n): Promise<Set<string>> {\n const failed = new Set<string>()\n if (!writes.length) return failed\n const batch = firestore.batch()\n for (const write of writes) batch.update(write.ref, write.update)\n let committed = false\n try {\n await batch.commit()\n committed = true\n } catch {\n // A batch names no document; the pass below finds the one it was.\n }\n if (!committed) {\n for (const write of writes) {\n try {\n await write.ref.update(write.update)\n } catch (error) {\n console.error('[crm] contact-update could not write a contact', write.contactId, error)\n failed.add(write.contactId)\n }\n }\n }\n // The owner, tags, company and custom values are what the Contacts list\n // filters by through `facetKeys` (AGL-3321): restamped from each row.\n await restampCrmListFieldsOf(\n writes.filter((write) => !failed.has(write.contactId)).map((write) => write.ref),\n 'contacts',\n )\n return failed\n}\n\n/** Each company's contacts count moved once, by the sum of what the written links moved. */\nasync function settleCompanyCounts(\n companies: FirebaseFirestore.CollectionReference,\n writes: readonly ContactWrite[],\n): Promise<void> {\n const deltas = new Map<string, number>()\n for (const write of writes) {\n for (const count of write.counts) {\n deltas.set(count.companyId, (deltas.get(count.companyId) ?? 0) + count.delta)\n }\n }\n await Promise.all(\n [...deltas]\n .filter(([, delta]) => delta !== 0)\n .map(([companyId, delta]) =>\n companies\n .doc(companyId)\n .update(companyContactsCountFields(delta))\n .catch((error: unknown) => {\n console.error('[crm] company contacts count could not move', companyId, delta, error)\n }),\n ),\n )\n}\n\nexport const crmContactUpdateHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n res.setHeader('Allow', 'POST')\n res.status(405).json({ error: 'Method not allowed' })\n return\n }\n const body = (req.body ?? {}) as Record<string, unknown>\n const scope = readCrmRouteScope(body)\n if (!scope) {\n res.status(400).json({ error: 'Missing hostId' })\n return\n }\n const contactIds = readContactIds(body['contactIds'])\n if (!contactIds) {\n res.status(400).json({ error: `Name between 1 and ${CRM_CONTACT_UPDATE_MAX} contacts.` })\n return\n }\n const read = readContactUpdateFields(body['set'])\n if (read.ok === false) {\n res.status(400).json({ error: read.error })\n return\n }\n const { fields } = read\n\n try {\n const writer = await authorizeCrmWriter(req, scope, { suiteAct: null })\n if (writer.ok === false) {\n res.status(writer.status).json(writer.body)\n return\n }\n if (scope.level === 'site' && !reachesSite(writer, scope.hostId)) {\n res.status(403).json({ error: 'Your access to this organization does not reach this site.' })\n return\n }\n // The plan after the person, and before any contact is read (AGL-2787).\n const suite = crmSuiteRefusal(writer.org, contactUpdateSuiteAct(fields))\n if (suite) {\n res.status(suite.status).json(suite.body)\n return\n }\n\n const firestore = firebaseAdmin.app().firestore()\n const orgRef = firestore.collection('orgs').doc(writer.orgId)\n const siteGroup =\n scope.level === 'site' ? consentGroupForHost(writer.org, scope.hostId) : null\n const siteTokens = siteGroup ? crmReadTokens(siteGroup) : null\n\n let custom: Record<string, ContactCustomValue> | undefined\n if (fields.custom) {\n const definitions = await orgRef\n .collection(CRM_COLLECTIONS.contactFields)\n .limit(CONTACT_FIELDS_MAX_PER_ORG)\n .get()\n const judged = readCrmCustomInput(\n fields.custom,\n definitions.docs.map((doc) => doc.data() as ContactFieldDefinition),\n 'contact',\n )\n if ('errors' in judged) {\n res.status(400).json({\n error: Object.values(judged.errors)[0] ?? 'A custom value could not be saved.',\n })\n return\n }\n custom = judged.values\n }\n\n /*\n * THE ORG'S LEAD SOURCES (AGL-3298), read only when the body names one.\n * Judged per contact below, because a contact keeps the value it\n * already holds even when the list has since deactivated it.\n */\n const leadSources = fields.leadSource\n ? effectiveCrmLeadSourcePicklist(\n (\n await orgRef.collection(CRM_COLLECTIONS.picklists).doc(CRM_LEAD_SOURCE_PICKLIST).get()\n ).data(),\n )\n : null\n\n if (fields.ownerUid) {\n const owner = await resolveOrgMembership(fields.ownerUid, writer.orgId).catch(() => null)\n if (!owner?.member) {\n res.status(400).json({ error: 'The owner is not a member of this organization.' })\n return\n }\n }\n\n // The company a link names has to exist and be visible to the caller and\n // the site, or a person could be filed under a record nobody here can open.\n let linkedCompanyName = ''\n if (fields.companyId) {\n const company = await orgRef\n .collection(CRM_COLLECTIONS.companies)\n .doc(fields.companyId)\n .get()\n const tokens = company.exists ? (company.get('visibleTo') as string[] | undefined) : undefined\n const visible =\n company.exists &&\n canReach(writer, tokens) &&\n (!siteTokens || visibleToTokens(tokens, siteTokens))\n if (!visible) {\n res.status(404).json({ error: 'Unknown company' })\n return\n }\n linkedCompanyName = typed(company.get('name'), 120)\n }\n\n const contacts = orgRef.collection('contacts')\n const snapshots = await firestore.getAll(...contactIds.map((id) => contacts.doc(id)))\n const outcomes = new Map<string, ContactUpdateOutcome>()\n const writes: ContactWrite[] = []\n snapshots.forEach((snapshot, index) => {\n const contactId = contactIds[index]\n if (!snapshot.exists) {\n outcomes.set(contactId, refused(contactId, 'That contact no longer exists.'))\n return\n }\n const data = (snapshot.data() ?? {}) as Record<string, unknown>\n const visibleTo = data['visibleTo'] as string[] | undefined\n if (!canReach(writer, visibleTo)) {\n outcomes.set(contactId, refused(contactId, 'That contact is not visible to you.'))\n return\n }\n if (siteTokens && !visibleToTokens(visibleTo, siteTokens)) {\n outcomes.set(contactId, refused(contactId, 'That contact is not visible to this site.'))\n return\n }\n const group = siteGroup ?? contactPrimaryGroup(data, writer.org)\n if (!group.groupId) {\n outcomes.set(contactId, refused(contactId, NO_HOLDER_REFUSAL))\n return\n }\n let rowFields = fields\n if (leadSources && fields.leadSource) {\n const judged = judgeCrmLeadSource(\n leadSources,\n fields.leadSource,\n readContactFacet(data, group.groupId).leadSource,\n )\n if (judged.ok === false) {\n outcomes.set(contactId, refused(contactId, judged.error))\n return\n }\n rowFields = { ...fields, leadSource: judged.value ?? '' }\n }\n const patch = contactPatch(data, group.groupId, rowFields, custom, linkedCompanyName)\n if ('refused' in patch) {\n outcomes.set(contactId, refused(contactId, patch.refused))\n return\n }\n outcomes.set(contactId, { contactId, ok: true })\n if (patch.update) {\n writes.push({ contactId, ref: snapshot.ref, update: patch.update, counts: patch.counts })\n }\n })\n\n const failed = await commitContactWrites(firestore, writes)\n for (const contactId of failed) {\n outcomes.set(contactId, refused(contactId, 'The contact could not be saved.'))\n }\n await settleCompanyCounts(\n orgRef.collection(CRM_COLLECTIONS.companies),\n writes.filter((write) => !failed.has(write.contactId)),\n )\n\n const answer: ContactUpdateResponse = {\n ok: true,\n results: contactIds.map(\n (contactId) => outcomes.get(contactId) ?? refused(contactId, 'The contact could not be saved.'),\n ),\n }\n res.status(200).json(answer)\n } catch (error) {\n console.error('[crm] contact-update failed', error)\n res.status(500).json({ error: 'The contact could not be saved.' })\n }\n}\n"],"names":["consentGroupForHost","CONTACT_FIELDS_MAX_PER_ORG","containerMembershipValue","contactContainerFieldPath","contactFacetPath","CRM_COLLECTIONS","CRM_LEAD_SOURCE_PICKLIST","CRM_MEDIA_IDS_FIELD","crmReadTokens","effectiveCrmLeadSourcePicklist","isOrgWideMember","judgeCrmLeadSource","normalizeAddress","normalizeCrmMediaIds","normalizeCrmPicklistLabel","normalizePhone","planContactCompanyLink","readContactCompanyLink","readContactFacet","readCrmCustomInput","visibleToTokens","companyContactsCountFields","contactCompanyLinkFields","firebaseAdmin","resolveOrgMembership","restampCrmListFieldsOf","FieldValue","CONTACT_UPDATE_FIELDS","CRM_CONTACT_UPDATE_MAX","CONTACT_NOTES_MAX","CONTACT_PHONE_REFUSAL","CONTACT_TAGS_MAX","normalizeTags","readBulkTag","typed","readCrmRouteScope","crmSuiteRefusal","authorizeCrmWriter","canReach","contactPrimaryGroup","SUITE_ACTS","name","phone","jobTitle","leadSource","address","notes","tags","addTag","removeTag","campaignIds","ownerUid","companyId","companyName","custom","mediaIds","NO_HOLDER_REFUSAL","contactUpdateSuiteAct","fields","field","find","key","undefined","readContactUpdateFields","input","raw","Array","isArray","ok","error","unknownKey","Object","keys","includes","text","String","slice","filter","length","tag","ids","map","id","readContactIds","value","some","Set","reachesSite","writer","hostId","staff","member","Boolean","hostAccess","refused","contactId","contactPatch","contact","groupId","linkedCompanyName","path","delete","update","counts","held","toLowerCase","arrayUnion","arrayRemove","plan","assign","label","entries","serverTimestamp","commitContactWrites","firestore","writes","failed","batch","write","ref","committed","commit","console","add","has","settleCompanyCounts","companies","deltas","Map","count","set","get","delta","Promise","all","doc","catch","crmContactUpdateHandler","req","res","method","setHeader","status","json","body","scope","contactIds","read","suiteAct","level","suite","org","app","orgRef","collection","orgId","siteGroup","siteTokens","definitions","contactFields","limit","judged","docs","data","values","errors","leadSources","picklists","owner","company","tokens","exists","visible","contacts","snapshots","getAll","outcomes","forEach","snapshot","index","visibleTo","group","rowFields","patch","push","answer","results"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/contact-update.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * `POST /api/crm/contact-update` — one holder's profile fields on one or\n * more contacts (AGL-2804).\n *\n * Body: `{ hostId, contactIds, set }` under a site, or `{ orgId, hostId?,\n * contactIds, set }` at the organization level, where `set` is\n * `ContactUpdateFields`. Answers `{ ok, results }`: one outcome per contact,\n * in the order the ids were sent.\n *\n * ## Why every facet write comes here\n *\n * A contact's fields live in the facet of the holder that keeps them —\n * `facets.{groupId}.*` — and the Firestore rules cannot tell one field of a\n * facet from another: the holder is a map key a rule cannot address, and a\n * write's affected keys name only `facets`. So the rules let a client update\n * change nothing in a facet beyond letting a holder go, and the console's\n * edits arrive here, where the plan is asked about the fields the request\n * actually carries.\n *\n * ## Who may call it\n *\n * The CRM writer the task routes admit (`authorizeCrmWriter`): under a site,\n * a member whose role writes org data, who holds `data.manage` and whose\n * access reaches the site; at the organization level, an org-wide member\n * with the permission. Each contact must then be one the caller may read —\n * the rules' `canReadScoped()` — and, under a site, one the site's hub lists.\n *\n * ## The plan\n *\n * Every field is the CRM's (AGL-2790), and a plan without the CRM has none\n * of it (AGL-2851), so it edits no contact: not the owner, company, custom\n * values or files, and not the profile a capture writes (name, phone, job\n * title, address), the tags, the notes or the campaign filing either. A\n * request carrying any field is refused to such a plan once the caller is\n * known, and before a contact is read — staff included, because the plan is\n * a fact about the workspace. Erasing a person is another route, open on\n * every plan.\n *\n * ## One holder, by dotted path\n *\n * Under a site the facet is the site's consent group's; at the organization\n * level it is each contact's own primary holder, the facet the record page\n * shows there. Every field is one dotted path — a nested `facets` object\n * would replace every other holder's map — and a text field sent empty is\n * deleted rather than stored blank, so \"no phone\" has one shape.\n *\n * ## Many contacts, one commit\n *\n * The contacts are read together and written in one batch. A batch that\n * fails is retried contact by contact, so the one that could not be written\n * is named and the rest still land. A company's contacts count follows the\n * link: summed per company and settled once the contacts are written, so a\n * company deleted in between cannot fail the save of the people at it — the\n * company page's live aggregate is what corrects a count that did not move.\n */\n\nimport {\n type AglynPostalAddress,\n composeContactName,\n CONTACT_BIRTHDATE_REFUSAL,\n CONTACT_EXTRA_PHONE_FIELDS,\n CONTACT_PHONE_FIELD_LABELS,\n consentGroupForHost,\n CONTACT_FIELDS_MAX_PER_ORG,\n type ContactCompanyLinkPlan,\n type ContactCustomValue,\n type ContactFieldDefinition,\n containerMembershipValue,\n contactContainerFieldPath,\n contactFacetPath,\n CRM_COLLECTIONS,\n CRM_LEAD_SOURCE_PICKLIST,\n CRM_MEDIA_IDS_FIELD,\n CRM_SALUTATION_PICKLIST,\n crmReadTokens,\n effectiveCrmLeadSourcePicklist,\n isOrgWideMember,\n judgeCrmLeadSource,\n judgeCrmPicklistValue,\n normalizeAddress,\n normalizeContactBirthdate,\n normalizeCrmMediaIds,\n normalizeCrmPicklistLabel,\n normalizePhone,\n type PluginApiHandler,\n planContactCompanyLink,\n readContactCompanyLink,\n readContactFacet,\n readCrmCustomInput,\n visibleToTokens,\n} from '@aglyn/aglyn/server'\nimport {\n companyContactsCountFields,\n contactCompanyLinkFields,\n firebaseAdmin,\n resolveOrgMembership,\n restampCrmListFieldsOf,\n} from '@aglyn/tenant-data-admin'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport {\n CONTACT_UPDATE_FIELDS,\n type ContactUpdateFields,\n type ContactUpdateOutcome,\n type ContactUpdateResponse,\n CRM_CONTACT_UPDATE_MAX,\n} from '../model/contact-update'\nimport {\n CONTACT_NOTES_MAX,\n CONTACT_PHONE_REFUSAL,\n CONTACT_TAGS_MAX,\n normalizeTags,\n readBulkTag,\n typed,\n} from './contact-profile'\nimport { readCrmRouteScope } from './org-caller'\nimport { crmSuiteRefusal } from './suite-gate'\nimport { authorizeCrmWriter, canReach, type Writer } from './task-routes'\nimport { contactPrimaryGroup } from '../model/contact-holder'\nimport {\n cachedContactReader,\n CONTACT_REPORTS_TO_LOOP_REFUSAL,\n CONTACT_REPORTS_TO_SELF_REFUSAL,\n contactReportsToLoops,\n} from './contact-reports-to'\nimport { readCrmPicklist } from './read-picklist'\n\n/** What the suite gate names for each field a request can carry. */\nconst SUITE_ACTS: Record<keyof ContactUpdateFields, string> = {\n name: \"Editing a contact's profile\",\n salutation: \"Editing a contact's profile\",\n firstName: \"Editing a contact's profile\",\n lastName: \"Editing a contact's profile\",\n phone: \"Editing a contact's profile\",\n mobilePhone: \"Editing a contact's profile\",\n homePhone: \"Editing a contact's profile\",\n otherPhone: \"Editing a contact's profile\",\n fax: \"Editing a contact's profile\",\n jobTitle: \"Editing a contact's profile\",\n department: \"Editing a contact's profile\",\n birthdate: \"Editing a contact's profile\",\n assistantName: \"Editing a contact's profile\",\n assistantPhone: \"Editing a contact's profile\",\n reportsToContactId: \"Editing a contact's profile\",\n leadSource: \"Editing a contact's profile\",\n address: \"Editing a contact's profile\",\n otherAddress: \"Editing a contact's profile\",\n doNotCall: \"Editing a contact's profile\",\n notes: \"Editing a contact's notes\",\n tags: 'Tagging a contact',\n addTag: 'Tagging a contact',\n removeTag: 'Tagging a contact',\n campaignIds: 'Filing a contact under a campaign',\n ownerUid: \"Assigning a contact's owner\",\n companyId: 'Filing a contact under a company',\n companyName: 'Filing a contact under a company',\n custom: \"Editing a contact's custom fields\",\n mediaIds: 'Attaching files to a contact',\n}\n\n/** The sentence a `name` that contradicts the holder's first and last names is refused with. */\nexport const CONTACT_COMPOSED_NAME_REFUSAL =\n \"This person's name is made of their first and last name — change those instead.\"\n\n/** A contact read at the organization level that no site has captured. */\nconst NO_HOLDER_REFUSAL = 'No site holds this contact yet, so it has no profile to edit.'\n\n/**\n * The act a request is refused for on a plan without the CRM: the first\n * field it carries, in the order the route reads them. Every field is the\n * CRM's, so whichever field a request names is the act its refusal names.\n */\nexport function contactUpdateSuiteAct(fields: ContactUpdateFields): string {\n const field = CONTACT_UPDATE_FIELDS.find((key) => fields[key] !== undefined)\n return field ? SUITE_ACTS[field] : \"Editing a contact's profile\"\n}\n\n/**\n * A body's `set`, read into the fields it names, or the sentence refusing\n * it. A key the route does not save is refused rather than dropped: a save\n * that silently lost a field would read as a save that kept it.\n */\nexport function readContactUpdateFields(\n input: unknown,\n): { ok: true; fields: ContactUpdateFields } | { ok: false; error: string } {\n if (!input || typeof input !== 'object' || Array.isArray(input)) {\n return { ok: false, error: 'Name the fields to save.' }\n }\n const raw = input as Record<string, unknown>\n const unknownKey = Object.keys(raw).find(\n (key) => !(CONTACT_UPDATE_FIELDS as readonly string[]).includes(key),\n )\n if (unknownKey) {\n return { ok: false, error: `\"${unknownKey}\" is not a field a contact's profile saves.` }\n }\n const fields: ContactUpdateFields = {}\n if ('name' in raw) fields.name = typed(raw['name'], 120)\n for (const key of ['phone', ...CONTACT_EXTRA_PHONE_FIELDS] as const) {\n if (!(key in raw)) continue\n const text = typed(raw[key], 40)\n const phone = text ? normalizePhone(text) : ''\n if (phone === null) {\n return {\n ok: false,\n error:\n key === 'phone'\n ? CONTACT_PHONE_REFUSAL\n : `${CONTACT_PHONE_FIELD_LABELS[key]}: ${CONTACT_PHONE_REFUSAL}`,\n }\n }\n fields[key] = phone\n }\n if ('jobTitle' in raw) fields.jobTitle = typed(raw['jobTitle'], 120)\n // Held to the label's shape here and judged against the org's list by the\n // handler, like the lead source (AGL-3515).\n if ('salutation' in raw) fields.salutation = normalizeCrmPicklistLabel(raw['salutation'])\n if ('firstName' in raw) fields.firstName = composeContactName(raw['firstName'], '')\n if ('lastName' in raw) fields.lastName = composeContactName('', raw['lastName'])\n if ('department' in raw) fields.department = typed(raw['department'], 120)\n if ('assistantName' in raw) fields.assistantName = typed(raw['assistantName'], 120)\n if ('birthdate' in raw) {\n const birthdate = normalizeContactBirthdate(raw['birthdate'])\n if (birthdate === null) return { ok: false, error: CONTACT_BIRTHDATE_REFUSAL }\n fields.birthdate = birthdate\n }\n if ('reportsToContactId' in raw) {\n const reportsTo =\n raw['reportsToContactId'] === null ? '' : typed(raw['reportsToContactId'], 200)\n if (reportsTo.includes('/')) {\n return { ok: false, error: 'The contact this person reports to could not be read.' }\n }\n fields.reportsToContactId = reportsTo || null\n }\n if ('doNotCall' in raw) {\n if (typeof raw['doNotCall'] !== 'boolean') {\n return { ok: false, error: 'Do not call must be true or false.' }\n }\n fields.doNotCall = raw['doNotCall']\n }\n // Held to the label's shape here; judged against the org's list by the\n // handler, which alone can read it (AGL-3298).\n if ('leadSource' in raw) fields.leadSource = normalizeCrmPicklistLabel(raw['leadSource'])\n for (const key of ['address', 'otherAddress'] as const) {\n if (!(key in raw)) continue\n const address = raw[key]\n fields[key] =\n address && typeof address === 'object' && !Array.isArray(address)\n ? normalizeAddress(address as AglynPostalAddress)\n : null\n }\n if ('notes' in raw) fields.notes = String(raw['notes'] ?? '').slice(0, CONTACT_NOTES_MAX)\n // One write may carry one transform per field, so the tags are replaced,\n // added to or taken from — never two of those at once.\n if (['tags', 'addTag', 'removeTag'].filter((key) => key in raw).length > 1) {\n return { ok: false, error: 'Send the tags, a tag to add or a tag to remove — one of them.' }\n }\n if ('tags' in raw) {\n if (!Array.isArray(raw['tags'])) return { ok: false, error: 'Tags must be a list.' }\n fields.tags = normalizeTags(raw['tags'])\n }\n for (const key of ['addTag', 'removeTag'] as const) {\n if (!(key in raw)) continue\n const tag = readBulkTag(raw[key])\n if (!tag) return { ok: false, error: 'Name the tag.' }\n fields[key] = tag\n }\n if ('campaignIds' in raw) {\n const ids = raw['campaignIds']\n if (!Array.isArray(ids)) return { ok: false, error: 'Campaigns must be a list.' }\n fields.campaignIds = containerMembershipValue(ids.map((id) => String(id ?? '')))\n }\n if ('ownerUid' in raw) fields.ownerUid = typed(raw['ownerUid'], 128)\n if ('companyId' in raw) {\n const companyId = raw['companyId'] === null ? null : typed(raw['companyId'], 128)\n if (companyId !== null && (!companyId || companyId.includes('/'))) {\n return { ok: false, error: 'The company could not be read.' }\n }\n fields.companyId = companyId\n }\n if ('companyName' in raw) fields.companyName = typed(raw['companyName'], 120)\n if ('custom' in raw) {\n const custom = raw['custom']\n if (!custom || typeof custom !== 'object' || Array.isArray(custom)) {\n return { ok: false, error: 'Custom fields must be values keyed by field key.' }\n }\n fields.custom = custom as Record<string, ContactCustomValue>\n }\n if ('mediaIds' in raw) {\n if (!Array.isArray(raw['mediaIds'])) {\n return { ok: false, error: 'Files must be a list of ids.' }\n }\n fields.mediaIds = normalizeCrmMediaIds(raw['mediaIds'])\n }\n if (!Object.keys(fields).length) return { ok: false, error: 'Name the fields to save.' }\n return { ok: true, fields }\n}\n\n/**\n * Salesforce's standard contact fields (AGL-3515) a create may carry beside\n * the identity — every one `upsertHostContact` writes into the capturing\n * facet. The reports-to is not one: it names another record, which only\n * this route checks the holder can see.\n */\nexport const CONTACT_CREATE_PROFILE_FIELDS = [\n 'salutation',\n 'firstName',\n 'lastName',\n 'department',\n 'mobilePhone',\n 'homePhone',\n 'otherPhone',\n 'fax',\n 'birthdate',\n 'assistantName',\n 'assistantPhone',\n 'otherAddress',\n 'doNotCall',\n] as const satisfies ReadonlyArray<keyof ContactUpdateFields>\n\n/**\n * Those fields of a create's body, read by the same rules an update reads\n * them with — or the sentence refusing one. A body naming none of them\n * reads as no fields.\n */\nexport function readContactCreateProfile(\n body: Record<string, unknown>,\n): { ok: true; fields: ContactUpdateFields } | { ok: false; error: string } {\n const given = Object.fromEntries(\n CONTACT_CREATE_PROFILE_FIELDS.filter((key) => body[key] !== undefined).map((key) => [\n key,\n body[key],\n ]),\n )\n if (!Object.keys(given).length) return { ok: true, fields: {} }\n return readContactUpdateFields(given)\n}\n\n/** The ids a body names, deduplicated — or `null` when any is unreadable or there are too many. */\nfunction readContactIds(value: unknown): string[] | null {\n if (!Array.isArray(value) || !value.length || value.length > CRM_CONTACT_UPDATE_MAX) {\n return null\n }\n const ids = value.map((id) => typed(id, 200))\n if (ids.some((id) => !id || id.includes('/'))) return null\n return [...new Set(ids)]\n}\n\n/**\n * Under a site, whether the caller's access reaches it. The rules could not\n * ask this of a facet write — they cannot see which holder a write is for —\n * and a collaborator on one site must not write another site's notes on a\n * person both of them hold.\n */\nfunction reachesSite(writer: Writer, hostId: string): boolean {\n return (\n writer.staff ||\n isOrgWideMember(writer.member) ||\n Boolean((writer.member?.hostAccess as Record<string, unknown> | undefined)?.[hostId])\n )\n}\n\nconst refused = (contactId: string, error: string): ContactUpdateOutcome => ({\n contactId,\n ok: false,\n error,\n})\n\n/** One contact's planned write, and the company counts it moves. */\nexport interface ContactWrite {\n contactId: string\n ref: FirebaseFirestore.DocumentReference\n update: Record<string, unknown>\n counts: ContactCompanyLinkPlan['counts']\n}\n\n/**\n * One contact's update for one holder, or the sentence refusing the row.\n * `update` is `null` when the document already says everything asked — a tag\n * the row already carries, a link already made.\n */\nexport function contactPatch(\n contact: Record<string, unknown>,\n groupId: string,\n fields: ContactUpdateFields,\n custom: Record<string, ContactCustomValue> | undefined,\n linkedCompanyName: string,\n):\n | { update: Record<string, unknown> | null; counts: ContactCompanyLinkPlan['counts'] }\n | { refused: string } {\n const path = (field: string) => contactFacetPath(groupId, field)\n /** A text field as stored: the value, or the field's absence. */\n const text = (value: string) => value || FieldValue.delete()\n const update: Record<string, unknown> = {}\n let counts: ContactCompanyLinkPlan['counts'] = []\n const stored = readContactFacet(contact, groupId)\n\n /*\n * THE NAME FOLLOWS THE FIRST AND LAST NAMES (AGL-3515). While the holder\n * keeps either, the facet's `name` is their composition, written whenever\n * either moves; a `name` sent beside them must agree, because a save that\n * stored one and showed the other would read as a save that kept both.\n * Both cleared leaves the name as it stands, unless one was sent.\n */\n const namesSent = fields.firstName !== undefined || fields.lastName !== undefined\n const composed = composeContactName(\n fields.firstName ?? stored.firstName,\n fields.lastName ?? stored.lastName,\n )\n if (composed && fields.name !== undefined && fields.name !== composed) {\n return { refused: CONTACT_COMPOSED_NAME_REFUSAL }\n }\n if (fields.firstName !== undefined) update[path('firstName')] = text(fields.firstName)\n if (fields.lastName !== undefined) update[path('lastName')] = text(fields.lastName)\n if (namesSent && composed) update[path('name')] = composed\n else if (fields.name !== undefined && !composed) update[path('name')] = text(fields.name)\n\n if (fields.salutation !== undefined) update[path('salutation')] = text(fields.salutation)\n if (fields.phone !== undefined) {\n update[path('phone')] = text(fields.phone)\n // The search echo — see `HostContact.phone`.\n update['phone'] = text(fields.phone)\n }\n for (const key of CONTACT_EXTRA_PHONE_FIELDS) {\n if (fields[key] !== undefined) update[path(key)] = text(fields[key] ?? '')\n }\n if (fields.jobTitle !== undefined) update[path('jobTitle')] = text(fields.jobTitle)\n if (fields.department !== undefined) update[path('department')] = text(fields.department)\n if (fields.birthdate !== undefined) update[path('birthdate')] = text(fields.birthdate)\n if (fields.assistantName !== undefined) {\n update[path('assistantName')] = text(fields.assistantName)\n }\n if (fields.reportsToContactId !== undefined) {\n update[path('reportsToContactId')] = text(fields.reportsToContactId ?? '')\n }\n if (fields.doNotCall !== undefined) {\n // Stored only as `true` — see `ContactFacet.doNotCall`.\n update[path('doNotCall')] = fields.doNotCall ? true : FieldValue.delete()\n }\n if (fields.leadSource !== undefined) update[path('leadSource')] = text(fields.leadSource)\n if (fields.address !== undefined) {\n update[path('address')] = fields.address ?? FieldValue.delete()\n }\n if (fields.otherAddress !== undefined) {\n update[path('otherAddress')] = fields.otherAddress ?? FieldValue.delete()\n }\n if (fields.notes !== undefined) update[path('notes')] = fields.notes\n if (fields.tags !== undefined) update[path('tags')] = fields.tags\n\n const held = (stored.tags ?? []).map((tag) =>\n String(tag).toLowerCase(),\n )\n if (fields.addTag && !held.includes(fields.addTag)) {\n // The record page's cap, which a selection must not slip past.\n if (held.length >= CONTACT_TAGS_MAX) {\n return { refused: `already has ${CONTACT_TAGS_MAX} tags` }\n }\n update[path('tags')] = FieldValue.arrayUnion(fields.addTag)\n }\n if (fields.removeTag && held.includes(fields.removeTag)) {\n update[path('tags')] = FieldValue.arrayRemove(fields.removeTag)\n }\n if (fields.campaignIds !== undefined) {\n update[contactContainerFieldPath(groupId, 'campaign')] = fields.campaignIds\n }\n if (fields.ownerUid !== undefined) update[path('ownerUid')] = text(fields.ownerUid)\n\n if (fields.companyId !== undefined) {\n const plan = planContactCompanyLink(\n readContactCompanyLink(contact, groupId),\n fields.companyId,\n )\n if (plan) {\n Object.assign(update, contactCompanyLinkFields(plan, groupId))\n counts = plan.counts\n if (fields.companyName === undefined) {\n // The label follows the link: the company's own name, or nothing.\n const label = plan.companyId ? linkedCompanyName : ''\n update[path('companyName')] = text(label)\n // The search echo — see `HostContact.companyName`.\n update['companyName'] = text(label)\n }\n }\n }\n if (fields.companyName !== undefined) {\n update[path('companyName')] = text(fields.companyName)\n update['companyName'] = text(fields.companyName)\n }\n for (const [key, value] of Object.entries(custom ?? {})) {\n update[path(`custom.${key}`)] = value\n }\n if (fields.mediaIds !== undefined) update[path(CRM_MEDIA_IDS_FIELD)] = fields.mediaIds\n\n if (!Object.keys(update).length) return { update: null, counts }\n update['updatedAt'] = FieldValue.serverTimestamp()\n return { update, counts }\n}\n\n/**\n * Every write in one batch, and — when the batch is refused — each on its\n * own, so the contact that could not be written is the only one reported.\n * Answers the ids that were not written.\n */\nasync function commitContactWrites(\n firestore: FirebaseFirestore.Firestore,\n writes: readonly ContactWrite[],\n): Promise<Set<string>> {\n const failed = new Set<string>()\n if (!writes.length) return failed\n const batch = firestore.batch()\n for (const write of writes) batch.update(write.ref, write.update)\n let committed = false\n try {\n await batch.commit()\n committed = true\n } catch {\n // A batch names no document; the pass below finds the one it was.\n }\n if (!committed) {\n for (const write of writes) {\n try {\n await write.ref.update(write.update)\n } catch (error) {\n console.error('[crm] contact-update could not write a contact', write.contactId, error)\n failed.add(write.contactId)\n }\n }\n }\n // The owner, tags, company and custom values are what the Contacts list\n // filters by through `facetKeys` (AGL-3321): restamped from each row.\n await restampCrmListFieldsOf(\n writes.filter((write) => !failed.has(write.contactId)).map((write) => write.ref),\n 'contacts',\n )\n return failed\n}\n\n/** Each company's contacts count moved once, by the sum of what the written links moved. */\nexport async function settleCompanyCounts(\n companies: FirebaseFirestore.CollectionReference,\n writes: readonly ContactWrite[],\n): Promise<void> {\n const deltas = new Map<string, number>()\n for (const write of writes) {\n for (const count of write.counts) {\n deltas.set(count.companyId, (deltas.get(count.companyId) ?? 0) + count.delta)\n }\n }\n await Promise.all(\n [...deltas]\n .filter(([, delta]) => delta !== 0)\n .map(([companyId, delta]) =>\n companies\n .doc(companyId)\n .update(companyContactsCountFields(delta))\n .catch((error: unknown) => {\n console.error('[crm] company contacts count could not move', companyId, delta, error)\n }),\n ),\n )\n}\n\nexport const crmContactUpdateHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n res.setHeader('Allow', 'POST')\n res.status(405).json({ error: 'Method not allowed' })\n return\n }\n const body = (req.body ?? {}) as Record<string, unknown>\n const scope = readCrmRouteScope(body)\n if (!scope) {\n res.status(400).json({ error: 'Missing hostId' })\n return\n }\n const contactIds = readContactIds(body['contactIds'])\n if (!contactIds) {\n res.status(400).json({ error: `Name between 1 and ${CRM_CONTACT_UPDATE_MAX} contacts.` })\n return\n }\n const read = readContactUpdateFields(body['set'])\n if (read.ok === false) {\n res.status(400).json({ error: read.error })\n return\n }\n const { fields } = read\n\n try {\n const writer = await authorizeCrmWriter(req, scope, { suiteAct: null })\n if (writer.ok === false) {\n res.status(writer.status).json(writer.body)\n return\n }\n if (scope.level === 'site' && !reachesSite(writer, scope.hostId)) {\n res.status(403).json({ error: 'Your access to this organization does not reach this site.' })\n return\n }\n // The plan after the person, and before any contact is read (AGL-2787).\n const suite = crmSuiteRefusal(writer.org, contactUpdateSuiteAct(fields))\n if (suite) {\n res.status(suite.status).json(suite.body)\n return\n }\n\n const firestore = firebaseAdmin.app().firestore()\n const orgRef = firestore.collection('orgs').doc(writer.orgId)\n const siteGroup =\n scope.level === 'site' ? consentGroupForHost(writer.org, scope.hostId) : null\n const siteTokens = siteGroup ? crmReadTokens(siteGroup) : null\n\n let custom: Record<string, ContactCustomValue> | undefined\n if (fields.custom) {\n const definitions = await orgRef\n .collection(CRM_COLLECTIONS.contactFields)\n .limit(CONTACT_FIELDS_MAX_PER_ORG)\n .get()\n const judged = readCrmCustomInput(\n fields.custom,\n definitions.docs.map((doc) => doc.data() as ContactFieldDefinition),\n 'contact',\n )\n if ('errors' in judged) {\n res.status(400).json({\n error: Object.values(judged.errors)[0] ?? 'A custom value could not be saved.',\n })\n return\n }\n custom = judged.values\n }\n\n /*\n * THE ORG'S LEAD SOURCES (AGL-3298), read only when the body names one.\n * Judged per contact below, because a contact keeps the value it\n * already holds even when the list has since deactivated it.\n */\n const leadSources = fields.leadSource\n ? effectiveCrmLeadSourcePicklist(\n (\n await orgRef.collection(CRM_COLLECTIONS.picklists).doc(CRM_LEAD_SOURCE_PICKLIST).get()\n ).data(),\n )\n : null\n\n // The org's salutations (AGL-3515), judged per contact like the lead source.\n const salutations = fields.salutation\n ? await readCrmPicklist(firestore, writer.orgId, CRM_SALUTATION_PICKLIST)\n : null\n\n if (fields.ownerUid) {\n const owner = await resolveOrgMembership(fields.ownerUid, writer.orgId).catch(() => null)\n if (!owner?.member) {\n res.status(400).json({ error: 'The owner is not a member of this organization.' })\n return\n }\n }\n\n // The company a link names has to exist and be visible to the caller and\n // the site, or a person could be filed under a record nobody here can open.\n let linkedCompanyName = ''\n if (fields.companyId) {\n const company = await orgRef\n .collection(CRM_COLLECTIONS.companies)\n .doc(fields.companyId)\n .get()\n const tokens = company.exists ? (company.get('visibleTo') as string[] | undefined) : undefined\n const visible =\n company.exists &&\n canReach(writer, tokens) &&\n (!siteTokens || visibleToTokens(tokens, siteTokens))\n if (!visible) {\n res.status(404).json({ error: 'Unknown company' })\n return\n }\n linkedCompanyName = typed(company.get('name'), 120)\n }\n\n const contacts = orgRef.collection('contacts')\n const readContact = cachedContactReader(contacts)\n\n // The manager a reports-to names has to be a contact the caller and the\n // site can see, for the company link's reason (AGL-3515).\n if (fields.reportsToContactId) {\n const manager = await contacts.doc(fields.reportsToContactId).get()\n const tokens = manager.exists ? (manager.get('visibleTo') as string[] | undefined) : undefined\n const visible =\n manager.exists &&\n canReach(writer, tokens) &&\n (!siteTokens || visibleToTokens(tokens, siteTokens))\n if (!visible) {\n res.status(404).json({ error: 'Unknown contact to report to' })\n return\n }\n }\n\n const snapshots = await firestore.getAll(...contactIds.map((id) => contacts.doc(id)))\n const outcomes = new Map<string, ContactUpdateOutcome>()\n const writes: ContactWrite[] = []\n const rows: Array<{\n contactId: string\n snapshot: FirebaseFirestore.DocumentSnapshot\n data: Record<string, unknown>\n groupId: string\n }> = []\n snapshots.forEach((snapshot, index) => {\n const contactId = contactIds[index]\n if (!snapshot.exists) {\n outcomes.set(contactId, refused(contactId, 'That contact no longer exists.'))\n return\n }\n const data = (snapshot.data() ?? {}) as Record<string, unknown>\n const visibleTo = data['visibleTo'] as string[] | undefined\n if (!canReach(writer, visibleTo)) {\n outcomes.set(contactId, refused(contactId, 'That contact is not visible to you.'))\n return\n }\n if (siteTokens && !visibleToTokens(visibleTo, siteTokens)) {\n outcomes.set(contactId, refused(contactId, 'That contact is not visible to this site.'))\n return\n }\n const group = siteGroup ?? contactPrimaryGroup(data, writer.org)\n if (!group.groupId) {\n outcomes.set(contactId, refused(contactId, NO_HOLDER_REFUSAL))\n return\n }\n rows.push({ contactId, snapshot, data, groupId: group.groupId })\n })\n\n for (const { contactId, snapshot, data, groupId } of rows) {\n if (fields.reportsToContactId) {\n if (fields.reportsToContactId === contactId) {\n outcomes.set(contactId, refused(contactId, CONTACT_REPORTS_TO_SELF_REFUSAL))\n continue\n }\n if (await contactReportsToLoops(readContact, contactId, fields.reportsToContactId, groupId)) {\n outcomes.set(contactId, refused(contactId, CONTACT_REPORTS_TO_LOOP_REFUSAL))\n continue\n }\n }\n let rowFields = fields\n if (salutations && fields.salutation) {\n const judged = judgeCrmPicklistValue(\n CRM_SALUTATION_PICKLIST,\n salutations,\n fields.salutation,\n readContactFacet(data, groupId).salutation,\n )\n if (judged.ok === false) {\n outcomes.set(contactId, refused(contactId, judged.error))\n continue\n }\n rowFields = { ...rowFields, salutation: judged.value ?? '' }\n }\n if (leadSources && fields.leadSource) {\n const judged = judgeCrmLeadSource(\n leadSources,\n fields.leadSource,\n readContactFacet(data, groupId).leadSource,\n )\n if (judged.ok === false) {\n outcomes.set(contactId, refused(contactId, judged.error))\n continue\n }\n rowFields = { ...rowFields, leadSource: judged.value ?? '' }\n }\n const patch = contactPatch(data, groupId, rowFields, custom, linkedCompanyName)\n if ('refused' in patch) {\n outcomes.set(contactId, refused(contactId, patch.refused))\n continue\n }\n outcomes.set(contactId, { contactId, ok: true })\n if (patch.update) {\n writes.push({ contactId, ref: snapshot.ref, update: patch.update, counts: patch.counts })\n }\n }\n\n const failed = await commitContactWrites(firestore, writes)\n for (const contactId of failed) {\n outcomes.set(contactId, refused(contactId, 'The contact could not be saved.'))\n }\n await settleCompanyCounts(\n orgRef.collection(CRM_COLLECTIONS.companies),\n writes.filter((write) => !failed.has(write.contactId)),\n )\n\n const answer: ContactUpdateResponse = {\n ok: true,\n results: contactIds.map(\n (contactId) => outcomes.get(contactId) ?? refused(contactId, 'The contact could not be saved.'),\n ),\n }\n res.status(200).json(answer)\n } catch (error) {\n console.error('[crm] contact-update failed', error)\n res.status(500).json({ error: 'The contact could not be saved.' })\n }\n}\n"],"names":["composeContactName","CONTACT_BIRTHDATE_REFUSAL","CONTACT_EXTRA_PHONE_FIELDS","CONTACT_PHONE_FIELD_LABELS","consentGroupForHost","CONTACT_FIELDS_MAX_PER_ORG","containerMembershipValue","contactContainerFieldPath","contactFacetPath","CRM_COLLECTIONS","CRM_LEAD_SOURCE_PICKLIST","CRM_MEDIA_IDS_FIELD","CRM_SALUTATION_PICKLIST","crmReadTokens","effectiveCrmLeadSourcePicklist","isOrgWideMember","judgeCrmLeadSource","judgeCrmPicklistValue","normalizeAddress","normalizeContactBirthdate","normalizeCrmMediaIds","normalizeCrmPicklistLabel","normalizePhone","planContactCompanyLink","readContactCompanyLink","readContactFacet","readCrmCustomInput","visibleToTokens","companyContactsCountFields","contactCompanyLinkFields","firebaseAdmin","resolveOrgMembership","restampCrmListFieldsOf","FieldValue","CONTACT_UPDATE_FIELDS","CRM_CONTACT_UPDATE_MAX","CONTACT_NOTES_MAX","CONTACT_PHONE_REFUSAL","CONTACT_TAGS_MAX","normalizeTags","readBulkTag","typed","readCrmRouteScope","crmSuiteRefusal","authorizeCrmWriter","canReach","contactPrimaryGroup","cachedContactReader","CONTACT_REPORTS_TO_LOOP_REFUSAL","CONTACT_REPORTS_TO_SELF_REFUSAL","contactReportsToLoops","readCrmPicklist","SUITE_ACTS","name","salutation","firstName","lastName","phone","mobilePhone","homePhone","otherPhone","fax","jobTitle","department","birthdate","assistantName","assistantPhone","reportsToContactId","leadSource","address","otherAddress","doNotCall","notes","tags","addTag","removeTag","campaignIds","ownerUid","companyId","companyName","custom","mediaIds","CONTACT_COMPOSED_NAME_REFUSAL","NO_HOLDER_REFUSAL","contactUpdateSuiteAct","fields","field","find","key","undefined","readContactUpdateFields","input","raw","Array","isArray","ok","error","unknownKey","Object","keys","includes","text","reportsTo","String","slice","filter","length","tag","ids","map","id","CONTACT_CREATE_PROFILE_FIELDS","readContactCreateProfile","body","given","fromEntries","readContactIds","value","some","Set","reachesSite","writer","hostId","staff","member","Boolean","hostAccess","refused","contactId","contactPatch","contact","groupId","linkedCompanyName","stored","path","delete","update","counts","namesSent","composed","held","toLowerCase","arrayUnion","arrayRemove","plan","assign","label","entries","serverTimestamp","commitContactWrites","firestore","writes","failed","batch","write","ref","committed","commit","console","add","has","settleCompanyCounts","companies","deltas","Map","count","set","get","delta","Promise","all","doc","catch","crmContactUpdateHandler","req","res","method","setHeader","status","json","scope","contactIds","read","suiteAct","level","suite","org","app","orgRef","collection","orgId","siteGroup","siteTokens","definitions","contactFields","limit","judged","docs","data","values","errors","leadSources","picklists","salutations","owner","company","tokens","exists","visible","contacts","readContact","manager","snapshots","getAll","outcomes","rows","forEach","snapshot","index","visibleTo","group","push","rowFields","patch","answer","results"],"mappings":"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"}
@@ -73,8 +73,27 @@
73
73
  * nobody chose an owner for — and no rule assigned — stays unowned, which
74
74
  * the console renders honestly as unassigned. Either way the caller has
75
75
  * already been authorized by its door; nothing here asks again.
76
+ *
77
+ * ## The deal carries the lead's lead source (AGL-3516)
78
+ *
79
+ * As Salesforce's conversion does, the deal opened here takes the lead's
80
+ * `leadSource` — a label of the same picklist, already judged when the lead
81
+ * was written — and its Type as the door judged it, or the org's default
82
+ * Type when the converter named none. Its forecast category is its stage's.
83
+ *
84
+ * ## Every standard field lands where Salesforce's conversion puts it (AGL-3513)
85
+ *
86
+ * The contact's facet takes the salutation, the name's parts, the mobile,
87
+ * the fax and Do not call. The company takes the account fields — industry,
88
+ * rating, revenue and its currency, head count, website, phone, fax, the
89
+ * address as its billing address, and the lead source as its Account
90
+ * Source — whole when the conversion creates it, and only into its BLANK
91
+ * fields when it links one the org already holds: a lead is one person's
92
+ * account of the business, and the account record may know better. The
93
+ * deal's Primary Campaign Source is the lead's most recent campaign — see
94
+ * {@link leadLatestCampaignId}.
76
95
  */
77
- import { type CrmDealStage, type CrmPipeline } from '@aglyn/aglyn/server';
96
+ import { type CrmDealStage, type CrmPipeline, type CrmPicklist, type CrmPicklistId } from '@aglyn/aglyn/server';
78
97
  /** Who is converting, as far as the writes need to know. */
79
98
  export interface LeadConvertActor {
80
99
  /**
@@ -128,6 +147,11 @@ export interface ConvertHostLeadInput {
128
147
  currency: string;
129
148
  /** A stage of the default pipeline; its first open stage when absent. */
130
149
  stageId?: string;
150
+ /**
151
+ * The deal's Type, as the door judged it against the org's
152
+ * `opportunityType` list (AGL-3516); absent takes the list's default.
153
+ */
154
+ type?: string;
131
155
  } | null;
132
156
  }
133
157
  /**
@@ -182,6 +206,27 @@ export type ConvertHostLeadResult = {
182
206
  * in the lead.
183
207
  */
184
208
  export declare function stageForNewDeal(pipeline: Pick<CrmPipeline, 'stages'>, requestedStageId: string | undefined): CrmDealStage | null;
209
+ /**
210
+ * The lead's MOST RECENT campaign, which becomes the deal's Primary
211
+ * Campaign Source as Salesforce's conversion makes it. A lead keeps its
212
+ * campaigns as one `campaignIds` array that every writer extends with
213
+ * `arrayUnion`, which appends — so the last id is the campaign the lead was
214
+ * filed under last. `undefined` for a lead filed under none.
215
+ */
216
+ export declare function leadLatestCampaignId(lead: Readonly<Record<string, unknown>>): string | undefined;
217
+ /**
218
+ * The account fields a lead hands its company (AGL-3513), normalized and
219
+ * judged as every company door judges them: the lead's industry and rating
220
+ * against the org's lists, its lead source as the Account Source, and its
221
+ * revenue, currency, head count, fax, website, phone and address (as the
222
+ * billing address). With `current` — a company the org already holds —
223
+ * only the fields it leaves blank are answered, and the picklists it does
224
+ * not name keep no default; without it, the company is being created and
225
+ * each picklist the lead leaves empty starts from its list's default. A
226
+ * value a list no longer holds is left behind rather than refusing the
227
+ * conversion.
228
+ */
229
+ export declare function leadCompanyFields(lead: Readonly<Record<string, unknown>>, lists: Readonly<Partial<Record<CrmPicklistId, CrmPicklist>>>, current: Readonly<Record<string, unknown>> | null): Record<string, unknown>;
185
230
  /**
186
231
  * Convert one lead. Throws only on infrastructure failure; every refusal
187
232
  * the data can produce is a `ConvertHostLeadResult`.
@@ -73,12 +73,33 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
73
73
  * nobody chose an owner for — and no rule assigned — stays unowned, which
74
74
  * the console renders honestly as unassigned. Either way the caller has
75
75
  * already been authorized by its door; nothing here asks again.
76
- */ import { consentGroupDisclosureKey, consentGroupScope, CRM_COLLECTIONS, crmScopeTokens, DEFAULT_DEAL_STAGES, grantedUnderConsentGroup, nameSearchFields, contactFacetPath, normalizeContactEmail, ORG_SCOPE_TOKEN, readContactFacet, crmNewRecordListFields, readMarketingBasis, soloConsentGroup } from "@aglyn/aglyn/server";
76
+ *
77
+ * ## The deal carries the lead's lead source (AGL-3516)
78
+ *
79
+ * As Salesforce's conversion does, the deal opened here takes the lead's
80
+ * `leadSource` — a label of the same picklist, already judged when the lead
81
+ * was written — and its Type as the door judged it, or the org's default
82
+ * Type when the converter named none. Its forecast category is its stage's.
83
+ *
84
+ * ## Every standard field lands where Salesforce's conversion puts it (AGL-3513)
85
+ *
86
+ * The contact's facet takes the salutation, the name's parts, the mobile,
87
+ * the fax and Do not call. The company takes the account fields — industry,
88
+ * rating, revenue and its currency, head count, website, phone, fax, the
89
+ * address as its billing address, and the lead source as its Account
90
+ * Source — whole when the conversion creates it, and only into its BLANK
91
+ * fields when it links one the org already holds: a lead is one person's
92
+ * account of the business, and the account record may know better. The
93
+ * deal's Primary Campaign Source is the lead's most recent campaign — see
94
+ * {@link leadLatestCampaignId}.
95
+ */ import { consentGroupDisclosureKey, consentGroupScope, CRM_COLLECTIONS, CRM_LEAD_STATUS_PICKLIST, crmLeadStatusLabelFor, CRM_COMPANY_PICKLIST_FIELDS, crmPicklistDefaultLabel, judgeCrmCompanyPicklists, readCrmCompanyAccountFields, crmScopeTokens, dealStageForecastCategory, DEFAULT_DEAL_STAGES, grantedUnderConsentGroup, nameSearchFields, contactFacetPath, normalizeContactEmail, ORG_SCOPE_TOKEN, readContactFacet, crmNewRecordListFields, readMarketingBasis, soloConsentGroup } from "@aglyn/aglyn/server";
77
96
  import { consentGroupForSite, crmRecordsQuotaForOrg, findContactByEmail, logHostActivity, orgDataCollectionForHost, restampCrmListFieldsAt, writeContactCompanyLink } from "@aglyn/tenant-data-admin";
78
97
  import { FieldPath, FieldValue } from "firebase-admin/firestore";
79
- import { assignOwnerForCapture, notifyRecordAssigned } from "@aglyn/tenant-runtime/assign-contact-owner";
80
- import { captureHostContact } from "@aglyn/tenant-runtime/capture-host-contact";
98
+ import { assignOwnerForCapture, notifyRecordAssigned } from "./assign-contact-owner.js";
99
+ import { captureHostContact } from "./capture-host-contact.js";
100
+ import { readCrmPicklist } from "./read-picklist.js";
81
101
  import { handOffLeadRecords } from "@aglyn/tenant-runtime/hand-off-lead";
102
+ import { readContainerIds } from "@aglyn/aglyn/app-utils/container-membership";
82
103
  /**
83
104
  * The stage a new deal opens in.
84
105
  *
@@ -98,6 +119,96 @@ import { handOffLeadRecords } from "@aglyn/tenant-runtime/hand-off-lead";
98
119
  const requested = requestedStageId ? stages.find((stage)=>stage.id === requestedStageId) : undefined;
99
120
  return (_ref = requested != null ? requested : stages.find((stage)=>stage.kind === 'open')) != null ? _ref : stages[0];
100
121
  }
122
+ /**
123
+ * The lead's MOST RECENT campaign, which becomes the deal's Primary
124
+ * Campaign Source as Salesforce's conversion makes it. A lead keeps its
125
+ * campaigns as one `campaignIds` array that every writer extends with
126
+ * `arrayUnion`, which appends — so the last id is the campaign the lead was
127
+ * filed under last. `undefined` for a lead filed under none.
128
+ */ export function leadLatestCampaignId(lead) {
129
+ const ids = readContainerIds(lead, 'campaign');
130
+ return ids.length ? ids[ids.length - 1] : undefined;
131
+ }
132
+ /** Whether a stored field holds nothing a fill would overwrite. */ const blankField = (value)=>value === undefined || value === null || typeof value === 'string' && !value.trim() || typeof value === 'object' && !Array.isArray(value) && !Object.keys(value).length;
133
+ /**
134
+ * The account fields a lead hands its company (AGL-3513), normalized and
135
+ * judged as every company door judges them: the lead's industry and rating
136
+ * against the org's lists, its lead source as the Account Source, and its
137
+ * revenue, currency, head count, fax, website, phone and address (as the
138
+ * billing address). With `current` — a company the org already holds —
139
+ * only the fields it leaves blank are answered, and the picklists it does
140
+ * not name keep no default; without it, the company is being created and
141
+ * each picklist the lead leaves empty starts from its list's default. A
142
+ * value a list no longer holds is left behind rather than refusing the
143
+ * conversion.
144
+ */ export function leadCompanyFields(lead, lists, current) {
145
+ const fill = (field)=>!current || blankField(current[field]);
146
+ const offered = (value)=>blankField(value) ? undefined : value;
147
+ const fields = {};
148
+ const requested = {};
149
+ const picklistSource = {
150
+ industry: lead['industry'],
151
+ rating: lead['rating'],
152
+ accountSource: lead['leadSource']
153
+ };
154
+ for (const { field } of CRM_COMPANY_PICKLIST_FIELDS){
155
+ const value = offered(picklistSource[field]);
156
+ if (value !== undefined && fill(field)) requested[field] = value;
157
+ }
158
+ const picklists = judgeCrmCompanyPicklists(lists, requested, {
159
+ current: current != null ? current : {},
160
+ created: !current
161
+ });
162
+ for (const [field, value] of Object.entries(picklists.values)){
163
+ if (value && !picklists.errors[field]) fields[field] = value;
164
+ }
165
+ const account = {};
166
+ for (const field of [
167
+ 'annualRevenueCents',
168
+ 'numberOfEmployees',
169
+ 'fax'
170
+ ]){
171
+ const value = offered(lead[field]);
172
+ if (value !== undefined && fill(field)) account[field] = value;
173
+ }
174
+ // The revenue's currency travels with the revenue it describes.
175
+ if (account['annualRevenueCents'] !== undefined && offered(lead['currency']) !== undefined) {
176
+ account['currency'] = lead['currency'];
177
+ }
178
+ const read = readCrmCompanyAccountFields(account);
179
+ for (const [field, value] of Object.entries(read.values)){
180
+ if (value !== null && value !== undefined && !read.errors[field]) fields[field] = value;
181
+ }
182
+ // Already in the company's shapes: the lead normalized them as a company does.
183
+ for (const [field, from] of [
184
+ [
185
+ 'website',
186
+ 'website'
187
+ ],
188
+ [
189
+ 'phone',
190
+ 'phone'
191
+ ],
192
+ [
193
+ 'address',
194
+ 'address'
195
+ ]
196
+ ]){
197
+ const value = offered(lead[from]);
198
+ if (value !== undefined && fill(field)) fields[field] = value;
199
+ }
200
+ return fields;
201
+ }
202
+ /** The org's lists behind every company picklist field, for {@link leadCompanyFields}. */ async function readCompanyPicklists(firestore, orgId) {
203
+ const ids = [
204
+ ...new Set(CRM_COMPANY_PICKLIST_FIELDS.map((entry)=>entry.picklistId))
205
+ ];
206
+ const lists = await Promise.all(ids.map((id)=>readCrmPicklist(firestore, orgId, id)));
207
+ return Object.fromEntries(ids.map((id, at)=>[
208
+ id,
209
+ lists[at]
210
+ ]));
211
+ }
101
212
  /** A stage's kind as the deal's denormalized status. */ function dealStatusForStage(stage) {
102
213
  return stage.kind === 'won' ? 'won' : stage.kind === 'lost' ? 'lost' : 'open';
103
214
  }
@@ -224,6 +335,17 @@ import { handOffLeadRecords } from "@aglyn/tenant-runtime/hand-off-lead";
224
335
  address: lead.address
225
336
  } : {}, typeof lead.company === 'string' && lead.company ? {
226
337
  companyName: lead.company
338
+ } : {}, Object.fromEntries([
339
+ 'salutation',
340
+ 'firstName',
341
+ 'lastName',
342
+ 'mobilePhone',
343
+ 'fax'
344
+ ].filter((key)=>typeof lead[key] === 'string' && lead[key]).map((key)=>[
345
+ key,
346
+ lead[key]
347
+ ])), lead.doNotCall === true ? {
348
+ doNotCall: true
227
349
  } : {})
228
350
  }));
229
351
  /*
@@ -312,6 +434,7 @@ import { handOffLeadRecords } from "@aglyn/tenant-runtime/hand-off-lead";
312
434
  /*==========================================
313
435
  * 2. THE COMPANY — linked, found by domain, or created.
314
436
  *=========================================*/ let companyId;
437
+ /** The company the conversion found rather than made — its blanks are filled below. */ let existingCompany;
315
438
  if (requestedCompanyId) {
316
439
  const companySnapshot = await orgRef.collection(CRM_COLLECTIONS.companies).doc(requestedCompanyId).get();
317
440
  if (!companySnapshot.exists) return {
@@ -319,6 +442,7 @@ import { handOffLeadRecords } from "@aglyn/tenant-runtime/hand-off-lead";
319
442
  reason: 'unknown-company'
320
443
  };
321
444
  companyId = requestedCompanyId;
445
+ existingCompany = companySnapshot;
322
446
  } else if (createCompany) {
323
447
  /*
324
448
  * FIND BEFORE CREATE. The NAME first (AGL-3233): a lead names its
@@ -328,25 +452,23 @@ import { handOffLeadRecords } from "@aglyn/tenant-runtime/hand-off-lead";
328
452
  * document for `acme.com` would split the account the key exists to
329
453
  * join. Only a company the caller can see counts as found — see
330
454
  * `readableTokens`.
331
- */ const visibleAmong = (snapshots)=>{
332
- var _snapshots_find;
333
- return (_snapshots_find = snapshots.find((snapshot)=>{
455
+ */ const visibleAmong = (snapshots)=>snapshots.find((snapshot)=>{
334
456
  const tokens = snapshot.get('visibleTo');
335
457
  return Array.isArray(tokens) && tokens.some((token)=>readableTokens.has(String(token)));
336
- })) == null ? void 0 : _snapshots_find.id;
337
- };
458
+ });
338
459
  const byName = await orgRef.collection(CRM_COLLECTIONS.companies).where('nameLower', '==', createCompany.name.toLowerCase()).limit(5).get();
339
- companyId = visibleAmong(byName.docs);
340
- if (!companyId && createCompany.domain) {
460
+ existingCompany = visibleAmong(byName.docs);
461
+ if (!existingCompany && createCompany.domain) {
341
462
  const byDomain = await orgRef.collection(CRM_COLLECTIONS.companies).where('domain', '==', createCompany.domain).limit(5).get();
342
- companyId = visibleAmong(byDomain.docs);
463
+ existingCompany = visibleAmong(byDomain.docs);
343
464
  }
465
+ companyId = existingCompany == null ? void 0 : existingCompany.id;
344
466
  if (!companyId) {
345
467
  if (await bandFull()) return {
346
468
  ok: false,
347
469
  reason: 'band-full'
348
470
  };
349
- const company = _extends({}, nameSearchFields(createCompany.name), createCompany.domain ? {
471
+ const company = _extends({}, leadCompanyFields(lead, await readCompanyPicklists(firestore, orgId), null), nameSearchFields(createCompany.name), createCompany.domain ? {
350
472
  domain: createCompany.domain
351
473
  } : {}, ownerUid ? {
352
474
  ownerUid
@@ -361,6 +483,20 @@ import { handOffLeadRecords } from "@aglyn/tenant-runtime/hand-off-lead";
361
483
  companyId = created.id;
362
484
  }
363
485
  }
486
+ if (companyId && existingCompany) {
487
+ var _existingCompany_data;
488
+ /*
489
+ * A company the org already held takes the lead's account fields only
490
+ * where it has none (AGL-3513) — and is restamped, because its
491
+ * industry, rating and account source are what its list filters by.
492
+ */ const filled = leadCompanyFields(lead, await readCompanyPicklists(firestore, orgId), (_existingCompany_data = existingCompany.data()) != null ? _existingCompany_data : {});
493
+ if (Object.keys(filled).length) {
494
+ await existingCompany.ref.update(_extends({}, filled, {
495
+ updatedAt: FieldValue.serverTimestamp()
496
+ }));
497
+ await restampCrmListFieldsAt(existingCompany.ref, 'companies');
498
+ }
499
+ }
364
500
  if (companyId) {
365
501
  /*
366
502
  * The one link writer (AGL-2613): the holder's facet field the contact
@@ -381,6 +517,7 @@ import { handOffLeadRecords } from "@aglyn/tenant-runtime/hand-off-lead";
381
517
  * 3. THE DEAL — in the default pipeline, seeded if the org has none.
382
518
  *=========================================*/ let dealId;
383
519
  if (deal) {
520
+ var _ref, _deal_type;
384
521
  const pipelinesRef = orgRef.collection(CRM_COLLECTIONS.pipelines);
385
522
  let pipelineId;
386
523
  let pipeline;
@@ -429,6 +566,10 @@ import { handOffLeadRecords } from "@aglyn/tenant-runtime/hand-off-lead";
429
566
  ok: false,
430
567
  reason: 'band-full'
431
568
  };
569
+ const type = (_ref = (_deal_type = deal.type) != null ? _deal_type : crmPicklistDefaultLabel(await readCrmPicklist(firestore, orgId, 'opportunityType'))) != null ? _ref : undefined;
570
+ const leadSource = typeof lead.leadSource === 'string' ? lead.leadSource.trim() : '';
571
+ // The Primary Campaign Source: the lead's most recent campaign (AGL-3513).
572
+ const campaignId = leadLatestCampaignId(lead);
432
573
  const dealRecord = _extends({
433
574
  title: deal.title,
434
575
  titleLower: deal.title.toLowerCase(),
@@ -439,11 +580,26 @@ import { handOffLeadRecords } from "@aglyn/tenant-runtime/hand-off-lead";
439
580
  amountCents: deal.amountCents
440
581
  } : {}, {
441
582
  currency: deal.currency,
442
- stageChangedAtMs: now
443
- }, ownerUid ? {
583
+ stageChangedAtMs: now,
584
+ forecastCategory: dealStageForecastCategory(stage)
585
+ }, type ? {
586
+ type
587
+ } : {}, leadSource ? {
588
+ leadSource
589
+ } : {}, campaignId ? {
590
+ campaignId
591
+ } : {}, ownerUid ? {
444
592
  ownerUid
445
593
  } : {}, {
446
- contactId
594
+ contactId,
595
+ // The converted person is the deal's Primary contact role, with no
596
+ // role yet, as Salesforce's conversion makes them (AGL-3521).
597
+ contactRoles: [
598
+ {
599
+ contactId,
600
+ primary: true
601
+ }
602
+ ]
447
603
  }, companyId ? {
448
604
  companyId
449
605
  } : {}, {
@@ -460,6 +616,8 @@ import { handOffLeadRecords } from "@aglyn/tenant-runtime/hand-off-lead";
460
616
  * 4. THE LEAD — stamped last, once everything it names exists.
461
617
  *=========================================*/ const stamp = _extends({
462
618
  status: 'qualified',
619
+ // The org's label for Qualified, beside the meaning (AGL-3512).
620
+ statusLabel: crmLeadStatusLabelFor(await readCrmPicklist(firestore, orgId, CRM_LEAD_STATUS_PICKLIST), 'qualified'),
463
621
  convertedContactId: contactId,
464
622
  convertedAtMs: now
465
623
  }, ownerUid ? {