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

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 (541) 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-next-activity-column.js +1 -1
  34. package/src/lib/components/crm-next-activity-column.js.map +1 -1
  35. package/src/lib/components/crm-record-picker.d.ts +15 -0
  36. package/src/lib/components/crm-record-picker.js +16 -10
  37. package/src/lib/components/crm-record-picker.js.map +1 -1
  38. package/src/lib/components/crm-transfer-buttons.d.ts +54 -0
  39. package/src/lib/components/crm-transfer-buttons.js +167 -0
  40. package/src/lib/components/crm-transfer-buttons.js.map +1 -0
  41. package/src/lib/components/deal-contact-roles-card.d.ts +46 -0
  42. package/src/lib/components/deal-contact-roles-card.js +368 -0
  43. package/src/lib/components/deal-contact-roles-card.js.map +1 -0
  44. package/src/lib/components/deal-detail-page.d.ts +3 -2
  45. package/src/lib/components/deal-detail-page.js +25 -6
  46. package/src/lib/components/deal-detail-page.js.map +1 -1
  47. package/src/lib/components/deal-edit-drawer.d.ts +10 -0
  48. package/src/lib/components/deal-edit-drawer.js +219 -10
  49. package/src/lib/components/deal-edit-drawer.js.map +1 -1
  50. package/src/lib/components/deal-properties-card.d.ts +7 -0
  51. package/src/lib/components/deal-properties-card.js +34 -3
  52. package/src/lib/components/deal-properties-card.js.map +1 -1
  53. package/src/lib/components/deal-stage-card.d.ts +4 -0
  54. package/src/lib/components/deal-stage-card.js +10 -2
  55. package/src/lib/components/deal-stage-card.js.map +1 -1
  56. package/src/lib/components/deals-bulk-bar.d.ts +0 -3
  57. package/src/lib/components/deals-bulk-bar.js +7 -20
  58. package/src/lib/components/deals-bulk-bar.js.map +1 -1
  59. package/src/lib/components/deals-section.d.ts +3 -3
  60. package/src/lib/components/deals-section.js +123 -63
  61. package/src/lib/components/deals-section.js.map +1 -1
  62. package/src/lib/components/fields-section.js +31 -17
  63. package/src/lib/components/fields-section.js.map +1 -1
  64. package/src/lib/components/lead-convert-dialog.d.ts +4 -0
  65. package/src/lib/components/lead-convert-dialog.js +23 -1
  66. package/src/lib/components/lead-convert-dialog.js.map +1 -1
  67. package/src/lib/components/lead-detail-page.js +8 -2
  68. package/src/lib/components/lead-detail-page.js.map +1 -1
  69. package/src/lib/components/lead-profile-fields.d.ts +71 -0
  70. package/src/lib/components/lead-profile-fields.js +386 -0
  71. package/src/lib/components/lead-profile-fields.js.map +1 -0
  72. package/src/lib/components/lead-properties-card.d.ts +2 -1
  73. package/src/lib/components/lead-properties-card.js +85 -176
  74. package/src/lib/components/lead-properties-card.js.map +1 -1
  75. package/src/lib/components/lead-source-select.d.ts +4 -23
  76. package/src/lib/components/lead-source-select.js +9 -73
  77. package/src/lib/components/lead-source-select.js.map +1 -1
  78. package/src/lib/components/lead-status-chip.d.ts +9 -3
  79. package/src/lib/components/lead-status-chip.js +8 -4
  80. package/src/lib/components/lead-status-chip.js.map +1 -1
  81. package/src/lib/components/lead-status-options.d.ts +20 -0
  82. package/src/lib/components/lead-status-options.js +51 -0
  83. package/src/lib/components/lead-status-options.js.map +1 -0
  84. package/src/lib/components/lead-unqualify-dialog.d.ts +5 -0
  85. package/src/lib/components/lead-unqualify-dialog.js +50 -18
  86. package/src/lib/components/lead-unqualify-dialog.js.map +1 -1
  87. package/src/lib/components/leads-bulk-bar.d.ts +0 -3
  88. package/src/lib/components/leads-bulk-bar.js +29 -29
  89. package/src/lib/components/leads-bulk-bar.js.map +1 -1
  90. package/src/lib/components/leads-section.js +120 -68
  91. package/src/lib/components/leads-section.js.map +1 -1
  92. package/src/lib/components/linked-deals-card.d.ts +4 -0
  93. package/src/lib/components/linked-deals-card.js +47 -3
  94. package/src/lib/components/linked-deals-card.js.map +1 -1
  95. package/src/lib/components/log-activity-dialog.js +43 -3
  96. package/src/lib/components/log-activity-dialog.js.map +1 -1
  97. package/src/lib/components/new-contact-drawer.d.ts +8 -0
  98. package/src/lib/components/new-contact-drawer.js +108 -6
  99. package/src/lib/components/new-contact-drawer.js.map +1 -1
  100. package/src/lib/components/new-lead-drawer.d.ts +9 -2
  101. package/src/lib/components/new-lead-drawer.js +88 -121
  102. package/src/lib/components/new-lead-drawer.js.map +1 -1
  103. package/src/lib/components/picklist-select.d.ts +36 -0
  104. package/src/lib/components/picklist-select.js +114 -0
  105. package/src/lib/components/picklist-select.js.map +1 -0
  106. package/src/lib/components/picklist-values-card.d.ts +34 -0
  107. package/src/lib/components/{lead-source-values-card.js → picklist-values-card.js} +241 -58
  108. package/src/lib/components/picklist-values-card.js.map +1 -0
  109. package/src/lib/components/pipeline-stages-dialog.d.ts +7 -0
  110. package/src/lib/components/pipeline-stages-dialog.js +26 -4
  111. package/src/lib/components/pipeline-stages-dialog.js.map +1 -1
  112. package/src/lib/components/pipelines-dialog.js +15 -6
  113. package/src/lib/components/pipelines-dialog.js.map +1 -1
  114. package/src/lib/components/recent-activity-feed.js +20 -5
  115. package/src/lib/components/recent-activity-feed.js.map +1 -1
  116. package/src/lib/components/recipes-card.js +4 -2
  117. package/src/lib/components/recipes-card.js.map +1 -1
  118. package/src/lib/components/record-tasks-card.js +15 -8
  119. package/src/lib/components/record-tasks-card.js.map +1 -1
  120. package/src/lib/components/reports/forecast-card.d.ts +6 -0
  121. package/src/lib/components/reports/forecast-card.js +101 -3
  122. package/src/lib/components/reports/forecast-card.js.map +1 -1
  123. package/src/lib/components/reports/lead-funnel-card.d.ts +12 -0
  124. package/src/lib/components/reports/lead-funnel-card.js +9 -5
  125. package/src/lib/components/reports/lead-funnel-card.js.map +1 -1
  126. package/src/lib/components/reports/lead-source-card.d.ts +23 -0
  127. package/src/lib/components/reports/lead-source-card.js +318 -0
  128. package/src/lib/components/reports/lead-source-card.js.map +1 -0
  129. package/src/lib/components/reports/pipeline-card.js +2 -1
  130. package/src/lib/components/reports/pipeline-card.js.map +1 -1
  131. package/src/lib/components/reports/report-export.js +1 -1
  132. package/src/lib/components/reports/report-export.js.map +1 -1
  133. package/src/lib/components/reports-section.js +8 -0
  134. package/src/lib/components/reports-section.js.map +1 -1
  135. package/src/lib/components/sharing-rule-drawer.d.ts +5 -0
  136. package/src/lib/components/sharing-rule-drawer.js +108 -19
  137. package/src/lib/components/sharing-rule-drawer.js.map +1 -1
  138. package/src/lib/components/sharing-rules-card.js +11 -3
  139. package/src/lib/components/sharing-rules-card.js.map +1 -1
  140. package/src/lib/components/task-cells.d.ts +9 -1
  141. package/src/lib/components/task-cells.js +9 -4
  142. package/src/lib/components/task-cells.js.map +1 -1
  143. package/src/lib/components/task-edit-drawer.js +111 -43
  144. package/src/lib/components/task-edit-drawer.js.map +1 -1
  145. package/src/lib/components/tasks-bulk-bar.d.ts +0 -3
  146. package/src/lib/components/tasks-bulk-bar.js +7 -20
  147. package/src/lib/components/tasks-bulk-bar.js.map +1 -1
  148. package/src/lib/components/tasks-calendar.d.ts +3 -0
  149. package/src/lib/components/tasks-calendar.js +8 -1
  150. package/src/lib/components/tasks-calendar.js.map +1 -1
  151. package/src/lib/components/tasks-section.d.ts +3 -3
  152. package/src/lib/components/tasks-section.js +58 -43
  153. package/src/lib/components/tasks-section.js.map +1 -1
  154. package/src/lib/constants/api-routes.d.ts +2 -9
  155. package/src/lib/constants/api-routes.js +1 -7
  156. package/src/lib/constants/api-routes.js.map +1 -1
  157. package/src/lib/constants/bundle-common.d.ts +6 -0
  158. package/src/lib/constants/bundle-common.js +11 -0
  159. package/src/lib/constants/bundle-common.js.map +1 -1
  160. package/src/lib/constants/company-filters.d.ts +14 -0
  161. package/src/lib/constants/company-filters.js +51 -1
  162. package/src/lib/constants/company-filters.js.map +1 -1
  163. package/src/lib/constants/contact-filters.d.ts +2 -0
  164. package/src/lib/constants/contact-filters.js +18 -1
  165. package/src/lib/constants/contact-filters.js.map +1 -1
  166. package/src/lib/constants/deal-filters.d.ts +22 -1
  167. package/src/lib/constants/deal-filters.js +67 -4
  168. package/src/lib/constants/deal-filters.js.map +1 -1
  169. package/src/lib/declarations.console-server.d.ts +3 -0
  170. package/src/lib/declarations.console-server.js +35 -0
  171. package/src/lib/declarations.console-server.js.map +1 -1
  172. package/src/lib/declarations.d.ts +26 -0
  173. package/src/lib/declarations.js +35 -0
  174. package/src/lib/declarations.js.map +1 -0
  175. package/src/lib/declarations.server.d.ts +11 -0
  176. package/src/lib/declarations.server.js +97 -1
  177. package/src/lib/declarations.server.js.map +1 -1
  178. package/src/lib/hooks/use-company-picklists.d.ts +15 -0
  179. package/src/lib/hooks/use-company-picklists.js +55 -0
  180. package/src/lib/hooks/use-company-picklists.js.map +1 -0
  181. package/src/lib/hooks/use-crm-picklist.d.ts +25 -0
  182. package/src/lib/hooks/use-crm-picklist.js +53 -0
  183. package/src/lib/hooks/use-crm-picklist.js.map +1 -0
  184. package/src/lib/hooks/use-crm-record-names.d.ts +16 -0
  185. package/src/lib/hooks/use-crm-record-names.js +45 -19
  186. package/src/lib/hooks/use-crm-record-names.js.map +1 -1
  187. package/src/lib/hooks/use-crm-task-picklists.d.ts +11 -0
  188. package/src/lib/hooks/use-crm-task-picklists.js +45 -0
  189. package/src/lib/hooks/use-crm-task-picklists.js.map +1 -0
  190. package/src/lib/hooks/use-deals.d.ts +14 -0
  191. package/src/lib/hooks/use-deals.js +29 -0
  192. package/src/lib/hooks/use-deals.js.map +1 -1
  193. package/src/lib/hooks/use-lead-picklists.d.ts +14 -0
  194. package/src/lib/hooks/use-lead-picklists.js +46 -0
  195. package/src/lib/hooks/use-lead-picklists.js.map +1 -0
  196. package/src/lib/hooks/use-lead-source-picklist.d.ts +5 -19
  197. package/src/lib/hooks/use-lead-source-picklist.js +12 -22
  198. package/src/lib/hooks/use-lead-source-picklist.js.map +1 -1
  199. package/src/lib/hooks/use-lead-status-picklist.d.ts +9 -0
  200. package/src/lib/hooks/use-lead-status-picklist.js +40 -0
  201. package/src/lib/hooks/use-lead-status-picklist.js.map +1 -0
  202. package/src/lib/model/companies.d.ts +32 -3
  203. package/src/lib/model/companies.js +133 -16
  204. package/src/lib/model/companies.js.map +1 -1
  205. package/src/lib/model/contact-merge.d.ts +27 -3
  206. package/src/lib/model/contact-merge.js +81 -7
  207. package/src/lib/model/contact-merge.js.map +1 -1
  208. package/src/lib/model/contact-record.d.ts +17 -0
  209. package/src/lib/model/contact-record.js +17 -2
  210. package/src/lib/model/contact-record.js.map +1 -1
  211. package/src/lib/model/contact-update.d.ts +31 -2
  212. package/src/lib/model/contact-update.js +14 -0
  213. package/src/lib/model/contact-update.js.map +1 -1
  214. package/src/lib/model/crm-list-query.d.ts +5 -0
  215. package/src/lib/model/crm-list-query.js +8 -0
  216. package/src/lib/model/crm-list-query.js.map +1 -1
  217. package/src/lib/model/crm-next-activity.d.ts +107 -0
  218. package/src/lib/model/crm-next-activity.js +140 -0
  219. package/src/lib/model/crm-next-activity.js.map +1 -0
  220. package/src/lib/model/crm-recipes.d.ts +40 -0
  221. package/src/lib/model/crm-recipes.js +259 -0
  222. package/src/lib/model/crm-recipes.js.map +1 -0
  223. package/src/lib/model/crm-record-counts.d.ts +36 -0
  224. package/src/lib/model/crm-record-counts.js +54 -0
  225. package/src/lib/model/crm-record-counts.js.map +1 -0
  226. package/src/lib/model/crm-record-lists.d.ts +23 -0
  227. package/src/lib/model/crm-record-lists.js +161 -0
  228. package/src/lib/model/crm-record-lists.js.map +1 -0
  229. package/src/lib/model/crm-reports.d.ts +73 -3
  230. package/src/lib/model/crm-reports.js +120 -1
  231. package/src/lib/model/crm-reports.js.map +1 -1
  232. package/src/lib/model/crm-sharing.d.ts +21 -3
  233. package/src/lib/model/crm-sharing.js +27 -6
  234. package/src/lib/model/crm-sharing.js.map +1 -1
  235. package/src/lib/model/crm-task-reminders.d.ts +152 -0
  236. package/src/lib/model/crm-task-reminders.js +161 -0
  237. package/src/lib/model/crm-task-reminders.js.map +1 -0
  238. package/src/lib/model/deal-board-model.d.ts +8 -2
  239. package/src/lib/model/deal-board-model.js +15 -2
  240. package/src/lib/model/deal-board-model.js.map +1 -1
  241. package/src/lib/model/deal-contact-roles.d.ts +45 -0
  242. package/src/lib/model/deal-contact-roles.js +73 -0
  243. package/src/lib/model/deal-contact-roles.js.map +1 -0
  244. package/src/lib/model/deal-form-model.d.ts +56 -0
  245. package/src/lib/model/deal-form-model.js +79 -7
  246. package/src/lib/model/deal-form-model.js.map +1 -1
  247. package/src/lib/model/download-text-file.d.ts +23 -0
  248. package/src/lib/model/download-text-file.js +34 -0
  249. package/src/lib/model/download-text-file.js.map +1 -0
  250. package/src/lib/model/lead-filters.d.ts +36 -2
  251. package/src/lib/model/lead-filters.js +123 -2
  252. package/src/lib/model/lead-filters.js.map +1 -1
  253. package/src/lib/model/lead-source-direction.d.ts +38 -0
  254. package/src/lib/model/lead-source-direction.js +52 -0
  255. package/src/lib/model/lead-source-direction.js.map +1 -0
  256. package/src/lib/model/next-activity.d.ts +1 -1
  257. package/src/lib/model/next-activity.js.map +1 -1
  258. package/src/lib/model/picklist-values.d.ts +119 -0
  259. package/src/lib/model/picklist-values.js +256 -0
  260. package/src/lib/model/picklist-values.js.map +1 -0
  261. package/src/lib/model/record-facts-disclosed.d.ts +81 -0
  262. package/src/lib/model/record-facts-disclosed.js +281 -0
  263. package/src/lib/model/record-facts-disclosed.js.map +1 -0
  264. package/src/lib/model/record-facts.d.ts +215 -34
  265. package/src/lib/model/record-facts.js +307 -128
  266. package/src/lib/model/record-facts.js.map +1 -1
  267. package/src/lib/model/task-picklists.d.ts +52 -0
  268. package/src/lib/model/task-picklists.js +83 -0
  269. package/src/lib/model/task-picklists.js.map +1 -0
  270. package/src/lib/model/task-routes.d.ts +13 -0
  271. package/src/lib/model/task-routes.js +23 -4
  272. package/src/lib/model/task-routes.js.map +1 -1
  273. package/src/lib/plan-entitlements.js +6 -4
  274. package/src/lib/plan-entitlements.js.map +1 -1
  275. package/src/lib/plugin.js +17 -0
  276. package/src/lib/plugin.js.map +1 -1
  277. package/src/lib/server/api-v1/contacts.js +220 -14
  278. package/src/lib/server/api-v1/contacts.js.map +1 -1
  279. package/src/lib/server/api-v1/crm-activities.js +14 -1
  280. package/src/lib/server/api-v1/crm-activities.js.map +1 -1
  281. package/src/lib/server/api-v1/crm-companies.js +123 -14
  282. package/src/lib/server/api-v1/crm-companies.js.map +1 -1
  283. package/src/lib/server/api-v1/crm-deals.js +291 -26
  284. package/src/lib/server/api-v1/crm-deals.js.map +1 -1
  285. package/src/lib/server/api-v1/crm-leads.js +144 -38
  286. package/src/lib/server/api-v1/crm-leads.js.map +1 -1
  287. package/src/lib/server/api-v1/crm-pipelines.d.ts +6 -0
  288. package/src/lib/server/api-v1/crm-pipelines.js +8 -2
  289. package/src/lib/server/api-v1/crm-pipelines.js.map +1 -1
  290. package/src/lib/server/api-v1/crm-tasks.js +78 -34
  291. package/src/lib/server/api-v1/crm-tasks.js.map +1 -1
  292. package/src/lib/server/api-v1/openapi.d.ts +0 -11
  293. package/src/lib/server/api-v1/openapi.js +171 -16
  294. package/src/lib/server/api-v1/openapi.js.map +1 -1
  295. package/src/lib/server/assign-contact-owner.d.ts +188 -0
  296. package/src/lib/server/assign-contact-owner.js +446 -0
  297. package/src/lib/server/assign-contact-owner.js.map +1 -0
  298. package/src/lib/server/associate-company-by-domain.d.ts +70 -0
  299. package/src/lib/server/associate-company-by-domain.js +153 -0
  300. package/src/lib/server/associate-company-by-domain.js.map +1 -0
  301. package/src/lib/server/automation-steps.d.ts +140 -0
  302. package/src/lib/server/automation-steps.js +669 -0
  303. package/src/lib/server/automation-steps.js.map +1 -0
  304. package/src/lib/server/capture-contact.d.ts +17 -0
  305. package/src/lib/server/capture-contact.js +95 -26
  306. package/src/lib/server/capture-contact.js.map +1 -1
  307. package/src/lib/server/capture-host-contact.d.ts +104 -0
  308. package/src/lib/server/capture-host-contact.js +151 -0
  309. package/src/lib/server/capture-host-contact.js.map +1 -0
  310. package/src/lib/server/company-children.d.ts +25 -0
  311. package/src/lib/server/company-children.js +42 -0
  312. package/src/lib/server/company-children.js.map +1 -0
  313. package/src/lib/server/company-delete.d.ts +18 -0
  314. package/src/lib/server/company-delete.js +44 -21
  315. package/src/lib/server/company-delete.js.map +1 -1
  316. package/src/lib/server/contact-email-history.js +21 -19
  317. package/src/lib/server/contact-email-history.js.map +1 -1
  318. package/src/lib/server/contact-remove.js +27 -2
  319. package/src/lib/server/contact-remove.js.map +1 -1
  320. package/src/lib/server/contact-reports-to.d.ts +61 -0
  321. package/src/lib/server/contact-reports-to.js +114 -0
  322. package/src/lib/server/contact-reports-to.js.map +1 -0
  323. package/src/lib/server/contact-update.d.ts +42 -1
  324. package/src/lib/server/contact-update.js +204 -23
  325. package/src/lib/server/contact-update.js.map +1 -1
  326. package/src/lib/server/convert-host-lead.d.ts +46 -1
  327. package/src/lib/server/convert-host-lead.js +173 -15
  328. package/src/lib/server/convert-host-lead.js.map +1 -1
  329. package/src/lib/server/convert-open-lead.js +12 -5
  330. package/src/lib/server/convert-open-lead.js.map +1 -1
  331. package/src/lib/server/crm-email-activity.d.ts +118 -0
  332. package/src/lib/server/crm-email-activity.js +148 -0
  333. package/src/lib/server/crm-email-activity.js.map +1 -0
  334. package/src/lib/server/crm-inbound-email.js +1 -1
  335. package/src/lib/server/crm-inbound-email.js.map +1 -1
  336. package/src/lib/server/crm-next-activity.d.ts +74 -0
  337. package/src/lib/server/crm-next-activity.js +194 -0
  338. package/src/lib/server/crm-next-activity.js.map +1 -0
  339. package/src/lib/server/crm-record-indexes.d.ts +45 -0
  340. package/src/lib/server/crm-record-indexes.js +78 -0
  341. package/src/lib/server/crm-record-indexes.js.map +1 -0
  342. package/src/lib/server/crm-sharing.d.ts +9 -1
  343. package/src/lib/server/crm-sharing.js +23 -3
  344. package/src/lib/server/crm-sharing.js.map +1 -1
  345. package/src/lib/server/deal-contact-roles.d.ts +27 -0
  346. package/src/lib/server/deal-contact-roles.js +89 -0
  347. package/src/lib/server/deal-contact-roles.js.map +1 -0
  348. package/src/lib/server/email-send.js +2 -1
  349. package/src/lib/server/email-send.js.map +1 -1
  350. package/src/lib/server/lead-convert.d.ts +5 -0
  351. package/src/lib/server/lead-convert.js +19 -3
  352. package/src/lib/server/lead-convert.js.map +1 -1
  353. package/src/lib/server/lead-create.d.ts +17 -2
  354. package/src/lib/server/lead-create.js +46 -14
  355. package/src/lib/server/lead-create.js.map +1 -1
  356. package/src/lib/server/lead-nurturing.d.ts +6 -0
  357. package/src/lib/server/lead-nurturing.js +57 -20
  358. package/src/lib/server/lead-nurturing.js.map +1 -1
  359. package/src/lib/server/lead-picklists.d.ts +35 -0
  360. package/src/lib/server/lead-picklists.js +52 -0
  361. package/src/lib/server/lead-picklists.js.map +1 -0
  362. package/src/lib/server/lead-source-picklist.d.ts +1 -13
  363. package/src/lib/server/lead-source-picklist.js +6 -33
  364. package/src/lib/server/lead-source-picklist.js.map +1 -1
  365. package/src/lib/server/merge-contacts.js +23 -5
  366. package/src/lib/server/merge-contacts.js.map +1 -1
  367. package/src/lib/server/next-activity-routes.js +2 -1
  368. package/src/lib/server/next-activity-routes.js.map +1 -1
  369. package/src/lib/server/person-eraser.d.ts +53 -0
  370. package/src/lib/server/person-eraser.js +149 -0
  371. package/src/lib/server/person-eraser.js.map +1 -0
  372. package/src/lib/server/person-records.d.ts +69 -0
  373. package/src/lib/server/person-records.js +317 -0
  374. package/src/lib/server/person-records.js.map +1 -0
  375. package/src/lib/server/person-refund.d.ts +97 -0
  376. package/src/lib/server/person-refund.js +221 -0
  377. package/src/lib/server/person-refund.js.map +1 -0
  378. package/src/lib/server/picklist-values.d.ts +21 -0
  379. package/src/lib/server/picklist-values.js +308 -0
  380. package/src/lib/server/picklist-values.js.map +1 -0
  381. package/src/lib/server/read-picklist.d.ts +44 -0
  382. package/src/lib/server/read-picklist.js +65 -0
  383. package/src/lib/server/read-picklist.js.map +1 -0
  384. package/src/lib/server/recipe-routes.js +22 -8
  385. package/src/lib/server/recipe-routes.js.map +1 -1
  386. package/src/lib/server/record-email-state.js +7 -1
  387. package/src/lib/server/record-email-state.js.map +1 -1
  388. package/src/lib/server/record-facts.d.ts +3 -0
  389. package/src/lib/server/record-facts.js +233 -31
  390. package/src/lib/server/record-facts.js.map +1 -1
  391. package/src/lib/server/record-origin.d.ts +27 -0
  392. package/src/lib/server/record-origin.js +104 -0
  393. package/src/lib/server/record-origin.js.map +1 -0
  394. package/src/lib/server/record-timeline.js +40 -3
  395. package/src/lib/server/record-timeline.js.map +1 -1
  396. package/src/lib/server/suite-gate.js +4 -3
  397. package/src/lib/server/suite-gate.js.map +1 -1
  398. package/src/lib/server/task-reminders-route.js +2 -1
  399. package/src/lib/server/task-reminders-route.js.map +1 -1
  400. package/src/lib/server/task-routes.js +33 -17
  401. package/src/lib/server/task-routes.js.map +1 -1
  402. package/src/lib/server-deal-stage.d.ts +5 -1
  403. package/src/lib/server-deal-stage.js +10 -5
  404. package/src/lib/server-deal-stage.js.map +1 -1
  405. package/src/lib/server.d.ts +8 -3
  406. package/src/lib/server.js +47 -43
  407. package/src/lib/server.js.map +1 -1
  408. package/src/lib/testing/memory-firestore.d.ts +55 -0
  409. package/src/lib/testing/memory-firestore.js +335 -0
  410. package/src/lib/testing/memory-firestore.js.map +1 -0
  411. package/src/lib/testing/stand-in-email-send-index.d.ts +25 -0
  412. package/src/lib/testing/stand-in-email-send-index.js +49 -0
  413. package/src/lib/testing/stand-in-email-send-index.js.map +1 -0
  414. package/src/lib/transfer/activities.d.ts +30 -0
  415. package/src/lib/transfer/activities.js +132 -0
  416. package/src/lib/transfer/activities.js.map +1 -0
  417. package/src/lib/transfer/aliases.d.ts +22 -0
  418. package/src/lib/transfer/aliases.js +1342 -0
  419. package/src/lib/transfer/aliases.js.map +1 -0
  420. package/src/lib/transfer/common.d.ts +222 -0
  421. package/src/lib/transfer/common.js +832 -0
  422. package/src/lib/transfer/common.js.map +1 -0
  423. package/src/lib/transfer/companies.d.ts +43 -0
  424. package/src/lib/transfer/companies.js +549 -0
  425. package/src/lib/transfer/companies.js.map +1 -0
  426. package/src/lib/transfer/contacts.d.ts +31 -0
  427. package/src/lib/transfer/contacts.js +727 -0
  428. package/src/lib/transfer/contacts.js.map +1 -0
  429. package/src/lib/transfer/deals.d.ts +66 -0
  430. package/src/lib/transfer/deals.js +698 -0
  431. package/src/lib/transfer/deals.js.map +1 -0
  432. package/src/lib/transfer/email-templates-package.d.ts +40 -0
  433. package/src/lib/transfer/email-templates-package.js +76 -0
  434. package/src/lib/transfer/email-templates-package.js.map +1 -0
  435. package/src/lib/transfer/email-templates-package.server.d.ts +32 -0
  436. package/src/lib/transfer/email-templates-package.server.js +176 -0
  437. package/src/lib/transfer/email-templates-package.server.js.map +1 -0
  438. package/src/lib/transfer/fields.d.ts +102 -0
  439. package/src/lib/transfer/fields.js +1755 -0
  440. package/src/lib/transfer/fields.js.map +1 -0
  441. package/src/lib/transfer/leads.d.ts +30 -0
  442. package/src/lib/transfer/leads.js +582 -0
  443. package/src/lib/transfer/leads.js.map +1 -0
  444. package/src/lib/transfer/pipelines.d.ts +21 -0
  445. package/src/lib/transfer/pipelines.js +172 -0
  446. package/src/lib/transfer/pipelines.js.map +1 -0
  447. package/src/lib/transfer/register.d.ts +47 -0
  448. package/src/lib/transfer/register.js +277 -0
  449. package/src/lib/transfer/register.js.map +1 -0
  450. package/src/lib/transfer/tasks.d.ts +30 -0
  451. package/src/lib/transfer/tasks.js +418 -0
  452. package/src/lib/transfer/tasks.js.map +1 -0
  453. package/src/lib/transfer/ui.d.ts +20 -0
  454. package/src/lib/transfer/ui.js +42 -0
  455. package/src/lib/transfer/ui.js.map +1 -0
  456. package/src/lib/components/company-import-drawer.d.ts +0 -42
  457. package/src/lib/components/company-import-drawer.js +0 -99
  458. package/src/lib/components/company-import-drawer.js.map +0 -1
  459. package/src/lib/components/contact-import-drawer.d.ts +0 -46
  460. package/src/lib/components/contact-import-drawer.js +0 -149
  461. package/src/lib/components/contact-import-drawer.js.map +0 -1
  462. package/src/lib/components/crm-export-all-button.d.ts +0 -39
  463. package/src/lib/components/crm-export-all-button.js +0 -98
  464. package/src/lib/components/crm-export-all-button.js.map +0 -1
  465. package/src/lib/components/csv-import-drawer.d.ts +0 -139
  466. package/src/lib/components/csv-import-drawer.js +0 -757
  467. package/src/lib/components/csv-import-drawer.js.map +0 -1
  468. package/src/lib/components/deal-import-drawer.d.ts +0 -24
  469. package/src/lib/components/deal-import-drawer.js +0 -83
  470. package/src/lib/components/deal-import-drawer.js.map +0 -1
  471. package/src/lib/components/lead-import-drawer.d.ts +0 -27
  472. package/src/lib/components/lead-import-drawer.js +0 -118
  473. package/src/lib/components/lead-import-drawer.js.map +0 -1
  474. package/src/lib/components/lead-source-values-card.d.ts +0 -27
  475. package/src/lib/components/lead-source-values-card.js.map +0 -1
  476. package/src/lib/components/task-import-drawer.d.ts +0 -24
  477. package/src/lib/components/task-import-drawer.js +0 -82
  478. package/src/lib/components/task-import-drawer.js.map +0 -1
  479. package/src/lib/model/companies-csv.d.ts +0 -36
  480. package/src/lib/model/companies-csv.js +0 -38
  481. package/src/lib/model/companies-csv.js.map +0 -1
  482. package/src/lib/model/contacts-csv.d.ts +0 -54
  483. package/src/lib/model/contacts-csv.js +0 -67
  484. package/src/lib/model/contacts-csv.js.map +0 -1
  485. package/src/lib/model/crm-company-import.d.ts +0 -154
  486. package/src/lib/model/crm-company-import.js +0 -331
  487. package/src/lib/model/crm-company-import.js.map +0 -1
  488. package/src/lib/model/crm-csv.d.ts +0 -239
  489. package/src/lib/model/crm-csv.js +0 -467
  490. package/src/lib/model/crm-csv.js.map +0 -1
  491. package/src/lib/model/crm-deal-import.d.ts +0 -112
  492. package/src/lib/model/crm-deal-import.js +0 -263
  493. package/src/lib/model/crm-deal-import.js.map +0 -1
  494. package/src/lib/model/crm-import.d.ts +0 -250
  495. package/src/lib/model/crm-import.js +0 -467
  496. package/src/lib/model/crm-import.js.map +0 -1
  497. package/src/lib/model/crm-lead-import.d.ts +0 -138
  498. package/src/lib/model/crm-lead-import.js +0 -446
  499. package/src/lib/model/crm-lead-import.js.map +0 -1
  500. package/src/lib/model/crm-task-import.d.ts +0 -94
  501. package/src/lib/model/crm-task-import.js +0 -275
  502. package/src/lib/model/crm-task-import.js.map +0 -1
  503. package/src/lib/model/deals-csv.d.ts +0 -32
  504. package/src/lib/model/deals-csv.js +0 -36
  505. package/src/lib/model/deals-csv.js.map +0 -1
  506. package/src/lib/model/import-column-shapes.d.ts +0 -36
  507. package/src/lib/model/import-column-shapes.js +0 -76
  508. package/src/lib/model/import-column-shapes.js.map +0 -1
  509. package/src/lib/model/lead-source-values.d.ts +0 -82
  510. package/src/lib/model/lead-source-values.js +0 -149
  511. package/src/lib/model/lead-source-values.js.map +0 -1
  512. package/src/lib/model/leads-csv.d.ts +0 -40
  513. package/src/lib/model/leads-csv.js +0 -40
  514. package/src/lib/model/leads-csv.js.map +0 -1
  515. package/src/lib/model/tasks-csv.d.ts +0 -28
  516. package/src/lib/model/tasks-csv.js +0 -28
  517. package/src/lib/model/tasks-csv.js.map +0 -1
  518. package/src/lib/server/companies-import.d.ts +0 -57
  519. package/src/lib/server/companies-import.js +0 -242
  520. package/src/lib/server/companies-import.js.map +0 -1
  521. package/src/lib/server/contacts-import.d.ts +0 -109
  522. package/src/lib/server/contacts-import.js +0 -393
  523. package/src/lib/server/contacts-import.js.map +0 -1
  524. package/src/lib/server/deals-import.d.ts +0 -76
  525. package/src/lib/server/deals-import.js +0 -248
  526. package/src/lib/server/deals-import.js.map +0 -1
  527. package/src/lib/server/export-route.d.ts +0 -60
  528. package/src/lib/server/export-route.js +0 -447
  529. package/src/lib/server/export-route.js.map +0 -1
  530. package/src/lib/server/import-context.d.ts +0 -87
  531. package/src/lib/server/import-context.js +0 -182
  532. package/src/lib/server/import-context.js.map +0 -1
  533. package/src/lib/server/lead-source-values.d.ts +0 -50
  534. package/src/lib/server/lead-source-values.js +0 -191
  535. package/src/lib/server/lead-source-values.js.map +0 -1
  536. package/src/lib/server/leads-import.d.ts +0 -84
  537. package/src/lib/server/leads-import.js +0 -367
  538. package/src/lib/server/leads-import.js.map +0 -1
  539. package/src/lib/server/tasks-import.d.ts +0 -54
  540. package/src/lib/server/tasks-import.js +0 -160
  541. package/src/lib/server/tasks-import.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/email-send.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n type AglynOrgBilling,\n type AglynOrgMember,\n buildCrmEmailActivity,\n contactCaptureHostIds,\n CRM_ACTIVITY_LOG_FULL_MESSAGE,\n CRM_COLLECTIONS,\n CRM_EMAIL_BODY_MAX,\n CRM_EMAIL_CONTEXT,\n CRM_EMAIL_SUBJECT_MAX,\n type CrmActivityLink,\n crmActivityLogHasRoom,\n crmEmailDeliveryTags,\n emailStateRefusal,\n type CrmMergeContext,\n crmMergeFieldsIn,\n crmScopeTokens,\n isOrgWideMember,\n normalizeContactEmail,\n type PluginApiHandler,\n type PluginApiRequest,\n type PluginApiResponse,\n readContactFacet,\n readEmailState,\n readMarketingBasis,\n renderCrmMergeFields,\n resolveOrgEntitlements,\n visibleToHost,\n} from '@aglyn/aglyn/server'\nimport {\n isEmailConfigured,\n sendEmail,\n sendFailureReason,\n type SendEmailResult,\n} from '@aglyn/shared-util-email'\nimport {\n consentGroupForSite,\n consumeRateLimit,\n countCrmActivitiesForRecord,\n filterSendableForHost,\n firebaseAdmin,\n getHostDocAdmin,\n getOrgForHost,\n hostSendingIdentity,\n logOrgActivity,\n memberHasOrgPermission,\n newCrmActivityRef,\n orgDataCollectionForHost,\n readLeadForHost,\n readOrgEmailRamp,\n recordEmailSends,\n releaseCrmEmailSend,\n reserveCrmEmailSend,\n resolveOrgMembership,\n writeCrmEmailActivity,\n} from '@aglyn/tenant-data-admin'\nimport { isRefusedIdToken } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport {\n authorizeOrgCaller,\n type CrmRouteScope,\n orgHostIds,\n readCrmRouteScope,\n} from './org-caller'\nimport { crmSuiteRefusal } from './suite-gate'\n\n/**\n * `POST /api/crm/email-send` — one email to one person, from their record\n * (AGL-2615).\n *\n * Body: `{ hostId, contactId? | leadId? | dealId?, subject, body }`. Answers\n * `{ ok: true, activityId, to, from, logged }` once the provider accepted\n * the message, and a refusal with a `reason` the dialog can act on\n * otherwise. The subject and the body may carry merge fields —\n * `{{contact.firstName}}`, `{{deal.amount}}` (AGL-2658) — which this route\n * fills from the record before the send, so the timeline row is the letter\n * as it left; see `crm-email-templates.ts` for the grammar.\n *\n * ## What it is, and what it is not\n *\n * A message a teammate wrote to one person they have a relationship with —\n * a reply to a lead, a follow-up on a deal. It is not marketing: no\n * campaign audience, no unsubscribe pair, no frequency window, no consent\n * split. What it does owe is everything a bounce or a complaint owes the\n * shared sending domain, which is why BOTH suppression lists are consulted\n * and why a person's own stated refusal — a `declined` basis — is a hard\n * stop even though the message is not the kind of mail that basis governs.\n * The recipient asked this site not to write; the kind of writing is not\n * the point.\n *\n * ## Why the recipient is resolved here and not taken from the body\n *\n * The dialog shows the address the record carries, and this route reads it\n * again off the record the caller named, through the same visibility check\n * every other CRM route makes. A `to` in the body would make this a route\n * that sends the site's mail to any address a member types, which is a\n * different product with a different abuse surface.\n *\n * ## The organization variant (AGL-2634)\n *\n * `{ orgId, hostId?, … }` from the org-level hub. The caller is authorized\n * by the org — an org-wide member holding `data.manage` — and the record is\n * read with no site's visibility to check, because an org-wide member reads\n * every row. What the org level cannot do away with is the SITE the message\n * leaves from: the sending identity, the suppression list and the person's\n * stated refusal are each a site's. So the org variant still names one —\n * the record's own capturing site or the site the reader picked, in the\n * body; failing that the record's own site read off the document; failing\n * that the org's only site — and refuses, with a `site` reason, when the\n * org has several and none was named. The row it writes is visible to\n * whoever may see the RECORD, so an email to a person another site\n * captured shows on their timeline under that site's hub; the act is\n * logged in the org's feed.\n *\n * ## The order of the gates\n *\n * Cheap and certain first. A malformed body and a missing token cost\n * nothing; the per-user pace is one counter; the record is one read and\n * decides everything after it; the ceiling one aggregate; the two\n * suppression lists two keyed reads; the sending identity next, because it\n * is the most expensive and the one a workspace fixes once. The daily cap\n * comes LAST and is the one gate that writes before the provider is called\n * (AGL-2645): the send is reserved on today's counter inside a transaction,\n * so two reps at the last slot cannot both pass one reading of it, and a\n * message the provider refuses gives the slot back. After the provider has\n * accepted, two more things are written, neither able to fail the send:\n * the activity row and the org's cost meter.\n *\n * ## The new-workspace ramp is part of that cap, not a gate beside it\n *\n * A message from a workspace created this morning lands on the sending\n * domain every other tenant's receipts leave on, so the plan's day is held\n * to the share of it the workspace has earned — the same ramp the campaign\n * path is paced by, `readOrgEmailRamp`, taken to the allowance this surface\n * meters instead of the campaign ceiling. It is handed to the reservation\n * rather than checked before it, so the ceiling and the count it is judged\n * against come out of the one transactional read; a ramp consulted\n * separately would be a second ceiling and two reps could pass it at once.\n * A workspace past its first week is not ramped and pays nothing to find\n * that out.\n */\n\n/** How many one-to-one emails one person may send in one minute. */\nexport const CRM_EMAIL_SENDS_PER_MINUTE = 20\nconst CRM_EMAIL_RATE_WINDOW_MS = 60_000\n\n/** What the route says when the plan carries no one-to-one email at all. */\nexport const CRM_EMAIL_NOT_INCLUDED_MESSAGE =\n 'Your plan does not include one-to-one email. Upgrade in Billing to email ' +\n 'people from their records.'\n\n/** What the route says at the daily cap; `included` is the plan's figure. */\nexport function crmEmailCapReachedMessage(included: number): string {\n return (\n `Today's one-to-one email limit (${included.toLocaleString('en-US')}) is ` +\n 'reached. It resets at midnight UTC — see Billing for the count.'\n )\n}\n\n/**\n * What the route says when the NEW-WORKSPACE RAMP is the ceiling that was\n * reached, rather than the plan's own.\n *\n * The campaign path's wording, pointed at this surface: the number, what was\n * spent against it, that nothing was sent or counted, and when it lifts. A\n * workspace told only \"limit reached\" against a figure far below the one its\n * plan page advertises would read it as a billing fault.\n */\nexport function crmEmailRampReachedMessage(\n ceiling: number,\n used: number,\n): string {\n return (\n `This workspace may send ${ceiling.toLocaleString('en-US')} one-to-one ` +\n `email${ceiling === 1 ? '' : 's'} a day while it establishes a sending ` +\n `history, and has sent ${used.toLocaleString('en-US')} today. Nothing ` +\n 'has been sent and nothing has been counted — the allowance grows as ' +\n \"the workspace sends, and is the plan's in full after its first week. \" +\n 'It resets at midnight UTC.'\n )\n}\n\n/** What the route says when the ramp's own history could not be read. */\nexport const CRM_EMAIL_RAMP_UNAVAILABLE_MESSAGE =\n \"This workspace's sending history could not be read just now, so how much \" +\n 'of its first-week email allowance it has earned is not known. Nothing has ' +\n 'been sent and nothing has been counted — try again in a moment.'\n\nexport const CRM_EMAIL_SUPPRESSED_MESSAGE =\n 'This address has bounced or reported a message as spam, so it cannot be ' +\n 'emailed.'\n\nexport const CRM_EMAIL_DECLINED_MESSAGE =\n 'This person asked not to be emailed by this site.'\n\nexport const CRM_EMAIL_RATE_MESSAGE =\n 'You have sent too many emails in the last minute. Wait a moment and try again.'\n\n/** What the org variant says when no site was named and the org has several. */\nexport const CRM_EMAIL_PICK_SITE_MESSAGE =\n 'Pick the site this email leaves from — its sending address is what the ' +\n 'message is sent as.'\n\ntype Refusal = { ok: false; status: number; body: Record<string, unknown> }\n\nconst refuse = (\n status: number,\n error: string,\n extra: Record<string, unknown> = {},\n): Refusal => ({ ok: false, status, body: { error, ...extra } })\n\n/** One typed field, trimmed and bounded. */\nfunction typed(value: unknown, max: number): string {\n return String(value ?? '')\n .trim()\n .slice(0, max)\n}\n\ninterface Sender {\n ok: true\n uid: string\n /** The address replies go to — the token's, never the body's. */\n email: string\n /** The name in front of the sending address, when the token carries one. */\n name: string\n orgId: string\n org: Partial<AglynOrgBilling>\n}\n\n/**\n * Who is sending, and whether they may: a verified ID token, then\n * `data.manage` on the site's org through the same three-layer read the\n * create route makes, admitted only for a site the member reaches. Staff\n * pass the permission check as they do on every CRM route, and send as\n * themselves.\n */\nasync function authorizeSender(\n req: PluginApiRequest,\n hostId: string,\n): Promise<Sender | Refusal> {\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return refuse(401, 'Unauthenticated')\n let decoded: { uid: string; email?: string; name?: string; staff?: unknown }\n try {\n decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n } catch (error) {\n // A refused credential is the caller's 401; a failure to check one is\n // ours and keeps a 5xx (AGL-2852).\n if (isRefusedIdToken(error)) return refuse(401, 'Unauthenticated')\n console.error('[crm] email-send could not verify the sender', error)\n return refuse(500, 'The sign-in could not be checked. Try again.')\n }\n const resolved = await getOrgForHost(hostId).catch(() => null)\n if (!resolved) return refuse(404, 'Unknown site')\n const { orgId } = resolved\n const org = (resolved.org ?? {}) as Partial<AglynOrgBilling>\n if (decoded.staff !== true) {\n const membership = await resolveOrgMembership(decoded.uid, orgId).catch(\n () => null,\n )\n const member = (membership?.member ?? null) as Partial<AglynOrgMember> | null\n const reaches =\n isOrgWideMember(member) || Boolean(member?.hostAccess?.[hostId])\n const allowed =\n member &&\n reaches &&\n (await memberHasOrgPermission(orgId, member, 'data.manage'))\n if (!allowed) {\n return refuse(\n 403,\n 'Sending email from a record requires the data.manage permission on this site',\n )\n }\n }\n const email = normalizeContactEmail(decoded.email)\n if (!email) {\n // A reply-to is owed and the token is the only place it can honestly\n // come from; an account with no address cannot be replied to.\n return refuse(403, 'Your account has no email address to receive replies at.')\n }\n return {\n ok: true,\n uid: decoded.uid,\n email,\n name: typed(decoded.name, 120),\n orgId,\n org,\n }\n}\n\n/**\n * The org variant's sender (AGL-2634): authorized by the org for an\n * org-wide member holding `data.manage`, with the same reply-to rule.\n */\nasync function authorizeOrgSender(\n req: PluginApiRequest,\n orgId: string,\n): Promise<Sender | Refusal> {\n const caller = await authorizeOrgCaller(req, orgId, {\n needs: 'data.manage',\n refusal:\n 'Sending email at the organization level requires the data.manage ' +\n 'permission across the whole workspace',\n })\n if (caller.ok === false) return refuse(caller.status, caller.error)\n const email = normalizeContactEmail(caller.email)\n if (!email) {\n return refuse(403, 'Your account has no email address to receive replies at.')\n }\n return {\n ok: true,\n uid: caller.uid,\n email,\n name: caller.name,\n orgId,\n org: caller.org as Partial<AglynOrgBilling>,\n }\n}\n\ninterface Recipient {\n ok: true\n email: string\n /** The document the consent basis is read off — the contact, or the lead. */\n record: Record<string, unknown>\n /** The contact document, when one was read — where the company link is. */\n contact: Record<string, unknown> | null\n link: CrmActivityLink\n /**\n * The site the record itself names — a contact's first capturing site, a\n * deal's own, a lead's — which is the site an org-level send leaves from\n * when the body named none.\n */\n siteHint: string\n /** The deal document, when one was named — what `{{deal.*}}` reads (AGL-2658). */\n deal: Record<string, unknown> | null\n /** The lead document, when the recipient is a lead — what `{{lead.*}}` reads. */\n lead: Record<string, unknown> | null\n}\n\n/**\n * The person the named record is about, as THIS site may see them.\n *\n * A deal names its contact; a lead carries its own address; a contact is\n * itself. Each is looked up by id and then checked against what the site\n * may read — the Admin SDK evaluates no rules, and a member of site A must\n * not be able to mail site B's contact by guessing an id. The links the\n * activity is filed under are whatever resolved: a deal's email is also the\n * contact's, so it lands on both timelines.\n *\n * At the organization level there is no site to check against and none is\n * checked: the caller was admitted as an org-wide member, who reads every\n * row. A lead still needs its site named — a lead lives under one.\n */\nasync function resolveRecipient(\n firestore: FirebaseFirestore.Firestore,\n scope: CrmRouteScope,\n orgId: string,\n ids: { contactId: string; leadId: string; dealId: string },\n): Promise<Recipient | Refusal> {\n const link: CrmActivityLink = {}\n const { hostId } = scope\n const scoped = scope.level === 'site'\n const visible = (tokens: unknown) =>\n !scoped || visibleToHost(tokens as readonly string[], hostId)\n let contactId = ids.contactId\n let siteHint = ''\n let dealRecord: Record<string, unknown> | null = null\n\n if (ids.dealId) {\n const deal = await firestore\n .collection('orgs')\n .doc(orgId)\n .collection(CRM_COLLECTIONS.deals)\n .doc(ids.dealId)\n .get()\n if (!deal.exists || !visible(deal.get('visibleTo'))) {\n return refuse(404, 'Unknown deal')\n }\n link.dealId = deal.id\n dealRecord = (deal.data() ?? {}) as Record<string, unknown>\n siteHint = typed(deal.get('hostId'), 128)\n contactId = contactId || String(deal.get('contactId') ?? '').trim()\n if (!contactId) {\n return refuse(400, 'This deal names no contact to email.')\n }\n }\n\n if (contactId) {\n const contacts = scoped\n ? await orgDataCollectionForHost(hostId, 'contacts')\n : firestore.collection('orgs').doc(orgId).collection('contacts')\n const contact = await contacts.doc(contactId).get()\n if (!contact.exists || !visible(contact.get('visibleTo'))) {\n return refuse(404, 'Unknown contact')\n }\n const email = normalizeContactEmail(contact.get('email'))\n if (!email) return refuse(400, 'This contact has no email address.')\n const record = (contact.data() ?? {}) as Record<string, unknown>\n link.contactId = contact.id\n if (ids.leadId) link.leadId = ids.leadId\n siteHint =\n siteHint || typed(record['hostId'], 128) || contactCaptureHostIds(record)[0] || ''\n return { ok: true, email, record, contact: record, link, siteHint, deal: dealRecord, lead: null }\n }\n\n if (ids.leadId) {\n if (!hostId) return refuse(400, 'Name the site the lead lives under.')\n const lead = await readLeadForHost(hostId, ids.leadId)\n if (!lead) return refuse(404, 'Unknown lead')\n const email = normalizeContactEmail(lead.get('email'))\n if (!email) return refuse(400, 'This lead has no email address.')\n const record = (lead.data() ?? {}) as Record<string, unknown>\n link.leadId = lead.id\n // A converted lead's email belongs on the contact it became as well.\n const converted = String(lead.get('convertedContactId') ?? '').trim()\n if (converted) link.contactId = converted\n return { ok: true, email, record, contact: null, link, siteHint: hostId, deal: null, lead: record }\n }\n\n return refuse(400, 'Name a contact, lead or deal to email.')\n}\n\n/**\n * The site an org-level send leaves from (AGL-2634): the body's, else the\n * record's own, else the org's only site — and a refusal the dialog turns\n * into a Site picker when the org has several and none was named.\n */\nasync function resolveSendingSite(\n firestore: FirebaseFirestore.Firestore,\n scope: CrmRouteScope,\n orgId: string,\n siteHint: string,\n): Promise<{ ok: true; hostId: string } | Refusal> {\n if (scope.hostId) return { ok: true, hostId: scope.hostId }\n if (siteHint) return { ok: true, hostId: siteHint }\n const hosts = await orgHostIds(firestore, orgId)\n if (hosts.length === 1) return { ok: true, hostId: hosts[0] }\n return refuse(409, CRM_EMAIL_PICK_SITE_MESSAGE, { reason: 'site' })\n}\n\n/** The HTTP shape of a send the provider did not accept. */\nfunction sendRefusal(result: SendEmailResult): Refusal {\n const reason = sendFailureReason(result)\n const detail = String((result as { detail?: unknown }).detail ?? '')\n if (reason === 'unverified-domain') {\n return refuse(409, detail || 'This site has no verified sending identity.', {\n reason: 'sending-identity',\n })\n }\n if (reason === 'suppressed' || reason === 'unengaged') {\n return refuse(409, CRM_EMAIL_SUPPRESSED_MESSAGE, { reason: 'suppressed' })\n }\n // The deliverability preflight (AGL-3328): the recipient's domain takes\n // no mail. A standing fact about the address, said in the engine's words\n // — not a provider fault the rep should retry.\n if (reason === 'undeliverable' || reason === 'gateway-held') {\n return refuse(409, detail || 'This address cannot receive email.', { reason: 'undeliverable' })\n }\n if (reason === 'rate-limited') {\n const retryAtMs = Number((result as { retryAtMs?: unknown }).retryAtMs)\n return refuse(\n 503,\n 'The mail provider asked us to slow down. Try again in a minute.',\n {\n reason: 'provider-rate',\n retryAfterSeconds: Number.isFinite(retryAtMs)\n ? Math.max(1, Math.ceil((retryAtMs - Date.now()) / 1000))\n : 60,\n },\n )\n }\n if (reason === 'unconfigured') {\n return refuse(501, 'Email is not configured on this deployment.', {\n reason: 'unconfigured',\n })\n }\n return refuse(502, 'The email could not be sent.', { reason: 'send-failed' })\n}\n\n/** What the route says when a subject or a message is nothing once its fields are filled. */\nexport const CRM_EMAIL_EMPTY_AFTER_MERGE_MESSAGE =\n 'The subject and the message are empty once their merge fields are filled ' +\n 'in. Check the fields against this record.'\n\n/**\n * What `{{…}}` reads from, for this send (AGL-2658): the documents already\n * in hand — the contact, the deal, the lead — the sender off their token,\n * and the site's name, read only when a field asks for it. The contact's\n * facet is the SENDING site's group's, because a name, a job title and a\n * company are one holder's knowledge of a person.\n */\nasync function mergeContextFor(options: {\n recipient: Recipient\n sender: Sender\n hostId: string\n groupId: string\n fields: readonly string[]\n}): Promise<CrmMergeContext> {\n const { recipient, sender, hostId, groupId, fields } = options\n const wantsSite = fields.some((field) => field.startsWith('site.'))\n const host = wantsSite ? await getHostDocAdmin(hostId).catch(() => null) : null\n return {\n contact: recipient.contact,\n contactGroupId: groupId,\n lead: recipient.lead,\n deal: recipient.deal,\n sender: { name: sender.name, email: sender.email },\n site: { name: typed(host?.['displayName'], 200) },\n }\n}\n\nfunction answer(res: PluginApiResponse, refusal: Refusal): void {\n const retry = refusal.body['retryAfterSeconds']\n if (typeof retry === 'number') res.setHeader('Retry-After', String(retry))\n res.status(refusal.status).json(refusal.body)\n}\n\nexport const crmEmailSendHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n res.setHeader('Allow', 'POST')\n res.status(405).json({ error: 'Method not allowed' })\n return\n }\n const body = (req.body ?? {}) as Record<string, unknown>\n const routeScope = readCrmRouteScope(body)\n if (!routeScope) {\n res.status(400).json({ error: 'Missing hostId' })\n return\n }\n const ids = {\n contactId: typed(body['contactId'], 128),\n leadId: typed(body['leadId'], 128),\n dealId: typed(body['dealId'], 128),\n }\n if (!ids.contactId && !ids.leadId && !ids.dealId) {\n res.status(400).json({ error: 'Name a contact, lead or deal to email.' })\n return\n }\n const draftSubject = typed(body['subject'], CRM_EMAIL_SUBJECT_MAX)\n if (!draftSubject) {\n res.status(400).json({ error: 'Enter a subject.' })\n return\n }\n // Paragraphs are what the dialog offers, and a plain-text part is what a\n // paragraph is; line endings are normalized so the stored body and the\n // sent body are one string.\n const draftText = String(body['body'] ?? '')\n .replace(/\\r\\n?/g, '\\n')\n .trim()\n .slice(0, CRM_EMAIL_BODY_MAX)\n if (!draftText) {\n res.status(400).json({ error: 'Write the message.' })\n return\n }\n\n try {\n const sender =\n routeScope.level === 'org'\n ? await authorizeOrgSender(req, routeScope.orgId)\n : await authorizeSender(req, routeScope.hostId)\n if (sender.ok === false) return answer(res, sender)\n const { orgId, org } = sender\n /*\n * THE PLAN, before the pace and the cap (AGL-2787). One-to-one email is\n * the suite's; a plan without the suite is refused as such, rather than\n * reaching the daily cap and being told about a zero it has no way to\n * raise short of the upgrade this names.\n */\n const suite = crmSuiteRefusal(org, 'Emailing one person from their record')\n if (suite) return answer(res, { ok: false, status: suite.status, body: suite.body })\n\n /*\n * THE PER-USER PACE, on its own key. The console dispatcher already\n * counts this write against the person's general console budget; this\n * is the narrower bucket the feature owes — twenty a minute is a rep\n * writing, and past it is a script — keyed by the uid the token proved\n * so one person cannot spend the bucket of everyone on their address.\n */\n const rate = await consumeRateLimit(`crm-email-send:uid:${sender.uid}`, {\n limit: CRM_EMAIL_SENDS_PER_MINUTE,\n windowMs: CRM_EMAIL_RATE_WINDOW_MS,\n })\n if (!rate.allowed) {\n return answer(\n res,\n refuse(429, CRM_EMAIL_RATE_MESSAGE, {\n reason: 'rate',\n retryAfterSeconds: Math.max(\n 1,\n Math.ceil((rate.resetMs - Date.now()) / 1000),\n ),\n }),\n )\n }\n\n const firestore = firebaseAdmin.app().firestore()\n const recipient = await resolveRecipient(firestore, routeScope, orgId, ids)\n if (recipient.ok === false) return answer(res, recipient)\n // The site the message leaves from — the mounted one under a site, and\n // at the organization level whatever the body, the record or the org\n // can answer.\n const site = await resolveSendingSite(firestore, routeScope, orgId, recipient.siteHint)\n if (site.ok === false) return answer(res, site)\n const { hostId } = site\n const group = await consentGroupForSite(hostId, org as Record<string, unknown>)\n // The company the sending site files this person under, so the email\n // shows on the company's log as the automation step's rows do.\n if (recipient.contact) {\n const facet = readContactFacet(recipient.contact, group.groupId)\n if (facet.companyId) recipient.link.companyId = facet.companyId\n }\n\n /*\n * MERGE FIELDS (AGL-2658), filled here rather than in the dialog: the\n * dialog previews with the same resolver, but what leaves and what is\n * logged is rendered by the route, off the documents it read to find\n * the recipient, so a preview the rep did not look at cannot differ\n * from the letter. A subject or a message that is nothing once its\n * fields are filled is refused before any gate is spent on it.\n */\n const fields = crmMergeFieldsIn(`${draftSubject}\\n${draftText}`)\n const merge = fields.length\n ? await mergeContextFor({ recipient, sender, hostId, groupId: group.groupId, fields })\n : null\n const subject = merge\n ? renderCrmMergeFields(draftSubject, merge).trim().slice(0, CRM_EMAIL_SUBJECT_MAX)\n : draftSubject\n const text = merge\n ? renderCrmMergeFields(draftText, merge).trim().slice(0, CRM_EMAIL_BODY_MAX)\n : draftText\n if (!subject || !text) {\n return answer(res, refuse(400, CRM_EMAIL_EMPTY_AFTER_MERGE_MESSAGE, { reason: 'merge' }))\n }\n\n // The per-record ceiling, before the send: an email that could not be\n // logged would be a message the timeline never shows, and the timeline\n // is the reason the send lives on the record.\n const orgRef = firestore.collection('orgs').doc(orgId)\n if (!crmActivityLogHasRoom(await countCrmActivitiesForRecord(orgRef, recipient.link))) {\n return answer(res, refuse(409, CRM_ACTIVITY_LOG_FULL_MESSAGE, { reason: 'ceiling' }))\n }\n\n // Both suppression lists, then the person's own refusal — each read\n // across the sending site's consent group, so leaving one site of a\n // declared group stops this one writing too.\n const sendable = await filterSendableForHost(\n hostId,\n [recipient.email],\n undefined,\n group,\n )\n if (!sendable.length) {\n return answer(res, refuse(409, CRM_EMAIL_SUPPRESSED_MESSAGE, { reason: 'suppressed' }))\n }\n if (readMarketingBasis(recipient.record, group).basis === 'declined') {\n return answer(res, refuse(409, CRM_EMAIL_DECLINED_MESSAGE, { reason: 'declined' }))\n }\n // The record's own verdict (AGL-3245): what the lists hold, in the words\n // the page shows — a bounce, a gateway block, a do-not-contact mark the\n // suppression lists do not carry.\n const stateRefusal = emailStateRefusal(readEmailState(recipient.record))\n if (stateRefusal) {\n return answer(res, refuse(409, stateRefusal, { reason: 'email-state' }))\n }\n\n if (!isEmailConfigured()) {\n return answer(\n res,\n refuse(501, 'Email is not configured on this deployment.', {\n reason: 'unconfigured',\n }),\n )\n }\n const identity = await hostSendingIdentity(hostId)\n if (identity.refusal) {\n return answer(\n res,\n refuse(409, identity.refusal.message, { reason: 'sending-identity' }),\n )\n }\n\n const now = new Date()\n /*\n * THE NEW-WORKSPACE RAMP, resolved for the ceiling below.\n *\n * A workspace past its first week graduates off the org record alone and\n * pays no read for this. One that is still on the ramp needs its\n * delivery history to know which step it has earned, and when that\n * history is unreadable the step is a floor rather than an answer.\n *\n * A floor is enough for a campaign, which defers what it cannot send\n * today and delivers it tomorrow. It is not enough here: this route\n * admits or refuses ONE message and has no third answer, so a ramp that\n * cannot say what a workspace has earned refuses and says so. The\n * refusal is transient by construction — it lasts as long as the window\n * read is failing — and it reaches only workspaces inside their first\n * week, since every other one graduated before the window was consulted.\n */\n const { ramp, degraded } = await readOrgEmailRamp({\n orgId,\n createdAt: (org as Record<string, unknown>)['createdAt'],\n now: now.getTime(),\n })\n if (degraded) {\n return answer(\n res,\n refuse(503, CRM_EMAIL_RAMP_UNAVAILABLE_MESSAGE, {\n reason: 'ramp-unavailable',\n }),\n )\n }\n\n // THE DAILY CAP (AGL-2611), taken as a slot rather than read as a\n // figure (AGL-2645): the transaction reads today's counter, judges it\n // with `checkCrmEmailQuota` against the ramp's share of the plan's day\n // and raises it in one step, so the count the cap enforces is the count\n // this send was admitted on.\n const claim = await reserveCrmEmailSend(firestore, orgId, org, now, ramp)\n if (!claim.ok) {\n const { quota } = claim\n // WHICH ceiling was reached, in the org's own terms. The reservation\n // answers with the number it judged, which is the plan's day unless\n // the ramp held this workspace below it — and a workspace shown a\n // figure under the one its plan page advertises is owed the reason.\n const rampBound =\n quota.included < resolveOrgEntitlements(org).crmEmailsPerDay\n return answer(\n res,\n refuse(\n 409,\n quota.included <= 0\n ? CRM_EMAIL_NOT_INCLUDED_MESSAGE\n : rampBound\n ? crmEmailRampReachedMessage(quota.included, quota.used)\n : crmEmailCapReachedMessage(quota.included),\n {\n reason: rampBound ? 'ramp' : 'quota',\n included: quota.included,\n used: quota.used,\n resetsAtMs: quota.resetsAt.getTime(),\n },\n ),\n )\n }\n\n // The row's id, minted before the send so the delivery webhook can find\n // it from the tags alone; written only once the message has left.\n const activityRef = newCrmActivityRef(firestore, orgId)\n let result: SendEmailResult\n try {\n result = await sendEmail({\n to: recipient.email,\n subject,\n text,\n sendingIdentity: identity,\n audience: 'tenant',\n context: CRM_EMAIL_CONTEXT,\n replyTo: sender.email,\n ...(sender.name ? { fromName: sender.name } : {}),\n tags: crmEmailDeliveryTags({ orgId, hostId, activityId: activityRef.id }),\n })\n } catch (error) {\n // The provider never answered, so no message left: the slot goes\n // back before the failure is reported.\n await releaseCrmEmailSend(firestore, claim.reservation)\n throw error\n }\n if (!result.sent) {\n await releaseCrmEmailSend(firestore, claim.reservation)\n return answer(res, sendRefusal(result))\n }\n\n const sentAtMs = Date.now()\n let logged = true\n /*\n * Who may list the row. Under a site, what a record created there is\n * stamped with. At the organization level the RECORD's own tokens, so\n * an email to a person another site captured shows on their timeline\n * under that site's hub — a row stamped with the sending site alone\n * would be invisible where the person is read.\n */\n const recordTokens = recipient.record['visibleTo']\n const visibleTo =\n routeScope.level === 'org' && Array.isArray(recordTokens) && recordTokens.length\n ? recordTokens.map(String)\n : crmScopeTokens(org as Record<string, unknown>, group)\n try {\n await writeCrmEmailActivity(\n activityRef,\n buildCrmEmailActivity({\n subject,\n body: text,\n to: recipient.email,\n atMs: sentAtMs,\n byUid: sender.uid,\n ...(sender.name ? { byName: sender.name } : {}),\n link: recipient.link,\n hostId,\n visibleTo,\n }),\n )\n } catch (error) {\n // The message has left. A row that could not be written is a gap on\n // the timeline, not a failed send, and it is said so in the answer.\n console.error('[crm] email activity write failed', orgId, activityRef.id, error)\n logged = false\n }\n // The cap's count was taken with the reservation; the org's cost meter\n // for COGS is stamped after delivery, unable to fail the send.\n await recordEmailSends({\n scope: { kind: 'org', orgId },\n count: 1,\n sendClass: 'transactional',\n firestore,\n })\n // The org-level act, in the org's feed: the site variant's is the\n // timeline row itself, under the site the record page is on.\n if (routeScope.level === 'org') {\n await logOrgActivity(\n orgId,\n { uid: sender.uid, email: sender.email },\n 'Sent email',\n recipient.link.contactId\n ? { type: 'contact', id: recipient.link.contactId, name: recipient.email }\n : { type: 'lead', id: recipient.link.leadId, name: recipient.email },\n )\n }\n res.status(200).json({\n ok: true,\n activityId: activityRef.id,\n to: recipient.email,\n from: identity.from,\n logged,\n })\n } catch (error) {\n console.error('[crm] email-send failed', routeScope, error)\n res.status(500).json({ error: 'The email could not be sent.' })\n }\n}\n\nexport default crmEmailSendHandler\n"],"names":["buildCrmEmailActivity","contactCaptureHostIds","CRM_ACTIVITY_LOG_FULL_MESSAGE","CRM_COLLECTIONS","CRM_EMAIL_BODY_MAX","CRM_EMAIL_CONTEXT","CRM_EMAIL_SUBJECT_MAX","crmActivityLogHasRoom","crmEmailDeliveryTags","emailStateRefusal","crmMergeFieldsIn","crmScopeTokens","isOrgWideMember","normalizeContactEmail","readContactFacet","readEmailState","readMarketingBasis","renderCrmMergeFields","resolveOrgEntitlements","visibleToHost","isEmailConfigured","sendEmail","sendFailureReason","consentGroupForSite","consumeRateLimit","countCrmActivitiesForRecord","filterSendableForHost","firebaseAdmin","getHostDocAdmin","getOrgForHost","hostSendingIdentity","logOrgActivity","memberHasOrgPermission","newCrmActivityRef","orgDataCollectionForHost","readLeadForHost","readOrgEmailRamp","recordEmailSends","releaseCrmEmailSend","reserveCrmEmailSend","resolveOrgMembership","writeCrmEmailActivity","isRefusedIdToken","authorizeOrgCaller","orgHostIds","readCrmRouteScope","crmSuiteRefusal","CRM_EMAIL_SENDS_PER_MINUTE","CRM_EMAIL_RATE_WINDOW_MS","CRM_EMAIL_NOT_INCLUDED_MESSAGE","crmEmailCapReachedMessage","included","toLocaleString","crmEmailRampReachedMessage","ceiling","used","CRM_EMAIL_RAMP_UNAVAILABLE_MESSAGE","CRM_EMAIL_SUPPRESSED_MESSAGE","CRM_EMAIL_DECLINED_MESSAGE","CRM_EMAIL_RATE_MESSAGE","CRM_EMAIL_PICK_SITE_MESSAGE","refuse","status","error","extra","ok","body","typed","value","max","String","trim","slice","authorizeSender","req","hostId","resolved","authorization","headers","idToken","startsWith","length","undefined","decoded","app","auth","verifyIdToken","console","catch","orgId","org","staff","member","membership","uid","reaches","Boolean","hostAccess","allowed","email","name","authorizeOrgSender","caller","needs","refusal","resolveRecipient","firestore","scope","ids","link","scoped","level","visible","tokens","contactId","siteHint","dealRecord","dealId","deal","collection","doc","deals","get","exists","id","data","contact","contacts","record","leadId","lead","converted","resolveSendingSite","hosts","reason","sendRefusal","result","detail","retryAtMs","Number","retryAfterSeconds","isFinite","Math","ceil","Date","now","CRM_EMAIL_EMPTY_AFTER_MERGE_MESSAGE","mergeContextFor","options","recipient","sender","groupId","fields","wantsSite","some","field","host","contactGroupId","site","answer","res","retry","setHeader","json","crmEmailSendHandler","method","routeScope","draftSubject","draftText","replace","suite","rate","limit","windowMs","resetMs","group","facet","companyId","merge","subject","text","orgRef","sendable","basis","stateRefusal","identity","message","ramp","degraded","createdAt","getTime","claim","quota","rampBound","crmEmailsPerDay","resetsAtMs","resetsAt","activityRef","to","sendingIdentity","audience","context","replyTo","fromName","tags","activityId","reservation","sent","sentAtMs","logged","recordTokens","visibleTo","Array","isArray","map","atMs","byUid","byName","kind","count","sendClass","type","from"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/email-send.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n type AglynOrgBilling,\n type AglynOrgMember,\n buildCrmEmailActivity,\n contactCaptureHostIds,\n CRM_ACTIVITY_LOG_FULL_MESSAGE,\n CRM_COLLECTIONS,\n CRM_EMAIL_BODY_MAX,\n CRM_EMAIL_CONTEXT,\n CRM_EMAIL_SUBJECT_MAX,\n type CrmActivityLink,\n crmActivityLogHasRoom,\n crmEmailDeliveryTags,\n emailStateRefusal,\n type CrmMergeContext,\n crmMergeFieldsIn,\n crmScopeTokens,\n isOrgWideMember,\n normalizeContactEmail,\n type PluginApiHandler,\n type PluginApiRequest,\n type PluginApiResponse,\n readContactFacet,\n readEmailState,\n readMarketingBasis,\n renderCrmMergeFields,\n resolveOrgEntitlements,\n visibleToHost,\n} from '@aglyn/aglyn/server'\nimport {\n isEmailConfigured,\n sendEmail,\n sendFailureReason,\n type SendEmailResult,\n} from '@aglyn/shared-util-email'\nimport {\n consentGroupForSite,\n consumeRateLimit,\n countCrmActivitiesForRecord,\n filterSendableForHost,\n firebaseAdmin,\n getHostDocAdmin,\n getOrgForHost,\n hostSendingIdentity,\n logOrgActivity,\n memberHasOrgPermission,\n orgDataCollectionForHost,\n readLeadForHost,\n readOrgEmailRamp,\n recordEmailSends,\n releaseCrmEmailSend,\n reserveCrmEmailSend,\n resolveOrgMembership,\n} from '@aglyn/tenant-data-admin'\nimport { isRefusedIdToken } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport {\n authorizeOrgCaller,\n type CrmRouteScope,\n orgHostIds,\n readCrmRouteScope,\n} from './org-caller'\nimport { crmSuiteRefusal } from './suite-gate'\nimport { newCrmActivityRef, writeCrmEmailActivity } from './crm-email-activity'\n\n/**\n * `POST /api/crm/email-send` — one email to one person, from their record\n * (AGL-2615).\n *\n * Body: `{ hostId, contactId? | leadId? | dealId?, subject, body }`. Answers\n * `{ ok: true, activityId, to, from, logged }` once the provider accepted\n * the message, and a refusal with a `reason` the dialog can act on\n * otherwise. The subject and the body may carry merge fields —\n * `{{contact.firstName}}`, `{{deal.amount}}` (AGL-2658) — which this route\n * fills from the record before the send, so the timeline row is the letter\n * as it left; see `crm-email-templates.ts` for the grammar.\n *\n * ## What it is, and what it is not\n *\n * A message a teammate wrote to one person they have a relationship with —\n * a reply to a lead, a follow-up on a deal. It is not marketing: no\n * campaign audience, no unsubscribe pair, no frequency window, no consent\n * split. What it does owe is everything a bounce or a complaint owes the\n * shared sending domain, which is why BOTH suppression lists are consulted\n * and why a person's own stated refusal — a `declined` basis — is a hard\n * stop even though the message is not the kind of mail that basis governs.\n * The recipient asked this site not to write; the kind of writing is not\n * the point.\n *\n * ## Why the recipient is resolved here and not taken from the body\n *\n * The dialog shows the address the record carries, and this route reads it\n * again off the record the caller named, through the same visibility check\n * every other CRM route makes. A `to` in the body would make this a route\n * that sends the site's mail to any address a member types, which is a\n * different product with a different abuse surface.\n *\n * ## The organization variant (AGL-2634)\n *\n * `{ orgId, hostId?, … }` from the org-level hub. The caller is authorized\n * by the org — an org-wide member holding `data.manage` — and the record is\n * read with no site's visibility to check, because an org-wide member reads\n * every row. What the org level cannot do away with is the SITE the message\n * leaves from: the sending identity, the suppression list and the person's\n * stated refusal are each a site's. So the org variant still names one —\n * the record's own capturing site or the site the reader picked, in the\n * body; failing that the record's own site read off the document; failing\n * that the org's only site — and refuses, with a `site` reason, when the\n * org has several and none was named. The row it writes is visible to\n * whoever may see the RECORD, so an email to a person another site\n * captured shows on their timeline under that site's hub; the act is\n * logged in the org's feed.\n *\n * ## The order of the gates\n *\n * Cheap and certain first. A malformed body and a missing token cost\n * nothing; the per-user pace is one counter; the record is one read and\n * decides everything after it; the ceiling one aggregate; the two\n * suppression lists two keyed reads; the sending identity next, because it\n * is the most expensive and the one a workspace fixes once. The daily cap\n * comes LAST and is the one gate that writes before the provider is called\n * (AGL-2645): the send is reserved on today's counter inside a transaction,\n * so two reps at the last slot cannot both pass one reading of it, and a\n * message the provider refuses gives the slot back. After the provider has\n * accepted, two more things are written, neither able to fail the send:\n * the activity row and the org's cost meter.\n *\n * ## The new-workspace ramp is part of that cap, not a gate beside it\n *\n * A message from a workspace created this morning lands on the sending\n * domain every other tenant's receipts leave on, so the plan's day is held\n * to the share of it the workspace has earned — the same ramp the campaign\n * path is paced by, `readOrgEmailRamp`, taken to the allowance this surface\n * meters instead of the campaign ceiling. It is handed to the reservation\n * rather than checked before it, so the ceiling and the count it is judged\n * against come out of the one transactional read; a ramp consulted\n * separately would be a second ceiling and two reps could pass it at once.\n * A workspace past its first week is not ramped and pays nothing to find\n * that out.\n */\n\n/** How many one-to-one emails one person may send in one minute. */\nexport const CRM_EMAIL_SENDS_PER_MINUTE = 20\nconst CRM_EMAIL_RATE_WINDOW_MS = 60_000\n\n/** What the route says when the plan carries no one-to-one email at all. */\nexport const CRM_EMAIL_NOT_INCLUDED_MESSAGE =\n 'Your plan does not include one-to-one email. Upgrade in Billing to email ' +\n 'people from their records.'\n\n/** What the route says at the daily cap; `included` is the plan's figure. */\nexport function crmEmailCapReachedMessage(included: number): string {\n return (\n `Today's one-to-one email limit (${included.toLocaleString('en-US')}) is ` +\n 'reached. It resets at midnight UTC — see Billing for the count.'\n )\n}\n\n/**\n * What the route says when the NEW-WORKSPACE RAMP is the ceiling that was\n * reached, rather than the plan's own.\n *\n * The campaign path's wording, pointed at this surface: the number, what was\n * spent against it, that nothing was sent or counted, and when it lifts. A\n * workspace told only \"limit reached\" against a figure far below the one its\n * plan page advertises would read it as a billing fault.\n */\nexport function crmEmailRampReachedMessage(\n ceiling: number,\n used: number,\n): string {\n return (\n `This workspace may send ${ceiling.toLocaleString('en-US')} one-to-one ` +\n `email${ceiling === 1 ? '' : 's'} a day while it establishes a sending ` +\n `history, and has sent ${used.toLocaleString('en-US')} today. Nothing ` +\n 'has been sent and nothing has been counted — the allowance grows as ' +\n \"the workspace sends, and is the plan's in full after its first week. \" +\n 'It resets at midnight UTC.'\n )\n}\n\n/** What the route says when the ramp's own history could not be read. */\nexport const CRM_EMAIL_RAMP_UNAVAILABLE_MESSAGE =\n \"This workspace's sending history could not be read just now, so how much \" +\n 'of its first-week email allowance it has earned is not known. Nothing has ' +\n 'been sent and nothing has been counted — try again in a moment.'\n\nexport const CRM_EMAIL_SUPPRESSED_MESSAGE =\n 'This address has bounced or reported a message as spam, so it cannot be ' +\n 'emailed.'\n\nexport const CRM_EMAIL_DECLINED_MESSAGE =\n 'This person asked not to be emailed by this site.'\n\nexport const CRM_EMAIL_RATE_MESSAGE =\n 'You have sent too many emails in the last minute. Wait a moment and try again.'\n\n/** What the org variant says when no site was named and the org has several. */\nexport const CRM_EMAIL_PICK_SITE_MESSAGE =\n 'Pick the site this email leaves from — its sending address is what the ' +\n 'message is sent as.'\n\ntype Refusal = { ok: false; status: number; body: Record<string, unknown> }\n\nconst refuse = (\n status: number,\n error: string,\n extra: Record<string, unknown> = {},\n): Refusal => ({ ok: false, status, body: { error, ...extra } })\n\n/** One typed field, trimmed and bounded. */\nfunction typed(value: unknown, max: number): string {\n return String(value ?? '')\n .trim()\n .slice(0, max)\n}\n\ninterface Sender {\n ok: true\n uid: string\n /** The address replies go to — the token's, never the body's. */\n email: string\n /** The name in front of the sending address, when the token carries one. */\n name: string\n orgId: string\n org: Partial<AglynOrgBilling>\n}\n\n/**\n * Who is sending, and whether they may: a verified ID token, then\n * `data.manage` on the site's org through the same three-layer read the\n * create route makes, admitted only for a site the member reaches. Staff\n * pass the permission check as they do on every CRM route, and send as\n * themselves.\n */\nasync function authorizeSender(\n req: PluginApiRequest,\n hostId: string,\n): Promise<Sender | Refusal> {\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return refuse(401, 'Unauthenticated')\n let decoded: { uid: string; email?: string; name?: string; staff?: unknown }\n try {\n decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n } catch (error) {\n // A refused credential is the caller's 401; a failure to check one is\n // ours and keeps a 5xx (AGL-2852).\n if (isRefusedIdToken(error)) return refuse(401, 'Unauthenticated')\n console.error('[crm] email-send could not verify the sender', error)\n return refuse(500, 'The sign-in could not be checked. Try again.')\n }\n const resolved = await getOrgForHost(hostId).catch(() => null)\n if (!resolved) return refuse(404, 'Unknown site')\n const { orgId } = resolved\n const org = (resolved.org ?? {}) as Partial<AglynOrgBilling>\n if (decoded.staff !== true) {\n const membership = await resolveOrgMembership(decoded.uid, orgId).catch(\n () => null,\n )\n const member = (membership?.member ?? null) as Partial<AglynOrgMember> | null\n const reaches =\n isOrgWideMember(member) || Boolean(member?.hostAccess?.[hostId])\n const allowed =\n member &&\n reaches &&\n (await memberHasOrgPermission(orgId, member, 'data.manage'))\n if (!allowed) {\n return refuse(\n 403,\n 'Sending email from a record requires the data.manage permission on this site',\n )\n }\n }\n const email = normalizeContactEmail(decoded.email)\n if (!email) {\n // A reply-to is owed and the token is the only place it can honestly\n // come from; an account with no address cannot be replied to.\n return refuse(403, 'Your account has no email address to receive replies at.')\n }\n return {\n ok: true,\n uid: decoded.uid,\n email,\n name: typed(decoded.name, 120),\n orgId,\n org,\n }\n}\n\n/**\n * The org variant's sender (AGL-2634): authorized by the org for an\n * org-wide member holding `data.manage`, with the same reply-to rule.\n */\nasync function authorizeOrgSender(\n req: PluginApiRequest,\n orgId: string,\n): Promise<Sender | Refusal> {\n const caller = await authorizeOrgCaller(req, orgId, {\n needs: 'data.manage',\n refusal:\n 'Sending email at the organization level requires the data.manage ' +\n 'permission across the whole workspace',\n })\n if (caller.ok === false) return refuse(caller.status, caller.error)\n const email = normalizeContactEmail(caller.email)\n if (!email) {\n return refuse(403, 'Your account has no email address to receive replies at.')\n }\n return {\n ok: true,\n uid: caller.uid,\n email,\n name: caller.name,\n orgId,\n org: caller.org as Partial<AglynOrgBilling>,\n }\n}\n\ninterface Recipient {\n ok: true\n email: string\n /** The document the consent basis is read off — the contact, or the lead. */\n record: Record<string, unknown>\n /** The contact document, when one was read — where the company link is. */\n contact: Record<string, unknown> | null\n link: CrmActivityLink\n /**\n * The site the record itself names — a contact's first capturing site, a\n * deal's own, a lead's — which is the site an org-level send leaves from\n * when the body named none.\n */\n siteHint: string\n /** The deal document, when one was named — what `{{deal.*}}` reads (AGL-2658). */\n deal: Record<string, unknown> | null\n /** The lead document, when the recipient is a lead — what `{{lead.*}}` reads. */\n lead: Record<string, unknown> | null\n}\n\n/**\n * The person the named record is about, as THIS site may see them.\n *\n * A deal names its contact; a lead carries its own address; a contact is\n * itself. Each is looked up by id and then checked against what the site\n * may read — the Admin SDK evaluates no rules, and a member of site A must\n * not be able to mail site B's contact by guessing an id. The links the\n * activity is filed under are whatever resolved: a deal's email is also the\n * contact's, so it lands on both timelines.\n *\n * At the organization level there is no site to check against and none is\n * checked: the caller was admitted as an org-wide member, who reads every\n * row. A lead still needs its site named — a lead lives under one.\n */\nasync function resolveRecipient(\n firestore: FirebaseFirestore.Firestore,\n scope: CrmRouteScope,\n orgId: string,\n ids: { contactId: string; leadId: string; dealId: string },\n): Promise<Recipient | Refusal> {\n const link: CrmActivityLink = {}\n const { hostId } = scope\n const scoped = scope.level === 'site'\n const visible = (tokens: unknown) =>\n !scoped || visibleToHost(tokens as readonly string[], hostId)\n let contactId = ids.contactId\n let siteHint = ''\n let dealRecord: Record<string, unknown> | null = null\n\n if (ids.dealId) {\n const deal = await firestore\n .collection('orgs')\n .doc(orgId)\n .collection(CRM_COLLECTIONS.deals)\n .doc(ids.dealId)\n .get()\n if (!deal.exists || !visible(deal.get('visibleTo'))) {\n return refuse(404, 'Unknown deal')\n }\n link.dealId = deal.id\n dealRecord = (deal.data() ?? {}) as Record<string, unknown>\n siteHint = typed(deal.get('hostId'), 128)\n contactId = contactId || String(deal.get('contactId') ?? '').trim()\n if (!contactId) {\n return refuse(400, 'This deal names no contact to email.')\n }\n }\n\n if (contactId) {\n const contacts = scoped\n ? await orgDataCollectionForHost(hostId, 'contacts')\n : firestore.collection('orgs').doc(orgId).collection('contacts')\n const contact = await contacts.doc(contactId).get()\n if (!contact.exists || !visible(contact.get('visibleTo'))) {\n return refuse(404, 'Unknown contact')\n }\n const email = normalizeContactEmail(contact.get('email'))\n if (!email) return refuse(400, 'This contact has no email address.')\n const record = (contact.data() ?? {}) as Record<string, unknown>\n link.contactId = contact.id\n if (ids.leadId) link.leadId = ids.leadId\n siteHint =\n siteHint || typed(record['hostId'], 128) || contactCaptureHostIds(record)[0] || ''\n return { ok: true, email, record, contact: record, link, siteHint, deal: dealRecord, lead: null }\n }\n\n if (ids.leadId) {\n if (!hostId) return refuse(400, 'Name the site the lead lives under.')\n const lead = await readLeadForHost(hostId, ids.leadId)\n if (!lead) return refuse(404, 'Unknown lead')\n const email = normalizeContactEmail(lead.get('email'))\n if (!email) return refuse(400, 'This lead has no email address.')\n const record = (lead.data() ?? {}) as Record<string, unknown>\n link.leadId = lead.id\n // A converted lead's email belongs on the contact it became as well.\n const converted = String(lead.get('convertedContactId') ?? '').trim()\n if (converted) link.contactId = converted\n return { ok: true, email, record, contact: null, link, siteHint: hostId, deal: null, lead: record }\n }\n\n return refuse(400, 'Name a contact, lead or deal to email.')\n}\n\n/**\n * The site an org-level send leaves from (AGL-2634): the body's, else the\n * record's own, else the org's only site — and a refusal the dialog turns\n * into a Site picker when the org has several and none was named.\n */\nasync function resolveSendingSite(\n firestore: FirebaseFirestore.Firestore,\n scope: CrmRouteScope,\n orgId: string,\n siteHint: string,\n): Promise<{ ok: true; hostId: string } | Refusal> {\n if (scope.hostId) return { ok: true, hostId: scope.hostId }\n if (siteHint) return { ok: true, hostId: siteHint }\n const hosts = await orgHostIds(firestore, orgId)\n if (hosts.length === 1) return { ok: true, hostId: hosts[0] }\n return refuse(409, CRM_EMAIL_PICK_SITE_MESSAGE, { reason: 'site' })\n}\n\n/** The HTTP shape of a send the provider did not accept. */\nfunction sendRefusal(result: SendEmailResult): Refusal {\n const reason = sendFailureReason(result)\n const detail = String((result as { detail?: unknown }).detail ?? '')\n if (reason === 'unverified-domain') {\n return refuse(409, detail || 'This site has no verified sending identity.', {\n reason: 'sending-identity',\n })\n }\n if (reason === 'suppressed' || reason === 'unengaged') {\n return refuse(409, CRM_EMAIL_SUPPRESSED_MESSAGE, { reason: 'suppressed' })\n }\n // The deliverability preflight (AGL-3328): the recipient's domain takes\n // no mail. A standing fact about the address, said in the engine's words\n // — not a provider fault the rep should retry.\n if (reason === 'undeliverable' || reason === 'gateway-held') {\n return refuse(409, detail || 'This address cannot receive email.', { reason: 'undeliverable' })\n }\n if (reason === 'rate-limited') {\n const retryAtMs = Number((result as { retryAtMs?: unknown }).retryAtMs)\n return refuse(\n 503,\n 'The mail provider asked us to slow down. Try again in a minute.',\n {\n reason: 'provider-rate',\n retryAfterSeconds: Number.isFinite(retryAtMs)\n ? Math.max(1, Math.ceil((retryAtMs - Date.now()) / 1000))\n : 60,\n },\n )\n }\n if (reason === 'unconfigured') {\n return refuse(501, 'Email is not configured on this deployment.', {\n reason: 'unconfigured',\n })\n }\n return refuse(502, 'The email could not be sent.', { reason: 'send-failed' })\n}\n\n/** What the route says when a subject or a message is nothing once its fields are filled. */\nexport const CRM_EMAIL_EMPTY_AFTER_MERGE_MESSAGE =\n 'The subject and the message are empty once their merge fields are filled ' +\n 'in. Check the fields against this record.'\n\n/**\n * What `{{…}}` reads from, for this send (AGL-2658): the documents already\n * in hand — the contact, the deal, the lead — the sender off their token,\n * and the site's name, read only when a field asks for it. The contact's\n * facet is the SENDING site's group's, because a name, a job title and a\n * company are one holder's knowledge of a person.\n */\nasync function mergeContextFor(options: {\n recipient: Recipient\n sender: Sender\n hostId: string\n groupId: string\n fields: readonly string[]\n}): Promise<CrmMergeContext> {\n const { recipient, sender, hostId, groupId, fields } = options\n const wantsSite = fields.some((field) => field.startsWith('site.'))\n const host = wantsSite ? await getHostDocAdmin(hostId).catch(() => null) : null\n return {\n contact: recipient.contact,\n contactGroupId: groupId,\n lead: recipient.lead,\n deal: recipient.deal,\n sender: { name: sender.name, email: sender.email },\n site: { name: typed(host?.['displayName'], 200) },\n }\n}\n\nfunction answer(res: PluginApiResponse, refusal: Refusal): void {\n const retry = refusal.body['retryAfterSeconds']\n if (typeof retry === 'number') res.setHeader('Retry-After', String(retry))\n res.status(refusal.status).json(refusal.body)\n}\n\nexport const crmEmailSendHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n res.setHeader('Allow', 'POST')\n res.status(405).json({ error: 'Method not allowed' })\n return\n }\n const body = (req.body ?? {}) as Record<string, unknown>\n const routeScope = readCrmRouteScope(body)\n if (!routeScope) {\n res.status(400).json({ error: 'Missing hostId' })\n return\n }\n const ids = {\n contactId: typed(body['contactId'], 128),\n leadId: typed(body['leadId'], 128),\n dealId: typed(body['dealId'], 128),\n }\n if (!ids.contactId && !ids.leadId && !ids.dealId) {\n res.status(400).json({ error: 'Name a contact, lead or deal to email.' })\n return\n }\n const draftSubject = typed(body['subject'], CRM_EMAIL_SUBJECT_MAX)\n if (!draftSubject) {\n res.status(400).json({ error: 'Enter a subject.' })\n return\n }\n // Paragraphs are what the dialog offers, and a plain-text part is what a\n // paragraph is; line endings are normalized so the stored body and the\n // sent body are one string.\n const draftText = String(body['body'] ?? '')\n .replace(/\\r\\n?/g, '\\n')\n .trim()\n .slice(0, CRM_EMAIL_BODY_MAX)\n if (!draftText) {\n res.status(400).json({ error: 'Write the message.' })\n return\n }\n\n try {\n const sender =\n routeScope.level === 'org'\n ? await authorizeOrgSender(req, routeScope.orgId)\n : await authorizeSender(req, routeScope.hostId)\n if (sender.ok === false) return answer(res, sender)\n const { orgId, org } = sender\n /*\n * THE PLAN, before the pace and the cap (AGL-2787). One-to-one email is\n * the suite's; a plan without the suite is refused as such, rather than\n * reaching the daily cap and being told about a zero it has no way to\n * raise short of the upgrade this names.\n */\n const suite = crmSuiteRefusal(org, 'Emailing one person from their record')\n if (suite) return answer(res, { ok: false, status: suite.status, body: suite.body })\n\n /*\n * THE PER-USER PACE, on its own key. The console dispatcher already\n * counts this write against the person's general console budget; this\n * is the narrower bucket the feature owes — twenty a minute is a rep\n * writing, and past it is a script — keyed by the uid the token proved\n * so one person cannot spend the bucket of everyone on their address.\n */\n const rate = await consumeRateLimit(`crm-email-send:uid:${sender.uid}`, {\n limit: CRM_EMAIL_SENDS_PER_MINUTE,\n windowMs: CRM_EMAIL_RATE_WINDOW_MS,\n })\n if (!rate.allowed) {\n return answer(\n res,\n refuse(429, CRM_EMAIL_RATE_MESSAGE, {\n reason: 'rate',\n retryAfterSeconds: Math.max(\n 1,\n Math.ceil((rate.resetMs - Date.now()) / 1000),\n ),\n }),\n )\n }\n\n const firestore = firebaseAdmin.app().firestore()\n const recipient = await resolveRecipient(firestore, routeScope, orgId, ids)\n if (recipient.ok === false) return answer(res, recipient)\n // The site the message leaves from — the mounted one under a site, and\n // at the organization level whatever the body, the record or the org\n // can answer.\n const site = await resolveSendingSite(firestore, routeScope, orgId, recipient.siteHint)\n if (site.ok === false) return answer(res, site)\n const { hostId } = site\n const group = await consentGroupForSite(hostId, org as Record<string, unknown>)\n // The company the sending site files this person under, so the email\n // shows on the company's log as the automation step's rows do.\n if (recipient.contact) {\n const facet = readContactFacet(recipient.contact, group.groupId)\n if (facet.companyId) recipient.link.companyId = facet.companyId\n }\n\n /*\n * MERGE FIELDS (AGL-2658), filled here rather than in the dialog: the\n * dialog previews with the same resolver, but what leaves and what is\n * logged is rendered by the route, off the documents it read to find\n * the recipient, so a preview the rep did not look at cannot differ\n * from the letter. A subject or a message that is nothing once its\n * fields are filled is refused before any gate is spent on it.\n */\n const fields = crmMergeFieldsIn(`${draftSubject}\\n${draftText}`)\n const merge = fields.length\n ? await mergeContextFor({ recipient, sender, hostId, groupId: group.groupId, fields })\n : null\n const subject = merge\n ? renderCrmMergeFields(draftSubject, merge).trim().slice(0, CRM_EMAIL_SUBJECT_MAX)\n : draftSubject\n const text = merge\n ? renderCrmMergeFields(draftText, merge).trim().slice(0, CRM_EMAIL_BODY_MAX)\n : draftText\n if (!subject || !text) {\n return answer(res, refuse(400, CRM_EMAIL_EMPTY_AFTER_MERGE_MESSAGE, { reason: 'merge' }))\n }\n\n // The per-record ceiling, before the send: an email that could not be\n // logged would be a message the timeline never shows, and the timeline\n // is the reason the send lives on the record.\n const orgRef = firestore.collection('orgs').doc(orgId)\n if (!crmActivityLogHasRoom(await countCrmActivitiesForRecord(orgRef, recipient.link))) {\n return answer(res, refuse(409, CRM_ACTIVITY_LOG_FULL_MESSAGE, { reason: 'ceiling' }))\n }\n\n // Both suppression lists, then the person's own refusal — each read\n // across the sending site's consent group, so leaving one site of a\n // declared group stops this one writing too.\n const sendable = await filterSendableForHost(\n hostId,\n [recipient.email],\n undefined,\n group,\n )\n if (!sendable.length) {\n return answer(res, refuse(409, CRM_EMAIL_SUPPRESSED_MESSAGE, { reason: 'suppressed' }))\n }\n if (readMarketingBasis(recipient.record, group).basis === 'declined') {\n return answer(res, refuse(409, CRM_EMAIL_DECLINED_MESSAGE, { reason: 'declined' }))\n }\n // The record's own verdict (AGL-3245): what the lists hold, in the words\n // the page shows — a bounce, a gateway block, a do-not-contact mark the\n // suppression lists do not carry.\n const stateRefusal = emailStateRefusal(readEmailState(recipient.record))\n if (stateRefusal) {\n return answer(res, refuse(409, stateRefusal, { reason: 'email-state' }))\n }\n\n if (!isEmailConfigured()) {\n return answer(\n res,\n refuse(501, 'Email is not configured on this deployment.', {\n reason: 'unconfigured',\n }),\n )\n }\n const identity = await hostSendingIdentity(hostId)\n if (identity.refusal) {\n return answer(\n res,\n refuse(409, identity.refusal.message, { reason: 'sending-identity' }),\n )\n }\n\n const now = new Date()\n /*\n * THE NEW-WORKSPACE RAMP, resolved for the ceiling below.\n *\n * A workspace past its first week graduates off the org record alone and\n * pays no read for this. One that is still on the ramp needs its\n * delivery history to know which step it has earned, and when that\n * history is unreadable the step is a floor rather than an answer.\n *\n * A floor is enough for a campaign, which defers what it cannot send\n * today and delivers it tomorrow. It is not enough here: this route\n * admits or refuses ONE message and has no third answer, so a ramp that\n * cannot say what a workspace has earned refuses and says so. The\n * refusal is transient by construction — it lasts as long as the window\n * read is failing — and it reaches only workspaces inside their first\n * week, since every other one graduated before the window was consulted.\n */\n const { ramp, degraded } = await readOrgEmailRamp({\n orgId,\n createdAt: (org as Record<string, unknown>)['createdAt'],\n now: now.getTime(),\n })\n if (degraded) {\n return answer(\n res,\n refuse(503, CRM_EMAIL_RAMP_UNAVAILABLE_MESSAGE, {\n reason: 'ramp-unavailable',\n }),\n )\n }\n\n // THE DAILY CAP (AGL-2611), taken as a slot rather than read as a\n // figure (AGL-2645): the transaction reads today's counter, judges it\n // with `checkCrmEmailQuota` against the ramp's share of the plan's day\n // and raises it in one step, so the count the cap enforces is the count\n // this send was admitted on.\n const claim = await reserveCrmEmailSend(firestore, orgId, org, now, ramp)\n if (!claim.ok) {\n const { quota } = claim\n // WHICH ceiling was reached, in the org's own terms. The reservation\n // answers with the number it judged, which is the plan's day unless\n // the ramp held this workspace below it — and a workspace shown a\n // figure under the one its plan page advertises is owed the reason.\n const rampBound =\n quota.included < resolveOrgEntitlements(org).crmEmailsPerDay\n return answer(\n res,\n refuse(\n 409,\n quota.included <= 0\n ? CRM_EMAIL_NOT_INCLUDED_MESSAGE\n : rampBound\n ? crmEmailRampReachedMessage(quota.included, quota.used)\n : crmEmailCapReachedMessage(quota.included),\n {\n reason: rampBound ? 'ramp' : 'quota',\n included: quota.included,\n used: quota.used,\n resetsAtMs: quota.resetsAt.getTime(),\n },\n ),\n )\n }\n\n // The row's id, minted before the send so the delivery webhook can find\n // it from the tags alone; written only once the message has left.\n const activityRef = newCrmActivityRef(firestore, orgId)\n let result: SendEmailResult\n try {\n result = await sendEmail({\n to: recipient.email,\n subject,\n text,\n sendingIdentity: identity,\n audience: 'tenant',\n context: CRM_EMAIL_CONTEXT,\n replyTo: sender.email,\n ...(sender.name ? { fromName: sender.name } : {}),\n tags: crmEmailDeliveryTags({ orgId, hostId, activityId: activityRef.id }),\n })\n } catch (error) {\n // The provider never answered, so no message left: the slot goes\n // back before the failure is reported.\n await releaseCrmEmailSend(firestore, claim.reservation)\n throw error\n }\n if (!result.sent) {\n await releaseCrmEmailSend(firestore, claim.reservation)\n return answer(res, sendRefusal(result))\n }\n\n const sentAtMs = Date.now()\n let logged = true\n /*\n * Who may list the row. Under a site, what a record created there is\n * stamped with. At the organization level the RECORD's own tokens, so\n * an email to a person another site captured shows on their timeline\n * under that site's hub — a row stamped with the sending site alone\n * would be invisible where the person is read.\n */\n const recordTokens = recipient.record['visibleTo']\n const visibleTo =\n routeScope.level === 'org' && Array.isArray(recordTokens) && recordTokens.length\n ? recordTokens.map(String)\n : crmScopeTokens(org as Record<string, unknown>, group)\n try {\n await writeCrmEmailActivity(\n activityRef,\n buildCrmEmailActivity({\n subject,\n body: text,\n to: recipient.email,\n atMs: sentAtMs,\n byUid: sender.uid,\n ...(sender.name ? { byName: sender.name } : {}),\n link: recipient.link,\n hostId,\n visibleTo,\n }),\n )\n } catch (error) {\n // The message has left. A row that could not be written is a gap on\n // the timeline, not a failed send, and it is said so in the answer.\n console.error('[crm] email activity write failed', orgId, activityRef.id, error)\n logged = false\n }\n // The cap's count was taken with the reservation; the org's cost meter\n // for COGS is stamped after delivery, unable to fail the send.\n await recordEmailSends({\n scope: { kind: 'org', orgId },\n count: 1,\n sendClass: 'transactional',\n firestore,\n })\n // The org-level act, in the org's feed: the site variant's is the\n // timeline row itself, under the site the record page is on.\n if (routeScope.level === 'org') {\n await logOrgActivity(\n orgId,\n { uid: sender.uid, email: sender.email },\n 'Sent email',\n recipient.link.contactId\n ? { type: 'contact', id: recipient.link.contactId, name: recipient.email }\n : { type: 'lead', id: recipient.link.leadId, name: recipient.email },\n )\n }\n res.status(200).json({\n ok: true,\n activityId: activityRef.id,\n to: recipient.email,\n from: identity.from,\n logged,\n })\n } catch (error) {\n console.error('[crm] email-send failed', routeScope, error)\n res.status(500).json({ error: 'The email could not be sent.' })\n }\n}\n\nexport default crmEmailSendHandler\n"],"names":["buildCrmEmailActivity","contactCaptureHostIds","CRM_ACTIVITY_LOG_FULL_MESSAGE","CRM_COLLECTIONS","CRM_EMAIL_BODY_MAX","CRM_EMAIL_CONTEXT","CRM_EMAIL_SUBJECT_MAX","crmActivityLogHasRoom","crmEmailDeliveryTags","emailStateRefusal","crmMergeFieldsIn","crmScopeTokens","isOrgWideMember","normalizeContactEmail","readContactFacet","readEmailState","readMarketingBasis","renderCrmMergeFields","resolveOrgEntitlements","visibleToHost","isEmailConfigured","sendEmail","sendFailureReason","consentGroupForSite","consumeRateLimit","countCrmActivitiesForRecord","filterSendableForHost","firebaseAdmin","getHostDocAdmin","getOrgForHost","hostSendingIdentity","logOrgActivity","memberHasOrgPermission","orgDataCollectionForHost","readLeadForHost","readOrgEmailRamp","recordEmailSends","releaseCrmEmailSend","reserveCrmEmailSend","resolveOrgMembership","isRefusedIdToken","authorizeOrgCaller","orgHostIds","readCrmRouteScope","crmSuiteRefusal","newCrmActivityRef","writeCrmEmailActivity","CRM_EMAIL_SENDS_PER_MINUTE","CRM_EMAIL_RATE_WINDOW_MS","CRM_EMAIL_NOT_INCLUDED_MESSAGE","crmEmailCapReachedMessage","included","toLocaleString","crmEmailRampReachedMessage","ceiling","used","CRM_EMAIL_RAMP_UNAVAILABLE_MESSAGE","CRM_EMAIL_SUPPRESSED_MESSAGE","CRM_EMAIL_DECLINED_MESSAGE","CRM_EMAIL_RATE_MESSAGE","CRM_EMAIL_PICK_SITE_MESSAGE","refuse","status","error","extra","ok","body","typed","value","max","String","trim","slice","authorizeSender","req","hostId","resolved","authorization","headers","idToken","startsWith","length","undefined","decoded","app","auth","verifyIdToken","console","catch","orgId","org","staff","member","membership","uid","reaches","Boolean","hostAccess","allowed","email","name","authorizeOrgSender","caller","needs","refusal","resolveRecipient","firestore","scope","ids","link","scoped","level","visible","tokens","contactId","siteHint","dealRecord","dealId","deal","collection","doc","deals","get","exists","id","data","contact","contacts","record","leadId","lead","converted","resolveSendingSite","hosts","reason","sendRefusal","result","detail","retryAtMs","Number","retryAfterSeconds","isFinite","Math","ceil","Date","now","CRM_EMAIL_EMPTY_AFTER_MERGE_MESSAGE","mergeContextFor","options","recipient","sender","groupId","fields","wantsSite","some","field","host","contactGroupId","site","answer","res","retry","setHeader","json","crmEmailSendHandler","method","routeScope","draftSubject","draftText","replace","suite","rate","limit","windowMs","resetMs","group","facet","companyId","merge","subject","text","orgRef","sendable","basis","stateRefusal","identity","message","ramp","degraded","createdAt","getTime","claim","quota","rampBound","crmEmailsPerDay","resetsAtMs","resetsAt","activityRef","to","sendingIdentity","audience","context","replyTo","fromName","tags","activityId","reservation","sent","sentAtMs","logged","recordTokens","visibleTo","Array","isArray","map","atMs","byUid","byName","kind","count","sendClass","type","from"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAGEA,qBAAqB,EACrBC,qBAAqB,EACrBC,6BAA6B,EAC7BC,eAAe,EACfC,kBAAkB,EAClBC,iBAAiB,EACjBC,qBAAqB,EAErBC,qBAAqB,EACrBC,oBAAoB,EACpBC,iBAAiB,EAEjBC,gBAAgB,EAChBC,cAAc,EACdC,eAAe,EACfC,qBAAqB,EAIrBC,gBAAgB,EAChBC,cAAc,EACdC,kBAAkB,EAClBC,oBAAoB,EACpBC,sBAAsB,EACtBC,aAAa,QACR,sBAAqB;AAC5B,SACEC,iBAAiB,EACjBC,SAAS,EACTC,iBAAiB,QAEZ,2BAA0B;AACjC,SACEC,mBAAmB,EACnBC,gBAAgB,EAChBC,2BAA2B,EAC3BC,qBAAqB,EACrBC,aAAa,EACbC,eAAe,EACfC,aAAa,EACbC,mBAAmB,EACnBC,cAAc,EACdC,sBAAsB,EACtBC,wBAAwB,EACxBC,eAAe,EACfC,gBAAgB,EAChBC,gBAAgB,EAChBC,mBAAmB,EACnBC,mBAAmB,EACnBC,oBAAoB,QACf,2BAA0B;AACjC,SAASC,gBAAgB,QAAQ,mDAAkD;AACnF,SACEC,kBAAkB,EAElBC,UAAU,EACVC,iBAAiB,QACZ,kBAAc;AACrB,SAASC,eAAe,QAAQ,kBAAc;AAC9C,SAASC,iBAAiB,EAAEC,qBAAqB,QAAQ,0BAAsB;AAE/E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0EC,GAED,kEAAkE,GAClE,OAAO,MAAMC,6BAA6B,GAAE;AAC5C,MAAMC,2BAA2B;AAEjC,0EAA0E,GAC1E,OAAO,MAAMC,iCACX,8EACA,6BAA4B;AAE9B,2EAA2E,GAC3E,OAAO,SAASC,0BAA0BC,QAAgB;IACxD,OACE,CAAC,gCAAgC,EAAEA,SAASC,cAAc,CAAC,SAAS,KAAK,CAAC,GAC1E;AAEJ;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASC,2BACdC,OAAe,EACfC,IAAY;IAEZ,OACE,CAAC,wBAAwB,EAAED,QAAQF,cAAc,CAAC,SAAS,YAAY,CAAC,GACxE,CAAC,KAAK,EAAEE,YAAY,IAAI,KAAK,IAAI,sCAAsC,CAAC,GACxE,CAAC,sBAAsB,EAAEC,KAAKH,cAAc,CAAC,SAAS,gBAAgB,CAAC,GACvE,yEACA,0EACA;AAEJ;AAEA,uEAAuE,GACvE,OAAO,MAAMI,qCACX,8EACA,+EACA,kEAAiE;AAEnE,OAAO,MAAMC,+BACX,6EACA,WAAU;AAEZ,OAAO,MAAMC,6BACX,oDAAmD;AAErD,OAAO,MAAMC,yBACX,iFAAgF;AAElF,8EAA8E,GAC9E,OAAO,MAAMC,8BACX,4EACA,sBAAqB;AAIvB,MAAMC,SAAS,CACbC,QACAC,OACAC,QAAiC,CAAC,CAAC,GACtB,CAAA;QAAEC,IAAI;QAAOH;QAAQI,MAAM;YAAEH;WAAUC;IAAQ,CAAA;AAE9D,0CAA0C,GAC1C,SAASG,MAAMC,KAAc,EAAEC,GAAW;IACxC,OAAOC,OAAOF,gBAAAA,QAAS,IACpBG,IAAI,GACJC,KAAK,CAAC,GAAGH;AACd;AAaA;;;;;;CAMC,GACD,eAAeI,gBACbC,GAAqB,EACrBC,MAAc;QAEeD,4BAkBhBE;IAlBb,MAAMC,gBAAgBP,QAAOI,6BAAAA,IAAII,OAAO,CAACD,aAAa,YAAzBH,6BAA6B;IAC1D,MAAMK,UAAUF,cAAcG,UAAU,CAAC,aACrCH,cAAcL,KAAK,CAAC,UAAUS,MAAM,IACpCC;IACJ,IAAI,CAACH,SAAS,OAAOlB,OAAO,KAAK;IACjC,IAAIsB;IACJ,IAAI;QACFA,UAAU,MAAMxD,cAAcyD,GAAG,GAAGC,IAAI,GAAGC,aAAa,CAACP;IAC3D,EAAE,OAAOhB,OAAO;QACd,sEAAsE;QACtE,mCAAmC;QACnC,IAAIvB,iBAAiBuB,QAAQ,OAAOF,OAAO,KAAK;QAChD0B,QAAQxB,KAAK,CAAC,gDAAgDA;QAC9D,OAAOF,OAAO,KAAK;IACrB;IACA,MAAMe,WAAW,MAAM/C,cAAc8C,QAAQa,KAAK,CAAC,IAAM;IACzD,IAAI,CAACZ,UAAU,OAAOf,OAAO,KAAK;IAClC,MAAM,EAAE4B,KAAK,EAAE,GAAGb;IAClB,MAAMc,OAAOd,gBAAAA,SAASc,GAAG,YAAZd,gBAAgB,CAAC;IAC9B,IAAIO,QAAQQ,KAAK,KAAK,MAAM;;YAMWC;QALrC,MAAMC,aAAa,MAAMtD,qBAAqB4C,QAAQW,GAAG,EAAEL,OAAOD,KAAK,CACrE,IAAM;QAER,MAAMI,iBAAUC,8BAAAA,WAAYD,MAAM,mBAAI;QACtC,MAAMG,UACJnF,gBAAgBgF,WAAWI,QAAQJ,2BAAAA,qBAAAA,OAAQK,UAAU,qBAAlBL,kBAAoB,CAACjB,OAAO;QACjE,MAAMuB,UACJN,UACAG,WACC,MAAM/D,uBAAuByD,OAAOG,QAAQ;QAC/C,IAAI,CAACM,SAAS;YACZ,OAAOrC,OACL,KACA;QAEJ;IACF;IACA,MAAMsC,QAAQtF,sBAAsBsE,QAAQgB,KAAK;IACjD,IAAI,CAACA,OAAO;QACV,qEAAqE;QACrE,8DAA8D;QAC9D,OAAOtC,OAAO,KAAK;IACrB;IACA,OAAO;QACLI,IAAI;QACJ6B,KAAKX,QAAQW,GAAG;QAChBK;QACAC,MAAMjC,MAAMgB,QAAQiB,IAAI,EAAE;QAC1BX;QACAC;IACF;AACF;AAEA;;;CAGC,GACD,eAAeW,mBACb3B,GAAqB,EACrBe,KAAa;IAEb,MAAMa,SAAS,MAAM7D,mBAAmBiC,KAAKe,OAAO;QAClDc,OAAO;QACPC,SACE,sEACA;IACJ;IACA,IAAIF,OAAOrC,EAAE,KAAK,OAAO,OAAOJ,OAAOyC,OAAOxC,MAAM,EAAEwC,OAAOvC,KAAK;IAClE,MAAMoC,QAAQtF,sBAAsByF,OAAOH,KAAK;IAChD,IAAI,CAACA,OAAO;QACV,OAAOtC,OAAO,KAAK;IACrB;IACA,OAAO;QACLI,IAAI;QACJ6B,KAAKQ,OAAOR,GAAG;QACfK;QACAC,MAAME,OAAOF,IAAI;QACjBX;QACAC,KAAKY,OAAOZ,GAAG;IACjB;AACF;AAsBA;;;;;;;;;;;;;CAaC,GACD,eAAee,iBACbC,SAAsC,EACtCC,KAAoB,EACpBlB,KAAa,EACbmB,GAA0D;IAE1D,MAAMC,OAAwB,CAAC;IAC/B,MAAM,EAAElC,MAAM,EAAE,GAAGgC;IACnB,MAAMG,SAASH,MAAMI,KAAK,KAAK;IAC/B,MAAMC,UAAU,CAACC,SACf,CAACH,UAAU3F,cAAc8F,QAA6BtC;IACxD,IAAIuC,YAAYN,IAAIM,SAAS;IAC7B,IAAIC,WAAW;IACf,IAAIC,aAA6C;IAEjD,IAAIR,IAAIS,MAAM,EAAE;YAWAC,YAEkBA;QAZhC,MAAMA,OAAO,MAAMZ,UAChBa,UAAU,CAAC,QACXC,GAAG,CAAC/B,OACJ8B,UAAU,CAACpH,gBAAgBsH,KAAK,EAChCD,GAAG,CAACZ,IAAIS,MAAM,EACdK,GAAG;QACN,IAAI,CAACJ,KAAKK,MAAM,IAAI,CAACX,QAAQM,KAAKI,GAAG,CAAC,eAAe;YACnD,OAAO7D,OAAO,KAAK;QACrB;QACAgD,KAAKQ,MAAM,GAAGC,KAAKM,EAAE;QACrBR,cAAcE,aAAAA,KAAKO,IAAI,cAATP,aAAe,CAAC;QAC9BH,WAAWhD,MAAMmD,KAAKI,GAAG,CAAC,WAAW;QACrCR,YAAYA,aAAa5C,QAAOgD,YAAAA,KAAKI,GAAG,CAAC,wBAATJ,YAAyB,IAAI/C,IAAI;QACjE,IAAI,CAAC2C,WAAW;YACd,OAAOrD,OAAO,KAAK;QACrB;IACF;IAEA,IAAIqD,WAAW;YAUGY;QAThB,MAAMC,WAAWjB,SACb,MAAM7E,yBAAyB0C,QAAQ,cACvC+B,UAAUa,UAAU,CAAC,QAAQC,GAAG,CAAC/B,OAAO8B,UAAU,CAAC;QACvD,MAAMO,UAAU,MAAMC,SAASP,GAAG,CAACN,WAAWQ,GAAG;QACjD,IAAI,CAACI,QAAQH,MAAM,IAAI,CAACX,QAAQc,QAAQJ,GAAG,CAAC,eAAe;YACzD,OAAO7D,OAAO,KAAK;QACrB;QACA,MAAMsC,QAAQtF,sBAAsBiH,QAAQJ,GAAG,CAAC;QAChD,IAAI,CAACvB,OAAO,OAAOtC,OAAO,KAAK;QAC/B,MAAMmE,UAAUF,gBAAAA,QAAQD,IAAI,cAAZC,gBAAkB,CAAC;QACnCjB,KAAKK,SAAS,GAAGY,QAAQF,EAAE;QAC3B,IAAIhB,IAAIqB,MAAM,EAAEpB,KAAKoB,MAAM,GAAGrB,IAAIqB,MAAM;QACxCd,WACEA,YAAYhD,MAAM6D,MAAM,CAAC,SAAS,EAAE,QAAQ/H,sBAAsB+H,OAAO,CAAC,EAAE,IAAI;QAClF,OAAO;YAAE/D,IAAI;YAAMkC;YAAO6B;YAAQF,SAASE;YAAQnB;YAAMM;YAAUG,MAAMF;YAAYc,MAAM;QAAK;IAClG;IAEA,IAAItB,IAAIqB,MAAM,EAAE;YAMEC,YAGSA;QARzB,IAAI,CAACvD,QAAQ,OAAOd,OAAO,KAAK;QAChC,MAAMqE,OAAO,MAAMhG,gBAAgByC,QAAQiC,IAAIqB,MAAM;QACrD,IAAI,CAACC,MAAM,OAAOrE,OAAO,KAAK;QAC9B,MAAMsC,QAAQtF,sBAAsBqH,KAAKR,GAAG,CAAC;QAC7C,IAAI,CAACvB,OAAO,OAAOtC,OAAO,KAAK;QAC/B,MAAMmE,UAAUE,aAAAA,KAAKL,IAAI,cAATK,aAAe,CAAC;QAChCrB,KAAKoB,MAAM,GAAGC,KAAKN,EAAE;QACrB,qEAAqE;QACrE,MAAMO,YAAY7D,QAAO4D,YAAAA,KAAKR,GAAG,CAAC,iCAATQ,YAAkC,IAAI3D,IAAI;QACnE,IAAI4D,WAAWtB,KAAKK,SAAS,GAAGiB;QAChC,OAAO;YAAElE,IAAI;YAAMkC;YAAO6B;YAAQF,SAAS;YAAMjB;YAAMM,UAAUxC;YAAQ2C,MAAM;YAAMY,MAAMF;QAAO;IACpG;IAEA,OAAOnE,OAAO,KAAK;AACrB;AAEA;;;;CAIC,GACD,eAAeuE,mBACb1B,SAAsC,EACtCC,KAAoB,EACpBlB,KAAa,EACb0B,QAAgB;IAEhB,IAAIR,MAAMhC,MAAM,EAAE,OAAO;QAAEV,IAAI;QAAMU,QAAQgC,MAAMhC,MAAM;IAAC;IAC1D,IAAIwC,UAAU,OAAO;QAAElD,IAAI;QAAMU,QAAQwC;IAAS;IAClD,MAAMkB,QAAQ,MAAM3F,WAAWgE,WAAWjB;IAC1C,IAAI4C,MAAMpD,MAAM,KAAK,GAAG,OAAO;QAAEhB,IAAI;QAAMU,QAAQ0D,KAAK,CAAC,EAAE;IAAC;IAC5D,OAAOxE,OAAO,KAAKD,6BAA6B;QAAE0E,QAAQ;IAAO;AACnE;AAEA,0DAA0D,GAC1D,SAASC,YAAYC,MAAuB;QAEpB;IADtB,MAAMF,SAAShH,kBAAkBkH;IACjC,MAAMC,SAASnE,QAAO,iBAAA,AAACkE,OAAgCC,MAAM,YAAvC,iBAA2C;IACjE,IAAIH,WAAW,qBAAqB;QAClC,OAAOzE,OAAO,KAAK4E,UAAU,+CAA+C;YAC1EH,QAAQ;QACV;IACF;IACA,IAAIA,WAAW,gBAAgBA,WAAW,aAAa;QACrD,OAAOzE,OAAO,KAAKJ,8BAA8B;YAAE6E,QAAQ;QAAa;IAC1E;IACA,wEAAwE;IACxE,yEAAyE;IACzE,+CAA+C;IAC/C,IAAIA,WAAW,mBAAmBA,WAAW,gBAAgB;QAC3D,OAAOzE,OAAO,KAAK4E,UAAU,sCAAsC;YAAEH,QAAQ;QAAgB;IAC/F;IACA,IAAIA,WAAW,gBAAgB;QAC7B,MAAMI,YAAYC,OAAO,AAACH,OAAmCE,SAAS;QACtE,OAAO7E,OACL,KACA,mEACA;YACEyE,QAAQ;YACRM,mBAAmBD,OAAOE,QAAQ,CAACH,aAC/BI,KAAKzE,GAAG,CAAC,GAAGyE,KAAKC,IAAI,CAAC,AAACL,CAAAA,YAAYM,KAAKC,GAAG,EAAC,IAAK,SACjD;QACN;IAEJ;IACA,IAAIX,WAAW,gBAAgB;QAC7B,OAAOzE,OAAO,KAAK,+CAA+C;YAChEyE,QAAQ;QACV;IACF;IACA,OAAOzE,OAAO,KAAK,gCAAgC;QAAEyE,QAAQ;IAAc;AAC7E;AAEA,2FAA2F,GAC3F,OAAO,MAAMY,sCACX,8EACA,4CAA2C;AAE7C;;;;;;CAMC,GACD,eAAeC,gBAAgBC,OAM9B;IACC,MAAM,EAAEC,SAAS,EAAEC,MAAM,EAAE3E,MAAM,EAAE4E,OAAO,EAAEC,MAAM,EAAE,GAAGJ;IACvD,MAAMK,YAAYD,OAAOE,IAAI,CAAC,CAACC,QAAUA,MAAM3E,UAAU,CAAC;IAC1D,MAAM4E,OAAOH,YAAY,MAAM7H,gBAAgB+C,QAAQa,KAAK,CAAC,IAAM,QAAQ;IAC3E,OAAO;QACLsC,SAASuB,UAAUvB,OAAO;QAC1B+B,gBAAgBN;QAChBrB,MAAMmB,UAAUnB,IAAI;QACpBZ,MAAM+B,UAAU/B,IAAI;QACpBgC,QAAQ;YAAElD,MAAMkD,OAAOlD,IAAI;YAAED,OAAOmD,OAAOnD,KAAK;QAAC;QACjD2D,MAAM;YAAE1D,MAAMjC,MAAMyF,wBAAAA,IAAM,CAAC,cAAc,EAAE;QAAK;IAClD;AACF;AAEA,SAASG,OAAOC,GAAsB,EAAExD,OAAgB;IACtD,MAAMyD,QAAQzD,QAAQtC,IAAI,CAAC,oBAAoB;IAC/C,IAAI,OAAO+F,UAAU,UAAUD,IAAIE,SAAS,CAAC,eAAe5F,OAAO2F;IACnED,IAAIlG,MAAM,CAAC0C,QAAQ1C,MAAM,EAAEqG,IAAI,CAAC3D,QAAQtC,IAAI;AAC9C;AAEA,OAAO,MAAMkG,sBAAwC,OAAO1F,KAAKsF;QAMjDtF,WAuBWR;IA5BzB,IAAIQ,IAAI2F,MAAM,KAAK,QAAQ;QACzBL,IAAIE,SAAS,CAAC,SAAS;QACvBF,IAAIlG,MAAM,CAAC,KAAKqG,IAAI,CAAC;YAAEpG,OAAO;QAAqB;QACnD;IACF;IACA,MAAMG,QAAQQ,YAAAA,IAAIR,IAAI,YAARQ,YAAY,CAAC;IAC3B,MAAM4F,aAAa3H,kBAAkBuB;IACrC,IAAI,CAACoG,YAAY;QACfN,IAAIlG,MAAM,CAAC,KAAKqG,IAAI,CAAC;YAAEpG,OAAO;QAAiB;QAC/C;IACF;IACA,MAAM6C,MAAM;QACVM,WAAW/C,MAAMD,IAAI,CAAC,YAAY,EAAE;QACpC+D,QAAQ9D,MAAMD,IAAI,CAAC,SAAS,EAAE;QAC9BmD,QAAQlD,MAAMD,IAAI,CAAC,SAAS,EAAE;IAChC;IACA,IAAI,CAAC0C,IAAIM,SAAS,IAAI,CAACN,IAAIqB,MAAM,IAAI,CAACrB,IAAIS,MAAM,EAAE;QAChD2C,IAAIlG,MAAM,CAAC,KAAKqG,IAAI,CAAC;YAAEpG,OAAO;QAAyC;QACvE;IACF;IACA,MAAMwG,eAAepG,MAAMD,IAAI,CAAC,UAAU,EAAE5D;IAC5C,IAAI,CAACiK,cAAc;QACjBP,IAAIlG,MAAM,CAAC,KAAKqG,IAAI,CAAC;YAAEpG,OAAO;QAAmB;QACjD;IACF;IACA,yEAAyE;IACzE,uEAAuE;IACvE,4BAA4B;IAC5B,MAAMyG,YAAYlG,QAAOJ,aAAAA,IAAI,CAAC,OAAO,YAAZA,aAAgB,IACtCuG,OAAO,CAAC,UAAU,MAClBlG,IAAI,GACJC,KAAK,CAAC,GAAGpE;IACZ,IAAI,CAACoK,WAAW;QACdR,IAAIlG,MAAM,CAAC,KAAKqG,IAAI,CAAC;YAAEpG,OAAO;QAAqB;QACnD;IACF;IAEA,IAAI;QACF,MAAMuF,SACJgB,WAAWvD,KAAK,KAAK,QACjB,MAAMV,mBAAmB3B,KAAK4F,WAAW7E,KAAK,IAC9C,MAAMhB,gBAAgBC,KAAK4F,WAAW3F,MAAM;QAClD,IAAI2E,OAAOrF,EAAE,KAAK,OAAO,OAAO8F,OAAOC,KAAKV;QAC5C,MAAM,EAAE7D,KAAK,EAAEC,GAAG,EAAE,GAAG4D;QACvB;;;;;KAKC,GACD,MAAMoB,QAAQ9H,gBAAgB8C,KAAK;QACnC,IAAIgF,OAAO,OAAOX,OAAOC,KAAK;YAAE/F,IAAI;YAAOH,QAAQ4G,MAAM5G,MAAM;YAAEI,MAAMwG,MAAMxG,IAAI;QAAC;QAElF;;;;;;KAMC,GACD,MAAMyG,OAAO,MAAMnJ,iBAAiB,CAAC,mBAAmB,EAAE8H,OAAOxD,GAAG,EAAE,EAAE;YACtE8E,OAAO7H;YACP8H,UAAU7H;QACZ;QACA,IAAI,CAAC2H,KAAKzE,OAAO,EAAE;YACjB,OAAO6D,OACLC,KACAnG,OAAO,KAAKF,wBAAwB;gBAClC2E,QAAQ;gBACRM,mBAAmBE,KAAKzE,GAAG,CACzB,GACAyE,KAAKC,IAAI,CAAC,AAAC4B,CAAAA,KAAKG,OAAO,GAAG9B,KAAKC,GAAG,EAAC,IAAK;YAE5C;QAEJ;QAEA,MAAMvC,YAAY/E,cAAcyD,GAAG,GAAGsB,SAAS;QAC/C,MAAM2C,YAAY,MAAM5C,iBAAiBC,WAAW4D,YAAY7E,OAAOmB;QACvE,IAAIyC,UAAUpF,EAAE,KAAK,OAAO,OAAO8F,OAAOC,KAAKX;QAC/C,uEAAuE;QACvE,qEAAqE;QACrE,cAAc;QACd,MAAMS,OAAO,MAAM1B,mBAAmB1B,WAAW4D,YAAY7E,OAAO4D,UAAUlC,QAAQ;QACtF,IAAI2C,KAAK7F,EAAE,KAAK,OAAO,OAAO8F,OAAOC,KAAKF;QAC1C,MAAM,EAAEnF,MAAM,EAAE,GAAGmF;QACnB,MAAMiB,QAAQ,MAAMxJ,oBAAoBoD,QAAQe;QAChD,qEAAqE;QACrE,+DAA+D;QAC/D,IAAI2D,UAAUvB,OAAO,EAAE;YACrB,MAAMkD,QAAQlK,iBAAiBuI,UAAUvB,OAAO,EAAEiD,MAAMxB,OAAO;YAC/D,IAAIyB,MAAMC,SAAS,EAAE5B,UAAUxC,IAAI,CAACoE,SAAS,GAAGD,MAAMC,SAAS;QACjE;QAEA;;;;;;;KAOC,GACD,MAAMzB,SAAS9I,iBAAiB,GAAG6J,aAAa,EAAE,EAAEC,WAAW;QAC/D,MAAMU,QAAQ1B,OAAOvE,MAAM,GACvB,MAAMkE,gBAAgB;YAAEE;YAAWC;YAAQ3E;YAAQ4E,SAASwB,MAAMxB,OAAO;YAAEC;QAAO,KAClF;QACJ,MAAM2B,UAAUD,QACZjK,qBAAqBsJ,cAAcW,OAAO3G,IAAI,GAAGC,KAAK,CAAC,GAAGlE,yBAC1DiK;QACJ,MAAMa,OAAOF,QACTjK,qBAAqBuJ,WAAWU,OAAO3G,IAAI,GAAGC,KAAK,CAAC,GAAGpE,sBACvDoK;QACJ,IAAI,CAACW,WAAW,CAACC,MAAM;YACrB,OAAOrB,OAAOC,KAAKnG,OAAO,KAAKqF,qCAAqC;gBAAEZ,QAAQ;YAAQ;QACxF;QAEA,sEAAsE;QACtE,uEAAuE;QACvE,8CAA8C;QAC9C,MAAM+C,SAAS3E,UAAUa,UAAU,CAAC,QAAQC,GAAG,CAAC/B;QAChD,IAAI,CAAClF,sBAAsB,MAAMkB,4BAA4B4J,QAAQhC,UAAUxC,IAAI,IAAI;YACrF,OAAOkD,OAAOC,KAAKnG,OAAO,KAAK3D,+BAA+B;gBAAEoI,QAAQ;YAAU;QACpF;QAEA,oEAAoE;QACpE,oEAAoE;QACpE,6CAA6C;QAC7C,MAAMgD,WAAW,MAAM5J,sBACrBiD,QACA;YAAC0E,UAAUlD,KAAK;SAAC,EACjBjB,WACA6F;QAEF,IAAI,CAACO,SAASrG,MAAM,EAAE;YACpB,OAAO8E,OAAOC,KAAKnG,OAAO,KAAKJ,8BAA8B;gBAAE6E,QAAQ;YAAa;QACtF;QACA,IAAItH,mBAAmBqI,UAAUrB,MAAM,EAAE+C,OAAOQ,KAAK,KAAK,YAAY;YACpE,OAAOxB,OAAOC,KAAKnG,OAAO,KAAKH,4BAA4B;gBAAE4E,QAAQ;YAAW;QAClF;QACA,yEAAyE;QACzE,wEAAwE;QACxE,kCAAkC;QAClC,MAAMkD,eAAe/K,kBAAkBM,eAAesI,UAAUrB,MAAM;QACtE,IAAIwD,cAAc;YAChB,OAAOzB,OAAOC,KAAKnG,OAAO,KAAK2H,cAAc;gBAAElD,QAAQ;YAAc;QACvE;QAEA,IAAI,CAAClH,qBAAqB;YACxB,OAAO2I,OACLC,KACAnG,OAAO,KAAK,+CAA+C;gBACzDyE,QAAQ;YACV;QAEJ;QACA,MAAMmD,WAAW,MAAM3J,oBAAoB6C;QAC3C,IAAI8G,SAASjF,OAAO,EAAE;YACpB,OAAOuD,OACLC,KACAnG,OAAO,KAAK4H,SAASjF,OAAO,CAACkF,OAAO,EAAE;gBAAEpD,QAAQ;YAAmB;QAEvE;QAEA,MAAMW,MAAM,IAAID;QAChB;;;;;;;;;;;;;;;KAeC,GACD,MAAM,EAAE2C,IAAI,EAAEC,QAAQ,EAAE,GAAG,MAAMzJ,iBAAiB;YAChDsD;YACAoG,WAAW,AAACnG,GAA+B,CAAC,YAAY;YACxDuD,KAAKA,IAAI6C,OAAO;QAClB;QACA,IAAIF,UAAU;YACZ,OAAO7B,OACLC,KACAnG,OAAO,KAAKL,oCAAoC;gBAC9C8E,QAAQ;YACV;QAEJ;QAEA,kEAAkE;QAClE,sEAAsE;QACtE,uEAAuE;QACvE,wEAAwE;QACxE,6BAA6B;QAC7B,MAAMyD,QAAQ,MAAMzJ,oBAAoBoE,WAAWjB,OAAOC,KAAKuD,KAAK0C;QACpE,IAAI,CAACI,MAAM9H,EAAE,EAAE;YACb,MAAM,EAAE+H,KAAK,EAAE,GAAGD;YAClB,qEAAqE;YACrE,oEAAoE;YACpE,kEAAkE;YAClE,oEAAoE;YACpE,MAAME,YACJD,MAAM7I,QAAQ,GAAGjC,uBAAuBwE,KAAKwG,eAAe;YAC9D,OAAOnC,OACLC,KACAnG,OACE,KACAmI,MAAM7I,QAAQ,IAAI,IACdF,iCACAgJ,YACE5I,2BAA2B2I,MAAM7I,QAAQ,EAAE6I,MAAMzI,IAAI,IACrDL,0BAA0B8I,MAAM7I,QAAQ,GAC9C;gBACEmF,QAAQ2D,YAAY,SAAS;gBAC7B9I,UAAU6I,MAAM7I,QAAQ;gBACxBI,MAAMyI,MAAMzI,IAAI;gBAChB4I,YAAYH,MAAMI,QAAQ,CAACN,OAAO;YACpC;QAGN;QAEA,wEAAwE;QACxE,kEAAkE;QAClE,MAAMO,cAAcxJ,kBAAkB6D,WAAWjB;QACjD,IAAI+C;QACJ,IAAI;YACFA,SAAS,MAAMnH,UAAU;gBACvBiL,IAAIjD,UAAUlD,KAAK;gBACnBgF;gBACAC;gBACAmB,iBAAiBd;gBACjBe,UAAU;gBACVC,SAASpM;gBACTqM,SAASpD,OAAOnD,KAAK;eACjBmD,OAAOlD,IAAI,GAAG;gBAAEuG,UAAUrD,OAAOlD,IAAI;YAAC,IAAI,CAAC;gBAC/CwG,MAAMpM,qBAAqB;oBAAEiF;oBAAOd;oBAAQkI,YAAYR,YAAYzE,EAAE;gBAAC;;QAE3E,EAAE,OAAO7D,OAAO;YACd,iEAAiE;YACjE,uCAAuC;YACvC,MAAM1B,oBAAoBqE,WAAWqF,MAAMe,WAAW;YACtD,MAAM/I;QACR;QACA,IAAI,CAACyE,OAAOuE,IAAI,EAAE;YAChB,MAAM1K,oBAAoBqE,WAAWqF,MAAMe,WAAW;YACtD,OAAO/C,OAAOC,KAAKzB,YAAYC;QACjC;QAEA,MAAMwE,WAAWhE,KAAKC,GAAG;QACzB,IAAIgE,SAAS;QACb;;;;;;KAMC,GACD,MAAMC,eAAe7D,UAAUrB,MAAM,CAAC,YAAY;QAClD,MAAMmF,YACJ7C,WAAWvD,KAAK,KAAK,SAASqG,MAAMC,OAAO,CAACH,iBAAiBA,aAAajI,MAAM,GAC5EiI,aAAaI,GAAG,CAAChJ,UACjB3D,eAAe+E,KAAgCqF;QACrD,IAAI;YACF,MAAMjI,sBACJuJ,aACArM,sBAAsB;gBACpBmL;gBACAjH,MAAMkH;gBACNkB,IAAIjD,UAAUlD,KAAK;gBACnBoH,MAAMP;gBACNQ,OAAOlE,OAAOxD,GAAG;eACbwD,OAAOlD,IAAI,GAAG;gBAAEqH,QAAQnE,OAAOlD,IAAI;YAAC,IAAI,CAAC;gBAC7CS,MAAMwC,UAAUxC,IAAI;gBACpBlC;gBACAwI;;QAGN,EAAE,OAAOpJ,OAAO;YACd,oEAAoE;YACpE,oEAAoE;YACpEwB,QAAQxB,KAAK,CAAC,qCAAqC0B,OAAO4G,YAAYzE,EAAE,EAAE7D;YAC1EkJ,SAAS;QACX;QACA,uEAAuE;QACvE,+DAA+D;QAC/D,MAAM7K,iBAAiB;YACrBuE,OAAO;gBAAE+G,MAAM;gBAAOjI;YAAM;YAC5BkI,OAAO;YACPC,WAAW;YACXlH;QACF;QACA,kEAAkE;QAClE,6DAA6D;QAC7D,IAAI4D,WAAWvD,KAAK,KAAK,OAAO;YAC9B,MAAMhF,eACJ0D,OACA;gBAAEK,KAAKwD,OAAOxD,GAAG;gBAAEK,OAAOmD,OAAOnD,KAAK;YAAC,GACvC,cACAkD,UAAUxC,IAAI,CAACK,SAAS,GACpB;gBAAE2G,MAAM;gBAAWjG,IAAIyB,UAAUxC,IAAI,CAACK,SAAS;gBAAEd,MAAMiD,UAAUlD,KAAK;YAAC,IACvE;gBAAE0H,MAAM;gBAAQjG,IAAIyB,UAAUxC,IAAI,CAACoB,MAAM;gBAAE7B,MAAMiD,UAAUlD,KAAK;YAAC;QAEzE;QACA6D,IAAIlG,MAAM,CAAC,KAAKqG,IAAI,CAAC;YACnBlG,IAAI;YACJ4I,YAAYR,YAAYzE,EAAE;YAC1B0E,IAAIjD,UAAUlD,KAAK;YACnB2H,MAAMrC,SAASqC,IAAI;YACnBb;QACF;IACF,EAAE,OAAOlJ,OAAO;QACdwB,QAAQxB,KAAK,CAAC,2BAA2BuG,YAAYvG;QACrDiG,IAAIlG,MAAM,CAAC,KAAKqG,IAAI,CAAC;YAAEpG,OAAO;QAA+B;IAC/D;AACF,EAAC;AAED,eAAeqG,oBAAmB"}
@@ -63,6 +63,11 @@ export interface LeadConvertRequest {
63
63
  currency?: string;
64
64
  /** A stage of the default pipeline; its first open stage when absent. */
65
65
  stageId?: string;
66
+ /**
67
+ * One of the org's deal types (AGL-3516); the list's default when
68
+ * absent. The deal's lead source is the lead's own.
69
+ */
70
+ type?: string;
66
71
  };
67
72
  }
68
73
  /** What the route answers on success. */
@@ -32,11 +32,12 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
32
32
  * Checked here with the Admin SDK rather than trusted from the client,
33
33
  * because this route writes into collections the caller's own rules would
34
34
  * otherwise decide about.
35
- */ import { CONTACT_ERASED_MESSAGE, CRM_RECORDS_BAND_FULL_MESSAGE, normalizeCompanyDomain } from "@aglyn/aglyn/server";
35
+ */ import { CONTACT_ERASED_MESSAGE, CRM_RECORDS_BAND_FULL_MESSAGE, judgeCrmPicklistValue, normalizeCompanyDomain } from "@aglyn/aglyn/server";
36
36
  import { firebaseAdmin, getOrgForHost } from "@aglyn/tenant-data-admin";
37
37
  import { convertHostLead } from "./convert-host-lead.js";
38
38
  import { resolveOrgPermissions } from "@aglyn/tenant-runtime/org-permissions";
39
39
  import { holdsDataManage } from "./org-caller.js";
40
+ import { readCrmPicklist } from "./read-picklist.js";
40
41
  import { crmSuiteRefusal } from "./suite-gate.js";
41
42
  // The stage picker moved to the runtime with the writes; re-exported so the
42
43
  // pipeline code that imports it from here keeps its import.
@@ -97,7 +98,8 @@ const CURRENCY_PATTERN = /^[a-z]{3}$/;
97
98
  }
98
99
  };
99
100
  export const leadConvertHandler = async (req, res)=>{
100
- var _req_headers_authorization, _req_body, _body_hostId, _body_leadId, _body_companyId;
101
+ var _req_headers_authorization, _req_body, _body_hostId, _body_leadId, _body_companyId, _ref;
102
+ var _body_deal;
101
103
  if (req.method !== 'POST') {
102
104
  res.setHeader('Allow', 'POST');
103
105
  res.status(405).json({
@@ -169,6 +171,7 @@ export const leadConvertHandler = async (req, res)=>{
169
171
  stageId: body.deal.stageId ? String(body.deal.stageId) : undefined
170
172
  };
171
173
  }
174
+ const requestedType = deal ? String((_ref = (_body_deal = body.deal) == null ? void 0 : _body_deal.type) != null ? _ref : '').trim() : '';
172
175
  try {
173
176
  var _decoded_email;
174
177
  const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
@@ -203,8 +206,21 @@ export const leadConvertHandler = async (req, res)=>{
203
206
  res.status(suite.status).json(suite.body);
204
207
  return;
205
208
  }
209
+ const firestore = firebaseAdmin.app().firestore();
210
+ // The deal's Type against the org's list (AGL-3516), before any write.
211
+ if (deal && requestedType) {
212
+ const judged = judgeCrmPicklistValue('opportunityType', await readCrmPicklist(firestore, orgId, 'opportunityType'), requestedType);
213
+ if (judged.ok === false) {
214
+ res.status(400).json({
215
+ error: judged.error,
216
+ field: 'deal.type'
217
+ });
218
+ return;
219
+ }
220
+ if (judged.value) deal.type = judged.value;
221
+ }
206
222
  const result = await convertHostLead(_extends({
207
- firestore: firebaseAdmin.app().firestore(),
223
+ firestore,
208
224
  hostId,
209
225
  orgId,
210
226
  org: org,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/lead-convert.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/lead-convert` — a lead becomes a contact, and optionally a\n * company and a deal (AGL-2608).\n *\n * This is the console's door onto `convertHostLead` (the tenancy runtime),\n * which owns the four writes, their order, and the idempotency on the lead;\n * the REST API's `POST /v1/leads/{id}/convert` is the other door onto the\n * same function (AGL-2627). What lives here is what only this door knows:\n * the ID token, the body the convert dialog posts, and the statuses and\n * sentences the dialog shows.\n *\n * ## What the caller has to be\n *\n * A signed-in member of the host's org holding `data.manage` — the same key\n * the console gate on the CRM surface enforces — with a role on this site.\n * Checked here with the Admin SDK rather than trusted from the client,\n * because this route writes into collections the caller's own rules would\n * otherwise decide about.\n */\n\nimport {\n CONTACT_ERASED_MESSAGE,\n CRM_RECORDS_BAND_FULL_MESSAGE,\n normalizeCompanyDomain,\n type PluginApiHandler,\n} from '@aglyn/aglyn/server'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport {\n type ConvertHostLeadRefusal,\n convertHostLead,\n} from './convert-host-lead'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport { holdsDataManage } from './org-caller'\nimport { crmSuiteRefusal } from './suite-gate'\n\n// The stage picker moved to the runtime with the writes; re-exported so the\n// pipeline code that imports it from here keeps its import.\nexport { stageForNewDeal } from './convert-host-lead'\n\n/** The body the console's convert dialog posts. */\nexport interface LeadConvertRequest {\n hostId: string\n /** `hosts/{hostId}/leads/{leadId}` — the document id, which is the person key. */\n leadId: string\n /**\n * Who owns the resulting contact (and deal). Defaults to the lead's own\n * owner; failing that the org's assignment rules and the site's default\n * owner decide (AGL-2618), and failing those the caller — somebody\n * converted this person, and a record with no owner is one nobody\n * follows up.\n */\n ownerUid?: string\n /** Link an existing `orgs/{orgId}/companies/{companyId}`. */\n companyId?: string\n /** Or create one. Ignored when `companyId` is given. */\n createCompany?: { name: string; domain?: string }\n /** Open a deal in the org's default pipeline. */\n deal?: {\n title: string\n amountCents?: number\n /** Lowercase ISO 4217; `usd` when absent or malformed. */\n currency?: string\n /** A stage of the default pipeline; its first open stage when absent. */\n stageId?: string\n }\n}\n\n/** What the route answers on success. */\nexport interface LeadConvertResponse {\n ok: true\n contactId: string\n companyId?: string\n dealId?: string\n /** The lead was converted before this call; nothing was created now. */\n alreadyConverted: boolean\n}\n\nconst COMPANY_NAME_MAX = 120\nconst DEAL_TITLE_MAX = 200\nconst CURRENCY_PATTERN = /^[a-z]{3}$/\n\n/**\n * How each refusal the writes can answer reads at this door — the status\n * and the sentence the convert dialog has always shown for it.\n */\nconst REFUSALS: Record<\n ConvertHostLeadRefusal,\n { status: number; body: Record<string, unknown> }\n> = {\n 'unknown-lead': { status: 404, body: { error: 'Unknown lead' } },\n 'no-email': {\n status: 422,\n body: { error: 'This lead has no usable email address to convert' },\n },\n 'contact-not-created': {\n status: 409,\n body: {\n error:\n 'The contact could not be created — the audience band may be ' +\n 'full. Nothing was changed.',\n },\n },\n 'band-full': {\n status: 409,\n body: { error: CRM_RECORDS_BAND_FULL_MESSAGE, reason: 'band' },\n },\n // The contact create route's own answer for an erased address (AGL-2623),\n // so the convert dialog and the add-contact drawer say one thing about\n // one person.\n erased: {\n status: 409,\n body: { error: CONTACT_ERASED_MESSAGE, reason: 'erased' },\n },\n 'unknown-company': { status: 404, body: { error: 'Unknown company' } },\n 'no-stages': {\n status: 409,\n body: { error: 'The default pipeline has no stages to open a deal in' },\n },\n}\n\nexport const leadConvertHandler: 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<LeadConvertRequest> =\n typeof req.body === 'string' ? JSON.parse(req.body) : (req.body ?? {})\n const hostId = String(body.hostId ?? '').trim()\n const leadId = String(body.leadId ?? '').trim()\n if (!hostId || !leadId) {\n res.status(400).json({ error: 'Missing hostId or leadId' })\n return\n }\n\n /*\n * Validate the optional parts BEFORE any read, so a malformed request costs\n * nothing and a bad company domain is refused rather than silently dropped\n * — a converter who typed a domain expects the company to carry it.\n */\n const requestedCompanyId = String(body.companyId ?? '').trim()\n let createCompany: { name: string; domain: string | null } | null = null\n if (!requestedCompanyId && body.createCompany) {\n const name = String(body.createCompany.name ?? '')\n .trim()\n .slice(0, COMPANY_NAME_MAX)\n if (!name) {\n res.status(400).json({ error: 'A company needs a name' })\n return\n }\n const rawDomain = String(body.createCompany.domain ?? '').trim()\n const domain = rawDomain ? normalizeCompanyDomain(rawDomain) : null\n if (rawDomain && !domain) {\n res.status(400).json({ error: 'That does not look like a domain' })\n return\n }\n createCompany = { name, domain }\n }\n let deal: {\n title: string\n amountCents: number | null\n currency: string\n stageId: string | undefined\n } | null = null\n if (body.deal) {\n const title = String(body.deal.title ?? '')\n .trim()\n .slice(0, DEAL_TITLE_MAX)\n if (!title) {\n res.status(400).json({ error: 'A deal needs a title' })\n return\n }\n const amount = Number(body.deal.amountCents)\n const currency = String(body.deal.currency ?? '')\n .trim()\n .toLowerCase()\n deal = {\n title,\n amountCents:\n Number.isFinite(amount) && amount >= 0 ? Math.round(amount) : null,\n currency: CURRENCY_PATTERN.test(currency) ? currency : 'usd',\n stageId: body.deal.stageId ? String(body.deal.stageId) : undefined,\n }\n }\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const staff = decoded['staff'] === true\n /*\n * The console's own gate on the CRM is `data.manage`, asked of the\n * permission catalog (`holdsDataManage`), and additionally that the\n * caller has a role on THIS site — an org member scoped to a sibling site\n * holds the key but not the host, and a lead is the host's record.\n * `resolveOrgPermissions` answers the site role and fails closed on a\n * lookup error when a host is named, so an absent membership refuses.\n */\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: 'Converting a lead requires the data permission on this site' })\n return\n }\n\n const resolved = await getOrgForHost(hostId)\n if (!resolved) {\n res.status(404).json({ error: 'Unknown site' })\n return\n }\n const { orgId, org } = resolved\n // A lead's conversion is the CRM's, like its status and owner (AGL-2787).\n const suite = crmSuiteRefusal(org, 'Converting a lead')\n if (suite) {\n res.status(suite.status).json(suite.body)\n return\n }\n const result = await convertHostLead({\n firestore: firebaseAdmin.app().firestore(),\n hostId,\n orgId,\n org: org as Record<string, unknown>,\n leadId,\n actor: { uid: decoded.uid, email: decoded.email ?? null, kind: 'member' },\n ...(body.ownerUid ? { ownerUid: String(body.ownerUid) } : {}),\n ...(requestedCompanyId ? { companyId: requestedCompanyId } : {}),\n createCompany,\n deal,\n })\n if (result.ok === false) {\n const refusal = REFUSALS[result.reason]\n res.status(refusal.status).json(refusal.body)\n return\n }\n const answer: LeadConvertResponse = {\n ok: true,\n contactId: result.contactId,\n ...(result.companyId ? { companyId: result.companyId } : {}),\n ...(result.dealId ? { dealId: result.dealId } : {}),\n alreadyConverted: result.alreadyConverted,\n }\n res.status(200).json(answer)\n } catch (error) {\n console.error('lead-convert failed', error)\n res.status(500).json({ error: 'The lead could not be converted.' })\n }\n}\n\nexport default leadConvertHandler\n"],"names":["CONTACT_ERASED_MESSAGE","CRM_RECORDS_BAND_FULL_MESSAGE","normalizeCompanyDomain","firebaseAdmin","getOrgForHost","convertHostLead","resolveOrgPermissions","holdsDataManage","crmSuiteRefusal","stageForNewDeal","COMPANY_NAME_MAX","DEAL_TITLE_MAX","CURRENCY_PATTERN","REFUSALS","status","body","error","reason","erased","leadConvertHandler","req","res","method","setHeader","json","authorization","String","headers","idToken","startsWith","slice","length","undefined","JSON","parse","hostId","trim","leadId","requestedCompanyId","companyId","createCompany","name","rawDomain","domain","deal","title","amount","Number","amountCents","currency","toLowerCase","isFinite","Math","round","test","stageId","decoded","app","auth","verifyIdToken","staff","membership","uid","hostRole","orgId","resolved","org","suite","result","firestore","actor","email","kind","ownerUid","ok","refusal","answer","contactId","dealId","alreadyConverted","console"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/lead-convert.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/lead-convert` — a lead becomes a contact, and optionally a\n * company and a deal (AGL-2608).\n *\n * This is the console's door onto `convertHostLead` (the tenancy runtime),\n * which owns the four writes, their order, and the idempotency on the lead;\n * the REST API's `POST /v1/leads/{id}/convert` is the other door onto the\n * same function (AGL-2627). What lives here is what only this door knows:\n * the ID token, the body the convert dialog posts, and the statuses and\n * sentences the dialog shows.\n *\n * ## What the caller has to be\n *\n * A signed-in member of the host's org holding `data.manage` — the same key\n * the console gate on the CRM surface enforces — with a role on this site.\n * Checked here with the Admin SDK rather than trusted from the client,\n * because this route writes into collections the caller's own rules would\n * otherwise decide about.\n */\n\nimport {\n CONTACT_ERASED_MESSAGE,\n CRM_RECORDS_BAND_FULL_MESSAGE,\n judgeCrmPicklistValue,\n normalizeCompanyDomain,\n type PluginApiHandler,\n} from '@aglyn/aglyn/server'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport {\n type ConvertHostLeadRefusal,\n convertHostLead,\n} from './convert-host-lead'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport { holdsDataManage } from './org-caller'\nimport { readCrmPicklist } from './read-picklist'\nimport { crmSuiteRefusal } from './suite-gate'\n\n// The stage picker moved to the runtime with the writes; re-exported so the\n// pipeline code that imports it from here keeps its import.\nexport { stageForNewDeal } from './convert-host-lead'\n\n/** The body the console's convert dialog posts. */\nexport interface LeadConvertRequest {\n hostId: string\n /** `hosts/{hostId}/leads/{leadId}` — the document id, which is the person key. */\n leadId: string\n /**\n * Who owns the resulting contact (and deal). Defaults to the lead's own\n * owner; failing that the org's assignment rules and the site's default\n * owner decide (AGL-2618), and failing those the caller — somebody\n * converted this person, and a record with no owner is one nobody\n * follows up.\n */\n ownerUid?: string\n /** Link an existing `orgs/{orgId}/companies/{companyId}`. */\n companyId?: string\n /** Or create one. Ignored when `companyId` is given. */\n createCompany?: { name: string; domain?: string }\n /** Open a deal in the org's default pipeline. */\n deal?: {\n title: string\n amountCents?: number\n /** Lowercase ISO 4217; `usd` when absent or malformed. */\n currency?: string\n /** A stage of the default pipeline; its first open stage when absent. */\n stageId?: string\n /**\n * One of the org's deal types (AGL-3516); the list's default when\n * absent. The deal's lead source is the lead's own.\n */\n type?: string\n }\n}\n\n/** What the route answers on success. */\nexport interface LeadConvertResponse {\n ok: true\n contactId: string\n companyId?: string\n dealId?: string\n /** The lead was converted before this call; nothing was created now. */\n alreadyConverted: boolean\n}\n\nconst COMPANY_NAME_MAX = 120\nconst DEAL_TITLE_MAX = 200\nconst CURRENCY_PATTERN = /^[a-z]{3}$/\n\n/**\n * How each refusal the writes can answer reads at this door — the status\n * and the sentence the convert dialog has always shown for it.\n */\nconst REFUSALS: Record<\n ConvertHostLeadRefusal,\n { status: number; body: Record<string, unknown> }\n> = {\n 'unknown-lead': { status: 404, body: { error: 'Unknown lead' } },\n 'no-email': {\n status: 422,\n body: { error: 'This lead has no usable email address to convert' },\n },\n 'contact-not-created': {\n status: 409,\n body: {\n error:\n 'The contact could not be created — the audience band may be ' +\n 'full. Nothing was changed.',\n },\n },\n 'band-full': {\n status: 409,\n body: { error: CRM_RECORDS_BAND_FULL_MESSAGE, reason: 'band' },\n },\n // The contact create route's own answer for an erased address (AGL-2623),\n // so the convert dialog and the add-contact drawer say one thing about\n // one person.\n erased: {\n status: 409,\n body: { error: CONTACT_ERASED_MESSAGE, reason: 'erased' },\n },\n 'unknown-company': { status: 404, body: { error: 'Unknown company' } },\n 'no-stages': {\n status: 409,\n body: { error: 'The default pipeline has no stages to open a deal in' },\n },\n}\n\nexport const leadConvertHandler: 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<LeadConvertRequest> =\n typeof req.body === 'string' ? JSON.parse(req.body) : (req.body ?? {})\n const hostId = String(body.hostId ?? '').trim()\n const leadId = String(body.leadId ?? '').trim()\n if (!hostId || !leadId) {\n res.status(400).json({ error: 'Missing hostId or leadId' })\n return\n }\n\n /*\n * Validate the optional parts BEFORE any read, so a malformed request costs\n * nothing and a bad company domain is refused rather than silently dropped\n * — a converter who typed a domain expects the company to carry it.\n */\n const requestedCompanyId = String(body.companyId ?? '').trim()\n let createCompany: { name: string; domain: string | null } | null = null\n if (!requestedCompanyId && body.createCompany) {\n const name = String(body.createCompany.name ?? '')\n .trim()\n .slice(0, COMPANY_NAME_MAX)\n if (!name) {\n res.status(400).json({ error: 'A company needs a name' })\n return\n }\n const rawDomain = String(body.createCompany.domain ?? '').trim()\n const domain = rawDomain ? normalizeCompanyDomain(rawDomain) : null\n if (rawDomain && !domain) {\n res.status(400).json({ error: 'That does not look like a domain' })\n return\n }\n createCompany = { name, domain }\n }\n let deal: {\n title: string\n amountCents: number | null\n currency: string\n stageId: string | undefined\n type?: string\n } | null = null\n if (body.deal) {\n const title = String(body.deal.title ?? '')\n .trim()\n .slice(0, DEAL_TITLE_MAX)\n if (!title) {\n res.status(400).json({ error: 'A deal needs a title' })\n return\n }\n const amount = Number(body.deal.amountCents)\n const currency = String(body.deal.currency ?? '')\n .trim()\n .toLowerCase()\n deal = {\n title,\n amountCents:\n Number.isFinite(amount) && amount >= 0 ? Math.round(amount) : null,\n currency: CURRENCY_PATTERN.test(currency) ? currency : 'usd',\n stageId: body.deal.stageId ? String(body.deal.stageId) : undefined,\n }\n }\n const requestedType = deal ? String(body.deal?.type ?? '').trim() : ''\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const staff = decoded['staff'] === true\n /*\n * The console's own gate on the CRM is `data.manage`, asked of the\n * permission catalog (`holdsDataManage`), and additionally that the\n * caller has a role on THIS site — an org member scoped to a sibling site\n * holds the key but not the host, and a lead is the host's record.\n * `resolveOrgPermissions` answers the site role and fails closed on a\n * lookup error when a host is named, so an absent membership refuses.\n */\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: 'Converting a lead requires the data permission on this site' })\n return\n }\n\n const resolved = await getOrgForHost(hostId)\n if (!resolved) {\n res.status(404).json({ error: 'Unknown site' })\n return\n }\n const { orgId, org } = resolved\n // A lead's conversion is the CRM's, like its status and owner (AGL-2787).\n const suite = crmSuiteRefusal(org, 'Converting a lead')\n if (suite) {\n res.status(suite.status).json(suite.body)\n return\n }\n const firestore = firebaseAdmin.app().firestore()\n // The deal's Type against the org's list (AGL-3516), before any write.\n if (deal && requestedType) {\n const judged = judgeCrmPicklistValue(\n 'opportunityType',\n await readCrmPicklist(firestore, orgId, 'opportunityType'),\n requestedType,\n )\n if (judged.ok === false) {\n res.status(400).json({ error: judged.error, field: 'deal.type' })\n return\n }\n if (judged.value) deal.type = judged.value\n }\n const result = await convertHostLead({\n firestore,\n hostId,\n orgId,\n org: org as Record<string, unknown>,\n leadId,\n actor: { uid: decoded.uid, email: decoded.email ?? null, kind: 'member' },\n ...(body.ownerUid ? { ownerUid: String(body.ownerUid) } : {}),\n ...(requestedCompanyId ? { companyId: requestedCompanyId } : {}),\n createCompany,\n deal,\n })\n if (result.ok === false) {\n const refusal = REFUSALS[result.reason]\n res.status(refusal.status).json(refusal.body)\n return\n }\n const answer: LeadConvertResponse = {\n ok: true,\n contactId: result.contactId,\n ...(result.companyId ? { companyId: result.companyId } : {}),\n ...(result.dealId ? { dealId: result.dealId } : {}),\n alreadyConverted: result.alreadyConverted,\n }\n res.status(200).json(answer)\n } catch (error) {\n console.error('lead-convert failed', error)\n res.status(500).json({ error: 'The lead could not be converted.' })\n }\n}\n\nexport default leadConvertHandler\n"],"names":["CONTACT_ERASED_MESSAGE","CRM_RECORDS_BAND_FULL_MESSAGE","judgeCrmPicklistValue","normalizeCompanyDomain","firebaseAdmin","getOrgForHost","convertHostLead","resolveOrgPermissions","holdsDataManage","readCrmPicklist","crmSuiteRefusal","stageForNewDeal","COMPANY_NAME_MAX","DEAL_TITLE_MAX","CURRENCY_PATTERN","REFUSALS","status","body","error","reason","erased","leadConvertHandler","req","res","method","setHeader","json","authorization","String","headers","idToken","startsWith","slice","length","undefined","JSON","parse","hostId","trim","leadId","requestedCompanyId","companyId","createCompany","name","rawDomain","domain","deal","title","amount","Number","amountCents","currency","toLowerCase","isFinite","Math","round","test","stageId","requestedType","type","decoded","app","auth","verifyIdToken","staff","membership","uid","hostRole","orgId","resolved","org","suite","firestore","judged","ok","field","value","result","actor","email","kind","ownerUid","refusal","answer","contactId","dealId","alreadyConverted","console"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;CAkBC,GAED,SACEA,sBAAsB,EACtBC,6BAA6B,EAC7BC,qBAAqB,EACrBC,sBAAsB,QAEjB,sBAAqB;AAC5B,SAASC,aAAa,EAAEC,aAAa,QAAQ,2BAA0B;AACvE,SAEEC,eAAe,QACV,yBAAqB;AAC5B,SAASC,qBAAqB,QAAQ,wCAAuC;AAC7E,SAASC,eAAe,QAAQ,kBAAc;AAC9C,SAASC,eAAe,QAAQ,qBAAiB;AACjD,SAASC,eAAe,QAAQ,kBAAc;AAE9C,4EAA4E;AAC5E,4DAA4D;AAC5D,SAASC,eAAe,QAAQ,yBAAqB;AA6CrD,MAAMC,mBAAmB;AACzB,MAAMC,iBAAiB;AACvB,MAAMC,mBAAmB;AAEzB;;;CAGC,GACD,MAAMC,WAGF;IACF,gBAAgB;QAAEC,QAAQ;QAAKC,MAAM;YAAEC,OAAO;QAAe;IAAE;IAC/D,YAAY;QACVF,QAAQ;QACRC,MAAM;YAAEC,OAAO;QAAmD;IACpE;IACA,uBAAuB;QACrBF,QAAQ;QACRC,MAAM;YACJC,OACE,iEACA;QACJ;IACF;IACA,aAAa;QACXF,QAAQ;QACRC,MAAM;YAAEC,OAAOjB;YAA+BkB,QAAQ;QAAO;IAC/D;IACA,0EAA0E;IAC1E,uEAAuE;IACvE,cAAc;IACdC,QAAQ;QACNJ,QAAQ;QACRC,MAAM;YAAEC,OAAOlB;YAAwBmB,QAAQ;QAAS;IAC1D;IACA,mBAAmB;QAAEH,QAAQ;QAAKC,MAAM;YAAEC,OAAO;QAAkB;IAAE;IACrE,aAAa;QACXF,QAAQ;QACRC,MAAM;YAAEC,OAAO;QAAuD;IACxE;AACF;AAEA,OAAO,MAAMG,qBAAuC,OAAOC,KAAKC;QAMjCD,4BAS4BA,WACnCL,cACAA,cAWYA;QA6CEA;IAxEpC,IAAIK,IAAIE,MAAM,KAAK,QAAQ;QACzBD,IAAIE,SAAS,CAAC,SAAS;QACvBF,IAAIP,MAAM,CAAC,KAAKU,IAAI,CAAC;YAAER,OAAO;QAAqB;QACnD;IACF;IACA,MAAMS,gBAAgBC,QAAON,6BAAAA,IAAIO,OAAO,CAACF,aAAa,YAAzBL,6BAA6B;IAC1D,MAAMQ,UAAUH,cAAcI,UAAU,CAAC,aACrCJ,cAAcK,KAAK,CAAC,UAAUC,MAAM,IACpCC;IACJ,IAAI,CAACJ,SAAS;QACZP,IAAIP,MAAM,CAAC,KAAKU,IAAI,CAAC;YAAER,OAAO;QAAkB;QAChD;IACF;IACA,MAAMD,OACJ,OAAOK,IAAIL,IAAI,KAAK,WAAWkB,KAAKC,KAAK,CAACd,IAAIL,IAAI,KAAKK,YAAAA,IAAIL,IAAI,YAARK,YAAY,CAAC;IACtE,MAAMe,SAAST,QAAOX,eAAAA,KAAKoB,MAAM,YAAXpB,eAAe,IAAIqB,IAAI;IAC7C,MAAMC,SAASX,QAAOX,eAAAA,KAAKsB,MAAM,YAAXtB,eAAe,IAAIqB,IAAI;IAC7C,IAAI,CAACD,UAAU,CAACE,QAAQ;QACtBhB,IAAIP,MAAM,CAAC,KAAKU,IAAI,CAAC;YAAER,OAAO;QAA2B;QACzD;IACF;IAEA;;;;GAIC,GACD,MAAMsB,qBAAqBZ,QAAOX,kBAAAA,KAAKwB,SAAS,YAAdxB,kBAAkB,IAAIqB,IAAI;IAC5D,IAAII,gBAAgE;IACpE,IAAI,CAACF,sBAAsBvB,KAAKyB,aAAa,EAAE;YACzBzB,0BAOKA;QAPzB,MAAM0B,OAAOf,QAAOX,2BAAAA,KAAKyB,aAAa,CAACC,IAAI,YAAvB1B,2BAA2B,IAC5CqB,IAAI,GACJN,KAAK,CAAC,GAAGpB;QACZ,IAAI,CAAC+B,MAAM;YACTpB,IAAIP,MAAM,CAAC,KAAKU,IAAI,CAAC;gBAAER,OAAO;YAAyB;YACvD;QACF;QACA,MAAM0B,YAAYhB,QAAOX,6BAAAA,KAAKyB,aAAa,CAACG,MAAM,YAAzB5B,6BAA6B,IAAIqB,IAAI;QAC9D,MAAMO,SAASD,YAAYzC,uBAAuByC,aAAa;QAC/D,IAAIA,aAAa,CAACC,QAAQ;YACxBtB,IAAIP,MAAM,CAAC,KAAKU,IAAI,CAAC;gBAAER,OAAO;YAAmC;YACjE;QACF;QACAwB,gBAAgB;YAAEC;YAAME;QAAO;IACjC;IACA,IAAIC,OAMO;IACX,IAAI7B,KAAK6B,IAAI,EAAE;YACQ7B,kBAQGA;QARxB,MAAM8B,QAAQnB,QAAOX,mBAAAA,KAAK6B,IAAI,CAACC,KAAK,YAAf9B,mBAAmB,IACrCqB,IAAI,GACJN,KAAK,CAAC,GAAGnB;QACZ,IAAI,CAACkC,OAAO;YACVxB,IAAIP,MAAM,CAAC,KAAKU,IAAI,CAAC;gBAAER,OAAO;YAAuB;YACrD;QACF;QACA,MAAM8B,SAASC,OAAOhC,KAAK6B,IAAI,CAACI,WAAW;QAC3C,MAAMC,WAAWvB,QAAOX,sBAAAA,KAAK6B,IAAI,CAACK,QAAQ,YAAlBlC,sBAAsB,IAC3CqB,IAAI,GACJc,WAAW;QACdN,OAAO;YACLC;YACAG,aACED,OAAOI,QAAQ,CAACL,WAAWA,UAAU,IAAIM,KAAKC,KAAK,CAACP,UAAU;YAChEG,UAAUrC,iBAAiB0C,IAAI,CAACL,YAAYA,WAAW;YACvDM,SAASxC,KAAK6B,IAAI,CAACW,OAAO,GAAG7B,OAAOX,KAAK6B,IAAI,CAACW,OAAO,IAAIvB;QAC3D;IACF;IACA,MAAMwB,gBAAgBZ,OAAOlB,gBAAOX,aAAAA,KAAK6B,IAAI,qBAAT7B,WAAW0C,IAAI,mBAAI,IAAIrB,IAAI,KAAK;IAEpE,IAAI;YAsDkCsB;QArDpC,MAAMA,UAAU,MAAMxD,cAAcyD,GAAG,GAAGC,IAAI,GAAGC,aAAa,CAACjC;QAC/D,MAAMkC,QAAQJ,OAAO,CAAC,QAAQ,KAAK;QACnC;;;;;;;KAOC,GACD,MAAMK,aAAa,MAAM1D,sBAAsBqD,QAAQM,GAAG,EAAE;YAAE7B;QAAO;QACrE,IACE,CAAC2B,SACD,CAAEC,CAAAA,WAAWE,QAAQ,IAAK,MAAM3D,gBAAgByD,WAAWG,KAAK,EAAER,QAAQM,GAAG,CAAC,GAC9E;YACA3C,IACGP,MAAM,CAAC,KACPU,IAAI,CAAC;gBAAER,OAAO;YAA8D;YAC/E;QACF;QAEA,MAAMmD,WAAW,MAAMhE,cAAcgC;QACrC,IAAI,CAACgC,UAAU;YACb9C,IAAIP,MAAM,CAAC,KAAKU,IAAI,CAAC;gBAAER,OAAO;YAAe;YAC7C;QACF;QACA,MAAM,EAAEkD,KAAK,EAAEE,GAAG,EAAE,GAAGD;QACvB,0EAA0E;QAC1E,MAAME,QAAQ7D,gBAAgB4D,KAAK;QACnC,IAAIC,OAAO;YACThD,IAAIP,MAAM,CAACuD,MAAMvD,MAAM,EAAEU,IAAI,CAAC6C,MAAMtD,IAAI;YACxC;QACF;QACA,MAAMuD,YAAYpE,cAAcyD,GAAG,GAAGW,SAAS;QAC/C,uEAAuE;QACvE,IAAI1B,QAAQY,eAAe;YACzB,MAAMe,SAASvE,sBACb,mBACA,MAAMO,gBAAgB+D,WAAWJ,OAAO,oBACxCV;YAEF,IAAIe,OAAOC,EAAE,KAAK,OAAO;gBACvBnD,IAAIP,MAAM,CAAC,KAAKU,IAAI,CAAC;oBAAER,OAAOuD,OAAOvD,KAAK;oBAAEyD,OAAO;gBAAY;gBAC/D;YACF;YACA,IAAIF,OAAOG,KAAK,EAAE9B,KAAKa,IAAI,GAAGc,OAAOG,KAAK;QAC5C;QACA,MAAMC,SAAS,MAAMvE,gBAAgB;YACnCkE;YACAnC;YACA+B;YACAE,KAAKA;YACL/B;YACAuC,OAAO;gBAAEZ,KAAKN,QAAQM,GAAG;gBAAEa,KAAK,GAAEnB,iBAAAA,QAAQmB,KAAK,YAAbnB,iBAAiB;gBAAMoB,MAAM;YAAS;WACpE/D,KAAKgE,QAAQ,GAAG;YAAEA,UAAUrD,OAAOX,KAAKgE,QAAQ;QAAE,IAAI,CAAC,GACvDzC,qBAAqB;YAAEC,WAAWD;QAAmB,IAAI,CAAC;YAC9DE;YACAI;;QAEF,IAAI+B,OAAOH,EAAE,KAAK,OAAO;YACvB,MAAMQ,UAAUnE,QAAQ,CAAC8D,OAAO1D,MAAM,CAAC;YACvCI,IAAIP,MAAM,CAACkE,QAAQlE,MAAM,EAAEU,IAAI,CAACwD,QAAQjE,IAAI;YAC5C;QACF;QACA,MAAMkE,SAA8B;YAClCT,IAAI;YACJU,WAAWP,OAAOO,SAAS;WACvBP,OAAOpC,SAAS,GAAG;YAAEA,WAAWoC,OAAOpC,SAAS;QAAC,IAAI,CAAC,GACtDoC,OAAOQ,MAAM,GAAG;YAAEA,QAAQR,OAAOQ,MAAM;QAAC,IAAI,CAAC;YACjDC,kBAAkBT,OAAOS,gBAAgB;;QAE3C/D,IAAIP,MAAM,CAAC,KAAKU,IAAI,CAACyD;IACvB,EAAE,OAAOjE,OAAO;QACdqE,QAAQrE,KAAK,CAAC,uBAAuBA;QACrCK,IAAIP,MAAM,CAAC,KAAKU,IAAI,CAAC;YAAER,OAAO;QAAmC;IACnE;AACF,EAAC;AAED,eAAeG,mBAAkB"}
@@ -45,7 +45,7 @@
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
47
  */
48
- import { type CrmCustomValue, type CrmLeadProfilePatch, type CrmLeadStatus, type PluginApiHandler } from '@aglyn/aglyn/server';
48
+ import { type CrmCustomValue, type CrmLeadProfilePatch, type PluginApiHandler } from '@aglyn/aglyn/server';
49
49
  import type { CampaignFilingRef } from '../model/campaign-filing-activity';
50
50
  /** The surface a hand-entered lead names, beside `signup`, `booking`, `form:{id}` and `import`. */
51
51
  export declare const LEAD_MANUAL_SOURCE = "manual";
@@ -61,11 +61,26 @@ export interface LeadCreateRequest {
61
61
  address?: Record<string, unknown> | null;
62
62
  tags?: string[];
63
63
  leadSource?: string;
64
- status?: CrmLeadStatus;
64
+ /**
65
+ * One of the org's Lead status values by its label, or a meaning — New
66
+ * or Working either way (AGL-3512).
67
+ */
68
+ status?: string;
65
69
  ownerUid?: string;
66
70
  notes?: string;
67
71
  /** The org's campaigns to file the lead under (AGL-3254), by container id. */
68
72
  campaignIds?: string[];
73
+ salutation?: string;
74
+ firstName?: string;
75
+ lastName?: string;
76
+ mobilePhone?: string;
77
+ fax?: string;
78
+ doNotCall?: boolean;
79
+ industry?: string;
80
+ rating?: string;
81
+ annualRevenueCents?: number;
82
+ currency?: string;
83
+ numberOfEmployees?: number;
69
84
  /**
70
85
  * The org's custom LEAD fields (AGL-3272), keyed by each definition's
71
86
  * `key`. Judged against the definitions whose `object` is `lead`, so a
@@ -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,