@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
@@ -44,15 +44,17 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
44
44
  * to a sibling site holds the key but not the host. Then the CRM suite gate,
45
45
  * because a lead entered by hand is suite work: the capture doors that fill
46
46
  * a Free workspace's leads do not come through here.
47
- */ import { checkVisitorRecordCeiling, CONTACT_FIELDS_MAX_PER_ORG, CRM_COLLECTIONS, LEADS_MAX_PER_HOST, normalizeContactEmail, normalizeCrmLeadProfile, personKey, readCrmCustomInput } from "@aglyn/aglyn/server";
47
+ */ import { checkVisitorRecordCeiling, CONTACT_FIELDS_MAX_PER_ORG, CRM_COLLECTIONS, crmLeadComposedName, CRM_LEAD_STATUS_PICKLIST, LEADS_MAX_PER_HOST, normalizeContactEmail, normalizeCrmLeadProfile, personKey, readCrmCustomInput, resolveCrmLeadStatusWrite } from "@aglyn/aglyn/server";
48
48
  import { addHostLead, firebaseAdmin, getOrgForHost, logHostActivity, orgLeadsForHost, restampCrmListFieldsAt, readLeadForHost, scopedToHost } from "@aglyn/tenant-data-admin";
49
49
  import { resolveOrgPermissions } from "@aglyn/tenant-runtime/org-permissions";
50
50
  import { normalizeContainerIds, readContainerIds } from "@aglyn/aglyn/app-utils/container-membership";
51
51
  import { readOrgContainers } from "@aglyn/tenant-data-admin/server/org-containers";
52
52
  import { FieldValue } from "firebase-admin/firestore";
53
53
  import { fileCampaignFilingActivities } from "./campaign-filing-activity.js";
54
+ import { judgeLeadPicklistPatch, readLeadPicklists } from "./lead-picklists.js";
54
55
  import { readLeadSourcePicklist, resolveLeadSourceWrite } from "./lead-source-picklist.js";
55
56
  import { holdsDataManage } from "./org-caller.js";
57
+ import { readCrmPicklist } from "./read-picklist.js";
56
58
  import { crmSuiteRefusal } from "./suite-gate.js";
57
59
  /** The statuses a lead may be entered in — everything but the converted state. */ const CREATE_STATUSES = [
58
60
  'new',
@@ -140,17 +142,11 @@ export const leadCreateHandler = async (req, res)=>{
140
142
  return;
141
143
  }
142
144
  const rawStatus = String((_body_status = body.status) != null ? _body_status : '').trim();
143
- if (rawStatus && !CREATE_STATUSES.includes(rawStatus)) {
144
- res.status(400).json({
145
- error: 'A new lead is New or Working.'
146
- });
147
- return;
148
- }
149
145
  const name = String((_body_name = body.name) != null ? _body_name : '').trim().replace(/\s+/g, ' ').slice(0, NAME_MAX);
150
146
  const ownerUid = String((_body_ownerUid = body.ownerUid) != null ? _body_ownerUid : '').trim().slice(0, 128);
151
147
  const notes = String((_body_notes = body.notes) != null ? _body_notes : '').trim().slice(0, NOTES_MAX);
152
148
  try {
153
- var _ref, _decoded_name, _decoded_email;
149
+ var _ref, _ref1, _ref2, _ref3, _decoded_name, _decoded_email;
154
150
  const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
155
151
  const staff = decoded['staff'] === true;
156
152
  const membership = await resolveOrgPermissions(decoded.uid, {
@@ -234,6 +230,39 @@ export const leadCreateHandler = async (req, res)=>{
234
230
  if (leadSource.write === undefined) delete patch.leadSource;
235
231
  else patch.leadSource = leadSource.write;
236
232
  /*
233
+ * SALUTATION, INDUSTRY AND RATING (AGL-3513), the contact's and the
234
+ * company's own lists, judged the same way: a value outside the list
235
+ * is refused under its field, the lead's current value is kept, and a
236
+ * new lead starts from each list's default.
237
+ */ const picklistRefusals = judgeLeadPicklistPatch(await readLeadPicklists(firestore, resolved.orgId), patch, {
238
+ current: (_ref = before == null ? void 0 : before.data()) != null ? _ref : null,
239
+ created
240
+ });
241
+ const [picklistRefusal] = Object.entries(picklistRefusals);
242
+ if (picklistRefusal) {
243
+ res.status(400).json({
244
+ error: picklistRefusal[1],
245
+ field: picklistRefusal[0]
246
+ });
247
+ return;
248
+ }
249
+ // While a first or last name is set, the name is theirs (AGL-3513).
250
+ const composedName = crmLeadComposedName((_ref1 = before == null ? void 0 : before.data()) != null ? _ref1 : null, patch);
251
+ /*
252
+ * THE ORG'S LEAD STATUSES (AGL-3512): a label or a meaning, of New or
253
+ * Working only, stored as the meaning and the org's label together.
254
+ */ const status = rawStatus ? resolveCrmLeadStatusWrite(await readCrmPicklist(firestore, resolved.orgId, CRM_LEAD_STATUS_PICKLIST), rawStatus, {
255
+ allowed: CREATE_STATUSES,
256
+ current: (_ref2 = before == null ? void 0 : before.data()) != null ? _ref2 : null
257
+ }) : null;
258
+ if ((status == null ? void 0 : status.ok) === false) {
259
+ res.status(400).json({
260
+ error: `A new lead is New or Working. ${status.error}`,
261
+ field: 'status'
262
+ });
263
+ return;
264
+ }
265
+ /*
237
266
  * The platform ceiling, judged here so the drawer can say WHY rather
238
267
  * than only that the lead could not be saved; the door re-judges it
239
268
  * inside its own transaction, so a race is still refused there.
@@ -254,8 +283,8 @@ export const leadCreateHandler = async (req, res)=>{
254
283
  hostId,
255
284
  lead: _extends({
256
285
  email
257
- }, name ? {
258
- name
286
+ }, composedName || name ? {
287
+ name: composedName || name
259
288
  } : {}, {
260
289
  source: LEAD_MANUAL_SOURCE
261
290
  })
@@ -266,8 +295,11 @@ export const leadCreateHandler = async (req, res)=>{
266
295
  });
267
296
  return;
268
297
  }
269
- const working = _extends({}, leadProfileWrite(patch), rawStatus ? {
270
- status: rawStatus
298
+ const working = _extends({}, leadProfileWrite(patch), composedName ? {
299
+ name: composedName
300
+ } : {}, (status == null ? void 0 : status.ok) ? {
301
+ status: status.status,
302
+ statusLabel: status.statusLabel
271
303
  } : {}, ownerUid ? {
272
304
  ownerUid
273
305
  } : {}, notes ? {
@@ -304,7 +336,7 @@ export const leadCreateHandler = async (req, res)=>{
304
336
  campaigns: campaigns.filter((campaign)=>!already.includes(campaign.id)),
305
337
  atMs: Date.now(),
306
338
  byUid: decoded.uid,
307
- byName: String((_ref = (_decoded_name = decoded['name']) != null ? _decoded_name : decoded.email) != null ? _ref : '').trim() || null
339
+ byName: String((_ref3 = (_decoded_name = decoded['name']) != null ? _decoded_name : decoded.email) != null ? _ref3 : '').trim() || null
308
340
  });
309
341
  /*
310
342
  * The audit line, written by the route that verified the caller and
@@ -316,7 +348,7 @@ export const leadCreateHandler = async (req, res)=>{
316
348
  }, created ? 'Added lead' : 'Updated lead', {
317
349
  type: 'lead',
318
350
  id: leadId,
319
- name: name || email
351
+ name: composedName || name || email
320
352
  });
321
353
  const answer = {
322
354
  leadId,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/lead-create.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/leads-create` — a lead typed in by hand (AGL-3231).\n *\n * Salesforce's New Lead: a person the team has heard of and has not yet\n * qualified, entered with what is known about them — the address, the\n * name, the company as text, a title, a phone, a website, an address, tags,\n * where they came from — and nothing else brought into being. No contact,\n * no company record: those are the conversion's to make, once the lead is\n * real. Until then the lead is the whole record, which is what lets the\n * Leads section be worked without a second row for every person in it.\n *\n * ## Through the one door\n *\n * `addHostLead` is the single writer of `hosts/{hostId}/leads` — the\n * capture doors, the import and the REST create all reach it — and it is\n * the writer here. That is what keys the lead on the person, so a second\n * \"new lead\" for one address updates the first rather than filing twice;\n * counts it against the platform ceiling inside the transaction that\n * writes; and records NO marketing basis, because a person typed in by the\n * team is not a person who ticked a box. The profile and the working state\n * are the CRM's own annotations and land in one merge write after the\n * door's, exactly as the import writes them.\n *\n * ## Who may call it\n *\n * The same two gates the convert route asks: `data.manage` in the org, and\n * a role on THIS site — a lead is the site's record, so an org member scoped\n * to a sibling site holds the key but not the host. Then the CRM suite gate,\n * because a lead entered by hand is suite work: the capture doors that fill\n * a Free workspace's leads do not come through here.\n */\n\nimport {\n checkVisitorRecordCeiling,\n CONTACT_FIELDS_MAX_PER_ORG,\n type ContactFieldDefinition,\n CRM_COLLECTIONS,\n type CrmCustomValue,\n type CrmLeadProfilePatch,\n type CrmLeadStatus,\n LEADS_MAX_PER_HOST,\n normalizeContactEmail,\n normalizeCrmLeadProfile,\n personKey,\n type PluginApiHandler,\n readCrmCustomInput,\n} from '@aglyn/aglyn/server'\nimport {\n addHostLead,\n firebaseAdmin,\n getOrgForHost,\n logHostActivity,\n orgLeadsForHost,\n restampCrmListFieldsAt,\n readLeadForHost,\n scopedToHost,\n} from '@aglyn/tenant-data-admin'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport { normalizeContainerIds, readContainerIds } from '@aglyn/aglyn/app-utils/container-membership'\nimport { readOrgContainers } from '@aglyn/tenant-data-admin/server/org-containers'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport type { CampaignFilingRef } from '../model/campaign-filing-activity'\nimport { fileCampaignFilingActivities } from './campaign-filing-activity'\nimport { readLeadSourcePicklist, resolveLeadSourceWrite } from './lead-source-picklist'\nimport { holdsDataManage } from './org-caller'\nimport { crmSuiteRefusal } from './suite-gate'\n\n/** The statuses a lead may be entered in — everything but the converted state. */\nconst CREATE_STATUSES: readonly CrmLeadStatus[] = ['new', 'working']\n\nconst NAME_MAX = 120\nconst NOTES_MAX = 4000\n\n/** The surface a hand-entered lead names, beside `signup`, `booking`, `form:{id}` and `import`. */\nexport const LEAD_MANUAL_SOURCE = 'manual'\n\n/** What the New lead drawer posts. */\nexport interface LeadCreateRequest {\n hostId: string\n email: string\n name?: string\n company?: string\n jobTitle?: string\n phone?: string\n website?: string\n address?: Record<string, unknown> | null\n tags?: string[]\n leadSource?: string\n status?: CrmLeadStatus\n ownerUid?: string\n notes?: string\n /** The org's campaigns to file the lead under (AGL-3254), by container id. */\n campaignIds?: string[]\n /**\n * The org's custom LEAD fields (AGL-3272), keyed by each definition's\n * `key`. Judged against the definitions whose `object` is `lead`, so a\n * contact field of the same name cannot be written through this door.\n */\n custom?: Record<string, CrmCustomValue>\n}\n\n/** The sentence a campaign that is not the org's live one is refused with, under the field. */\nexport const LEAD_CAMPAIGN_REFUSAL =\n \"One of the campaigns picked doesn't exist any more. Pick it again.\"\n\n/**\n * The campaigns a request may file a lead under: the organization's own\n * live containers of the `campaign` kind, and nothing else. A\n * request can claim any id, and a lead filed under another org's campaign\n * — or one the console soft-deleted — would sit on a page nobody in this\n * org can open. A lead is an org record, so any live campaign of the org\n * qualifies, whichever sites it is placed on. Answered with the names as\n * they stand, for the timeline entries the filing writes (AGL-3274).\n *\n * @returns the clean ids with their names, or `null` when one is not the org's.\n */\nexport async function orgCampaigns(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n raw: unknown,\n): Promise<CampaignFilingRef[] | null> {\n const ids = normalizeContainerIds(raw)\n if (!ids.length) return []\n if (!orgId || ids.some((id) => id.includes('/'))) return null\n const found = await readOrgContainers(firestore, 'campaign', orgId, ids)\n return found.every((container) => container.live)\n ? found.map((container) => ({ id: container.id, name: container.name || container.id }))\n : null\n}\n\n/** The ids alone — see {@link orgCampaigns}. */\nexport async function orgCampaignIds(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n raw: unknown,\n): Promise<string[] | null> {\n const campaigns = await orgCampaigns(firestore, orgId, raw)\n return campaigns ? campaigns.map((campaign) => campaign.id) : null\n}\n\n/** What the route answers on success. */\nexport interface LeadCreateResponse {\n leadId: string\n /** False when the site already held a lead for the address — it was updated. */\n created: boolean\n}\n\n/** The lead's own fields as one write, `null` in the patch meaning a clear. */\nexport function leadProfileWrite(patch: CrmLeadProfilePatch): Record<string, unknown> {\n const write: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(patch)) {\n if (value === undefined) continue\n write[key] = value === null ? FieldValue.delete() : value\n }\n return write\n}\n\nexport const leadCreateHandler: 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 authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) {\n res.status(401).json({ error: 'Unauthenticated' })\n return\n }\n const body: Partial<LeadCreateRequest> & Record<string, unknown> =\n typeof req.body === 'string' ? JSON.parse(req.body) : (req.body ?? {})\n const hostId = String(body.hostId ?? '')\n .trim()\n .slice(0, 128)\n if (!hostId) {\n res.status(400).json({ error: 'Missing hostId' })\n return\n }\n const email = normalizeContactEmail(body.email)\n if (!email) {\n res.status(400).json({ error: 'Enter a valid email address.' })\n return\n }\n /*\n * The profile, refused BEFORE any read: a phone or a website the record\n * cannot hold is answered under its field, the sentence the drawer shows\n * beneath the input, and nothing was written to retry against.\n */\n const { patch, errors } = normalizeCrmLeadProfile(body)\n const [firstError] = Object.entries(errors)\n if (firstError) {\n res.status(400).json({ error: firstError[1], field: firstError[0] })\n return\n }\n const rawStatus = String(body.status ?? '').trim()\n if (rawStatus && !CREATE_STATUSES.includes(rawStatus as CrmLeadStatus)) {\n res.status(400).json({ error: 'A new lead is New or Working.' })\n return\n }\n const name = String(body.name ?? '')\n .trim()\n .replace(/\\s+/g, ' ')\n .slice(0, NAME_MAX)\n const ownerUid = String(body.ownerUid ?? '')\n .trim()\n .slice(0, 128)\n const notes = String(body.notes ?? '')\n .trim()\n .slice(0, NOTES_MAX)\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const staff = decoded['staff'] === true\n const membership = await resolveOrgPermissions(decoded.uid, { hostId })\n if (\n !staff &&\n !(membership.hostRole && (await holdsDataManage(membership.orgId, decoded.uid)))\n ) {\n res\n .status(403)\n .json({ error: 'Adding a lead requires the data permission on this site' })\n return\n }\n const resolved = await getOrgForHost(hostId)\n if (!resolved) {\n res.status(404).json({ error: 'Unknown site' })\n return\n }\n const suite = crmSuiteRefusal(resolved.org, 'Adding a lead by hand')\n if (suite) {\n res.status(suite.status).json(suite.body)\n return\n }\n\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const leadsRef = await orgLeadsForHost(hostId)\n /*\n * The org's own lead fields (AGL-3272), judged before any write and\n * only when the body carried a map — a body without `custom` pays for\n * no definition read. The refusal is the definition reader's own\n * sentence, so a client learns which key it spelled wrong rather than\n * that the lead could not be saved.\n */\n let custom: Record<string, CrmCustomValue> | undefined\n if (body.custom !== undefined) {\n const definitions = await firestore\n .collection('orgs')\n .doc(resolved.orgId)\n .collection(CRM_COLLECTIONS.contactFields)\n .limit(CONTACT_FIELDS_MAX_PER_ORG)\n .get()\n const judged = readCrmCustomInput(\n body.custom,\n definitions.docs.map((entry) => entry.data() as ContactFieldDefinition),\n 'lead',\n )\n if ('errors' in judged) {\n const [field, message] = Object.entries(judged.errors)[0] ?? []\n res.status(400).json({\n error: message ?? 'A custom value could not be saved.',\n ...(field ? { field } : {}),\n })\n return\n }\n custom = judged.values\n }\n // The campaigns, judged before any write, so a refused pick leaves\n // nothing to retry against (AGL-3254).\n const campaigns = await orgCampaigns(firestore, resolved.orgId, body.campaignIds)\n if (campaigns === null) {\n res.status(400).json({ error: LEAD_CAMPAIGN_REFUSAL, field: 'campaignIds' })\n return\n }\n const campaignIds = campaigns.map((campaign) => campaign.id)\n // Non-null by construction: the normalizer refused every address this\n // derivation could not key.\n const leadId = personKey(email) as string\n const before = await readLeadForHost(hostId, leadId)\n const created = !before\n /*\n * THE ORG'S LEAD SOURCES (AGL-3298), judged before any write: a value\n * outside the list is refused under the field naming what the list\n * allows, a lead the site already held keeps the value it has, and a\n * new lead that named none starts from the list's default.\n */\n const leadSource = resolveLeadSourceWrite(\n await readLeadSourcePicklist(firestore, resolved.orgId),\n patch.leadSource,\n { current: before?.get('leadSource'), created },\n )\n if (leadSource.ok === false) {\n res.status(400).json({ error: leadSource.error, field: 'leadSource' })\n return\n }\n if (leadSource.write === undefined) delete patch.leadSource\n else patch.leadSource = leadSource.write\n /*\n * The platform ceiling, judged here so the drawer can say WHY rather\n * than only that the lead could not be saved; the door re-judges it\n * inside its own transaction, so a race is still refused there.\n */\n if (created) {\n // What THIS site may see, not the whole org's collection (AGL-3275) —\n // the same narrowing `addHostLead` re-applies inside its transaction.\n const used = (await scopedToHost(leadsRef, hostId).count().get()).data().count\n if (checkVisitorRecordCeiling(used, LEADS_MAX_PER_HOST).exceeded) {\n res.status(409).json({\n error:\n 'This site is at the platform lead limit, so the lead was not ' +\n 'added. Remove some leads, or contact support if this is real ' +\n 'traffic.',\n reason: 'lead-ceiling',\n })\n return\n }\n }\n const stored = await addHostLead({\n hostRef,\n hostId,\n lead: {\n email,\n ...(name ? { name } : {}),\n source: LEAD_MANUAL_SOURCE,\n },\n })\n if (!stored) {\n res.status(500).json({ error: 'The lead could not be saved.' })\n return\n }\n const working: Record<string, unknown> = {\n ...leadProfileWrite(patch),\n ...(rawStatus ? { status: rawStatus } : {}),\n ...(ownerUid ? { ownerUid } : {}),\n ...(notes ? { notes } : {}),\n // Added to what a lead the site already held carries, never in place\n // of it: a second \"new lead\" for an address files it under one more\n // campaign, as the bulk bar would.\n ...(campaignIds.length ? { campaignIds: FieldValue.arrayUnion(...campaignIds) } : {}),\n /*\n * The map as a map, because the write below is a `set` with `merge`\n * and a merge is a DEEP one: the keys this body named land, and a\n * second \"new lead\" for an address the site already holds leaves\n * every other key's value where it was. A dotted path would not do\n * it — only `update` reads dots as paths, and `set` would file a\n * field literally called `custom.tier`.\n */\n ...(custom && Object.keys(custom).length ? { custom } : {}),\n }\n if (Object.keys(working).length) {\n await leadsRef\n .doc(leadId)\n .set({ ...working, updatedAt: FieldValue.serverTimestamp() }, { merge: true })\n // The profile, tags and lead source are what the Leads list searches\n // and filters by (AGL-3321): restamped from the merged lead.\n await restampCrmListFieldsAt(leadsRef.doc(leadId), 'leads')\n }\n /*\n * The filing on the lead's Activity (AGL-3274): one \"Filed under\" per\n * campaign the lead was not already in, by the member who added it.\n * After the write and never before, and never failing it — the\n * membership is on the document; the entry records the act.\n */\n const already = readContainerIds(before ? (before.data() as Record<string, unknown>) : null, 'campaign')\n await fileCampaignFilingActivities(firestore, {\n orgId: resolved.orgId,\n org: resolved.org as Record<string, unknown>,\n hostId,\n link: { leadId },\n action: 'filed',\n campaigns: campaigns.filter((campaign) => !already.includes(campaign.id)),\n atMs: Date.now(),\n byUid: decoded.uid,\n byName: String(decoded['name'] ?? decoded.email ?? '').trim() || null,\n })\n /*\n * The audit line, written by the route that verified the caller and\n * performed the write — the one writer that cannot record an add that\n * did not happen. An update of a lead the site already held says so.\n */\n await logHostActivity(\n hostId,\n { uid: decoded.uid, email: decoded.email ?? null },\n created ? 'Added lead' : 'Updated lead',\n { type: 'lead', id: leadId, name: name || email },\n )\n const answer: LeadCreateResponse = { leadId, created }\n res.status(created ? 201 : 200).json(answer)\n } catch (error) {\n console.error('leads-create failed', error)\n res.status(500).json({ error: 'The lead could not be saved.' })\n }\n}\n\nexport default leadCreateHandler\n"],"names":["checkVisitorRecordCeiling","CONTACT_FIELDS_MAX_PER_ORG","CRM_COLLECTIONS","LEADS_MAX_PER_HOST","normalizeContactEmail","normalizeCrmLeadProfile","personKey","readCrmCustomInput","addHostLead","firebaseAdmin","getOrgForHost","logHostActivity","orgLeadsForHost","restampCrmListFieldsAt","readLeadForHost","scopedToHost","resolveOrgPermissions","normalizeContainerIds","readContainerIds","readOrgContainers","FieldValue","fileCampaignFilingActivities","readLeadSourcePicklist","resolveLeadSourceWrite","holdsDataManage","crmSuiteRefusal","CREATE_STATUSES","NAME_MAX","NOTES_MAX","LEAD_MANUAL_SOURCE","LEAD_CAMPAIGN_REFUSAL","orgCampaigns","firestore","orgId","raw","ids","length","some","id","includes","found","every","container","live","map","name","orgCampaignIds","campaigns","campaign","leadProfileWrite","patch","write","key","value","Object","entries","undefined","delete","leadCreateHandler","req","res","body","method","setHeader","status","json","error","authorization","String","headers","idToken","startsWith","slice","JSON","parse","hostId","trim","email","errors","firstError","field","rawStatus","replace","ownerUid","notes","decoded","app","auth","verifyIdToken","staff","membership","uid","hostRole","resolved","suite","org","hostRef","collection","doc","leadsRef","custom","definitions","contactFields","limit","get","judged","docs","entry","data","message","values","campaignIds","leadId","before","created","leadSource","current","ok","used","count","exceeded","reason","stored","lead","source","working","arrayUnion","keys","set","updatedAt","serverTimestamp","merge","already","link","action","filter","atMs","Date","now","byUid","byName","type","answer","console"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/lead-create.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/leads-create` — a lead typed in by hand (AGL-3231).\n *\n * Salesforce's New Lead: a person the team has heard of and has not yet\n * qualified, entered with what is known about them — the address, the\n * name, the company as text, a title, a phone, a website, an address, tags,\n * where they came from — and nothing else brought into being. No contact,\n * no company record: those are the conversion's to make, once the lead is\n * real. Until then the lead is the whole record, which is what lets the\n * Leads section be worked without a second row for every person in it.\n *\n * ## Through the one door\n *\n * `addHostLead` is the single writer of `hosts/{hostId}/leads` — the\n * capture doors, the import and the REST create all reach it — and it is\n * the writer here. That is what keys the lead on the person, so a second\n * \"new lead\" for one address updates the first rather than filing twice;\n * counts it against the platform ceiling inside the transaction that\n * writes; and records NO marketing basis, because a person typed in by the\n * team is not a person who ticked a box. The profile and the working state\n * are the CRM's own annotations and land in one merge write after the\n * door's, exactly as the import writes them.\n *\n * ## Who may call it\n *\n * The same two gates the convert route asks: `data.manage` in the org, and\n * a role on THIS site — a lead is the site's record, so an org member scoped\n * to a sibling site holds the key but not the host. Then the CRM suite gate,\n * because a lead entered by hand is suite work: the capture doors that fill\n * a Free workspace's leads do not come through here.\n */\n\nimport {\n checkVisitorRecordCeiling,\n CONTACT_FIELDS_MAX_PER_ORG,\n type ContactFieldDefinition,\n CRM_COLLECTIONS,\n type CrmCustomValue,\n crmLeadComposedName,\n type CrmLeadProfilePatch,\n CRM_LEAD_STATUS_PICKLIST,\n type CrmLeadStatus,\n LEADS_MAX_PER_HOST,\n normalizeContactEmail,\n normalizeCrmLeadProfile,\n personKey,\n type PluginApiHandler,\n readCrmCustomInput,\n resolveCrmLeadStatusWrite,\n} from '@aglyn/aglyn/server'\nimport {\n addHostLead,\n firebaseAdmin,\n getOrgForHost,\n logHostActivity,\n orgLeadsForHost,\n restampCrmListFieldsAt,\n readLeadForHost,\n scopedToHost,\n} from '@aglyn/tenant-data-admin'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport { normalizeContainerIds, readContainerIds } from '@aglyn/aglyn/app-utils/container-membership'\nimport { readOrgContainers } from '@aglyn/tenant-data-admin/server/org-containers'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport type { CampaignFilingRef } from '../model/campaign-filing-activity'\nimport { fileCampaignFilingActivities } from './campaign-filing-activity'\nimport { judgeLeadPicklistPatch, readLeadPicklists } from './lead-picklists'\nimport { readLeadSourcePicklist, resolveLeadSourceWrite } from './lead-source-picklist'\nimport { holdsDataManage } from './org-caller'\nimport { readCrmPicklist } from './read-picklist'\nimport { crmSuiteRefusal } from './suite-gate'\n\n/** The statuses a lead may be entered in — everything but the converted state. */\nconst CREATE_STATUSES: readonly CrmLeadStatus[] = ['new', 'working']\n\nconst NAME_MAX = 120\nconst NOTES_MAX = 4000\n\n/** The surface a hand-entered lead names, beside `signup`, `booking`, `form:{id}` and `import`. */\nexport const LEAD_MANUAL_SOURCE = 'manual'\n\n/** What the New lead drawer posts. */\nexport interface LeadCreateRequest {\n hostId: string\n email: string\n name?: string\n company?: string\n jobTitle?: string\n phone?: string\n website?: string\n address?: Record<string, unknown> | null\n tags?: string[]\n leadSource?: string\n /**\n * One of the org's Lead status values by its label, or a meaning — New\n * or Working either way (AGL-3512).\n */\n status?: string\n ownerUid?: string\n notes?: string\n /** The org's campaigns to file the lead under (AGL-3254), by container id. */\n campaignIds?: string[]\n /*\n * Salesforce's standard lead fields (AGL-3513): `salutation`,\n * `firstName`, `lastName`, `mobilePhone`, `fax`, `doNotCall`, `industry`,\n * `rating`, `annualRevenueCents`, `currency`, `numberOfEmployees` — read\n * by `normalizeCrmLeadProfile` with the rest of the profile.\n */\n salutation?: string\n firstName?: string\n lastName?: string\n mobilePhone?: string\n fax?: string\n doNotCall?: boolean\n industry?: string\n rating?: string\n annualRevenueCents?: number\n currency?: string\n numberOfEmployees?: number\n /**\n * The org's custom LEAD fields (AGL-3272), keyed by each definition's\n * `key`. Judged against the definitions whose `object` is `lead`, so a\n * contact field of the same name cannot be written through this door.\n */\n custom?: Record<string, CrmCustomValue>\n}\n\n/** The sentence a campaign that is not the org's live one is refused with, under the field. */\nexport const LEAD_CAMPAIGN_REFUSAL =\n \"One of the campaigns picked doesn't exist any more. Pick it again.\"\n\n/**\n * The campaigns a request may file a lead under: the organization's own\n * live containers of the `campaign` kind, and nothing else. A\n * request can claim any id, and a lead filed under another org's campaign\n * — or one the console soft-deleted — would sit on a page nobody in this\n * org can open. A lead is an org record, so any live campaign of the org\n * qualifies, whichever sites it is placed on. Answered with the names as\n * they stand, for the timeline entries the filing writes (AGL-3274).\n *\n * @returns the clean ids with their names, or `null` when one is not the org's.\n */\nexport async function orgCampaigns(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n raw: unknown,\n): Promise<CampaignFilingRef[] | null> {\n const ids = normalizeContainerIds(raw)\n if (!ids.length) return []\n if (!orgId || ids.some((id) => id.includes('/'))) return null\n const found = await readOrgContainers(firestore, 'campaign', orgId, ids)\n return found.every((container) => container.live)\n ? found.map((container) => ({ id: container.id, name: container.name || container.id }))\n : null\n}\n\n/** The ids alone — see {@link orgCampaigns}. */\nexport async function orgCampaignIds(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n raw: unknown,\n): Promise<string[] | null> {\n const campaigns = await orgCampaigns(firestore, orgId, raw)\n return campaigns ? campaigns.map((campaign) => campaign.id) : null\n}\n\n/** What the route answers on success. */\nexport interface LeadCreateResponse {\n leadId: string\n /** False when the site already held a lead for the address — it was updated. */\n created: boolean\n}\n\n/** The lead's own fields as one write, `null` in the patch meaning a clear. */\nexport function leadProfileWrite(patch: CrmLeadProfilePatch): Record<string, unknown> {\n const write: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(patch)) {\n if (value === undefined) continue\n write[key] = value === null ? FieldValue.delete() : value\n }\n return write\n}\n\nexport const leadCreateHandler: 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 authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) {\n res.status(401).json({ error: 'Unauthenticated' })\n return\n }\n const body: Partial<LeadCreateRequest> & Record<string, unknown> =\n typeof req.body === 'string' ? JSON.parse(req.body) : (req.body ?? {})\n const hostId = String(body.hostId ?? '')\n .trim()\n .slice(0, 128)\n if (!hostId) {\n res.status(400).json({ error: 'Missing hostId' })\n return\n }\n const email = normalizeContactEmail(body.email)\n if (!email) {\n res.status(400).json({ error: 'Enter a valid email address.' })\n return\n }\n /*\n * The profile, refused BEFORE any read: a phone or a website the record\n * cannot hold is answered under its field, the sentence the drawer shows\n * beneath the input, and nothing was written to retry against.\n */\n const { patch, errors } = normalizeCrmLeadProfile(body)\n const [firstError] = Object.entries(errors)\n if (firstError) {\n res.status(400).json({ error: firstError[1], field: firstError[0] })\n return\n }\n const rawStatus = String(body.status ?? '').trim()\n const name = String(body.name ?? '')\n .trim()\n .replace(/\\s+/g, ' ')\n .slice(0, NAME_MAX)\n const ownerUid = String(body.ownerUid ?? '')\n .trim()\n .slice(0, 128)\n const notes = String(body.notes ?? '')\n .trim()\n .slice(0, NOTES_MAX)\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const staff = decoded['staff'] === true\n const membership = await resolveOrgPermissions(decoded.uid, { hostId })\n if (\n !staff &&\n !(membership.hostRole && (await holdsDataManage(membership.orgId, decoded.uid)))\n ) {\n res\n .status(403)\n .json({ error: 'Adding a lead requires the data permission on this site' })\n return\n }\n const resolved = await getOrgForHost(hostId)\n if (!resolved) {\n res.status(404).json({ error: 'Unknown site' })\n return\n }\n const suite = crmSuiteRefusal(resolved.org, 'Adding a lead by hand')\n if (suite) {\n res.status(suite.status).json(suite.body)\n return\n }\n\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const leadsRef = await orgLeadsForHost(hostId)\n /*\n * The org's own lead fields (AGL-3272), judged before any write and\n * only when the body carried a map — a body without `custom` pays for\n * no definition read. The refusal is the definition reader's own\n * sentence, so a client learns which key it spelled wrong rather than\n * that the lead could not be saved.\n */\n let custom: Record<string, CrmCustomValue> | undefined\n if (body.custom !== undefined) {\n const definitions = await firestore\n .collection('orgs')\n .doc(resolved.orgId)\n .collection(CRM_COLLECTIONS.contactFields)\n .limit(CONTACT_FIELDS_MAX_PER_ORG)\n .get()\n const judged = readCrmCustomInput(\n body.custom,\n definitions.docs.map((entry) => entry.data() as ContactFieldDefinition),\n 'lead',\n )\n if ('errors' in judged) {\n const [field, message] = Object.entries(judged.errors)[0] ?? []\n res.status(400).json({\n error: message ?? 'A custom value could not be saved.',\n ...(field ? { field } : {}),\n })\n return\n }\n custom = judged.values\n }\n // The campaigns, judged before any write, so a refused pick leaves\n // nothing to retry against (AGL-3254).\n const campaigns = await orgCampaigns(firestore, resolved.orgId, body.campaignIds)\n if (campaigns === null) {\n res.status(400).json({ error: LEAD_CAMPAIGN_REFUSAL, field: 'campaignIds' })\n return\n }\n const campaignIds = campaigns.map((campaign) => campaign.id)\n // Non-null by construction: the normalizer refused every address this\n // derivation could not key.\n const leadId = personKey(email) as string\n const before = await readLeadForHost(hostId, leadId)\n const created = !before\n /*\n * THE ORG'S LEAD SOURCES (AGL-3298), judged before any write: a value\n * outside the list is refused under the field naming what the list\n * allows, a lead the site already held keeps the value it has, and a\n * new lead that named none starts from the list's default.\n */\n const leadSource = resolveLeadSourceWrite(\n await readLeadSourcePicklist(firestore, resolved.orgId),\n patch.leadSource,\n { current: before?.get('leadSource'), created },\n )\n if (leadSource.ok === false) {\n res.status(400).json({ error: leadSource.error, field: 'leadSource' })\n return\n }\n if (leadSource.write === undefined) delete patch.leadSource\n else patch.leadSource = leadSource.write\n /*\n * SALUTATION, INDUSTRY AND RATING (AGL-3513), the contact's and the\n * company's own lists, judged the same way: a value outside the list\n * is refused under its field, the lead's current value is kept, and a\n * new lead starts from each list's default.\n */\n const picklistRefusals = judgeLeadPicklistPatch(\n await readLeadPicklists(firestore, resolved.orgId),\n patch,\n { current: before?.data() ?? null, created },\n )\n const [picklistRefusal] = Object.entries(picklistRefusals)\n if (picklistRefusal) {\n res.status(400).json({ error: picklistRefusal[1], field: picklistRefusal[0] })\n return\n }\n // While a first or last name is set, the name is theirs (AGL-3513).\n const composedName = crmLeadComposedName(before?.data() ?? null, patch)\n /*\n * THE ORG'S LEAD STATUSES (AGL-3512): a label or a meaning, of New or\n * Working only, stored as the meaning and the org's label together.\n */\n const status = rawStatus\n ? resolveCrmLeadStatusWrite(\n await readCrmPicklist(firestore, resolved.orgId, CRM_LEAD_STATUS_PICKLIST),\n rawStatus,\n { allowed: CREATE_STATUSES, current: before?.data() ?? null },\n )\n : null\n if (status?.ok === false) {\n res.status(400).json({ error: `A new lead is New or Working. ${status.error}`, field: 'status' })\n return\n }\n /*\n * The platform ceiling, judged here so the drawer can say WHY rather\n * than only that the lead could not be saved; the door re-judges it\n * inside its own transaction, so a race is still refused there.\n */\n if (created) {\n // What THIS site may see, not the whole org's collection (AGL-3275) —\n // the same narrowing `addHostLead` re-applies inside its transaction.\n const used = (await scopedToHost(leadsRef, hostId).count().get()).data().count\n if (checkVisitorRecordCeiling(used, LEADS_MAX_PER_HOST).exceeded) {\n res.status(409).json({\n error:\n 'This site is at the platform lead limit, so the lead was not ' +\n 'added. Remove some leads, or contact support if this is real ' +\n 'traffic.',\n reason: 'lead-ceiling',\n })\n return\n }\n }\n const stored = await addHostLead({\n hostRef,\n hostId,\n lead: {\n email,\n ...(composedName || name ? { name: composedName || name } : {}),\n source: LEAD_MANUAL_SOURCE,\n },\n })\n if (!stored) {\n res.status(500).json({ error: 'The lead could not be saved.' })\n return\n }\n const working: Record<string, unknown> = {\n ...leadProfileWrite(patch),\n ...(composedName ? { name: composedName } : {}),\n ...(status?.ok ? { status: status.status, statusLabel: status.statusLabel } : {}),\n ...(ownerUid ? { ownerUid } : {}),\n ...(notes ? { notes } : {}),\n // Added to what a lead the site already held carries, never in place\n // of it: a second \"new lead\" for an address files it under one more\n // campaign, as the bulk bar would.\n ...(campaignIds.length ? { campaignIds: FieldValue.arrayUnion(...campaignIds) } : {}),\n /*\n * The map as a map, because the write below is a `set` with `merge`\n * and a merge is a DEEP one: the keys this body named land, and a\n * second \"new lead\" for an address the site already holds leaves\n * every other key's value where it was. A dotted path would not do\n * it — only `update` reads dots as paths, and `set` would file a\n * field literally called `custom.tier`.\n */\n ...(custom && Object.keys(custom).length ? { custom } : {}),\n }\n if (Object.keys(working).length) {\n await leadsRef\n .doc(leadId)\n .set({ ...working, updatedAt: FieldValue.serverTimestamp() }, { merge: true })\n // The profile, tags and lead source are what the Leads list searches\n // and filters by (AGL-3321): restamped from the merged lead.\n await restampCrmListFieldsAt(leadsRef.doc(leadId), 'leads')\n }\n /*\n * The filing on the lead's Activity (AGL-3274): one \"Filed under\" per\n * campaign the lead was not already in, by the member who added it.\n * After the write and never before, and never failing it — the\n * membership is on the document; the entry records the act.\n */\n const already = readContainerIds(before ? (before.data() as Record<string, unknown>) : null, 'campaign')\n await fileCampaignFilingActivities(firestore, {\n orgId: resolved.orgId,\n org: resolved.org as Record<string, unknown>,\n hostId,\n link: { leadId },\n action: 'filed',\n campaigns: campaigns.filter((campaign) => !already.includes(campaign.id)),\n atMs: Date.now(),\n byUid: decoded.uid,\n byName: String(decoded['name'] ?? decoded.email ?? '').trim() || null,\n })\n /*\n * The audit line, written by the route that verified the caller and\n * performed the write — the one writer that cannot record an add that\n * did not happen. An update of a lead the site already held says so.\n */\n await logHostActivity(\n hostId,\n { uid: decoded.uid, email: decoded.email ?? null },\n created ? 'Added lead' : 'Updated lead',\n { type: 'lead', id: leadId, name: composedName || name || email },\n )\n const answer: LeadCreateResponse = { leadId, created }\n res.status(created ? 201 : 200).json(answer)\n } catch (error) {\n console.error('leads-create failed', error)\n res.status(500).json({ error: 'The lead could not be saved.' })\n }\n}\n\nexport default leadCreateHandler\n"],"names":["checkVisitorRecordCeiling","CONTACT_FIELDS_MAX_PER_ORG","CRM_COLLECTIONS","crmLeadComposedName","CRM_LEAD_STATUS_PICKLIST","LEADS_MAX_PER_HOST","normalizeContactEmail","normalizeCrmLeadProfile","personKey","readCrmCustomInput","resolveCrmLeadStatusWrite","addHostLead","firebaseAdmin","getOrgForHost","logHostActivity","orgLeadsForHost","restampCrmListFieldsAt","readLeadForHost","scopedToHost","resolveOrgPermissions","normalizeContainerIds","readContainerIds","readOrgContainers","FieldValue","fileCampaignFilingActivities","judgeLeadPicklistPatch","readLeadPicklists","readLeadSourcePicklist","resolveLeadSourceWrite","holdsDataManage","readCrmPicklist","crmSuiteRefusal","CREATE_STATUSES","NAME_MAX","NOTES_MAX","LEAD_MANUAL_SOURCE","LEAD_CAMPAIGN_REFUSAL","orgCampaigns","firestore","orgId","raw","ids","length","some","id","includes","found","every","container","live","map","name","orgCampaignIds","campaigns","campaign","leadProfileWrite","patch","write","key","value","Object","entries","undefined","delete","leadCreateHandler","req","res","body","method","setHeader","status","json","error","authorization","String","headers","idToken","startsWith","slice","JSON","parse","hostId","trim","email","errors","firstError","field","rawStatus","replace","ownerUid","notes","decoded","app","auth","verifyIdToken","staff","membership","uid","hostRole","resolved","suite","org","hostRef","collection","doc","leadsRef","custom","definitions","contactFields","limit","get","judged","docs","entry","data","message","values","campaignIds","leadId","before","created","leadSource","current","ok","picklistRefusals","picklistRefusal","composedName","allowed","used","count","exceeded","reason","stored","lead","source","working","statusLabel","arrayUnion","keys","set","updatedAt","serverTimestamp","merge","already","link","action","filter","atMs","Date","now","byUid","byName","type","answer","console"],"mappings":"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"}
@@ -21,3 +21,9 @@ export declare function nurtureReachedLeads(firestore: Firestore, input: {
21
21
  hostId: string;
22
22
  emails: readonly string[];
23
23
  }): Promise<number>;
24
+ /** Move the leads that wrote back, still New or Nurturing, to Working; how many moved. */
25
+ export declare function workRepliedLeads(firestore: Firestore, input: {
26
+ orgId: string;
27
+ hostId: string;
28
+ emails: readonly string[];
29
+ }): Promise<number>;
@@ -1,3 +1,4 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
1
2
  /**
2
3
  * @license
3
4
  * Copyright 2026 Aglyn LLC
@@ -14,39 +15,60 @@
14
15
  * See the License for the specific language governing permissions and
15
16
  * limitations under the License.
16
17
  */ /**
17
- * WHAT A DELIVERED MARKETING EMAIL DOES TO A LEAD (AGL-3446).
18
+ * WHAT MAIL DOES TO A LEAD'S STAGE: delivered (AGL-3446), and answered
19
+ * (AGL-3080).
18
20
  *
19
- * A lead nobody has touched, once a campaign email has been delivered to
20
- * it, moves from New to Nurturing: automated email is reaching it, so the
21
- * CRM digest must not count it as unworked, but no person has engaged yet.
22
- * Sequences make the same move from their own send job. A sender reaches
23
- * this through the record email-state seam's `reached`
24
- * (`stampRecordEmailReach`), never by importing the CRM.
21
+ * A lead nobody has touched, once a campaign email or a sequence step has
22
+ * been delivered to it, moves from New to Nurturing: automated email is
23
+ * reaching it, so the CRM digest must not count it as unworked, but no person
24
+ * has engaged yet. A lead that WROTE BACK — New or Nurturing — moves to
25
+ * Working: somebody answered, the lead is being worked, and a queue that
26
+ * still showed it as New would send a rep to open a conversation already
27
+ * open. A sender reaches both through the record email-state seam's
28
+ * `reached` and `replied` (`stampRecordEmailReach`, `stampRecordEmailReply`),
29
+ * never by importing the CRM.
25
30
  *
26
31
  * Found by address rather than by audience: a lead's document id IS the
27
32
  * `personKey` of its address (`orgs/{orgId}/leads/{personKey}`, AGL-3275),
28
- * so every audience — the leads, a list, a segment, typed-in addresses —
29
- * resolves the people it reached to their lead rows by one batched read,
30
- * with no query and no index.
33
+ * so every audience — the leads, a list, a segment, typed-in addresses, a
34
+ * sequence's enrollment — resolves the people it reached to their lead rows
35
+ * by one batched read, with no query and no index.
31
36
  *
32
37
  * Only a lead the sending site holds is moved — `visibleTo` names it — so
33
- * one brand's campaign never writes the stage of a lead another brand in
34
- * the same organization keeps. Only `new` moves; Nurturing, Working, the
35
- * closed states and a converted lead are left as they stand.
38
+ * one brand's mail never writes the stage of a lead another brand in the
39
+ * same organization keeps. Only the stages named move; Working, the closed
40
+ * states and a converted lead are left as they stand, so mail never reopens
41
+ * a verdict or moves a contact's record.
36
42
  *
37
43
  * Never throws: the mail has already gone, and a stage that could not be
38
44
  * written is one a rep sets by hand.
39
- */ import { crmLeadStatus } from "@aglyn/aglyn/app-utils/crm";
45
+ */ import { CRM_COLLECTIONS, CRM_LEAD_STATUS_PICKLIST, crmLeadStatus, crmLeadStatusLabelFor, effectiveCrmLeadStatusPicklist } from "@aglyn/aglyn/app-utils/crm";
40
46
  import { personKey } from "@aglyn/aglyn/app-utils/person-key";
41
47
  import { visibleToHost } from "@aglyn/aglyn/app-utils/scope-tokens";
42
48
  import { FieldValue } from "firebase-admin/firestore";
43
49
  /** `getAll` takes this many references per call, and a batch this many writes. */ const CHUNK = 300;
44
- /** Move the delivered-to leads still New to Nurturing; how many moved. */ export async function nurtureReachedLeads(firestore, input) {
50
+ /** Move the leads at these addresses that stand in `from` to `to`; how many moved. */ async function advanceLeadsAt(firestore, input, from, to) {
45
51
  if (!input.orgId || !input.emails.length) return 0;
46
52
  const keys = [
47
53
  ...new Set(input.emails.map((email)=>personKey(email)).filter((key)=>Boolean(key)))
48
54
  ];
49
55
  const leads = firestore.collection('orgs').doc(input.orgId).collection('leads');
56
+ /*
57
+ * The org's label for the stage, stamped beside the meaning (AGL-3512) —
58
+ * read once, and only once a lead is due to move. A list that cannot be
59
+ * read stamps the meaning alone, which every reader labels itself.
60
+ */ let label;
61
+ const labelFor = async ()=>{
62
+ if (label !== undefined) return label;
63
+ try {
64
+ const list = await firestore.collection('orgs').doc(input.orgId).collection(CRM_COLLECTIONS.picklists).doc(CRM_LEAD_STATUS_PICKLIST).get();
65
+ label = crmLeadStatusLabelFor(effectiveCrmLeadStatusPicklist(list.data()), to);
66
+ } catch (error) {
67
+ console.error('[crm] the lead statuses could not be read', input.orgId, error);
68
+ label = null;
69
+ }
70
+ return label;
71
+ };
50
72
  let moved = 0;
51
73
  for(let start = 0; start < keys.length; start += CHUNK){
52
74
  try {
@@ -59,11 +81,15 @@ import { FieldValue } from "firebase-admin/firestore";
59
81
  const lead = (_snapshot_data = snapshot.data()) != null ? _snapshot_data : {};
60
82
  if (lead['convertedContactId']) continue;
61
83
  if (!visibleToHost(lead['visibleTo'], input.hostId)) continue;
62
- if (crmLeadStatus(lead) !== 'new') continue;
63
- batch.set(snapshot.ref, {
64
- status: 'nurturing',
84
+ if (!from.includes(crmLeadStatus(lead))) continue;
85
+ const statusLabel = await labelFor();
86
+ batch.set(snapshot.ref, _extends({
87
+ status: to
88
+ }, statusLabel ? {
89
+ statusLabel
90
+ } : {}, {
65
91
  updatedAt: FieldValue.serverTimestamp()
66
- }, {
92
+ }), {
67
93
  merge: true
68
94
  });
69
95
  writes += 1;
@@ -73,10 +99,21 @@ import { FieldValue } from "firebase-admin/firestore";
73
99
  moved += writes;
74
100
  }
75
101
  } catch (error) {
76
- console.error('[crm] the reached leads could not be marked nurturing', input.orgId, error);
102
+ console.error(`[crm] the leads could not be marked ${to}`, input.orgId, error);
77
103
  }
78
104
  }
79
105
  return moved;
80
106
  }
107
+ /** Move the delivered-to leads still New to Nurturing; how many moved. */ export function nurtureReachedLeads(firestore, input) {
108
+ return advanceLeadsAt(firestore, input, [
109
+ 'new'
110
+ ], 'nurturing');
111
+ }
112
+ /** Move the leads that wrote back, still New or Nurturing, to Working; how many moved. */ export function workRepliedLeads(firestore, input) {
113
+ return advanceLeadsAt(firestore, input, [
114
+ 'new',
115
+ 'nurturing'
116
+ ], 'working');
117
+ }
81
118
 
82
119
  //# sourceMappingURL=lead-nurturing.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/lead-nurturing.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 * WHAT A DELIVERED MARKETING EMAIL DOES TO A LEAD (AGL-3446).\n *\n * A lead nobody has touched, once a campaign email has been delivered to\n * it, moves from New to Nurturing: automated email is reaching it, so the\n * CRM digest must not count it as unworked, but no person has engaged yet.\n * Sequences make the same move from their own send job. A sender reaches\n * this through the record email-state seam's `reached`\n * (`stampRecordEmailReach`), never by importing the CRM.\n *\n * Found by address rather than by audience: a lead's document id IS the\n * `personKey` of its address (`orgs/{orgId}/leads/{personKey}`, AGL-3275),\n * so every audience — the leads, a list, a segment, typed-in addresses —\n * resolves the people it reached to their lead rows by one batched read,\n * with no query and no index.\n *\n * Only a lead the sending site holds is moved — `visibleTo` names it — so\n * one brand's campaign never writes the stage of a lead another brand in\n * the same organization keeps. Only `new` moves; Nurturing, Working, the\n * closed states and a converted lead are left as they stand.\n *\n * Never throws: the mail has already gone, and a stage that could not be\n * written is one a rep sets by hand.\n */\n\nimport { crmLeadStatus } from '@aglyn/aglyn/app-utils/crm'\nimport { personKey } from '@aglyn/aglyn/app-utils/person-key'\nimport { visibleToHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport { FieldValue, type Firestore } from 'firebase-admin/firestore'\n\n/** `getAll` takes this many references per call, and a batch this many writes. */\nconst CHUNK = 300\n\n/** Move the delivered-to leads still New to Nurturing; how many moved. */\nexport async function nurtureReachedLeads(\n firestore: Firestore,\n input: { orgId: string; hostId: string; emails: readonly string[] },\n): Promise<number> {\n if (!input.orgId || !input.emails.length) return 0\n const keys = [\n ...new Set(input.emails.map((email) => personKey(email)).filter((key): key is string => Boolean(key))),\n ]\n const leads = firestore.collection('orgs').doc(input.orgId).collection('leads')\n let moved = 0\n for (let start = 0; start < keys.length; start += CHUNK) {\n try {\n const snapshots = await firestore.getAll(...keys.slice(start, start + CHUNK).map((key) => leads.doc(key)))\n const batch = firestore.batch()\n let writes = 0\n for (const snapshot of snapshots) {\n if (!snapshot.exists) continue\n const lead = snapshot.data() ?? {}\n if (lead['convertedContactId']) continue\n if (!visibleToHost(lead['visibleTo'] as string[] | undefined, input.hostId)) continue\n if (crmLeadStatus(lead as never) !== 'new') continue\n batch.set(snapshot.ref, { status: 'nurturing', updatedAt: FieldValue.serverTimestamp() }, { merge: true })\n writes += 1\n }\n if (writes) {\n await batch.commit()\n moved += writes\n }\n } catch (error) {\n console.error('[crm] the reached leads could not be marked nurturing', input.orgId, error)\n }\n }\n return moved\n}\n"],"names":["crmLeadStatus","personKey","visibleToHost","FieldValue","CHUNK","nurtureReachedLeads","firestore","input","orgId","emails","length","keys","Set","map","email","filter","key","Boolean","leads","collection","doc","moved","start","snapshots","getAll","slice","batch","writes","snapshot","exists","lead","data","hostId","set","ref","status","updatedAt","serverTimestamp","merge","commit","error","console"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;CAuBC,GAED,SAASA,aAAa,QAAQ,6BAA4B;AAC1D,SAASC,SAAS,QAAQ,oCAAmC;AAC7D,SAASC,aAAa,QAAQ,sCAAqC;AACnE,SAASC,UAAU,QAAwB,2BAA0B;AAErE,gFAAgF,GAChF,MAAMC,QAAQ;AAEd,wEAAwE,GACxE,OAAO,eAAeC,oBACpBC,SAAoB,EACpBC,KAAmE;IAEnE,IAAI,CAACA,MAAMC,KAAK,IAAI,CAACD,MAAME,MAAM,CAACC,MAAM,EAAE,OAAO;IACjD,MAAMC,OAAO;WACR,IAAIC,IAAIL,MAAME,MAAM,CAACI,GAAG,CAAC,CAACC,QAAUb,UAAUa,QAAQC,MAAM,CAAC,CAACC,MAAuBC,QAAQD;KACjG;IACD,MAAME,QAAQZ,UAAUa,UAAU,CAAC,QAAQC,GAAG,CAACb,MAAMC,KAAK,EAAEW,UAAU,CAAC;IACvE,IAAIE,QAAQ;IACZ,IAAK,IAAIC,QAAQ,GAAGA,QAAQX,KAAKD,MAAM,EAAEY,SAASlB,MAAO;QACvD,IAAI;YACF,MAAMmB,YAAY,MAAMjB,UAAUkB,MAAM,IAAIb,KAAKc,KAAK,CAACH,OAAOA,QAAQlB,OAAOS,GAAG,CAAC,CAACG,MAAQE,MAAME,GAAG,CAACJ;YACpG,MAAMU,QAAQpB,UAAUoB,KAAK;YAC7B,IAAIC,SAAS;YACb,KAAK,MAAMC,YAAYL,UAAW;oBAEnBK;gBADb,IAAI,CAACA,SAASC,MAAM,EAAE;gBACtB,MAAMC,QAAOF,iBAAAA,SAASG,IAAI,cAAbH,iBAAmB,CAAC;gBACjC,IAAIE,IAAI,CAAC,qBAAqB,EAAE;gBAChC,IAAI,CAAC5B,cAAc4B,IAAI,CAAC,YAAY,EAA0BvB,MAAMyB,MAAM,GAAG;gBAC7E,IAAIhC,cAAc8B,UAAmB,OAAO;gBAC5CJ,MAAMO,GAAG,CAACL,SAASM,GAAG,EAAE;oBAAEC,QAAQ;oBAAaC,WAAWjC,WAAWkC,eAAe;gBAAG,GAAG;oBAAEC,OAAO;gBAAK;gBACxGX,UAAU;YACZ;YACA,IAAIA,QAAQ;gBACV,MAAMD,MAAMa,MAAM;gBAClBlB,SAASM;YACX;QACF,EAAE,OAAOa,OAAO;YACdC,QAAQD,KAAK,CAAC,yDAAyDjC,MAAMC,KAAK,EAAEgC;QACtF;IACF;IACA,OAAOnB;AACT"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/lead-nurturing.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 * WHAT MAIL DOES TO A LEAD'S STAGE: delivered (AGL-3446), and answered\n * (AGL-3080).\n *\n * A lead nobody has touched, once a campaign email or a sequence step has\n * been delivered to it, moves from New to Nurturing: automated email is\n * reaching it, so the CRM digest must not count it as unworked, but no person\n * has engaged yet. A lead that WROTE BACK — New or Nurturing — moves to\n * Working: somebody answered, the lead is being worked, and a queue that\n * still showed it as New would send a rep to open a conversation already\n * open. A sender reaches both through the record email-state seam's\n * `reached` and `replied` (`stampRecordEmailReach`, `stampRecordEmailReply`),\n * never by importing the CRM.\n *\n * Found by address rather than by audience: a lead's document id IS the\n * `personKey` of its address (`orgs/{orgId}/leads/{personKey}`, AGL-3275),\n * so every audience — the leads, a list, a segment, typed-in addresses, a\n * sequence's enrollment — resolves the people it reached to their lead rows\n * by one batched read, with no query and no index.\n *\n * Only a lead the sending site holds is moved — `visibleTo` names it — so\n * one brand's mail never writes the stage of a lead another brand in the\n * same organization keeps. Only the stages named move; Working, the closed\n * states and a converted lead are left as they stand, so mail never reopens\n * a verdict or moves a contact's record.\n *\n * Never throws: the mail has already gone, and a stage that could not be\n * written is one a rep sets by hand.\n */\n\nimport {\n CRM_COLLECTIONS,\n CRM_LEAD_STATUS_PICKLIST,\n type CrmLeadStatus,\n crmLeadStatus,\n crmLeadStatusLabelFor,\n effectiveCrmLeadStatusPicklist,\n} from '@aglyn/aglyn/app-utils/crm'\nimport { personKey } from '@aglyn/aglyn/app-utils/person-key'\nimport { visibleToHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport { FieldValue, type Firestore } from 'firebase-admin/firestore'\n\n/** `getAll` takes this many references per call, and a batch this many writes. */\nconst CHUNK = 300\n\n/** Move the leads at these addresses that stand in `from` to `to`; how many moved. */\nasync function advanceLeadsAt(\n firestore: Firestore,\n input: { orgId: string; hostId: string; emails: readonly string[] },\n from: readonly CrmLeadStatus[],\n to: CrmLeadStatus,\n): Promise<number> {\n if (!input.orgId || !input.emails.length) return 0\n const keys = [\n ...new Set(input.emails.map((email) => personKey(email)).filter((key): key is string => Boolean(key))),\n ]\n const leads = firestore.collection('orgs').doc(input.orgId).collection('leads')\n /*\n * The org's label for the stage, stamped beside the meaning (AGL-3512) —\n * read once, and only once a lead is due to move. A list that cannot be\n * read stamps the meaning alone, which every reader labels itself.\n */\n let label: string | null | undefined\n const labelFor = async (): Promise<string | null> => {\n if (label !== undefined) return label\n try {\n const list = await firestore\n .collection('orgs')\n .doc(input.orgId)\n .collection(CRM_COLLECTIONS.picklists)\n .doc(CRM_LEAD_STATUS_PICKLIST)\n .get()\n label = crmLeadStatusLabelFor(effectiveCrmLeadStatusPicklist(list.data()), to)\n } catch (error) {\n console.error('[crm] the lead statuses could not be read', input.orgId, error)\n label = null\n }\n return label\n }\n let moved = 0\n for (let start = 0; start < keys.length; start += CHUNK) {\n try {\n const snapshots = await firestore.getAll(...keys.slice(start, start + CHUNK).map((key) => leads.doc(key)))\n const batch = firestore.batch()\n let writes = 0\n for (const snapshot of snapshots) {\n if (!snapshot.exists) continue\n const lead = snapshot.data() ?? {}\n if (lead['convertedContactId']) continue\n if (!visibleToHost(lead['visibleTo'] as string[] | undefined, input.hostId)) continue\n if (!from.includes(crmLeadStatus(lead as never))) continue\n const statusLabel = await labelFor()\n batch.set(\n snapshot.ref,\n { status: to, ...(statusLabel ? { statusLabel } : {}), updatedAt: FieldValue.serverTimestamp() },\n { merge: true },\n )\n writes += 1\n }\n if (writes) {\n await batch.commit()\n moved += writes\n }\n } catch (error) {\n console.error(`[crm] the leads could not be marked ${to}`, input.orgId, error)\n }\n }\n return moved\n}\n\n/** Move the delivered-to leads still New to Nurturing; how many moved. */\nexport function nurtureReachedLeads(\n firestore: Firestore,\n input: { orgId: string; hostId: string; emails: readonly string[] },\n): Promise<number> {\n return advanceLeadsAt(firestore, input, ['new'], 'nurturing')\n}\n\n/** Move the leads that wrote back, still New or Nurturing, to Working; how many moved. */\nexport function workRepliedLeads(\n firestore: Firestore,\n input: { orgId: string; hostId: string; emails: readonly string[] },\n): Promise<number> {\n return advanceLeadsAt(firestore, input, ['new', 'nurturing'], 'working')\n}\n"],"names":["CRM_COLLECTIONS","CRM_LEAD_STATUS_PICKLIST","crmLeadStatus","crmLeadStatusLabelFor","effectiveCrmLeadStatusPicklist","personKey","visibleToHost","FieldValue","CHUNK","advanceLeadsAt","firestore","input","from","to","orgId","emails","length","keys","Set","map","email","filter","key","Boolean","leads","collection","doc","label","labelFor","undefined","list","picklists","get","data","error","console","moved","start","snapshots","getAll","slice","batch","writes","snapshot","exists","lead","hostId","includes","statusLabel","set","ref","status","updatedAt","serverTimestamp","merge","commit","nurtureReachedLeads","workRepliedLeads"],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { type CrmLeadPicklistField, type CrmLeadProfilePatch, type CrmPicklist, type CrmPicklistId } from '@aglyn/aglyn/server';
18
+ /**
19
+ * The org's lists behind a lead's Salutation, Industry and Rating
20
+ * (AGL-3513), read together — one document each.
21
+ */
22
+ export declare function readLeadPicklists(firestore: FirebaseFirestore.Firestore, orgId: string): Promise<Partial<Record<CrmPicklistId, CrmPicklist>>>;
23
+ /** Whether a profile names a lead picklist field, so a write that names none reads no list. */
24
+ export declare function namesLeadPicklist(patch: Readonly<CrmLeadProfilePatch>): boolean;
25
+ /**
26
+ * A profile's Salutation, Industry and Rating judged against the org's
27
+ * lists, IN PLACE: each value the list allows is stored as the list spells
28
+ * it, the lead's `current` value is kept, and on a create a field not named
29
+ * starts from its list's default. Answers the refusal of each value the
30
+ * list does not hold, by field — every write door of a lead runs it.
31
+ */
32
+ export declare function judgeLeadPicklistPatch(lists: Readonly<Partial<Record<CrmPicklistId, CrmPicklist>>>, patch: CrmLeadProfilePatch, options?: {
33
+ current?: Readonly<Record<string, unknown>> | null;
34
+ created?: boolean;
35
+ }): Partial<Record<CrmLeadPicklistField, string>>;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { CRM_LEAD_PICKLIST_FIELDS, judgeCrmLeadPicklists } from "@aglyn/aglyn/server";
17
+ import { readCrmPicklist } from "./read-picklist.js";
18
+ /**
19
+ * The org's lists behind a lead's Salutation, Industry and Rating
20
+ * (AGL-3513), read together — one document each.
21
+ */ export async function readLeadPicklists(firestore, orgId) {
22
+ const ids = CRM_LEAD_PICKLIST_FIELDS.map((entry)=>entry.picklistId);
23
+ const lists = await Promise.all(ids.map((id)=>readCrmPicklist(firestore, orgId, id)));
24
+ return Object.fromEntries(ids.map((id, at)=>[
25
+ id,
26
+ lists[at]
27
+ ]));
28
+ }
29
+ /** Whether a profile names a lead picklist field, so a write that names none reads no list. */ export function namesLeadPicklist(patch) {
30
+ return CRM_LEAD_PICKLIST_FIELDS.some((entry)=>patch[entry.field] !== undefined);
31
+ }
32
+ /**
33
+ * A profile's Salutation, Industry and Rating judged against the org's
34
+ * lists, IN PLACE: each value the list allows is stored as the list spells
35
+ * it, the lead's `current` value is kept, and on a create a field not named
36
+ * starts from its list's default. Answers the refusal of each value the
37
+ * list does not hold, by field — every write door of a lead runs it.
38
+ */ export function judgeLeadPicklistPatch(lists, patch, options = {}) {
39
+ const requested = {};
40
+ for (const { field } of CRM_LEAD_PICKLIST_FIELDS){
41
+ if (patch[field] !== undefined) requested[field] = patch[field];
42
+ }
43
+ const judged = judgeCrmLeadPicklists(lists, requested, options);
44
+ for (const { field } of CRM_LEAD_PICKLIST_FIELDS){
45
+ var _judged_values_field;
46
+ if (judged.errors[field]) continue;
47
+ if (field in judged.values) patch[field] = (_judged_values_field = judged.values[field]) != null ? _judged_values_field : null;
48
+ }
49
+ return judged.errors;
50
+ }
51
+
52
+ //# sourceMappingURL=lead-picklists.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/lead-picklists.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 */\nimport {\n CRM_LEAD_PICKLIST_FIELDS,\n type CrmLeadPicklistField,\n type CrmLeadProfilePatch,\n type CrmPicklist,\n type CrmPicklistId,\n judgeCrmLeadPicklists,\n} from '@aglyn/aglyn/server'\nimport { readCrmPicklist } from './read-picklist'\n\n/**\n * The org's lists behind a lead's Salutation, Industry and Rating\n * (AGL-3513), read together — one document each.\n */\nexport async function readLeadPicklists(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n): Promise<Partial<Record<CrmPicklistId, CrmPicklist>>> {\n const ids = CRM_LEAD_PICKLIST_FIELDS.map((entry) => entry.picklistId)\n const lists = await Promise.all(ids.map((id) => readCrmPicklist(firestore, orgId, id)))\n return Object.fromEntries(ids.map((id, at) => [id, lists[at]]))\n}\n\n/** Whether a profile names a lead picklist field, so a write that names none reads no list. */\nexport function namesLeadPicklist(patch: Readonly<CrmLeadProfilePatch>): boolean {\n return CRM_LEAD_PICKLIST_FIELDS.some((entry) => patch[entry.field] !== undefined)\n}\n\n/**\n * A profile's Salutation, Industry and Rating judged against the org's\n * lists, IN PLACE: each value the list allows is stored as the list spells\n * it, the lead's `current` value is kept, and on a create a field not named\n * starts from its list's default. Answers the refusal of each value the\n * list does not hold, by field — every write door of a lead runs it.\n */\nexport function judgeLeadPicklistPatch(\n lists: Readonly<Partial<Record<CrmPicklistId, CrmPicklist>>>,\n patch: CrmLeadProfilePatch,\n options: { current?: Readonly<Record<string, unknown>> | null; created?: boolean } = {},\n): Partial<Record<CrmLeadPicklistField, string>> {\n const requested: Partial<Record<string, unknown>> = {}\n for (const { field } of CRM_LEAD_PICKLIST_FIELDS) {\n if (patch[field] !== undefined) requested[field] = patch[field]\n }\n const judged = judgeCrmLeadPicklists(lists, requested, options)\n for (const { field } of CRM_LEAD_PICKLIST_FIELDS) {\n if (judged.errors[field]) continue\n if (field in judged.values) patch[field] = judged.values[field] ?? null\n }\n return judged.errors\n}\n"],"names":["CRM_LEAD_PICKLIST_FIELDS","judgeCrmLeadPicklists","readCrmPicklist","readLeadPicklists","firestore","orgId","ids","map","entry","picklistId","lists","Promise","all","id","Object","fromEntries","at","namesLeadPicklist","patch","some","field","undefined","judgeLeadPicklistPatch","options","requested","judged","errors","values"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD,SACEA,wBAAwB,EAKxBC,qBAAqB,QAChB,sBAAqB;AAC5B,SAASC,eAAe,QAAQ,qBAAiB;AAEjD;;;CAGC,GACD,OAAO,eAAeC,kBACpBC,SAAsC,EACtCC,KAAa;IAEb,MAAMC,MAAMN,yBAAyBO,GAAG,CAAC,CAACC,QAAUA,MAAMC,UAAU;IACpE,MAAMC,QAAQ,MAAMC,QAAQC,GAAG,CAACN,IAAIC,GAAG,CAAC,CAACM,KAAOX,gBAAgBE,WAAWC,OAAOQ;IAClF,OAAOC,OAAOC,WAAW,CAACT,IAAIC,GAAG,CAAC,CAACM,IAAIG,KAAO;YAACH;YAAIH,KAAK,CAACM,GAAG;SAAC;AAC/D;AAEA,6FAA6F,GAC7F,OAAO,SAASC,kBAAkBC,KAAoC;IACpE,OAAOlB,yBAAyBmB,IAAI,CAAC,CAACX,QAAUU,KAAK,CAACV,MAAMY,KAAK,CAAC,KAAKC;AACzE;AAEA;;;;;;CAMC,GACD,OAAO,SAASC,uBACdZ,KAA4D,EAC5DQ,KAA0B,EAC1BK,UAAqF,CAAC,CAAC;IAEvF,MAAMC,YAA8C,CAAC;IACrD,KAAK,MAAM,EAAEJ,KAAK,EAAE,IAAIpB,yBAA0B;QAChD,IAAIkB,KAAK,CAACE,MAAM,KAAKC,WAAWG,SAAS,CAACJ,MAAM,GAAGF,KAAK,CAACE,MAAM;IACjE;IACA,MAAMK,SAASxB,sBAAsBS,OAAOc,WAAWD;IACvD,KAAK,MAAM,EAAEH,KAAK,EAAE,IAAIpB,yBAA0B;YAELyB;QAD3C,IAAIA,OAAOC,MAAM,CAACN,MAAM,EAAE;QAC1B,IAAIA,SAASK,OAAOE,MAAM,EAAET,KAAK,CAACE,MAAM,IAAGK,uBAAAA,OAAOE,MAAM,CAACP,MAAM,YAApBK,uBAAwB;IACrE;IACA,OAAOA,OAAOC,MAAM;AACtB"}
@@ -17,19 +17,7 @@
17
17
  import { type CrmPicklist } from '@aglyn/aglyn/server';
18
18
  /** The org's lead source list as every server door judges against it (AGL-3298). */
19
19
  export declare function readLeadSourcePicklist(firestore: FirebaseFirestore.Firestore, orgId: string): Promise<CrmPicklist>;
20
- /**
21
- * What a lead write stores as its lead source (AGL-3298), from what the
22
- * request said about it:
23
- *
24
- * - `undefined` (not named): nothing — except on a CREATE, which starts
25
- * from the list's default when it has one, as Salesforce's does.
26
- * - `null` (cleared): the clear.
27
- * - a label: {@link judgeCrmLeadSource} — an active value's label, the
28
- * record's own current value kept, anything else refused naming the
29
- * values the list allows.
30
- *
31
- * `write` undefined means leave the field alone.
32
- */
20
+ /** What a lead write stores as its lead source — {@link resolveCrmPicklistWrite} for it. */
33
21
  export declare function resolveLeadSourceWrite(picklist: CrmPicklist, requested: string | null | undefined, options: {
34
22
  current?: unknown;
35
23
  created: boolean;
@@ -13,40 +13,13 @@
13
13
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
- */ import { CRM_COLLECTIONS, CRM_LEAD_SOURCE_PICKLIST, crmPicklistDefaultLabel, effectiveCrmLeadSourcePicklist, judgeCrmLeadSource } from "@aglyn/aglyn/server";
17
- /** The org's lead source list as every server door judges against it (AGL-3298). */ export async function readLeadSourcePicklist(firestore, orgId) {
18
- const snapshot = await firestore.collection('orgs').doc(orgId).collection(CRM_COLLECTIONS.picklists).doc(CRM_LEAD_SOURCE_PICKLIST).get();
19
- return effectiveCrmLeadSourcePicklist(snapshot.data());
16
+ */ import { CRM_LEAD_SOURCE_PICKLIST } from "@aglyn/aglyn/server";
17
+ import { readCrmPicklist, resolveCrmPicklistWrite } from "./read-picklist.js";
18
+ /** The org's lead source list as every server door judges against it (AGL-3298). */ export function readLeadSourcePicklist(firestore, orgId) {
19
+ return readCrmPicklist(firestore, orgId, CRM_LEAD_SOURCE_PICKLIST);
20
20
  }
21
- /**
22
- * What a lead write stores as its lead source (AGL-3298), from what the
23
- * request said about it:
24
- *
25
- * - `undefined` (not named): nothing — except on a CREATE, which starts
26
- * from the list's default when it has one, as Salesforce's does.
27
- * - `null` (cleared): the clear.
28
- * - a label: {@link judgeCrmLeadSource} — an active value's label, the
29
- * record's own current value kept, anything else refused naming the
30
- * values the list allows.
31
- *
32
- * `write` undefined means leave the field alone.
33
- */ export function resolveLeadSourceWrite(picklist, requested, options) {
34
- if (requested === undefined) {
35
- const fallback = options.created ? crmPicklistDefaultLabel(picklist) : null;
36
- return {
37
- ok: true,
38
- write: fallback != null ? fallback : undefined
39
- };
40
- }
41
- if (requested === null) return {
42
- ok: true,
43
- write: null
44
- };
45
- const judged = judgeCrmLeadSource(picklist, requested, options.current);
46
- return judged.ok === false ? judged : {
47
- ok: true,
48
- write: judged.value
49
- };
21
+ /** What a lead write stores as its lead source — {@link resolveCrmPicklistWrite} for it. */ export function resolveLeadSourceWrite(picklist, requested, options) {
22
+ return resolveCrmPicklistWrite(CRM_LEAD_SOURCE_PICKLIST, picklist, requested, options);
50
23
  }
51
24
 
52
25
  //# sourceMappingURL=lead-source-picklist.js.map