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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (514) hide show
  1. package/README.md +2 -1
  2. package/package.json +13 -13
  3. package/src/lib/components/activity-list.js +12 -5
  4. package/src/lib/components/activity-list.js.map +1 -1
  5. package/src/lib/components/companies-bulk-bar.d.ts +1 -4
  6. package/src/lib/components/companies-bulk-bar.js +7 -20
  7. package/src/lib/components/companies-bulk-bar.js.map +1 -1
  8. package/src/lib/components/companies-section.d.ts +3 -3
  9. package/src/lib/components/companies-section.js +149 -46
  10. package/src/lib/components/companies-section.js.map +1 -1
  11. package/src/lib/components/company-edit-drawer.js +281 -20
  12. package/src/lib/components/company-edit-drawer.js.map +1 -1
  13. package/src/lib/components/company-properties-card.js +99 -8
  14. package/src/lib/components/company-properties-card.js.map +1 -1
  15. package/src/lib/components/contact-detail-page.js +3 -1
  16. package/src/lib/components/contact-detail-page.js.map +1 -1
  17. package/src/lib/components/contact-merge-dialog.js +33 -10
  18. package/src/lib/components/contact-merge-dialog.js.map +1 -1
  19. package/src/lib/components/contact-properties-card.d.ts +0 -69
  20. package/src/lib/components/contact-properties-card.js +350 -60
  21. package/src/lib/components/contact-properties-card.js.map +1 -1
  22. package/src/lib/components/contact-reports-to-field.d.ts +46 -0
  23. package/src/lib/components/contact-reports-to-field.js +67 -0
  24. package/src/lib/components/contact-reports-to-field.js.map +1 -0
  25. package/src/lib/components/contacts-bulk-bar.d.ts +1 -8
  26. package/src/lib/components/contacts-bulk-bar.js +7 -19
  27. package/src/lib/components/contacts-bulk-bar.js.map +1 -1
  28. package/src/lib/components/contacts-section.js +46 -33
  29. package/src/lib/components/contacts-section.js.map +1 -1
  30. package/src/lib/components/crm-call-actions.d.ts +10 -0
  31. package/src/lib/components/crm-call-actions.js +26 -9
  32. package/src/lib/components/crm-call-actions.js.map +1 -1
  33. package/src/lib/components/crm-record-picker.d.ts +15 -0
  34. package/src/lib/components/crm-record-picker.js +16 -10
  35. package/src/lib/components/crm-record-picker.js.map +1 -1
  36. package/src/lib/components/crm-transfer-buttons.d.ts +54 -0
  37. package/src/lib/components/crm-transfer-buttons.js +167 -0
  38. package/src/lib/components/crm-transfer-buttons.js.map +1 -0
  39. package/src/lib/components/deal-contact-roles-card.d.ts +46 -0
  40. package/src/lib/components/deal-contact-roles-card.js +368 -0
  41. package/src/lib/components/deal-contact-roles-card.js.map +1 -0
  42. package/src/lib/components/deal-detail-page.d.ts +3 -2
  43. package/src/lib/components/deal-detail-page.js +25 -6
  44. package/src/lib/components/deal-detail-page.js.map +1 -1
  45. package/src/lib/components/deal-edit-drawer.d.ts +10 -0
  46. package/src/lib/components/deal-edit-drawer.js +219 -10
  47. package/src/lib/components/deal-edit-drawer.js.map +1 -1
  48. package/src/lib/components/deal-properties-card.d.ts +7 -0
  49. package/src/lib/components/deal-properties-card.js +34 -3
  50. package/src/lib/components/deal-properties-card.js.map +1 -1
  51. package/src/lib/components/deal-stage-card.d.ts +4 -0
  52. package/src/lib/components/deal-stage-card.js +10 -2
  53. package/src/lib/components/deal-stage-card.js.map +1 -1
  54. package/src/lib/components/deals-bulk-bar.d.ts +0 -3
  55. package/src/lib/components/deals-bulk-bar.js +7 -20
  56. package/src/lib/components/deals-bulk-bar.js.map +1 -1
  57. package/src/lib/components/deals-section.d.ts +3 -3
  58. package/src/lib/components/deals-section.js +123 -63
  59. package/src/lib/components/deals-section.js.map +1 -1
  60. package/src/lib/components/fields-section.js +31 -17
  61. package/src/lib/components/fields-section.js.map +1 -1
  62. package/src/lib/components/lead-convert-dialog.d.ts +4 -0
  63. package/src/lib/components/lead-convert-dialog.js +23 -1
  64. package/src/lib/components/lead-convert-dialog.js.map +1 -1
  65. package/src/lib/components/lead-detail-page.js +8 -2
  66. package/src/lib/components/lead-detail-page.js.map +1 -1
  67. package/src/lib/components/lead-profile-fields.d.ts +71 -0
  68. package/src/lib/components/lead-profile-fields.js +386 -0
  69. package/src/lib/components/lead-profile-fields.js.map +1 -0
  70. package/src/lib/components/lead-properties-card.d.ts +2 -1
  71. package/src/lib/components/lead-properties-card.js +85 -176
  72. package/src/lib/components/lead-properties-card.js.map +1 -1
  73. package/src/lib/components/lead-source-select.d.ts +4 -23
  74. package/src/lib/components/lead-source-select.js +9 -73
  75. package/src/lib/components/lead-source-select.js.map +1 -1
  76. package/src/lib/components/lead-status-chip.d.ts +9 -3
  77. package/src/lib/components/lead-status-chip.js +8 -4
  78. package/src/lib/components/lead-status-chip.js.map +1 -1
  79. package/src/lib/components/lead-status-options.d.ts +20 -0
  80. package/src/lib/components/lead-status-options.js +51 -0
  81. package/src/lib/components/lead-status-options.js.map +1 -0
  82. package/src/lib/components/lead-unqualify-dialog.d.ts +5 -0
  83. package/src/lib/components/lead-unqualify-dialog.js +50 -18
  84. package/src/lib/components/lead-unqualify-dialog.js.map +1 -1
  85. package/src/lib/components/leads-bulk-bar.d.ts +0 -3
  86. package/src/lib/components/leads-bulk-bar.js +29 -29
  87. package/src/lib/components/leads-bulk-bar.js.map +1 -1
  88. package/src/lib/components/leads-section.js +120 -68
  89. package/src/lib/components/leads-section.js.map +1 -1
  90. package/src/lib/components/linked-deals-card.d.ts +4 -0
  91. package/src/lib/components/linked-deals-card.js +47 -3
  92. package/src/lib/components/linked-deals-card.js.map +1 -1
  93. package/src/lib/components/log-activity-dialog.js +43 -3
  94. package/src/lib/components/log-activity-dialog.js.map +1 -1
  95. package/src/lib/components/new-contact-drawer.d.ts +8 -0
  96. package/src/lib/components/new-contact-drawer.js +108 -6
  97. package/src/lib/components/new-contact-drawer.js.map +1 -1
  98. package/src/lib/components/new-lead-drawer.d.ts +9 -2
  99. package/src/lib/components/new-lead-drawer.js +88 -121
  100. package/src/lib/components/new-lead-drawer.js.map +1 -1
  101. package/src/lib/components/picklist-select.d.ts +36 -0
  102. package/src/lib/components/picklist-select.js +114 -0
  103. package/src/lib/components/picklist-select.js.map +1 -0
  104. package/src/lib/components/picklist-values-card.d.ts +34 -0
  105. package/src/lib/components/{lead-source-values-card.js → picklist-values-card.js} +241 -58
  106. package/src/lib/components/picklist-values-card.js.map +1 -0
  107. package/src/lib/components/pipeline-stages-dialog.d.ts +7 -0
  108. package/src/lib/components/pipeline-stages-dialog.js +26 -4
  109. package/src/lib/components/pipeline-stages-dialog.js.map +1 -1
  110. package/src/lib/components/pipelines-dialog.js +15 -6
  111. package/src/lib/components/pipelines-dialog.js.map +1 -1
  112. package/src/lib/components/recent-activity-feed.js +20 -5
  113. package/src/lib/components/recent-activity-feed.js.map +1 -1
  114. package/src/lib/components/recipes-card.js +4 -2
  115. package/src/lib/components/recipes-card.js.map +1 -1
  116. package/src/lib/components/record-tasks-card.js +15 -8
  117. package/src/lib/components/record-tasks-card.js.map +1 -1
  118. package/src/lib/components/reports/forecast-card.d.ts +6 -0
  119. package/src/lib/components/reports/forecast-card.js +101 -3
  120. package/src/lib/components/reports/forecast-card.js.map +1 -1
  121. package/src/lib/components/reports/lead-funnel-card.d.ts +12 -0
  122. package/src/lib/components/reports/lead-funnel-card.js +9 -5
  123. package/src/lib/components/reports/lead-funnel-card.js.map +1 -1
  124. package/src/lib/components/reports/lead-source-card.d.ts +23 -0
  125. package/src/lib/components/reports/lead-source-card.js +318 -0
  126. package/src/lib/components/reports/lead-source-card.js.map +1 -0
  127. package/src/lib/components/reports/report-export.js +1 -1
  128. package/src/lib/components/reports/report-export.js.map +1 -1
  129. package/src/lib/components/reports-section.js +8 -0
  130. package/src/lib/components/reports-section.js.map +1 -1
  131. package/src/lib/components/sharing-rule-drawer.d.ts +5 -0
  132. package/src/lib/components/sharing-rule-drawer.js +108 -19
  133. package/src/lib/components/sharing-rule-drawer.js.map +1 -1
  134. package/src/lib/components/sharing-rules-card.js +11 -3
  135. package/src/lib/components/sharing-rules-card.js.map +1 -1
  136. package/src/lib/components/task-cells.d.ts +9 -1
  137. package/src/lib/components/task-cells.js +9 -4
  138. package/src/lib/components/task-cells.js.map +1 -1
  139. package/src/lib/components/task-edit-drawer.js +111 -43
  140. package/src/lib/components/task-edit-drawer.js.map +1 -1
  141. package/src/lib/components/tasks-bulk-bar.d.ts +0 -3
  142. package/src/lib/components/tasks-bulk-bar.js +7 -20
  143. package/src/lib/components/tasks-bulk-bar.js.map +1 -1
  144. package/src/lib/components/tasks-calendar.d.ts +3 -0
  145. package/src/lib/components/tasks-calendar.js +8 -1
  146. package/src/lib/components/tasks-calendar.js.map +1 -1
  147. package/src/lib/components/tasks-section.d.ts +3 -3
  148. package/src/lib/components/tasks-section.js +58 -43
  149. package/src/lib/components/tasks-section.js.map +1 -1
  150. package/src/lib/constants/api-routes.d.ts +2 -9
  151. package/src/lib/constants/api-routes.js +1 -7
  152. package/src/lib/constants/api-routes.js.map +1 -1
  153. package/src/lib/constants/bundle-common.d.ts +6 -0
  154. package/src/lib/constants/bundle-common.js +11 -0
  155. package/src/lib/constants/bundle-common.js.map +1 -1
  156. package/src/lib/constants/company-filters.d.ts +14 -0
  157. package/src/lib/constants/company-filters.js +51 -1
  158. package/src/lib/constants/company-filters.js.map +1 -1
  159. package/src/lib/constants/contact-filters.d.ts +2 -0
  160. package/src/lib/constants/contact-filters.js +18 -1
  161. package/src/lib/constants/contact-filters.js.map +1 -1
  162. package/src/lib/constants/deal-filters.d.ts +22 -1
  163. package/src/lib/constants/deal-filters.js +67 -4
  164. package/src/lib/constants/deal-filters.js.map +1 -1
  165. package/src/lib/declarations.console-server.d.ts +3 -0
  166. package/src/lib/declarations.console-server.js +35 -0
  167. package/src/lib/declarations.console-server.js.map +1 -1
  168. package/src/lib/declarations.d.ts +26 -0
  169. package/src/lib/declarations.js +35 -0
  170. package/src/lib/declarations.js.map +1 -0
  171. package/src/lib/declarations.server.d.ts +11 -0
  172. package/src/lib/declarations.server.js +92 -1
  173. package/src/lib/declarations.server.js.map +1 -1
  174. package/src/lib/hooks/use-company-picklists.d.ts +15 -0
  175. package/src/lib/hooks/use-company-picklists.js +55 -0
  176. package/src/lib/hooks/use-company-picklists.js.map +1 -0
  177. package/src/lib/hooks/use-crm-picklist.d.ts +25 -0
  178. package/src/lib/hooks/use-crm-picklist.js +53 -0
  179. package/src/lib/hooks/use-crm-picklist.js.map +1 -0
  180. package/src/lib/hooks/use-crm-record-names.d.ts +16 -0
  181. package/src/lib/hooks/use-crm-record-names.js +45 -19
  182. package/src/lib/hooks/use-crm-record-names.js.map +1 -1
  183. package/src/lib/hooks/use-crm-task-picklists.d.ts +11 -0
  184. package/src/lib/hooks/use-crm-task-picklists.js +45 -0
  185. package/src/lib/hooks/use-crm-task-picklists.js.map +1 -0
  186. package/src/lib/hooks/use-deals.d.ts +14 -0
  187. package/src/lib/hooks/use-deals.js +29 -0
  188. package/src/lib/hooks/use-deals.js.map +1 -1
  189. package/src/lib/hooks/use-lead-picklists.d.ts +14 -0
  190. package/src/lib/hooks/use-lead-picklists.js +46 -0
  191. package/src/lib/hooks/use-lead-picklists.js.map +1 -0
  192. package/src/lib/hooks/use-lead-source-picklist.d.ts +5 -19
  193. package/src/lib/hooks/use-lead-source-picklist.js +12 -22
  194. package/src/lib/hooks/use-lead-source-picklist.js.map +1 -1
  195. package/src/lib/hooks/use-lead-status-picklist.d.ts +9 -0
  196. package/src/lib/hooks/use-lead-status-picklist.js +40 -0
  197. package/src/lib/hooks/use-lead-status-picklist.js.map +1 -0
  198. package/src/lib/model/companies.d.ts +32 -3
  199. package/src/lib/model/companies.js +133 -16
  200. package/src/lib/model/companies.js.map +1 -1
  201. package/src/lib/model/contact-merge.d.ts +27 -3
  202. package/src/lib/model/contact-merge.js +81 -7
  203. package/src/lib/model/contact-merge.js.map +1 -1
  204. package/src/lib/model/contact-record.d.ts +17 -0
  205. package/src/lib/model/contact-record.js +15 -1
  206. package/src/lib/model/contact-record.js.map +1 -1
  207. package/src/lib/model/contact-update.d.ts +31 -2
  208. package/src/lib/model/contact-update.js +14 -0
  209. package/src/lib/model/contact-update.js.map +1 -1
  210. package/src/lib/model/crm-list-query.d.ts +5 -0
  211. package/src/lib/model/crm-list-query.js +8 -0
  212. package/src/lib/model/crm-list-query.js.map +1 -1
  213. package/src/lib/model/crm-recipes.d.ts +40 -0
  214. package/src/lib/model/crm-recipes.js +244 -0
  215. package/src/lib/model/crm-recipes.js.map +1 -0
  216. package/src/lib/model/crm-record-counts.d.ts +36 -0
  217. package/src/lib/model/crm-record-counts.js +54 -0
  218. package/src/lib/model/crm-record-counts.js.map +1 -0
  219. package/src/lib/model/crm-record-lists.d.ts +23 -0
  220. package/src/lib/model/crm-record-lists.js +161 -0
  221. package/src/lib/model/crm-record-lists.js.map +1 -0
  222. package/src/lib/model/crm-reports.d.ts +73 -3
  223. package/src/lib/model/crm-reports.js +120 -1
  224. package/src/lib/model/crm-reports.js.map +1 -1
  225. package/src/lib/model/crm-sharing.d.ts +21 -3
  226. package/src/lib/model/crm-sharing.js +27 -6
  227. package/src/lib/model/crm-sharing.js.map +1 -1
  228. package/src/lib/model/deal-board-model.d.ts +8 -2
  229. package/src/lib/model/deal-board-model.js +15 -2
  230. package/src/lib/model/deal-board-model.js.map +1 -1
  231. package/src/lib/model/deal-contact-roles.d.ts +45 -0
  232. package/src/lib/model/deal-contact-roles.js +73 -0
  233. package/src/lib/model/deal-contact-roles.js.map +1 -0
  234. package/src/lib/model/deal-form-model.d.ts +56 -0
  235. package/src/lib/model/deal-form-model.js +79 -7
  236. package/src/lib/model/deal-form-model.js.map +1 -1
  237. package/src/lib/model/download-text-file.d.ts +23 -0
  238. package/src/lib/model/download-text-file.js +34 -0
  239. package/src/lib/model/download-text-file.js.map +1 -0
  240. package/src/lib/model/lead-filters.d.ts +36 -2
  241. package/src/lib/model/lead-filters.js +123 -2
  242. package/src/lib/model/lead-filters.js.map +1 -1
  243. package/src/lib/model/lead-source-direction.d.ts +38 -0
  244. package/src/lib/model/lead-source-direction.js +52 -0
  245. package/src/lib/model/lead-source-direction.js.map +1 -0
  246. package/src/lib/model/picklist-values.d.ts +119 -0
  247. package/src/lib/model/picklist-values.js +256 -0
  248. package/src/lib/model/picklist-values.js.map +1 -0
  249. package/src/lib/model/record-facts-disclosed.d.ts +81 -0
  250. package/src/lib/model/record-facts-disclosed.js +281 -0
  251. package/src/lib/model/record-facts-disclosed.js.map +1 -0
  252. package/src/lib/model/record-facts.d.ts +215 -34
  253. package/src/lib/model/record-facts.js +307 -128
  254. package/src/lib/model/record-facts.js.map +1 -1
  255. package/src/lib/model/task-picklists.d.ts +52 -0
  256. package/src/lib/model/task-picklists.js +83 -0
  257. package/src/lib/model/task-picklists.js.map +1 -0
  258. package/src/lib/model/task-routes.d.ts +13 -0
  259. package/src/lib/model/task-routes.js +23 -4
  260. package/src/lib/model/task-routes.js.map +1 -1
  261. package/src/lib/plan-entitlements.js +6 -4
  262. package/src/lib/plan-entitlements.js.map +1 -1
  263. package/src/lib/plugin.js +17 -0
  264. package/src/lib/plugin.js.map +1 -1
  265. package/src/lib/server/api-v1/contacts.js +220 -14
  266. package/src/lib/server/api-v1/contacts.js.map +1 -1
  267. package/src/lib/server/api-v1/crm-activities.js +14 -1
  268. package/src/lib/server/api-v1/crm-activities.js.map +1 -1
  269. package/src/lib/server/api-v1/crm-companies.js +123 -14
  270. package/src/lib/server/api-v1/crm-companies.js.map +1 -1
  271. package/src/lib/server/api-v1/crm-deals.js +291 -26
  272. package/src/lib/server/api-v1/crm-deals.js.map +1 -1
  273. package/src/lib/server/api-v1/crm-leads.js +144 -38
  274. package/src/lib/server/api-v1/crm-leads.js.map +1 -1
  275. package/src/lib/server/api-v1/crm-pipelines.d.ts +6 -0
  276. package/src/lib/server/api-v1/crm-pipelines.js +8 -2
  277. package/src/lib/server/api-v1/crm-pipelines.js.map +1 -1
  278. package/src/lib/server/api-v1/crm-tasks.js +75 -33
  279. package/src/lib/server/api-v1/crm-tasks.js.map +1 -1
  280. package/src/lib/server/api-v1/openapi.d.ts +0 -11
  281. package/src/lib/server/api-v1/openapi.js +171 -16
  282. package/src/lib/server/api-v1/openapi.js.map +1 -1
  283. package/src/lib/server/assign-contact-owner.d.ts +162 -0
  284. package/src/lib/server/assign-contact-owner.js +340 -0
  285. package/src/lib/server/assign-contact-owner.js.map +1 -0
  286. package/src/lib/server/associate-company-by-domain.d.ts +70 -0
  287. package/src/lib/server/associate-company-by-domain.js +153 -0
  288. package/src/lib/server/associate-company-by-domain.js.map +1 -0
  289. package/src/lib/server/automation-steps.d.ts +139 -0
  290. package/src/lib/server/automation-steps.js +472 -0
  291. package/src/lib/server/automation-steps.js.map +1 -0
  292. package/src/lib/server/capture-contact.js +69 -22
  293. package/src/lib/server/capture-contact.js.map +1 -1
  294. package/src/lib/server/capture-host-contact.d.ts +104 -0
  295. package/src/lib/server/capture-host-contact.js +151 -0
  296. package/src/lib/server/capture-host-contact.js.map +1 -0
  297. package/src/lib/server/company-children.d.ts +25 -0
  298. package/src/lib/server/company-children.js +42 -0
  299. package/src/lib/server/company-children.js.map +1 -0
  300. package/src/lib/server/company-delete.d.ts +18 -0
  301. package/src/lib/server/company-delete.js +44 -21
  302. package/src/lib/server/company-delete.js.map +1 -1
  303. package/src/lib/server/contact-email-history.js +21 -19
  304. package/src/lib/server/contact-email-history.js.map +1 -1
  305. package/src/lib/server/contact-remove.js +27 -2
  306. package/src/lib/server/contact-remove.js.map +1 -1
  307. package/src/lib/server/contact-reports-to.d.ts +61 -0
  308. package/src/lib/server/contact-reports-to.js +114 -0
  309. package/src/lib/server/contact-reports-to.js.map +1 -0
  310. package/src/lib/server/contact-update.d.ts +42 -1
  311. package/src/lib/server/contact-update.js +204 -23
  312. package/src/lib/server/contact-update.js.map +1 -1
  313. package/src/lib/server/convert-host-lead.d.ts +46 -1
  314. package/src/lib/server/convert-host-lead.js +173 -15
  315. package/src/lib/server/convert-host-lead.js.map +1 -1
  316. package/src/lib/server/convert-open-lead.js +12 -5
  317. package/src/lib/server/convert-open-lead.js.map +1 -1
  318. package/src/lib/server/crm-record-indexes.d.ts +45 -0
  319. package/src/lib/server/crm-record-indexes.js +78 -0
  320. package/src/lib/server/crm-record-indexes.js.map +1 -0
  321. package/src/lib/server/crm-sharing.d.ts +9 -1
  322. package/src/lib/server/crm-sharing.js +23 -3
  323. package/src/lib/server/crm-sharing.js.map +1 -1
  324. package/src/lib/server/deal-contact-roles.d.ts +27 -0
  325. package/src/lib/server/deal-contact-roles.js +89 -0
  326. package/src/lib/server/deal-contact-roles.js.map +1 -0
  327. package/src/lib/server/lead-convert.d.ts +5 -0
  328. package/src/lib/server/lead-convert.js +19 -3
  329. package/src/lib/server/lead-convert.js.map +1 -1
  330. package/src/lib/server/lead-create.d.ts +17 -2
  331. package/src/lib/server/lead-create.js +46 -14
  332. package/src/lib/server/lead-create.js.map +1 -1
  333. package/src/lib/server/lead-nurturing.d.ts +6 -0
  334. package/src/lib/server/lead-nurturing.js +57 -20
  335. package/src/lib/server/lead-nurturing.js.map +1 -1
  336. package/src/lib/server/lead-picklists.d.ts +35 -0
  337. package/src/lib/server/lead-picklists.js +52 -0
  338. package/src/lib/server/lead-picklists.js.map +1 -0
  339. package/src/lib/server/lead-source-picklist.d.ts +1 -13
  340. package/src/lib/server/lead-source-picklist.js +6 -33
  341. package/src/lib/server/lead-source-picklist.js.map +1 -1
  342. package/src/lib/server/merge-contacts.js +22 -4
  343. package/src/lib/server/merge-contacts.js.map +1 -1
  344. package/src/lib/server/person-eraser.d.ts +53 -0
  345. package/src/lib/server/person-eraser.js +149 -0
  346. package/src/lib/server/person-eraser.js.map +1 -0
  347. package/src/lib/server/person-records.d.ts +69 -0
  348. package/src/lib/server/person-records.js +317 -0
  349. package/src/lib/server/person-records.js.map +1 -0
  350. package/src/lib/server/person-refund.d.ts +97 -0
  351. package/src/lib/server/person-refund.js +221 -0
  352. package/src/lib/server/person-refund.js.map +1 -0
  353. package/src/lib/server/picklist-values.d.ts +21 -0
  354. package/src/lib/server/picklist-values.js +308 -0
  355. package/src/lib/server/picklist-values.js.map +1 -0
  356. package/src/lib/server/read-picklist.d.ts +44 -0
  357. package/src/lib/server/read-picklist.js +65 -0
  358. package/src/lib/server/read-picklist.js.map +1 -0
  359. package/src/lib/server/recipe-routes.js +15 -7
  360. package/src/lib/server/recipe-routes.js.map +1 -1
  361. package/src/lib/server/record-email-state.js +7 -1
  362. package/src/lib/server/record-email-state.js.map +1 -1
  363. package/src/lib/server/record-facts.d.ts +3 -0
  364. package/src/lib/server/record-facts.js +233 -31
  365. package/src/lib/server/record-facts.js.map +1 -1
  366. package/src/lib/server/record-origin.d.ts +27 -0
  367. package/src/lib/server/record-origin.js +104 -0
  368. package/src/lib/server/record-origin.js.map +1 -0
  369. package/src/lib/server/record-timeline.js +15 -2
  370. package/src/lib/server/record-timeline.js.map +1 -1
  371. package/src/lib/server/suite-gate.js +4 -3
  372. package/src/lib/server/suite-gate.js.map +1 -1
  373. package/src/lib/server/task-routes.js +30 -16
  374. package/src/lib/server/task-routes.js.map +1 -1
  375. package/src/lib/server-deal-stage.d.ts +5 -1
  376. package/src/lib/server-deal-stage.js +10 -5
  377. package/src/lib/server-deal-stage.js.map +1 -1
  378. package/src/lib/server.d.ts +8 -3
  379. package/src/lib/server.js +47 -43
  380. package/src/lib/server.js.map +1 -1
  381. package/src/lib/testing/memory-firestore.d.ts +55 -0
  382. package/src/lib/testing/memory-firestore.js +335 -0
  383. package/src/lib/testing/memory-firestore.js.map +1 -0
  384. package/src/lib/testing/stand-in-email-send-index.d.ts +25 -0
  385. package/src/lib/testing/stand-in-email-send-index.js +49 -0
  386. package/src/lib/testing/stand-in-email-send-index.js.map +1 -0
  387. package/src/lib/transfer/activities.d.ts +30 -0
  388. package/src/lib/transfer/activities.js +132 -0
  389. package/src/lib/transfer/activities.js.map +1 -0
  390. package/src/lib/transfer/aliases.d.ts +22 -0
  391. package/src/lib/transfer/aliases.js +1342 -0
  392. package/src/lib/transfer/aliases.js.map +1 -0
  393. package/src/lib/transfer/common.d.ts +222 -0
  394. package/src/lib/transfer/common.js +832 -0
  395. package/src/lib/transfer/common.js.map +1 -0
  396. package/src/lib/transfer/companies.d.ts +43 -0
  397. package/src/lib/transfer/companies.js +549 -0
  398. package/src/lib/transfer/companies.js.map +1 -0
  399. package/src/lib/transfer/contacts.d.ts +31 -0
  400. package/src/lib/transfer/contacts.js +727 -0
  401. package/src/lib/transfer/contacts.js.map +1 -0
  402. package/src/lib/transfer/deals.d.ts +66 -0
  403. package/src/lib/transfer/deals.js +698 -0
  404. package/src/lib/transfer/deals.js.map +1 -0
  405. package/src/lib/transfer/email-templates-package.d.ts +40 -0
  406. package/src/lib/transfer/email-templates-package.js +76 -0
  407. package/src/lib/transfer/email-templates-package.js.map +1 -0
  408. package/src/lib/transfer/email-templates-package.server.d.ts +32 -0
  409. package/src/lib/transfer/email-templates-package.server.js +176 -0
  410. package/src/lib/transfer/email-templates-package.server.js.map +1 -0
  411. package/src/lib/transfer/fields.d.ts +102 -0
  412. package/src/lib/transfer/fields.js +1755 -0
  413. package/src/lib/transfer/fields.js.map +1 -0
  414. package/src/lib/transfer/leads.d.ts +30 -0
  415. package/src/lib/transfer/leads.js +582 -0
  416. package/src/lib/transfer/leads.js.map +1 -0
  417. package/src/lib/transfer/pipelines.d.ts +21 -0
  418. package/src/lib/transfer/pipelines.js +172 -0
  419. package/src/lib/transfer/pipelines.js.map +1 -0
  420. package/src/lib/transfer/register.d.ts +47 -0
  421. package/src/lib/transfer/register.js +277 -0
  422. package/src/lib/transfer/register.js.map +1 -0
  423. package/src/lib/transfer/tasks.d.ts +30 -0
  424. package/src/lib/transfer/tasks.js +418 -0
  425. package/src/lib/transfer/tasks.js.map +1 -0
  426. package/src/lib/transfer/ui.d.ts +20 -0
  427. package/src/lib/transfer/ui.js +42 -0
  428. package/src/lib/transfer/ui.js.map +1 -0
  429. package/src/lib/components/company-import-drawer.d.ts +0 -42
  430. package/src/lib/components/company-import-drawer.js +0 -99
  431. package/src/lib/components/company-import-drawer.js.map +0 -1
  432. package/src/lib/components/contact-import-drawer.d.ts +0 -46
  433. package/src/lib/components/contact-import-drawer.js +0 -149
  434. package/src/lib/components/contact-import-drawer.js.map +0 -1
  435. package/src/lib/components/crm-export-all-button.d.ts +0 -39
  436. package/src/lib/components/crm-export-all-button.js +0 -98
  437. package/src/lib/components/crm-export-all-button.js.map +0 -1
  438. package/src/lib/components/csv-import-drawer.d.ts +0 -139
  439. package/src/lib/components/csv-import-drawer.js +0 -757
  440. package/src/lib/components/csv-import-drawer.js.map +0 -1
  441. package/src/lib/components/deal-import-drawer.d.ts +0 -24
  442. package/src/lib/components/deal-import-drawer.js +0 -83
  443. package/src/lib/components/deal-import-drawer.js.map +0 -1
  444. package/src/lib/components/lead-import-drawer.d.ts +0 -27
  445. package/src/lib/components/lead-import-drawer.js +0 -118
  446. package/src/lib/components/lead-import-drawer.js.map +0 -1
  447. package/src/lib/components/lead-source-values-card.d.ts +0 -27
  448. package/src/lib/components/lead-source-values-card.js.map +0 -1
  449. package/src/lib/components/task-import-drawer.d.ts +0 -24
  450. package/src/lib/components/task-import-drawer.js +0 -82
  451. package/src/lib/components/task-import-drawer.js.map +0 -1
  452. package/src/lib/model/companies-csv.d.ts +0 -36
  453. package/src/lib/model/companies-csv.js +0 -38
  454. package/src/lib/model/companies-csv.js.map +0 -1
  455. package/src/lib/model/contacts-csv.d.ts +0 -54
  456. package/src/lib/model/contacts-csv.js +0 -67
  457. package/src/lib/model/contacts-csv.js.map +0 -1
  458. package/src/lib/model/crm-company-import.d.ts +0 -154
  459. package/src/lib/model/crm-company-import.js +0 -331
  460. package/src/lib/model/crm-company-import.js.map +0 -1
  461. package/src/lib/model/crm-csv.d.ts +0 -239
  462. package/src/lib/model/crm-csv.js +0 -467
  463. package/src/lib/model/crm-csv.js.map +0 -1
  464. package/src/lib/model/crm-deal-import.d.ts +0 -112
  465. package/src/lib/model/crm-deal-import.js +0 -263
  466. package/src/lib/model/crm-deal-import.js.map +0 -1
  467. package/src/lib/model/crm-import.d.ts +0 -250
  468. package/src/lib/model/crm-import.js +0 -467
  469. package/src/lib/model/crm-import.js.map +0 -1
  470. package/src/lib/model/crm-lead-import.d.ts +0 -138
  471. package/src/lib/model/crm-lead-import.js +0 -446
  472. package/src/lib/model/crm-lead-import.js.map +0 -1
  473. package/src/lib/model/crm-task-import.d.ts +0 -94
  474. package/src/lib/model/crm-task-import.js +0 -275
  475. package/src/lib/model/crm-task-import.js.map +0 -1
  476. package/src/lib/model/deals-csv.d.ts +0 -32
  477. package/src/lib/model/deals-csv.js +0 -36
  478. package/src/lib/model/deals-csv.js.map +0 -1
  479. package/src/lib/model/import-column-shapes.d.ts +0 -36
  480. package/src/lib/model/import-column-shapes.js +0 -76
  481. package/src/lib/model/import-column-shapes.js.map +0 -1
  482. package/src/lib/model/lead-source-values.d.ts +0 -82
  483. package/src/lib/model/lead-source-values.js +0 -149
  484. package/src/lib/model/lead-source-values.js.map +0 -1
  485. package/src/lib/model/leads-csv.d.ts +0 -40
  486. package/src/lib/model/leads-csv.js +0 -40
  487. package/src/lib/model/leads-csv.js.map +0 -1
  488. package/src/lib/model/tasks-csv.d.ts +0 -28
  489. package/src/lib/model/tasks-csv.js +0 -28
  490. package/src/lib/model/tasks-csv.js.map +0 -1
  491. package/src/lib/server/companies-import.d.ts +0 -57
  492. package/src/lib/server/companies-import.js +0 -242
  493. package/src/lib/server/companies-import.js.map +0 -1
  494. package/src/lib/server/contacts-import.d.ts +0 -109
  495. package/src/lib/server/contacts-import.js +0 -393
  496. package/src/lib/server/contacts-import.js.map +0 -1
  497. package/src/lib/server/deals-import.d.ts +0 -76
  498. package/src/lib/server/deals-import.js +0 -248
  499. package/src/lib/server/deals-import.js.map +0 -1
  500. package/src/lib/server/export-route.d.ts +0 -60
  501. package/src/lib/server/export-route.js +0 -447
  502. package/src/lib/server/export-route.js.map +0 -1
  503. package/src/lib/server/import-context.d.ts +0 -87
  504. package/src/lib/server/import-context.js +0 -182
  505. package/src/lib/server/import-context.js.map +0 -1
  506. package/src/lib/server/lead-source-values.d.ts +0 -50
  507. package/src/lib/server/lead-source-values.js +0 -191
  508. package/src/lib/server/lead-source-values.js.map +0 -1
  509. package/src/lib/server/leads-import.d.ts +0 -84
  510. package/src/lib/server/leads-import.js +0 -367
  511. package/src/lib/server/leads-import.js.map +0 -1
  512. package/src/lib/server/tasks-import.d.ts +0 -54
  513. package/src/lib/server/tasks-import.js +0 -160
  514. package/src/lib/server/tasks-import.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/record-origin.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type {\n PluginRecordOriginReport,\n PluginRecordOriginRequest,\n PluginRecordOriginWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-origin'\nimport {\n consentGroupForHost,\n CONTACT_FACETS_FIELD,\n contactFacetPath,\n crmLeadSourceForOrigin,\n crmPicklistKey,\n normalizeContactEmail,\n personKey,\n} from '@aglyn/aglyn/server'\nimport { visibleToHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport {\n findContactByEmail,\n firebaseAdmin,\n getOrgForHost,\n restampCrmListFieldsAt,\n} from '@aglyn/tenant-data-admin'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { readLeadSourcePicklist } from './lead-source-picklist'\n\n/*==========================================\n * A PERSON'S ORIGIN, AS A LEAD SOURCE (AGL-3519).\n *\n * The CRM's side of the platform's record-origin seam, and what its own\n * capture door calls after filing a person: a door's word for how it met\n * somebody — a form, a booking, a sequence — becomes the Lead source its\n * built-in value names (`CRM_LEAD_SOURCE_ORIGINS`), as the org spells it.\n *\n * - ORIGINAL SOURCE. A record that already names a lead source keeps it;\n * only an empty one is stamped, and a person the org cleared stays clear\n * to every door that meets them again (`firstTouchOnly`).\n * - A DEACTIVATED value stamps nothing: the org has said not to file\n * people under it.\n * - Both records the address is, as the site sees them: the lead, when the\n * site may see it, and the site's own facet on the contact — a holder's\n * knowledge of the person, like the rest of its profile.\n *\n * Each stamp is a transaction that reads the field and writes only into an\n * empty one, so a rep's pick made a moment earlier is never overwritten;\n * the list fields follow (`leadSourceKey`, `facetKeys`).\n *=========================================*/\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** Where one stamp lands, for a caller that already holds the org. */\nexport interface CrmRecordOriginInput extends PluginRecordOriginRequest {\n org?: Record<string, unknown> | null\n}\n\n/** Stamps the origin on the lead and the site's contact facet the address is; how many took it. */\nexport async function stampCrmRecordOrigin(\n input: CrmRecordOriginInput,\n firestore: Firestore = firebaseAdmin.app().firestore(),\n): Promise<PluginRecordOriginReport> {\n const email = normalizeContactEmail(input.email)\n const key = email ? personKey(email) : null\n if (!email || !key || !input.hostId) return { records: 0 }\n let orgId = input.orgId\n let org = input.org ?? null\n if (!orgId || !org) {\n const resolved = await getOrgForHost(input.hostId)\n if (!resolved || (orgId && resolved.orgId !== orgId)) return { records: 0 }\n orgId = resolved.orgId\n org = resolved.org as Record<string, unknown>\n }\n const label = crmLeadSourceForOrigin(await readLeadSourcePicklist(firestore, orgId), input.origin)\n if (!label) return { records: 0 }\n const orgRef = firestore.collection('orgs').doc(orgId)\n let records = 0\n\n /* THE LEAD, keyed on the person and seen by the site. */\n const leadRef = orgRef.collection('leads').doc(key)\n const leadStamped = await firestore.runTransaction(async (tx) => {\n const snapshot = await tx.get(leadRef)\n if (!snapshot.exists) return false\n const lead = snapshot.data() ?? {}\n if (!visibleToHost(lead['visibleTo'] as string[] | undefined, input.hostId)) return false\n if (typeof lead['leadSource'] === 'string' && lead['leadSource'].trim()) return false\n if (input.firstTouchOnly && Number(lead['submissionCount'] ?? 1) > 1) return false\n tx.update(leadRef, {\n leadSource: label,\n leadSourceKey: crmPicklistKey(label),\n updatedAt: FieldValue.serverTimestamp(),\n })\n return true\n })\n if (leadStamped) {\n records += 1\n await restampCrmListFieldsAt(leadRef, 'leads')\n }\n\n /* THE CONTACT, on the site's own facet — present only once the site holds the person. */\n const found = await findContactByEmail(orgRef.collection('contacts'), email)\n if (found) {\n const groupId = consentGroupForHost(org, input.hostId).groupId\n const contactStamped = await firestore.runTransaction(async (tx) => {\n const snapshot = await tx.get(found.ref)\n const facets = (snapshot.data() ?? {})[CONTACT_FACETS_FIELD] as Record<string, unknown> | undefined\n const facet = facets?.[groupId] as Record<string, unknown> | undefined\n if (!facet || typeof facet !== 'object') return false\n if (typeof facet['leadSource'] === 'string' && facet['leadSource'].trim()) return false\n const interactions = Array.isArray(facet['interactions']) ? facet['interactions'].length : 0\n if (input.firstTouchOnly && interactions > 1) return false\n tx.update(found.ref, {\n [contactFacetPath(groupId, 'leadSource')]: label,\n updatedAt: FieldValue.serverTimestamp(),\n })\n return true\n })\n if (contactStamped) {\n records += 1\n await restampCrmListFieldsAt(found.ref, 'contacts')\n }\n }\n return { records }\n}\n\n/** The writer the CRM registers on the platform's record-origin seam. Never throws. */\nexport const crmRecordOriginWriter: PluginRecordOriginWriter = {\n async stamp(request) {\n try {\n return await stampCrmRecordOrigin(request)\n } catch (error) {\n console.error('[crm] the lead source could not be stamped', request.hostId, error)\n return { records: 0 }\n }\n },\n}\n"],"names":["consentGroupForHost","CONTACT_FACETS_FIELD","contactFacetPath","crmLeadSourceForOrigin","crmPicklistKey","normalizeContactEmail","personKey","visibleToHost","findContactByEmail","firebaseAdmin","getOrgForHost","restampCrmListFieldsAt","FieldValue","readLeadSourcePicklist","stampCrmRecordOrigin","input","firestore","app","email","key","hostId","records","orgId","org","resolved","label","origin","orgRef","collection","doc","leadRef","leadStamped","runTransaction","tx","snapshot","lead","get","exists","data","trim","firstTouchOnly","Number","update","leadSource","leadSourceKey","updatedAt","serverTimestamp","found","groupId","contactStamped","ref","facets","facet","interactions","Array","isArray","length","crmRecordOriginWriter","stamp","request","error","console"],"mappings":"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"}
@@ -15,12 +15,13 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
17
  */ import { registerPluginRecordTimelineWriter } from "@aglyn/aglyn/plugin-manager/plugin-record-timeline";
18
- import { checkEntitlement, consentGroupForHost, CRM_ACTIVITY_LOG_FULL_MESSAGE, CRM_COLLECTIONS, crmActivityLogHasRoom, crmScopeTokens, crmTaskListFields, crmTaskReminderAfterEdit, isCrmActivityKind, isCrmTaskKind, readContactFacet, visibleToHost } from "@aglyn/aglyn/server";
18
+ import { checkEntitlement, consentGroupForHost, CRM_ACTIVITY_LOG_FULL_MESSAGE, CRM_COLLECTIONS, crmActivityLogHasRoom, crmScopeTokens, crmTaskLabelsForNew, crmTaskListFields, crmTaskReminderAfterEdit, isCrmActivityKind, isCrmTaskKind, readContactFacet, visibleToHost } from "@aglyn/aglyn/server";
19
19
  import { countCrmActivitiesForRecord, createCrmEmailActivity, crmActivityRef, crmCapturedEmailActivityRef, findContactByEmail, firebaseAdmin, readLeadForHost, recomputeCrmNextTaskAt, recordCrmEmailDelivery } from "@aglyn/tenant-data-admin";
20
20
  import { createHash } from "crypto";
21
21
  import { FieldValue } from "firebase-admin/firestore";
22
22
  import { BUNDLE_ID } from "../constants/bundle-common.js";
23
23
  import { buildCrmCapturedEmailActivity, crmCapturedEmailKey } from "../model/crm-inbound.js";
24
+ import { readCrmTaskPicklists } from "./read-picklist.js";
24
25
  import { CRM_SUITE_FEATURE } from "./suite-gate.js";
25
26
  /** gRPC `ALREADY_EXISTS`, which `create()` rejects with when the document is there. */ const ALREADY_EXISTS = 6;
26
27
  /** The longest task title the CRM keeps, as its own forms do. */ const TASK_TITLE_MAX = 200;
@@ -274,6 +275,11 @@ export function createCrmRecordTimelineWriter(deps) {
274
275
  const notes = String((_request_notes = request.notes) != null ? _request_notes : '').trim().slice(0, NOTE_MAX);
275
276
  // `null` is "whoever holds the record", which admission read.
276
277
  const assigneeUid = request.assigneeUid === null ? (_admitted_holderUid = admitted.holderUid) != null ? _admitted_holderUid : '' : String((_request_assigneeUid = request.assigneeUid) != null ? _request_assigneeUid : '').trim();
278
+ // The org's labels for the meanings it is filed with (AGL-3517).
279
+ const labels = crmTaskLabelsForNew(await readCrmTaskPicklists(firestore, orgRef.id), {
280
+ kind: request.kind,
281
+ priority: 'normal'
282
+ });
277
283
  const task = _extends({
278
284
  title
279
285
  }, notes ? {
@@ -281,7 +287,8 @@ export function createCrmRecordTimelineWriter(deps) {
281
287
  } : {}, {
282
288
  kind: request.kind,
283
289
  priority: 'normal',
284
- status: 'open',
290
+ status: 'open'
291
+ }, labels, {
285
292
  dueAtMs: request.dueAtMs,
286
293
  // The reminder a person's task gets (AGL-2659): the due time.
287
294
  remindAtMs: crmTaskReminderAfterEdit({
@@ -338,6 +345,12 @@ export function createCrmRecordTimelineWriter(deps) {
338
345
  id: ref.id,
339
346
  created: outcome === 'advanced'
340
347
  };
348
+ },
349
+ // An automation's email to the person its event is about (AGL-2615):
350
+ // see `automation-steps.ts`, loaded with the first one.
351
+ async prepareEmail (request) {
352
+ const { prepareCrmRecordEmail } = await import("./automation-steps.js");
353
+ return prepareCrmRecordEmail(request);
341
354
  }
342
355
  };
343
356
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/record-timeline.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 registerPluginRecordTimelineWriter,\n type PluginRecordActivityRequest,\n type PluginRecordDeliveryRequest,\n type PluginRecordEntryContext,\n type PluginRecordTaskRequest,\n type PluginRecordTimelineWriter,\n type PluginRecordWrite,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-timeline'\nimport {\n checkEntitlement,\n consentGroupForHost,\n CRM_ACTIVITY_LOG_FULL_MESSAGE,\n CRM_COLLECTIONS,\n crmActivityLogHasRoom,\n crmScopeTokens,\n crmTaskListFields,\n crmTaskReminderAfterEdit,\n isCrmActivityKind,\n isCrmTaskKind,\n readContactFacet,\n visibleToHost,\n type CrmActivity,\n type CrmActivityLink,\n type CrmTask,\n} from '@aglyn/aglyn/server'\nimport {\n countCrmActivitiesForRecord,\n createCrmEmailActivity,\n crmActivityRef,\n crmCapturedEmailActivityRef,\n findContactByEmail,\n firebaseAdmin,\n readLeadForHost,\n recomputeCrmNextTaskAt,\n recordCrmEmailDelivery,\n} from '@aglyn/tenant-data-admin'\nimport { createHash } from 'crypto'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { buildCrmCapturedEmailActivity, crmCapturedEmailKey } from '../model/crm-inbound'\nimport { CRM_SUITE_FEATURE } from './suite-gate'\n\n/**\n * THE CRM'S WRITER ON THE CORE'S RECORD-TIMELINE SEAM (AGL-2981).\n *\n * Another plugin files an activity or a task on a contact, company or deal —\n * an email a sequence sent, the reply it read, the call it asks for — and\n * this writes it as the CRM writes its own:\n *\n * 1. the workspace's plan carries the CRM (`features.crm`), as at every\n * CRM door; a Free workspace keeps no records to file on;\n * 2. the entry carries the scope a record made on that site carries\n * (`crmScopeTokens`), so exactly the people who see the record see it;\n * 3. an EMAIL is filed under the id the capture address files a copy of the\n * same message by (`cap_` + the digest of its `Message-ID`), and written\n * with `create()`: the send, a copy the rep forwarded to the capture\n * address, and a second run of the caller are one row;\n * 4. anything else — a note, a task — is filed under the caller's own key,\n * hashed with the caller's plugin id, and written once the same way;\n * 5. a record at its activity ceiling is refused with the sentence every\n * CRM writer answers with, before anything is written;\n * 6. a bounce or a complaint on a filed email (AGL-3245) advances that\n * email's own delivery state — the row the send was filed under, found\n * by its `Message-ID` — exactly as the campaign webhook advances one,\n * so the timeline reads Sent, then Bounced, with what the server said.\n *\n * Each entry names the plugin that filed it (`sourcePluginId`), and a task's\n * `nextTaskAtMs` is recomputed on the records it names, as every server\n * writer of a task does.\n *\n * ## A record found rather than named\n *\n * A caller that holds no id of the CRM's — a booking made through a link a\n * rep dropped from a record, or one taken off the widget cold (AGL-2660) —\n * hands back the record the link carried, or the booker's address, and the\n * entry is filed where the CRM finds it: the record the reference names\n * when this site may see it, otherwise the contact the address names\n * through the org's address index, narrowed to the site. A deal carries its\n * contact and company onto the entry; a contact its company, from the\n * site's own facet. A task with no assignee of the caller's goes to whoever\n * holds that record.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\nexport interface CrmRecordTimelineDeps {\n firestore(): Firestore\n}\n\n/** gRPC `ALREADY_EXISTS`, which `create()` rejects with when the document is there. */\nconst ALREADY_EXISTS = 6\n\n/** The longest task title the CRM keeps, as its own forms do. */\nconst TASK_TITLE_MAX = 200\n/** The longest note body or task note an entry filed here keeps. */\nconst NOTE_MAX = 2_000\n\nconst refuse = (status: 400 | 403 | 404 | 409, error: string): PluginRecordWrite => ({\n ok: false,\n status,\n error,\n})\n\n/** The record fields of a link, only the ones it names. */\nfunction linkFields(link: PluginRecordEntryContext['link']): CrmActivityLink {\n return {\n ...(link?.contactId ? { contactId: String(link.contactId) } : {}),\n ...(link?.companyId ? { companyId: String(link.companyId) } : {}),\n // A lead (AGL-3234): host-scoped by path, so the entry carries the\n // site's scope like any record created from that site.\n ...(link?.leadId ? { leadId: String(link.leadId) } : {}),\n }\n}\n\n/** What a reference handed back by a caller may name: the records a booking link is dropped from. */\nconst FOUND_RECORD_KINDS: ReadonlySet<string> = new Set(['contact', 'lead', 'deal'])\n/**\n * A record id as the CRM mints them: Firestore's for a contact and a deal, a\n * person key for a lead. A reference arrives on a public request, so one\n * that could not be an id is passed over rather than read.\n */\nconst RECORD_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/\n\n/** The record an entry lands on, and who holds it. */\ninterface FoundRecord {\n link: CrmActivityLink\n holderUid?: string\n}\n\n/** A contact's link and holder, from this site's facet on it. */\nfunction contactRecord(\n contact: FirebaseFirestore.DocumentSnapshot,\n groupId: string,\n): FoundRecord {\n const facet = readContactFacet(contact.data() ?? {}, groupId)\n return {\n link: { contactId: contact.id, ...(facet.companyId ? { companyId: facet.companyId } : {}) },\n ...(facet.ownerUid ? { holderUid: facet.ownerUid } : {}),\n }\n}\n\n/**\n * The record a caller's reference names, then the contact its address does,\n * as `hostId` may see them; `null` when neither finds one. See the module\n * comment.\n */\nasync function findRecord(\n orgRef: FirebaseFirestore.DocumentReference,\n hostId: string,\n groupId: string,\n link: PluginRecordEntryContext['link'],\n): Promise<FoundRecord | null> {\n const contacts = orgRef.collection('contacts')\n const kind = String(link?.record?.kind ?? '')\n const id = String(link?.record?.id ?? '')\n if (FOUND_RECORD_KINDS.has(kind) && RECORD_ID_PATTERN.test(id)) {\n if (kind === 'contact') {\n const contact = await contacts.doc(id).get()\n if (contact.exists && visibleToHost(contact.get('visibleTo'), hostId)) {\n return contactRecord(contact, groupId)\n }\n } else if (kind === 'deal') {\n const deal = await orgRef.collection(CRM_COLLECTIONS.deals).doc(id).get()\n if (deal.exists && visibleToHost(deal.get('visibleTo'), hostId)) {\n const contactId = String(deal.get('contactId') ?? '')\n const companyId = String(deal.get('companyId') ?? '')\n const ownerUid = String(deal.get('ownerUid') ?? '')\n return {\n link: { dealId: deal.id, ...(contactId ? { contactId } : {}), ...(companyId ? { companyId } : {}) },\n ...(ownerUid ? { holderUid: ownerUid } : {}),\n }\n }\n } else {\n // A lead carries its own `visibleTo` (AGL-3275), and the site has to be\n // allowed to see it, or one brand's booking would name another's lead.\n const lead = await readLeadForHost(hostId, id)\n if (lead && visibleToHost(lead.get('visibleTo'), hostId)) {\n const ownerUid = String(lead.get('ownerUid') ?? '')\n return { link: { leadId: lead.id }, ...(ownerUid ? { holderUid: ownerUid } : {}) }\n }\n }\n }\n const email = String(link?.email ?? '').trim()\n if (!email) return null\n const hit = await findContactByEmail(contacts, email, { hostId })\n return hit ? contactRecord(hit, groupId) : null\n}\n\n/** Who holds a record a caller named by id: its contact's site facet, or its lead. */\nasync function holderOf(\n orgRef: FirebaseFirestore.DocumentReference,\n hostId: string,\n groupId: string,\n link: CrmActivityLink,\n): Promise<string | undefined> {\n if (link.contactId) {\n const contact = await orgRef.collection('contacts').doc(link.contactId).get()\n return contact.exists ? contactRecord(contact, groupId).holderUid : undefined\n }\n if (link.leadId) {\n const lead = await readLeadForHost(hostId, link.leadId)\n return String(lead?.get('ownerUid') ?? '') || undefined\n }\n return undefined\n}\n\n/** The document id a caller's key files under: its own namespace, never a raw key. */\nfunction keyedId(sourcePluginId: string, key: string): string {\n return `plg_${createHash('sha256').update(`${sourcePluginId}:${key}`).digest('hex').slice(0, 28)}`\n}\n\nexport function createCrmRecordTimelineWriter(deps: CrmRecordTimelineDeps): PluginRecordTimelineWriter {\n /**\n * The org, its plan, the link and the scope an entry is stamped with — and,\n * when `wantHolder`, who holds the record it lands on.\n */\n async function admit(\n context: PluginRecordEntryContext,\n wantHolder = false,\n ): Promise<\n | PluginRecordWrite\n | {\n firestore: Firestore\n orgRef: FirebaseFirestore.DocumentReference\n link: CrmActivityLink\n visibleTo: string[]\n holderUid?: string\n }\n > {\n const orgId = String(context.orgId ?? '').trim()\n const hostId = String(context.hostId ?? '').trim()\n const named = linkFields(context.link)\n const isNamed = Boolean(named.contactId || named.companyId || named.leadId)\n if (!orgId || !hostId || !String(context.sourcePluginId ?? '').trim()) {\n return refuse(400, 'An entry names its organization, its site and the plugin filing it.')\n }\n if (!isNamed && !context.link?.record && !String(context.link?.email ?? '').trim()) {\n return refuse(400, 'An entry is filed on a contact, a company or a lead, or names how to find one.')\n }\n const firestore = deps.firestore()\n const orgRef = firestore.collection('orgs').doc(orgId)\n const snapshot = await orgRef.get()\n if (!snapshot.exists) return refuse(404, 'That organization no longer exists.')\n const org = (snapshot.data() ?? {}) as Record<string, unknown>\n if (!checkEntitlement(org, CRM_SUITE_FEATURE)) {\n return refuse(403, \"This workspace's plan doesn't include the CRM.\")\n }\n const group = consentGroupForHost(org, hostId)\n let link = named\n let holderUid: string | undefined\n if (isNamed) {\n if (wantHolder) holderUid = await holderOf(orgRef, hostId, group.groupId, named)\n } else {\n const found = await findRecord(orgRef, hostId, group.groupId, context.link)\n if (!found) return refuse(404, 'Nothing this site keeps matches the record or the address.')\n link = found.link\n holderUid = found.holderUid\n }\n return {\n firestore,\n orgRef,\n link,\n visibleTo: crmScopeTokens(org, group),\n ...(holderUid ? { holderUid } : {}),\n }\n }\n\n /** Creates a keyed entry once: `created: false` for one already filed. */\n async function createOnce(\n ref: FirebaseFirestore.DocumentReference,\n data: Record<string, unknown>,\n ): Promise<boolean> {\n try {\n await ref.create({\n ...data,\n createdAt: FieldValue.serverTimestamp(),\n updatedAt: FieldValue.serverTimestamp(),\n })\n return true\n } catch (error) {\n if ((error as { code?: unknown })?.code === ALREADY_EXISTS) return false\n throw error\n }\n }\n\n return {\n async logActivity(request: PluginRecordActivityRequest): Promise<PluginRecordWrite> {\n if (!isCrmActivityKind(request.kind)) return refuse(400, `\"${String(request.kind)}\" isn't an activity.`)\n const admitted = await admit(request)\n if ('ok' in admitted) return admitted\n const { firestore, orgRef, link, visibleTo } = admitted\n const orgId = orgRef.id\n\n let ref: FirebaseFirestore.DocumentReference\n let activity: CrmActivity\n if (request.kind === 'email') {\n const email = request.email\n const key = email ? crmCapturedEmailKey(email.messageId, null) : null\n if (!email || !key) return refuse(400, 'An email is filed by its Message-ID.')\n ref = crmCapturedEmailActivityRef(firestore, orgId, key)\n activity = buildCrmCapturedEmailActivity({\n direction: email.direction === 'inbound' ? 'inbound' : 'outbound',\n subject: email.subject,\n excerpt: request.body,\n from: email.from,\n to: email.to,\n messageId: email.messageId,\n inReplyTo: email.inReplyTo ?? null,\n atMs: request.atMs,\n byUid: String(request.byUid ?? ''),\n byName: request.byName ?? null,\n link,\n hostId: request.hostId,\n visibleTo,\n })\n } else {\n const key = String(request.dedupeKey ?? '').trim()\n if (!key) return refuse(400, 'An entry that is not an email names its own key.')\n ref = crmActivityRef(firestore, orgId, keyedId(request.sourcePluginId, key))\n const byName = String(request.byName ?? '').trim()\n activity = {\n kind: request.kind,\n body: String(request.body ?? '').slice(0, NOTE_MAX),\n atMs: request.atMs,\n byUid: String(request.byUid ?? ''),\n ...(byName ? { byName } : {}),\n ...link,\n hostId: request.hostId,\n visibleTo,\n }\n }\n // Filed before: answered off the one read, ahead of the ceiling's count.\n if ((await ref.get()).exists) return { ok: true, id: ref.id, created: false }\n if (!crmActivityLogHasRoom(await countCrmActivitiesForRecord(orgRef, link))) {\n return refuse(409, CRM_ACTIVITY_LOG_FULL_MESSAGE)\n }\n const stamped = { ...activity, sourcePluginId: request.sourcePluginId }\n const created =\n request.kind === 'email'\n ? (await createCrmEmailActivity(ref, stamped)) === 'created'\n : await createOnce(ref, stamped as unknown as Record<string, unknown>)\n return { ok: true, id: ref.id, created }\n },\n\n async createTask(request: PluginRecordTaskRequest): Promise<PluginRecordWrite> {\n const title = String(request.title ?? '').replace(/\\s+/g, ' ').trim().slice(0, TASK_TITLE_MAX)\n const key = String(request.dedupeKey ?? '').trim()\n if (!title || !key) return refuse(400, 'A task has a title and the caller’s own key.')\n if (!isCrmTaskKind(request.kind)) return refuse(400, `\"${String(request.kind)}\" isn't a task.`)\n if (!Number.isFinite(request.dueAtMs)) return refuse(400, 'A task has a due time.')\n const admitted = await admit(request, request.assigneeUid === null)\n if ('ok' in admitted) return admitted\n const { firestore, orgRef, link, visibleTo } = admitted\n const ref = orgRef.collection(CRM_COLLECTIONS.tasks).doc(keyedId(request.sourcePluginId, key))\n const notes = String(request.notes ?? '').trim().slice(0, NOTE_MAX)\n // `null` is \"whoever holds the record\", which admission read.\n const assigneeUid =\n request.assigneeUid === null\n ? (admitted.holderUid ?? '')\n : String(request.assigneeUid ?? '').trim()\n const task: CrmTask = {\n title,\n ...(notes ? { notes } : {}),\n kind: request.kind,\n priority: 'normal',\n status: 'open',\n dueAtMs: request.dueAtMs,\n // The reminder a person's task gets (AGL-2659): the due time.\n remindAtMs: crmTaskReminderAfterEdit({ dueAtMs: request.dueAtMs, previous: null }),\n ...(assigneeUid ? { assigneeUid } : {}),\n createdByUid: String(request.createdByUid ?? ''),\n sourcePluginId: request.sourcePluginId,\n ...link,\n hostId: request.hostId,\n visibleTo,\n }\n // What the Tasks list searches by (AGL-3321), beside the task.\n const created = await createOnce(ref, { ...task, ...crmTaskListFields(task) })\n if (created) {\n // The records the task names carry `nextTaskAtMs` (AGL-2661); a figure\n // that could not move is the Fields section's recompute's.\n await recomputeCrmNextTaskAt(firestore, orgRef.id, [link]).catch((error: unknown) => {\n console.error('[crm] next activity could not be recomputed', orgRef.id, error)\n })\n }\n return { ok: true, id: ref.id, created }\n },\n\n async recordEmailDelivery(request: PluginRecordDeliveryRequest): Promise<PluginRecordWrite> {\n const orgId = String(request.orgId ?? '').trim()\n const key = crmCapturedEmailKey(request.messageId, null)\n if (!orgId || !key) return refuse(400, 'A delivery names its organization and the email’s Message-ID.')\n if (request.state !== 'bounced' && request.state !== 'complained') {\n return refuse(400, `\"${String(request.state)}\" isn't a delivery failure.`)\n }\n const firestore = deps.firestore()\n const ref = crmCapturedEmailActivityRef(firestore, orgId, key)\n const outcome = await recordCrmEmailDelivery(firestore, {\n orgId,\n activityId: ref.id,\n state: request.state,\n atMs: request.atMs,\n detail: request.detail ?? null,\n })\n if (outcome === 'missing') return refuse(404, 'No email by that Message-ID is filed here.')\n if (outcome === 'failed') return refuse(409, 'The delivery state could not be written.')\n return { ok: true, id: ref.id, created: outcome === 'advanced' }\n },\n }\n}\n\n/** The platform's own dependencies. Specs build their own. */\nexport function defaultCrmRecordTimelineDeps(): CrmRecordTimelineDeps {\n return { firestore: () => firebaseAdmin.app().firestore() }\n}\n\n/** Registers the CRM as the workspace's record system on the timeline seam. */\nexport function registerCrmRecordTimelineWriter(\n deps: CrmRecordTimelineDeps = defaultCrmRecordTimelineDeps(),\n): void {\n registerPluginRecordTimelineWriter(createCrmRecordTimelineWriter(deps), { pluginId: BUNDLE_ID })\n}\n"],"names":["registerPluginRecordTimelineWriter","checkEntitlement","consentGroupForHost","CRM_ACTIVITY_LOG_FULL_MESSAGE","CRM_COLLECTIONS","crmActivityLogHasRoom","crmScopeTokens","crmTaskListFields","crmTaskReminderAfterEdit","isCrmActivityKind","isCrmTaskKind","readContactFacet","visibleToHost","countCrmActivitiesForRecord","createCrmEmailActivity","crmActivityRef","crmCapturedEmailActivityRef","findContactByEmail","firebaseAdmin","readLeadForHost","recomputeCrmNextTaskAt","recordCrmEmailDelivery","createHash","FieldValue","BUNDLE_ID","buildCrmCapturedEmailActivity","crmCapturedEmailKey","CRM_SUITE_FEATURE","ALREADY_EXISTS","TASK_TITLE_MAX","NOTE_MAX","refuse","status","error","ok","linkFields","link","contactId","String","companyId","leadId","FOUND_RECORD_KINDS","Set","RECORD_ID_PATTERN","contactRecord","contact","groupId","facet","data","id","ownerUid","holderUid","findRecord","orgRef","hostId","contacts","collection","kind","record","has","test","doc","get","exists","deal","deals","dealId","lead","email","trim","hit","holderOf","undefined","keyedId","sourcePluginId","key","update","digest","slice","createCrmRecordTimelineWriter","deps","admit","context","wantHolder","snapshot","orgId","named","isNamed","Boolean","firestore","org","group","found","visibleTo","createOnce","ref","create","createdAt","serverTimestamp","updatedAt","code","logActivity","request","admitted","activity","messageId","direction","subject","excerpt","body","from","to","inReplyTo","atMs","byUid","byName","dedupeKey","created","stamped","createTask","title","replace","Number","isFinite","dueAtMs","assigneeUid","tasks","notes","task","priority","remindAtMs","previous","createdByUid","catch","console","recordEmailDelivery","state","outcome","activityId","detail","defaultCrmRecordTimelineDeps","app","registerCrmRecordTimelineWriter","pluginId"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/record-timeline.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 registerPluginRecordTimelineWriter,\n type PluginRecordActivityRequest,\n type PluginRecordDeliveryRequest,\n type PluginRecordEntryContext,\n type PluginRecordTaskRequest,\n type PluginRecordTimelineWriter,\n type PluginRecordWrite,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-timeline'\nimport {\n checkEntitlement,\n consentGroupForHost,\n CRM_ACTIVITY_LOG_FULL_MESSAGE,\n CRM_COLLECTIONS,\n crmActivityLogHasRoom,\n crmScopeTokens,\n crmTaskLabelsForNew,\n crmTaskListFields,\n crmTaskReminderAfterEdit,\n isCrmActivityKind,\n isCrmTaskKind,\n readContactFacet,\n visibleToHost,\n type CrmActivity,\n type CrmActivityLink,\n type CrmTask,\n} from '@aglyn/aglyn/server'\nimport {\n countCrmActivitiesForRecord,\n createCrmEmailActivity,\n crmActivityRef,\n crmCapturedEmailActivityRef,\n findContactByEmail,\n firebaseAdmin,\n readLeadForHost,\n recomputeCrmNextTaskAt,\n recordCrmEmailDelivery,\n} from '@aglyn/tenant-data-admin'\nimport { createHash } from 'crypto'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { buildCrmCapturedEmailActivity, crmCapturedEmailKey } from '../model/crm-inbound'\nimport { readCrmTaskPicklists } from './read-picklist'\nimport { CRM_SUITE_FEATURE } from './suite-gate'\n\n/**\n * THE CRM'S WRITER ON THE CORE'S RECORD-TIMELINE SEAM (AGL-2981).\n *\n * Another plugin files an activity or a task on a contact, company or deal —\n * an email a sequence sent, the reply it read, the call it asks for — and\n * this writes it as the CRM writes its own:\n *\n * 1. the workspace's plan carries the CRM (`features.crm`), as at every\n * CRM door; a Free workspace keeps no records to file on;\n * 2. the entry carries the scope a record made on that site carries\n * (`crmScopeTokens`), so exactly the people who see the record see it;\n * 3. an EMAIL is filed under the id the capture address files a copy of the\n * same message by (`cap_` + the digest of its `Message-ID`), and written\n * with `create()`: the send, a copy the rep forwarded to the capture\n * address, and a second run of the caller are one row;\n * 4. anything else — a note, a task — is filed under the caller's own key,\n * hashed with the caller's plugin id, and written once the same way;\n * 5. a record at its activity ceiling is refused with the sentence every\n * CRM writer answers with, before anything is written;\n * 6. a bounce or a complaint on a filed email (AGL-3245) advances that\n * email's own delivery state — the row the send was filed under, found\n * by its `Message-ID` — exactly as the campaign webhook advances one,\n * so the timeline reads Sent, then Bounced, with what the server said.\n *\n * Each entry names the plugin that filed it (`sourcePluginId`), and a task's\n * `nextTaskAtMs` is recomputed on the records it names, as every server\n * writer of a task does.\n *\n * ## A record found rather than named\n *\n * A caller that holds no id of the CRM's — a booking made through a link a\n * rep dropped from a record, or one taken off the widget cold (AGL-2660) —\n * hands back the record the link carried, or the booker's address, and the\n * entry is filed where the CRM finds it: the record the reference names\n * when this site may see it, otherwise the contact the address names\n * through the org's address index, narrowed to the site. A deal carries its\n * contact and company onto the entry; a contact its company, from the\n * site's own facet. A task with no assignee of the caller's goes to whoever\n * holds that record.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\nexport interface CrmRecordTimelineDeps {\n firestore(): Firestore\n}\n\n/** gRPC `ALREADY_EXISTS`, which `create()` rejects with when the document is there. */\nconst ALREADY_EXISTS = 6\n\n/** The longest task title the CRM keeps, as its own forms do. */\nconst TASK_TITLE_MAX = 200\n/** The longest note body or task note an entry filed here keeps. */\nconst NOTE_MAX = 2_000\n\nconst refuse = (status: 400 | 403 | 404 | 409, error: string): PluginRecordWrite => ({\n ok: false,\n status,\n error,\n})\n\n/** The record fields of a link, only the ones it names. */\nfunction linkFields(link: PluginRecordEntryContext['link']): CrmActivityLink {\n return {\n ...(link?.contactId ? { contactId: String(link.contactId) } : {}),\n ...(link?.companyId ? { companyId: String(link.companyId) } : {}),\n // A lead (AGL-3234): host-scoped by path, so the entry carries the\n // site's scope like any record created from that site.\n ...(link?.leadId ? { leadId: String(link.leadId) } : {}),\n }\n}\n\n/** What a reference handed back by a caller may name: the records a booking link is dropped from. */\nconst FOUND_RECORD_KINDS: ReadonlySet<string> = new Set(['contact', 'lead', 'deal'])\n/**\n * A record id as the CRM mints them: Firestore's for a contact and a deal, a\n * person key for a lead. A reference arrives on a public request, so one\n * that could not be an id is passed over rather than read.\n */\nconst RECORD_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/\n\n/** The record an entry lands on, and who holds it. */\ninterface FoundRecord {\n link: CrmActivityLink\n holderUid?: string\n}\n\n/** A contact's link and holder, from this site's facet on it. */\nfunction contactRecord(\n contact: FirebaseFirestore.DocumentSnapshot,\n groupId: string,\n): FoundRecord {\n const facet = readContactFacet(contact.data() ?? {}, groupId)\n return {\n link: { contactId: contact.id, ...(facet.companyId ? { companyId: facet.companyId } : {}) },\n ...(facet.ownerUid ? { holderUid: facet.ownerUid } : {}),\n }\n}\n\n/**\n * The record a caller's reference names, then the contact its address does,\n * as `hostId` may see them; `null` when neither finds one. See the module\n * comment.\n */\nasync function findRecord(\n orgRef: FirebaseFirestore.DocumentReference,\n hostId: string,\n groupId: string,\n link: PluginRecordEntryContext['link'],\n): Promise<FoundRecord | null> {\n const contacts = orgRef.collection('contacts')\n const kind = String(link?.record?.kind ?? '')\n const id = String(link?.record?.id ?? '')\n if (FOUND_RECORD_KINDS.has(kind) && RECORD_ID_PATTERN.test(id)) {\n if (kind === 'contact') {\n const contact = await contacts.doc(id).get()\n if (contact.exists && visibleToHost(contact.get('visibleTo'), hostId)) {\n return contactRecord(contact, groupId)\n }\n } else if (kind === 'deal') {\n const deal = await orgRef.collection(CRM_COLLECTIONS.deals).doc(id).get()\n if (deal.exists && visibleToHost(deal.get('visibleTo'), hostId)) {\n const contactId = String(deal.get('contactId') ?? '')\n const companyId = String(deal.get('companyId') ?? '')\n const ownerUid = String(deal.get('ownerUid') ?? '')\n return {\n link: { dealId: deal.id, ...(contactId ? { contactId } : {}), ...(companyId ? { companyId } : {}) },\n ...(ownerUid ? { holderUid: ownerUid } : {}),\n }\n }\n } else {\n // A lead carries its own `visibleTo` (AGL-3275), and the site has to be\n // allowed to see it, or one brand's booking would name another's lead.\n const lead = await readLeadForHost(hostId, id)\n if (lead && visibleToHost(lead.get('visibleTo'), hostId)) {\n const ownerUid = String(lead.get('ownerUid') ?? '')\n return { link: { leadId: lead.id }, ...(ownerUid ? { holderUid: ownerUid } : {}) }\n }\n }\n }\n const email = String(link?.email ?? '').trim()\n if (!email) return null\n const hit = await findContactByEmail(contacts, email, { hostId })\n return hit ? contactRecord(hit, groupId) : null\n}\n\n/** Who holds a record a caller named by id: its contact's site facet, or its lead. */\nasync function holderOf(\n orgRef: FirebaseFirestore.DocumentReference,\n hostId: string,\n groupId: string,\n link: CrmActivityLink,\n): Promise<string | undefined> {\n if (link.contactId) {\n const contact = await orgRef.collection('contacts').doc(link.contactId).get()\n return contact.exists ? contactRecord(contact, groupId).holderUid : undefined\n }\n if (link.leadId) {\n const lead = await readLeadForHost(hostId, link.leadId)\n return String(lead?.get('ownerUid') ?? '') || undefined\n }\n return undefined\n}\n\n/** The document id a caller's key files under: its own namespace, never a raw key. */\nfunction keyedId(sourcePluginId: string, key: string): string {\n return `plg_${createHash('sha256').update(`${sourcePluginId}:${key}`).digest('hex').slice(0, 28)}`\n}\n\nexport function createCrmRecordTimelineWriter(deps: CrmRecordTimelineDeps): PluginRecordTimelineWriter {\n /**\n * The org, its plan, the link and the scope an entry is stamped with — and,\n * when `wantHolder`, who holds the record it lands on.\n */\n async function admit(\n context: PluginRecordEntryContext,\n wantHolder = false,\n ): Promise<\n | PluginRecordWrite\n | {\n firestore: Firestore\n orgRef: FirebaseFirestore.DocumentReference\n link: CrmActivityLink\n visibleTo: string[]\n holderUid?: string\n }\n > {\n const orgId = String(context.orgId ?? '').trim()\n const hostId = String(context.hostId ?? '').trim()\n const named = linkFields(context.link)\n const isNamed = Boolean(named.contactId || named.companyId || named.leadId)\n if (!orgId || !hostId || !String(context.sourcePluginId ?? '').trim()) {\n return refuse(400, 'An entry names its organization, its site and the plugin filing it.')\n }\n if (!isNamed && !context.link?.record && !String(context.link?.email ?? '').trim()) {\n return refuse(400, 'An entry is filed on a contact, a company or a lead, or names how to find one.')\n }\n const firestore = deps.firestore()\n const orgRef = firestore.collection('orgs').doc(orgId)\n const snapshot = await orgRef.get()\n if (!snapshot.exists) return refuse(404, 'That organization no longer exists.')\n const org = (snapshot.data() ?? {}) as Record<string, unknown>\n if (!checkEntitlement(org, CRM_SUITE_FEATURE)) {\n return refuse(403, \"This workspace's plan doesn't include the CRM.\")\n }\n const group = consentGroupForHost(org, hostId)\n let link = named\n let holderUid: string | undefined\n if (isNamed) {\n if (wantHolder) holderUid = await holderOf(orgRef, hostId, group.groupId, named)\n } else {\n const found = await findRecord(orgRef, hostId, group.groupId, context.link)\n if (!found) return refuse(404, 'Nothing this site keeps matches the record or the address.')\n link = found.link\n holderUid = found.holderUid\n }\n return {\n firestore,\n orgRef,\n link,\n visibleTo: crmScopeTokens(org, group),\n ...(holderUid ? { holderUid } : {}),\n }\n }\n\n /** Creates a keyed entry once: `created: false` for one already filed. */\n async function createOnce(\n ref: FirebaseFirestore.DocumentReference,\n data: Record<string, unknown>,\n ): Promise<boolean> {\n try {\n await ref.create({\n ...data,\n createdAt: FieldValue.serverTimestamp(),\n updatedAt: FieldValue.serverTimestamp(),\n })\n return true\n } catch (error) {\n if ((error as { code?: unknown })?.code === ALREADY_EXISTS) return false\n throw error\n }\n }\n\n return {\n async logActivity(request: PluginRecordActivityRequest): Promise<PluginRecordWrite> {\n if (!isCrmActivityKind(request.kind)) return refuse(400, `\"${String(request.kind)}\" isn't an activity.`)\n const admitted = await admit(request)\n if ('ok' in admitted) return admitted\n const { firestore, orgRef, link, visibleTo } = admitted\n const orgId = orgRef.id\n\n let ref: FirebaseFirestore.DocumentReference\n let activity: CrmActivity\n if (request.kind === 'email') {\n const email = request.email\n const key = email ? crmCapturedEmailKey(email.messageId, null) : null\n if (!email || !key) return refuse(400, 'An email is filed by its Message-ID.')\n ref = crmCapturedEmailActivityRef(firestore, orgId, key)\n activity = buildCrmCapturedEmailActivity({\n direction: email.direction === 'inbound' ? 'inbound' : 'outbound',\n subject: email.subject,\n excerpt: request.body,\n from: email.from,\n to: email.to,\n messageId: email.messageId,\n inReplyTo: email.inReplyTo ?? null,\n atMs: request.atMs,\n byUid: String(request.byUid ?? ''),\n byName: request.byName ?? null,\n link,\n hostId: request.hostId,\n visibleTo,\n })\n } else {\n const key = String(request.dedupeKey ?? '').trim()\n if (!key) return refuse(400, 'An entry that is not an email names its own key.')\n ref = crmActivityRef(firestore, orgId, keyedId(request.sourcePluginId, key))\n const byName = String(request.byName ?? '').trim()\n activity = {\n kind: request.kind,\n body: String(request.body ?? '').slice(0, NOTE_MAX),\n atMs: request.atMs,\n byUid: String(request.byUid ?? ''),\n ...(byName ? { byName } : {}),\n ...link,\n hostId: request.hostId,\n visibleTo,\n }\n }\n // Filed before: answered off the one read, ahead of the ceiling's count.\n if ((await ref.get()).exists) return { ok: true, id: ref.id, created: false }\n if (!crmActivityLogHasRoom(await countCrmActivitiesForRecord(orgRef, link))) {\n return refuse(409, CRM_ACTIVITY_LOG_FULL_MESSAGE)\n }\n const stamped = { ...activity, sourcePluginId: request.sourcePluginId }\n const created =\n request.kind === 'email'\n ? (await createCrmEmailActivity(ref, stamped)) === 'created'\n : await createOnce(ref, stamped as unknown as Record<string, unknown>)\n return { ok: true, id: ref.id, created }\n },\n\n async createTask(request: PluginRecordTaskRequest): Promise<PluginRecordWrite> {\n const title = String(request.title ?? '').replace(/\\s+/g, ' ').trim().slice(0, TASK_TITLE_MAX)\n const key = String(request.dedupeKey ?? '').trim()\n if (!title || !key) return refuse(400, 'A task has a title and the caller’s own key.')\n if (!isCrmTaskKind(request.kind)) return refuse(400, `\"${String(request.kind)}\" isn't a task.`)\n if (!Number.isFinite(request.dueAtMs)) return refuse(400, 'A task has a due time.')\n const admitted = await admit(request, request.assigneeUid === null)\n if ('ok' in admitted) return admitted\n const { firestore, orgRef, link, visibleTo } = admitted\n const ref = orgRef.collection(CRM_COLLECTIONS.tasks).doc(keyedId(request.sourcePluginId, key))\n const notes = String(request.notes ?? '').trim().slice(0, NOTE_MAX)\n // `null` is \"whoever holds the record\", which admission read.\n const assigneeUid =\n request.assigneeUid === null\n ? (admitted.holderUid ?? '')\n : String(request.assigneeUid ?? '').trim()\n // The org's labels for the meanings it is filed with (AGL-3517).\n const labels = crmTaskLabelsForNew(await readCrmTaskPicklists(firestore, orgRef.id), {\n kind: request.kind,\n priority: 'normal',\n })\n const task: CrmTask = {\n title,\n ...(notes ? { notes } : {}),\n kind: request.kind,\n priority: 'normal',\n status: 'open',\n ...labels,\n dueAtMs: request.dueAtMs,\n // The reminder a person's task gets (AGL-2659): the due time.\n remindAtMs: crmTaskReminderAfterEdit({ dueAtMs: request.dueAtMs, previous: null }),\n ...(assigneeUid ? { assigneeUid } : {}),\n createdByUid: String(request.createdByUid ?? ''),\n sourcePluginId: request.sourcePluginId,\n ...link,\n hostId: request.hostId,\n visibleTo,\n }\n // What the Tasks list searches by (AGL-3321), beside the task.\n const created = await createOnce(ref, { ...task, ...crmTaskListFields(task) })\n if (created) {\n // The records the task names carry `nextTaskAtMs` (AGL-2661); a figure\n // that could not move is the Fields section's recompute's.\n await recomputeCrmNextTaskAt(firestore, orgRef.id, [link]).catch((error: unknown) => {\n console.error('[crm] next activity could not be recomputed', orgRef.id, error)\n })\n }\n return { ok: true, id: ref.id, created }\n },\n\n async recordEmailDelivery(request: PluginRecordDeliveryRequest): Promise<PluginRecordWrite> {\n const orgId = String(request.orgId ?? '').trim()\n const key = crmCapturedEmailKey(request.messageId, null)\n if (!orgId || !key) return refuse(400, 'A delivery names its organization and the email’s Message-ID.')\n if (request.state !== 'bounced' && request.state !== 'complained') {\n return refuse(400, `\"${String(request.state)}\" isn't a delivery failure.`)\n }\n const firestore = deps.firestore()\n const ref = crmCapturedEmailActivityRef(firestore, orgId, key)\n const outcome = await recordCrmEmailDelivery(firestore, {\n orgId,\n activityId: ref.id,\n state: request.state,\n atMs: request.atMs,\n detail: request.detail ?? null,\n })\n if (outcome === 'missing') return refuse(404, 'No email by that Message-ID is filed here.')\n if (outcome === 'failed') return refuse(409, 'The delivery state could not be written.')\n return { ok: true, id: ref.id, created: outcome === 'advanced' }\n },\n\n // An automation's email to the person its event is about (AGL-2615):\n // see `automation-steps.ts`, loaded with the first one.\n async prepareEmail(request) {\n const { prepareCrmRecordEmail } = await import('./automation-steps')\n return prepareCrmRecordEmail(request)\n },\n }\n}\n\n/** The platform's own dependencies. Specs build their own. */\nexport function defaultCrmRecordTimelineDeps(): CrmRecordTimelineDeps {\n return { firestore: () => firebaseAdmin.app().firestore() }\n}\n\n/** Registers the CRM as the workspace's record system on the timeline seam. */\nexport function registerCrmRecordTimelineWriter(\n deps: CrmRecordTimelineDeps = defaultCrmRecordTimelineDeps(),\n): void {\n registerPluginRecordTimelineWriter(createCrmRecordTimelineWriter(deps), { pluginId: BUNDLE_ID })\n}\n"],"names":["registerPluginRecordTimelineWriter","checkEntitlement","consentGroupForHost","CRM_ACTIVITY_LOG_FULL_MESSAGE","CRM_COLLECTIONS","crmActivityLogHasRoom","crmScopeTokens","crmTaskLabelsForNew","crmTaskListFields","crmTaskReminderAfterEdit","isCrmActivityKind","isCrmTaskKind","readContactFacet","visibleToHost","countCrmActivitiesForRecord","createCrmEmailActivity","crmActivityRef","crmCapturedEmailActivityRef","findContactByEmail","firebaseAdmin","readLeadForHost","recomputeCrmNextTaskAt","recordCrmEmailDelivery","createHash","FieldValue","BUNDLE_ID","buildCrmCapturedEmailActivity","crmCapturedEmailKey","readCrmTaskPicklists","CRM_SUITE_FEATURE","ALREADY_EXISTS","TASK_TITLE_MAX","NOTE_MAX","refuse","status","error","ok","linkFields","link","contactId","String","companyId","leadId","FOUND_RECORD_KINDS","Set","RECORD_ID_PATTERN","contactRecord","contact","groupId","facet","data","id","ownerUid","holderUid","findRecord","orgRef","hostId","contacts","collection","kind","record","has","test","doc","get","exists","deal","deals","dealId","lead","email","trim","hit","holderOf","undefined","keyedId","sourcePluginId","key","update","digest","slice","createCrmRecordTimelineWriter","deps","admit","context","wantHolder","snapshot","orgId","named","isNamed","Boolean","firestore","org","group","found","visibleTo","createOnce","ref","create","createdAt","serverTimestamp","updatedAt","code","logActivity","request","admitted","activity","messageId","direction","subject","excerpt","body","from","to","inReplyTo","atMs","byUid","byName","dedupeKey","created","stamped","createTask","title","replace","Number","isFinite","dueAtMs","assigneeUid","tasks","notes","labels","priority","task","remindAtMs","previous","createdByUid","catch","console","recordEmailDelivery","state","outcome","activityId","detail","prepareEmail","prepareCrmRecordEmail","defaultCrmRecordTimelineDeps","app","registerCrmRecordTimelineWriter","pluginId"],"mappings":"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"}
@@ -29,9 +29,10 @@
29
29
  *
30
30
  * Capture — a form, a sign-up, a booking, an order — does not come through
31
31
  * these routes, and records what it records on every plan. The compliance
32
- * doors are every plan's too: `crm/erase-person`, and the contacts and leads
33
- * files of `/api/crm/export`, because exporting and erasing the people a
34
- * workspace holds are obligations rather than CRM features.
32
+ * doors are every plan's too: `crm/erase-person` here, and the `crm.contacts`
33
+ * and `crm.leads` exports of the transfer framework, which ask no plan,
34
+ * because exporting and erasing the people a workspace holds are
35
+ * obligations rather than CRM features.
35
36
  *
36
37
  * ## After authorization, never before
37
38
  *
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/suite-gate.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * THE CRM'S PLAN GATE, for the routes the console calls (AGL-2787, AGL-2851).\n *\n * The CRM is included from Starter: `features.crm` is false on Free, and a\n * Free workspace has none of it — no section, no read and no write. Three\n * other doors onto the CRM's records ask the same question: the shell draws\n * every CRM section locked and refuses its body, the `/v1` dispatcher refuses\n * the CRM's resources, and the Firestore rules refuse a client's CRM writes.\n * These routes are the fourth door, and every `crm/*` read and write asks it.\n * A lock drawn on a page is advice to a script holding a member's token, so\n * the same question is asked where the read or the write happens.\n *\n * ## What is never asked\n *\n * Capture — a form, a sign-up, a booking, an order — does not come through\n * these routes, and records what it records on every plan. The compliance\n * doors are every plan's too: `crm/erase-person`, and the contacts and leads\n * files of `/api/crm/export`, because exporting and erasing the people a\n * workspace holds are obligations rather than CRM features.\n *\n * ## After authorization, never before\n *\n * Each route asks once it knows who is calling and has read the org to\n * answer that — the order the shell refuses in. A reader who may not open a\n * surface is not shown its upgrade path, and a caller with no standing in\n * the workspace learns nothing about its plan.\n *\n * ## One refusal shape\n *\n * 403, as the REST refusal is, with its two machine-readable facts in the\n * console routes' flat body: `reason: 'plan_required'` is the REST\n * `error.type` and `code: 'crm'` its `error.code`. Beside them, a sentence\n * the console relays unchanged, as it relays every route refusal. The plan\n * it names comes from the plan tables, so it cannot name a tier that has\n * stopped carrying the suite.\n *\n * ## Staff are refused too\n *\n * The plan is a fact about the workspace, not about the person asking. A\n * support engineer acting inside a Free workspace would otherwise create\n * suite records that the workspace then holds without the suite.\n */\n\nimport { checkEntitlement, planLabelGrantingFeature } from '@aglyn/aglyn/server'\n\n/** The entitlement the suite is sold under — the REST refusal's `error.code`. */\nexport const CRM_SUITE_FEATURE = 'crm'\n\n/** The REST refusal's `error.type`, as a console route's `reason`. */\nexport const CRM_SUITE_REFUSAL_REASON = 'plan_required'\n\n/** What a route answers a workspace whose plan does not carry the suite. */\nexport interface CrmSuiteRefusal {\n status: 403\n body: {\n error: string\n reason: typeof CRM_SUITE_REFUSAL_REASON\n code: typeof CRM_SUITE_FEATURE\n }\n}\n\n/**\n * The refusal's sentence, for an act named as a phrase that takes \"is\" —\n * \"Importing a CSV file\", \"A CRM recipe\".\n */\nexport function crmSuiteRefusalMessage(act: string): string {\n const plan = planLabelGrantingFeature(CRM_SUITE_FEATURE)\n return (\n `${act} is part of the CRM, which is not included in your current ` +\n 'plan. Manage your plan and add-ons from Billing.' +\n (plan ? ` Included from ${plan}.` : '')\n )\n}\n\n/**\n * `null` when the org's plan carries the suite, else the refusal to send.\n *\n * Resolved through `checkEntitlement`, so a per-org\n * `entitlements.features.crm` grant or revocation, and a paid plan whose\n * subscription has died, answer here exactly as they answer everywhere else.\n */\nexport function crmSuiteRefusal(org: unknown, act: string): CrmSuiteRefusal | null {\n if (checkEntitlement(org as Parameters<typeof checkEntitlement>[0], CRM_SUITE_FEATURE)) {\n return null\n }\n return {\n status: 403,\n body: {\n error: crmSuiteRefusalMessage(act),\n reason: CRM_SUITE_REFUSAL_REASON,\n code: CRM_SUITE_FEATURE,\n },\n }\n}\n"],"names":["checkEntitlement","planLabelGrantingFeature","CRM_SUITE_FEATURE","CRM_SUITE_REFUSAL_REASON","crmSuiteRefusalMessage","act","plan","crmSuiteRefusal","org","status","body","error","reason","code"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyCC,GAED,SAASA,gBAAgB,EAAEC,wBAAwB,QAAQ,sBAAqB;AAEhF,+EAA+E,GAC/E,OAAO,MAAMC,oBAAoB,MAAK;AAEtC,oEAAoE,GACpE,OAAO,MAAMC,2BAA2B,gBAAe;AAYvD;;;CAGC,GACD,OAAO,SAASC,uBAAuBC,GAAW;IAChD,MAAMC,OAAOL,yBAAyBC;IACtC,OACE,GAAGG,IAAI,2DAA2D,CAAC,GACnE,qDACCC,CAAAA,OAAO,CAAC,eAAe,EAAEA,KAAK,CAAC,CAAC,GAAG,EAAC;AAEzC;AAEA;;;;;;CAMC,GACD,OAAO,SAASC,gBAAgBC,GAAY,EAAEH,GAAW;IACvD,IAAIL,iBAAiBQ,KAA+CN,oBAAoB;QACtF,OAAO;IACT;IACA,OAAO;QACLO,QAAQ;QACRC,MAAM;YACJC,OAAOP,uBAAuBC;YAC9BO,QAAQT;YACRU,MAAMX;QACR;IACF;AACF"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/suite-gate.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * THE CRM'S PLAN GATE, for the routes the console calls (AGL-2787, AGL-2851).\n *\n * The CRM is included from Starter: `features.crm` is false on Free, and a\n * Free workspace has none of it — no section, no read and no write. Three\n * other doors onto the CRM's records ask the same question: the shell draws\n * every CRM section locked and refuses its body, the `/v1` dispatcher refuses\n * the CRM's resources, and the Firestore rules refuse a client's CRM writes.\n * These routes are the fourth door, and every `crm/*` read and write asks it.\n * A lock drawn on a page is advice to a script holding a member's token, so\n * the same question is asked where the read or the write happens.\n *\n * ## What is never asked\n *\n * Capture — a form, a sign-up, a booking, an order — does not come through\n * these routes, and records what it records on every plan. The compliance\n * doors are every plan's too: `crm/erase-person` here, and the `crm.contacts`\n * and `crm.leads` exports of the transfer framework, which ask no plan,\n * because exporting and erasing the people a workspace holds are\n * obligations rather than CRM features.\n *\n * ## After authorization, never before\n *\n * Each route asks once it knows who is calling and has read the org to\n * answer that — the order the shell refuses in. A reader who may not open a\n * surface is not shown its upgrade path, and a caller with no standing in\n * the workspace learns nothing about its plan.\n *\n * ## One refusal shape\n *\n * 403, as the REST refusal is, with its two machine-readable facts in the\n * console routes' flat body: `reason: 'plan_required'` is the REST\n * `error.type` and `code: 'crm'` its `error.code`. Beside them, a sentence\n * the console relays unchanged, as it relays every route refusal. The plan\n * it names comes from the plan tables, so it cannot name a tier that has\n * stopped carrying the suite.\n *\n * ## Staff are refused too\n *\n * The plan is a fact about the workspace, not about the person asking. A\n * support engineer acting inside a Free workspace would otherwise create\n * suite records that the workspace then holds without the suite.\n */\n\nimport { checkEntitlement, planLabelGrantingFeature } from '@aglyn/aglyn/server'\n\n/** The entitlement the suite is sold under — the REST refusal's `error.code`. */\nexport const CRM_SUITE_FEATURE = 'crm'\n\n/** The REST refusal's `error.type`, as a console route's `reason`. */\nexport const CRM_SUITE_REFUSAL_REASON = 'plan_required'\n\n/** What a route answers a workspace whose plan does not carry the suite. */\nexport interface CrmSuiteRefusal {\n status: 403\n body: {\n error: string\n reason: typeof CRM_SUITE_REFUSAL_REASON\n code: typeof CRM_SUITE_FEATURE\n }\n}\n\n/**\n * The refusal's sentence, for an act named as a phrase that takes \"is\" —\n * \"Importing a CSV file\", \"A CRM recipe\".\n */\nexport function crmSuiteRefusalMessage(act: string): string {\n const plan = planLabelGrantingFeature(CRM_SUITE_FEATURE)\n return (\n `${act} is part of the CRM, which is not included in your current ` +\n 'plan. Manage your plan and add-ons from Billing.' +\n (plan ? ` Included from ${plan}.` : '')\n )\n}\n\n/**\n * `null` when the org's plan carries the suite, else the refusal to send.\n *\n * Resolved through `checkEntitlement`, so a per-org\n * `entitlements.features.crm` grant or revocation, and a paid plan whose\n * subscription has died, answer here exactly as they answer everywhere else.\n */\nexport function crmSuiteRefusal(org: unknown, act: string): CrmSuiteRefusal | null {\n if (checkEntitlement(org as Parameters<typeof checkEntitlement>[0], CRM_SUITE_FEATURE)) {\n return null\n }\n return {\n status: 403,\n body: {\n error: crmSuiteRefusalMessage(act),\n reason: CRM_SUITE_REFUSAL_REASON,\n code: CRM_SUITE_FEATURE,\n },\n }\n}\n"],"names":["checkEntitlement","planLabelGrantingFeature","CRM_SUITE_FEATURE","CRM_SUITE_REFUSAL_REASON","crmSuiteRefusalMessage","act","plan","crmSuiteRefusal","org","status","body","error","reason","code"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0CC,GAED,SAASA,gBAAgB,EAAEC,wBAAwB,QAAQ,sBAAqB;AAEhF,+EAA+E,GAC/E,OAAO,MAAMC,oBAAoB,MAAK;AAEtC,oEAAoE,GACpE,OAAO,MAAMC,2BAA2B,gBAAe;AAYvD;;;CAGC,GACD,OAAO,SAASC,uBAAuBC,GAAW;IAChD,MAAMC,OAAOL,yBAAyBC;IACtC,OACE,GAAGG,IAAI,2DAA2D,CAAC,GACnE,qDACCC,CAAAA,OAAO,CAAC,eAAe,EAAEA,KAAK,CAAC,CAAC,GAAG,EAAC;AAEzC;AAEA;;;;;;CAMC,GACD,OAAO,SAASC,gBAAgBC,GAAY,EAAEH,GAAW;IACvD,IAAIL,iBAAiBQ,KAA+CN,oBAAoB;QACtF,OAAO;IACT;IACA,OAAO;QACLO,QAAQ;QACRC,MAAM;YACJC,OAAOP,uBAAuBC;YAC9BO,QAAQT;YACRU,MAAMX;QACR;IACF;AACF"}
@@ -14,12 +14,14 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
14
14
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
- */ import { consentGroupForHost, CRM_COLLECTIONS, crmScopeTokens, crmTaskListFields, crmTaskReminderAfterEdit, crmTaskReminderPending, crmTaskReminderWhen, isOrgWideMember, memberCanSee } from "@aglyn/aglyn/server";
17
+ */ import { consentGroupForHost, CRM_COLLECTIONS, crmScopeTokens, crmTaskListFields, crmTaskStatusWrite, crmTaskReminderAfterEdit, crmTaskReminderPending, crmTaskReminderWhen, isOrgWideMember, memberCanSee } from "@aglyn/aglyn/server";
18
18
  import { crmNextActivityLinksOf, firebaseAdmin, getOrgForHost, memberHasOrgPermission, notifyUsers, recomputeCrmNextTaskAt, resolveOrgMembership } from "@aglyn/tenant-data-admin";
19
19
  import { emitHostEvent } from "@aglyn/tenant-runtime";
20
20
  import { FieldValue } from "firebase-admin/firestore";
21
21
  import { CRM_TASK_BATCH_MAX, readCrmTaskFields } from "../model/task-routes.js";
22
22
  import { CRM_ORG_TASK_SCOPE } from "../model/task-scope.js";
23
+ import { resolveCrmTaskPicklistWrite } from "../model/task-picklists.js";
24
+ import { readCrmTaskPicklists } from "./read-picklist.js";
23
25
  import { digestTimeZone } from "../model/crm-digest.js";
24
26
  import { authorizeOrgCaller, readCrmRouteScope } from "./org-caller.js";
25
27
  import { crmSuiteRefusal } from "./suite-gate.js";
@@ -162,7 +164,7 @@ const ORG_REFUSAL = 'Editing tasks at the organization level requires the "Manag
162
164
  * `dueAtMs` is the one field written as `null` when unset, because every
163
165
  * view orders by it and a document missing the field drops out of an
164
166
  * `orderBy` entirely.
165
- */ function storedFields(fields, mode) {
167
+ */ function storedFields(fields, mode, picked) {
166
168
  const absent = mode === 'update' ? FieldValue.delete() : undefined;
167
169
  const optional = (value)=>value ? {
168
170
  present: true,
@@ -171,10 +173,14 @@ const ORG_REFUSAL = 'Editing tasks at the organization level requires the "Manag
171
173
  present: mode === 'update',
172
174
  value: absent
173
175
  };
176
+ // The meanings every query reads, with the org's labels beside them (AGL-3517).
174
177
  const out = {
175
178
  title: fields.title,
176
- kind: fields.kind,
177
- priority: fields.priority,
179
+ kind: picked.kind,
180
+ typeLabel: picked.typeLabel,
181
+ priority: picked.priority,
182
+ priorityLabel: picked.priorityLabel,
183
+ statusLabel: picked.statusLabel,
178
184
  dueAtMs: fields.dueAtMs,
179
185
  notes: fields.notes
180
186
  };
@@ -286,7 +292,7 @@ const tasksCollection = (orgId)=>firebaseAdmin.app().firestore().collection('org
286
292
  * and `createdByUid` are provenance and never rewritten. The notification
287
293
  * opens under the site the caller is on, or — at the organization level —
288
294
  * under the task's own site, and under the org hub for a task with none.
289
- */ async function updateTask(writer, scope, tasks, taskId, fields, onRoster) {
295
+ */ async function updateTask(writer, scope, tasks, taskId, fields, onRoster, picklists) {
290
296
  var _existing_get, _existing_get1;
291
297
  if (fields.assigneeUid && !await onRoster(fields.assigneeUid)) {
292
298
  return refuse(400, 'The assignee is not a member of this organization.');
@@ -296,9 +302,11 @@ const tasksCollection = (orgId)=>firebaseAdmin.app().firestore().collection('org
296
302
  if (!canReach(writer, existing.get('visibleTo'))) {
297
303
  return refuse(403, 'That task is not visible to you.');
298
304
  }
305
+ const picked = resolveCrmTaskPicklistWrite(picklists, fields, existing.data());
306
+ if (picked.ok === false) return refuse(400, picked.error);
299
307
  const previousAssignee = String((_existing_get = existing.get('assigneeUid')) != null ? _existing_get : '') || null;
300
308
  const previousLinks = crmNextActivityLinksOf(existing.data());
301
- const stored = storedFields(fields, 'update');
309
+ const stored = storedFields(fields, 'update', picked.write);
302
310
  await tasks.doc(taskId).update(_extends({}, stored, reminderFields(fields, existing), crmTaskListFields(_extends({}, existing.data(), stored)), {
303
311
  updatedAt: FieldValue.serverTimestamp()
304
312
  }));
@@ -347,10 +355,12 @@ const tasksCollection = (orgId)=>firebaseAdmin.app().firestore().collection('org
347
355
  * refused, which is the rules' `canCreateScoped()` said before the write
348
356
  * instead of after. An organization task — no site — carries
349
357
  * `CRM_ORG_TASK_SCOPE` and `hostId: null`.
350
- */ async function createTask(writer, scope, tasks, fields, onRoster) {
358
+ */ async function createTask(writer, scope, tasks, fields, onRoster, picklists) {
351
359
  if (fields.assigneeUid && !await onRoster(fields.assigneeUid)) {
352
360
  return refuse(400, 'The assignee is not a member of this organization.');
353
361
  }
362
+ const picked = resolveCrmTaskPicklistWrite(picklists, fields, null);
363
+ if (picked.ok === false) return refuse(400, picked.error);
354
364
  const filed = await resolveFiledFrom(writer, scope);
355
365
  if (filed.ok === false) return filed;
356
366
  const { hostId } = filed;
@@ -361,7 +371,8 @@ const tasksCollection = (orgId)=>firebaseAdmin.app().firestore().collection('org
361
371
  return refuse(403, 'Your access to this organization does not reach this site.');
362
372
  }
363
373
  const ref = tasks.doc();
364
- await ref.set(_extends({}, storedFields(fields, 'create'), reminderFields(fields, null), crmTaskListFields(_extends({}, storedFields(fields, 'create'), {
374
+ const stored = storedFields(fields, 'create', picked.write);
375
+ await ref.set(_extends({}, stored, reminderFields(fields, null), crmTaskListFields(_extends({}, stored, {
365
376
  visibleTo
366
377
  })), {
367
378
  status: 'open',
@@ -435,9 +446,10 @@ const tasksCollection = (orgId)=>firebaseAdmin.app().firestore().collection('org
435
446
  }
436
447
  const tasks = tasksCollection(writer.orgId);
437
448
  const onRoster = rosterOf(writer.orgId);
449
+ const picklists = await readCrmTaskPicklists(firebaseAdmin.app().firestore(), writer.orgId);
438
450
  let body;
439
451
  if (taskId) {
440
- const saved = await updateTask(writer, scope, tasks, taskId, fields, onRoster);
452
+ const saved = await updateTask(writer, scope, tasks, taskId, fields, onRoster, picklists);
441
453
  if (saved.ok === false) {
442
454
  res.status(saved.status).json(saved.body);
443
455
  return;
@@ -448,7 +460,7 @@ const tasksCollection = (orgId)=>firebaseAdmin.app().firestore().collection('org
448
460
  notified: saved.notified
449
461
  };
450
462
  } else {
451
- const saved = await createTask(writer, scope, tasks, fields, onRoster);
463
+ const saved = await createTask(writer, scope, tasks, fields, onRoster, picklists);
452
464
  if (saved.ok === false) {
453
465
  res.status(saved.status).json(saved.body);
454
466
  return;
@@ -511,9 +523,10 @@ const tasksCollection = (orgId)=>firebaseAdmin.app().firestore().collection('org
511
523
  }
512
524
  const tasks = tasksCollection(writer.orgId);
513
525
  const onRoster = rosterOf(writer.orgId);
526
+ const picklists = await readCrmTaskPicklists(firebaseAdmin.app().firestore(), writer.orgId);
514
527
  const results = [];
515
528
  for (const { taskId, fields } of parsed){
516
- const saved = await updateTask(writer, scope, tasks, taskId, fields, onRoster);
529
+ const saved = await updateTask(writer, scope, tasks, taskId, fields, onRoster, picklists);
517
530
  results.push(saved.ok === false ? {
518
531
  taskId,
519
532
  ok: false,
@@ -548,7 +561,7 @@ const tasksCollection = (orgId)=>firebaseAdmin.app().firestore().collection('org
548
561
  * Idempotent on purpose: two people ticking the same box, or one person
549
562
  * double-clicking, completes a task once and fires its event once. The
550
563
  * second call reads the stored `completedAtMs` back and writes nothing.
551
- */ async function completeTask(writer, scope, tasks, taskId) {
564
+ */ async function completeTask(writer, scope, tasks, taskId, picklists) {
552
565
  var _task_hostId;
553
566
  const ref = tasks.doc(taskId);
554
567
  const snapshot = await ref.get();
@@ -577,8 +590,7 @@ const tasksCollection = (orgId)=>firebaseAdmin.app().firestore().collection('org
577
590
  };
578
591
  }
579
592
  const completedAtMs = Date.now();
580
- await ref.update(_extends({
581
- status: 'done',
593
+ await ref.update(_extends({}, crmTaskStatusWrite(picklists.status, true), {
582
594
  completedAtMs,
583
595
  completedByUid: writer.uid
584
596
  }, crmTaskReminderPending(task) ? {
@@ -660,7 +672,8 @@ const tasksCollection = (orgId)=>firebaseAdmin.app().firestore().collection('org
660
672
  res.status(writer.status).json(writer.body);
661
673
  return;
662
674
  }
663
- const outcome = await completeTask(writer, scope, tasksCollection(writer.orgId), taskId);
675
+ const picklists = await readCrmTaskPicklists(firebaseAdmin.app().firestore(), writer.orgId);
676
+ const outcome = await completeTask(writer, scope, tasksCollection(writer.orgId), taskId, picklists);
664
677
  if (outcome.ok === false) {
665
678
  res.status(completeStatus(outcome)).json({
666
679
  error: outcome.error
@@ -710,9 +723,10 @@ const tasksCollection = (orgId)=>firebaseAdmin.app().firestore().collection('org
710
723
  return;
711
724
  }
712
725
  const tasks = tasksCollection(writer.orgId);
726
+ const picklists = await readCrmTaskPicklists(firebaseAdmin.app().firestore(), writer.orgId);
713
727
  const results = [];
714
728
  for (const taskId of taskIds){
715
- results.push(await completeTask(writer, scope, tasks, taskId));
729
+ results.push(await completeTask(writer, scope, tasks, taskId, picklists));
716
730
  }
717
731
  const body = {
718
732
  ok: true,