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

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 (346) hide show
  1. package/README.md +2 -0
  2. package/package.json +14 -14
  3. package/src/index.d.ts +1 -0
  4. package/src/index.js.map +1 -1
  5. package/src/lib/components/companies-bulk-bar.js +12 -4
  6. package/src/lib/components/companies-bulk-bar.js.map +1 -1
  7. package/src/lib/components/company-detail-page.js +11 -0
  8. package/src/lib/components/company-detail-page.js.map +1 -1
  9. package/src/lib/components/company-edit-drawer.js +12 -2
  10. package/src/lib/components/company-edit-drawer.js.map +1 -1
  11. package/src/lib/components/company-import-drawer.d.ts +1 -1
  12. package/src/lib/components/company-picker.js +10 -3
  13. package/src/lib/components/company-picker.js.map +1 -1
  14. package/src/lib/components/contact-associations-card.js +4 -3
  15. package/src/lib/components/contact-associations-card.js.map +1 -1
  16. package/src/lib/components/contact-detail-page.d.ts +4 -4
  17. package/src/lib/components/contact-detail-page.js +50 -53
  18. package/src/lib/components/contact-detail-page.js.map +1 -1
  19. package/src/lib/components/contact-list-columns.d.ts +3 -0
  20. package/src/lib/components/contact-list-columns.js +21 -5
  21. package/src/lib/components/contact-list-columns.js.map +1 -1
  22. package/src/lib/components/contact-merge-dialog.js +2 -1
  23. package/src/lib/components/contact-merge-dialog.js.map +1 -1
  24. package/src/lib/components/contact-timeline-card.js +4 -3
  25. package/src/lib/components/contact-timeline-card.js.map +1 -1
  26. package/src/lib/components/contacts-bulk-bar.d.ts +3 -3
  27. package/src/lib/components/contacts-bulk-bar.js +24 -51
  28. package/src/lib/components/contacts-bulk-bar.js.map +1 -1
  29. package/src/lib/components/contacts-section.js +9 -3
  30. package/src/lib/components/contacts-section.js.map +1 -1
  31. package/src/lib/components/crm-booking-zone.d.ts +8 -2
  32. package/src/lib/components/crm-booking-zone.js.map +1 -1
  33. package/src/lib/components/crm-export-all-button.d.ts +1 -1
  34. package/src/lib/components/crm-export-all-button.js.map +1 -1
  35. package/src/lib/components/crm-glance-card.js +2 -1
  36. package/src/lib/components/crm-glance-card.js.map +1 -1
  37. package/src/lib/components/crm-send-email-dialog.js +9 -6
  38. package/src/lib/components/crm-send-email-dialog.js.map +1 -1
  39. package/src/lib/components/csv-import-drawer.js +2 -2
  40. package/src/lib/components/csv-import-drawer.js.map +1 -1
  41. package/src/lib/components/deal-detail-page.js +18 -2
  42. package/src/lib/components/deal-detail-page.js.map +1 -1
  43. package/src/lib/components/deal-edit-drawer.js +10 -0
  44. package/src/lib/components/deal-edit-drawer.js.map +1 -1
  45. package/src/lib/components/deal-products-card.js +27 -16
  46. package/src/lib/components/deal-products-card.js.map +1 -1
  47. package/src/lib/components/deals-bulk-bar.js +13 -5
  48. package/src/lib/components/deals-bulk-bar.js.map +1 -1
  49. package/src/lib/components/lead-campaigns-card.d.ts +4 -4
  50. package/src/lib/components/lead-campaigns-card.js +15 -8
  51. package/src/lib/components/lead-campaigns-card.js.map +1 -1
  52. package/src/lib/components/lead-detail-page.js +25 -5
  53. package/src/lib/components/lead-detail-page.js.map +1 -1
  54. package/src/lib/components/lead-properties-card.js +10 -0
  55. package/src/lib/components/lead-properties-card.js.map +1 -1
  56. package/src/lib/components/lead-status-chip.js +3 -1
  57. package/src/lib/components/lead-status-chip.js.map +1 -1
  58. package/src/lib/components/lead-surfaces-note.js +4 -5
  59. package/src/lib/components/lead-surfaces-note.js.map +1 -1
  60. package/src/lib/components/lead-unqualify-dialog.js +6 -0
  61. package/src/lib/components/lead-unqualify-dialog.js.map +1 -1
  62. package/src/lib/components/leads-bulk-bar.js +34 -11
  63. package/src/lib/components/leads-bulk-bar.js.map +1 -1
  64. package/src/lib/components/leads-section.js +41 -8
  65. package/src/lib/components/leads-section.js.map +1 -1
  66. package/src/lib/components/new-lead-drawer.js +5 -4
  67. package/src/lib/components/new-lead-drawer.js.map +1 -1
  68. package/src/lib/components/org-lead-surfaces-note.js +6 -2
  69. package/src/lib/components/org-lead-surfaces-note.js.map +1 -1
  70. package/src/lib/components/recipes-card.js +9 -4
  71. package/src/lib/components/recipes-card.js.map +1 -1
  72. package/src/lib/components/record-sharing-card.d.ts +57 -0
  73. package/src/lib/components/record-sharing-card.js +244 -0
  74. package/src/lib/components/record-sharing-card.js.map +1 -0
  75. package/src/lib/components/reports/activity-card.js +4 -3
  76. package/src/lib/components/reports/activity-card.js.map +1 -1
  77. package/src/lib/components/reports/closed-deals-card.js +4 -3
  78. package/src/lib/components/reports/closed-deals-card.js.map +1 -1
  79. package/src/lib/components/reports/contacts-mix-card.js +3 -2
  80. package/src/lib/components/reports/contacts-mix-card.js.map +1 -1
  81. package/src/lib/components/reports/contacts-trend-card.js +2 -1
  82. package/src/lib/components/reports/contacts-trend-card.js.map +1 -1
  83. package/src/lib/components/reports/forecast-card.js +3 -2
  84. package/src/lib/components/reports/forecast-card.js.map +1 -1
  85. package/src/lib/components/reports/lead-funnel-card.js +8 -6
  86. package/src/lib/components/reports/lead-funnel-card.js.map +1 -1
  87. package/src/lib/components/reports/pipeline-card.js +4 -3
  88. package/src/lib/components/reports/pipeline-card.js.map +1 -1
  89. package/src/lib/components/reports/report-format.d.ts +1 -1
  90. package/src/lib/components/reports/report-format.js.map +1 -1
  91. package/src/lib/components/reports/report-scope.d.ts +1 -1
  92. package/src/lib/components/reports/report-scope.js.map +1 -1
  93. package/src/lib/components/reports/source-conversion-card.js +2 -1
  94. package/src/lib/components/reports/source-conversion-card.js.map +1 -1
  95. package/src/lib/components/reports/tasks-card.js +2 -1
  96. package/src/lib/components/reports/tasks-card.js.map +1 -1
  97. package/src/lib/components/reports/won-lost-by-owner-card.js +3 -2
  98. package/src/lib/components/reports/won-lost-by-owner-card.js.map +1 -1
  99. package/src/lib/components/reports-section.js +4 -4
  100. package/src/lib/components/reports-section.js.map +1 -1
  101. package/src/lib/components/settings-section.js +7 -0
  102. package/src/lib/components/settings-section.js.map +1 -1
  103. package/src/lib/components/share-records-dialog.d.ts +28 -0
  104. package/src/lib/components/share-records-dialog.js +184 -0
  105. package/src/lib/components/share-records-dialog.js.map +1 -0
  106. package/src/lib/components/sharing-rule-drawer.d.ts +37 -0
  107. package/src/lib/components/sharing-rule-drawer.js +374 -0
  108. package/src/lib/components/sharing-rule-drawer.js.map +1 -0
  109. package/src/lib/components/sharing-rules-card.d.ts +24 -0
  110. package/src/lib/components/sharing-rules-card.js +287 -0
  111. package/src/lib/components/sharing-rules-card.js.map +1 -0
  112. package/src/lib/components/use-crm-api.d.ts +1 -1
  113. package/src/lib/components/use-crm-api.js.map +1 -1
  114. package/src/lib/components/use-site-route-context.d.ts +25 -0
  115. package/src/lib/components/use-site-route-context.js +58 -0
  116. package/src/lib/components/use-site-route-context.js.map +1 -0
  117. package/src/lib/constants/api-routes.d.ts +31 -0
  118. package/src/lib/constants/api-routes.js +27 -1
  119. package/src/lib/constants/api-routes.js.map +1 -1
  120. package/src/lib/constants/bundle-common.d.ts +2 -0
  121. package/src/lib/constants/bundle-common.js +1 -0
  122. package/src/lib/constants/bundle-common.js.map +1 -1
  123. package/src/lib/declarations.console-server.d.ts +41 -0
  124. package/src/lib/declarations.console-server.js +115 -0
  125. package/src/lib/declarations.console-server.js.map +1 -0
  126. package/src/lib/declarations.server.d.ts +12 -0
  127. package/src/lib/declarations.server.js +84 -1
  128. package/src/lib/declarations.server.js.map +1 -1
  129. package/src/lib/hooks/use-contact-remove.d.ts +35 -0
  130. package/src/lib/hooks/use-contact-remove.js +77 -0
  131. package/src/lib/hooks/use-contact-remove.js.map +1 -0
  132. package/src/lib/hooks/use-crm-campaigns.d.ts +2 -2
  133. package/src/lib/hooks/use-crm-campaigns.js +3 -3
  134. package/src/lib/hooks/use-crm-campaigns.js.map +1 -1
  135. package/src/lib/hooks/use-crm-sharing.d.ts +30 -0
  136. package/src/lib/hooks/use-crm-sharing.js +109 -0
  137. package/src/lib/hooks/use-crm-sharing.js.map +1 -0
  138. package/src/lib/model/campaign-filing-activity.d.ts +1 -1
  139. package/src/lib/model/campaign-filing-activity.js +1 -1
  140. package/src/lib/model/campaign-filing-activity.js.map +1 -1
  141. package/src/lib/model/companies-csv.d.ts +1 -1
  142. package/src/lib/model/companies-csv.js +2 -2
  143. package/src/lib/model/companies-csv.js.map +1 -1
  144. package/src/lib/model/contact-holder.d.ts +68 -0
  145. package/src/lib/model/contact-holder.js +129 -0
  146. package/src/lib/model/contact-holder.js.map +1 -0
  147. package/src/lib/model/contact-interaction-links.d.ts +46 -0
  148. package/src/lib/model/contact-interaction-links.js +65 -0
  149. package/src/lib/model/contact-interaction-links.js.map +1 -0
  150. package/src/lib/model/contact-merge.d.ts +136 -0
  151. package/src/lib/model/contact-merge.js +412 -0
  152. package/src/lib/model/contact-merge.js.map +1 -0
  153. package/src/lib/model/contact-record.d.ts +7 -1
  154. package/src/lib/model/contact-record.js +7 -6
  155. package/src/lib/model/contact-record.js.map +1 -1
  156. package/src/lib/model/contact-remove.d.ts +48 -0
  157. package/src/lib/model/contact-remove.js +22 -0
  158. package/src/lib/model/contact-remove.js.map +1 -0
  159. package/src/lib/model/contacts-bulk-writes.d.ts +2 -71
  160. package/src/lib/model/contacts-bulk-writes.js +2 -82
  161. package/src/lib/model/contacts-bulk-writes.js.map +1 -1
  162. package/src/lib/model/contacts-csv.d.ts +2 -2
  163. package/src/lib/model/contacts-csv.js +2 -2
  164. package/src/lib/model/contacts-csv.js.map +1 -1
  165. package/src/lib/model/crm-csv.d.ts +239 -0
  166. package/src/lib/model/crm-csv.js +467 -0
  167. package/src/lib/model/crm-csv.js.map +1 -0
  168. package/src/lib/model/crm-digest.d.ts +227 -0
  169. package/src/lib/model/crm-digest.js +336 -0
  170. package/src/lib/model/crm-digest.js.map +1 -0
  171. package/src/lib/model/crm-import.d.ts +7 -1
  172. package/src/lib/model/crm-import.js +2 -1
  173. package/src/lib/model/crm-import.js.map +1 -1
  174. package/src/lib/model/crm-inbound.d.ts +162 -0
  175. package/src/lib/model/crm-inbound.js +208 -0
  176. package/src/lib/model/crm-inbound.js.map +1 -0
  177. package/src/lib/model/crm-lead-import.d.ts +1 -1
  178. package/src/lib/model/crm-lead-import.js +4 -4
  179. package/src/lib/model/crm-lead-import.js.map +1 -1
  180. package/src/lib/model/crm-record-routes.d.ts +5 -0
  181. package/src/lib/model/crm-record-routes.js +7 -0
  182. package/src/lib/model/crm-record-routes.js.map +1 -1
  183. package/src/lib/model/crm-reports.d.ts +467 -0
  184. package/src/lib/model/crm-reports.js +667 -0
  185. package/src/lib/model/crm-reports.js.map +1 -0
  186. package/src/lib/model/crm-routes.d.ts +6 -5
  187. package/src/lib/model/crm-routes.js +7 -7
  188. package/src/lib/model/crm-routes.js.map +1 -1
  189. package/src/lib/model/crm-search-sources.d.ts +15 -0
  190. package/src/lib/model/crm-search-sources.js +162 -0
  191. package/src/lib/model/crm-search-sources.js.map +1 -0
  192. package/src/lib/model/crm-sharing-api.d.ts +32 -0
  193. package/src/lib/model/crm-sharing-api.js +59 -0
  194. package/src/lib/model/crm-sharing-api.js.map +1 -0
  195. package/src/lib/model/crm-sharing.d.ts +333 -0
  196. package/src/lib/model/crm-sharing.js +603 -0
  197. package/src/lib/model/crm-sharing.js.map +1 -0
  198. package/src/lib/model/deals-csv.d.ts +1 -1
  199. package/src/lib/model/deals-csv.js +4 -5
  200. package/src/lib/model/deals-csv.js.map +1 -1
  201. package/src/lib/model/lead-filters.d.ts +5 -4
  202. package/src/lib/model/lead-filters.js +7 -5
  203. package/src/lib/model/lead-filters.js.map +1 -1
  204. package/src/lib/model/leads-csv.d.ts +1 -1
  205. package/src/lib/model/leads-csv.js +1 -1
  206. package/src/lib/model/leads-csv.js.map +1 -1
  207. package/src/lib/model/tasks-csv.d.ts +1 -1
  208. package/src/lib/model/tasks-csv.js +1 -1
  209. package/src/lib/model/tasks-csv.js.map +1 -1
  210. package/src/lib/plan-entitlements.d.ts +75 -0
  211. package/src/lib/plan-entitlements.js +56 -0
  212. package/src/lib/plan-entitlements.js.map +1 -0
  213. package/src/lib/plugin.js +39 -0
  214. package/src/lib/plugin.js.map +1 -1
  215. package/src/lib/server/api-v1/contacts-merge.d.ts +20 -0
  216. package/src/lib/server/api-v1/contacts-merge.js +120 -0
  217. package/src/lib/server/api-v1/contacts-merge.js.map +1 -0
  218. package/src/lib/server/api-v1/contacts.d.ts +18 -0
  219. package/src/lib/server/api-v1/contacts.js +1076 -0
  220. package/src/lib/server/api-v1/contacts.js.map +1 -0
  221. package/src/lib/server/api-v1/crm-activities.d.ts +18 -0
  222. package/src/lib/server/api-v1/crm-activities.js +269 -0
  223. package/src/lib/server/api-v1/crm-activities.js.map +1 -0
  224. package/src/lib/server/api-v1/crm-companies.d.ts +18 -0
  225. package/src/lib/server/api-v1/crm-companies.js +391 -0
  226. package/src/lib/server/api-v1/crm-companies.js.map +1 -0
  227. package/src/lib/server/api-v1/crm-deals.d.ts +18 -0
  228. package/src/lib/server/api-v1/crm-deals.js +631 -0
  229. package/src/lib/server/api-v1/crm-deals.js.map +1 -0
  230. package/src/lib/server/api-v1/crm-email-templates.d.ts +18 -0
  231. package/src/lib/server/api-v1/crm-email-templates.js +336 -0
  232. package/src/lib/server/api-v1/crm-email-templates.js.map +1 -0
  233. package/src/lib/server/api-v1/crm-leads.d.ts +18 -0
  234. package/src/lib/server/api-v1/crm-leads.js +833 -0
  235. package/src/lib/server/api-v1/crm-leads.js.map +1 -0
  236. package/src/lib/server/api-v1/crm-pipelines.d.ts +76 -0
  237. package/src/lib/server/api-v1/crm-pipelines.js +146 -0
  238. package/src/lib/server/api-v1/crm-pipelines.js.map +1 -0
  239. package/src/lib/server/api-v1/crm-shared.d.ts +271 -0
  240. package/src/lib/server/api-v1/crm-shared.js +436 -0
  241. package/src/lib/server/api-v1/crm-shared.js.map +1 -0
  242. package/src/lib/server/api-v1/crm-tasks.d.ts +18 -0
  243. package/src/lib/server/api-v1/crm-tasks.js +398 -0
  244. package/src/lib/server/api-v1/crm-tasks.js.map +1 -0
  245. package/src/lib/server/api-v1/openapi.d.ts +29 -0
  246. package/src/lib/server/api-v1/openapi.js +985 -0
  247. package/src/lib/server/api-v1/openapi.js.map +1 -0
  248. package/src/lib/server/api-v1/usage.d.ts +26 -0
  249. package/src/lib/server/api-v1/usage.js +73 -0
  250. package/src/lib/server/api-v1/usage.js.map +1 -0
  251. package/src/lib/server/campaign-filing-activity.d.ts +1 -1
  252. package/src/lib/server/campaign-filing-activity.js +7 -11
  253. package/src/lib/server/campaign-filing-activity.js.map +1 -1
  254. package/src/lib/server/capture-contact.js +15 -4
  255. package/src/lib/server/capture-contact.js.map +1 -1
  256. package/src/lib/server/consent-group-participant.js +1 -1
  257. package/src/lib/server/consent-group-participant.js.map +1 -1
  258. package/src/lib/server/contact-email-engagement.d.ts +33 -0
  259. package/src/lib/server/contact-email-engagement.js +146 -0
  260. package/src/lib/server/contact-email-engagement.js.map +1 -0
  261. package/src/lib/server/contact-lifecycle-floor.d.ts +89 -0
  262. package/src/lib/server/contact-lifecycle-floor.js +91 -0
  263. package/src/lib/server/contact-lifecycle-floor.js.map +1 -0
  264. package/src/lib/server/contact-remove.d.ts +75 -0
  265. package/src/lib/server/contact-remove.js +244 -0
  266. package/src/lib/server/contact-remove.js.map +1 -0
  267. package/src/lib/server/contact-update.js +4 -3
  268. package/src/lib/server/contact-update.js.map +1 -1
  269. package/src/lib/server/contacts-import.d.ts +47 -11
  270. package/src/lib/server/contacts-import.js +171 -71
  271. package/src/lib/server/contacts-import.js.map +1 -1
  272. package/src/lib/server/contacts-merge.d.ts +1 -1
  273. package/src/lib/server/contacts-merge.js +3 -2
  274. package/src/lib/server/contacts-merge.js.map +1 -1
  275. package/src/lib/server/container-detach.d.ts +31 -0
  276. package/src/lib/server/container-detach.js +115 -0
  277. package/src/lib/server/container-detach.js.map +1 -0
  278. package/src/lib/server/convert-host-lead.d.ts +189 -0
  279. package/src/lib/server/convert-host-lead.js +518 -0
  280. package/src/lib/server/convert-host-lead.js.map +1 -0
  281. package/src/lib/server/crm-inbound-email.d.ts +162 -0
  282. package/src/lib/server/crm-inbound-email.js +259 -0
  283. package/src/lib/server/crm-inbound-email.js.map +1 -0
  284. package/src/lib/server/crm-records-meter.d.ts +50 -0
  285. package/src/lib/server/crm-records-meter.js +126 -0
  286. package/src/lib/server/crm-records-meter.js.map +1 -0
  287. package/src/lib/server/crm-sharing.d.ts +108 -0
  288. package/src/lib/server/crm-sharing.js +790 -0
  289. package/src/lib/server/crm-sharing.js.map +1 -0
  290. package/src/lib/server/daily-digest-route.d.ts +101 -0
  291. package/src/lib/server/daily-digest-route.js +578 -0
  292. package/src/lib/server/daily-digest-route.js.map +1 -0
  293. package/src/lib/server/export-route.d.ts +60 -0
  294. package/src/lib/server/export-route.js +447 -0
  295. package/src/lib/server/export-route.js.map +1 -0
  296. package/src/lib/server/inbound-address.js +5 -3
  297. package/src/lib/server/inbound-address.js.map +1 -1
  298. package/src/lib/server/inbound-route.d.ts +74 -0
  299. package/src/lib/server/inbound-route.js +260 -0
  300. package/src/lib/server/inbound-route.js.map +1 -0
  301. package/src/lib/server/lead-campaign-carry.js +3 -3
  302. package/src/lib/server/lead-campaign-carry.js.map +1 -1
  303. package/src/lib/server/lead-convert.d.ts +1 -1
  304. package/src/lib/server/lead-convert.js +2 -2
  305. package/src/lib/server/lead-convert.js.map +1 -1
  306. package/src/lib/server/lead-create.d.ts +1 -1
  307. package/src/lib/server/lead-create.js +10 -15
  308. package/src/lib/server/lead-create.js.map +1 -1
  309. package/src/lib/server/lead-nurturing.d.ts +23 -0
  310. package/src/lib/server/lead-nurturing.js +82 -0
  311. package/src/lib/server/lead-nurturing.js.map +1 -0
  312. package/src/lib/server/leads-import.js +7 -7
  313. package/src/lib/server/leads-import.js.map +1 -1
  314. package/src/lib/server/merge-contacts.d.ts +55 -0
  315. package/src/lib/server/merge-contacts.js +274 -0
  316. package/src/lib/server/merge-contacts.js.map +1 -0
  317. package/src/lib/server/pipeline-record-index.d.ts +36 -0
  318. package/src/lib/server/pipeline-record-index.js +87 -0
  319. package/src/lib/server/pipeline-record-index.js.map +1 -0
  320. package/src/lib/server/record-email-state.js +23 -0
  321. package/src/lib/server/record-email-state.js.map +1 -1
  322. package/src/lib/server/record-facts.js +2 -1
  323. package/src/lib/server/record-facts.js.map +1 -1
  324. package/src/lib/server/record-timeline.d.ts +12 -0
  325. package/src/lib/server/record-timeline.js +127 -15
  326. package/src/lib/server/record-timeline.js.map +1 -1
  327. package/src/lib/server/task-reminders-route.d.ts +82 -0
  328. package/src/lib/server/task-reminders-route.js +499 -0
  329. package/src/lib/server/task-reminders-route.js.map +1 -0
  330. package/src/lib/server/task-routes.d.ts +2 -0
  331. package/src/lib/server/task-routes.js +22 -3
  332. package/src/lib/server/task-routes.js.map +1 -1
  333. package/src/lib/server-deal-stage.js +2 -1
  334. package/src/lib/server-deal-stage.js.map +1 -1
  335. package/src/lib/server.d.ts +5 -2
  336. package/src/lib/server.js +47 -2
  337. package/src/lib/server.js.map +1 -1
  338. package/src/lib/testing/stand-in-site-record-routes.d.ts +26 -0
  339. package/src/lib/testing/stand-in-site-record-routes.js +55 -0
  340. package/src/lib/testing/stand-in-site-record-routes.js.map +1 -0
  341. package/src/lib/usage-axes.d.ts +35 -0
  342. package/src/lib/usage-axes.js +75 -0
  343. package/src/lib/usage-axes.js.map +1 -0
  344. package/src/lib/components/use-emails-hub-path.d.ts +0 -24
  345. package/src/lib/components/use-emails-hub-path.js +0 -59
  346. package/src/lib/components/use-emails-hub-path.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/crm-sharing.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 * CRM SHARING FOR MULTI-BRAND ORGS (AGL-3336).\n *\n * An org manager shares a lead, a contact, a company or a deal with other\n * sites — by hand, from the record or a list's bulk bar, or by a SHARING\n * RULE that applies to every record it matches, now and later. The model is\n * Salesforce's three layers:\n *\n * 1. the org-wide default — the consent-group scope a record is captured\n * with (`consentGroupScope`), untouched by anything here;\n * 2. sharing rules — `crm.sharingRules` on the org document;\n * 3. manual shares — per record, per target.\n *\n * ## One field is enforced, so one field is widened\n *\n * Both enforcement layers read `visibleTo`: the rules with `hasAny`, every\n * list with `array-contains-any`. A share therefore ADDS its tokens to\n * `visibleTo`, which is what puts the record on the target site's lists as a\n * real query — no second field, no second index, no loaded-window match.\n * `'org'` is the token for \"all sites\", and it covers a site created later\n * with nothing to recompute, because every site's read set carries it.\n *\n * ## …and the sources stay distinguishable\n *\n * The record keeps its grants under `sharing` (`SCOPE_GRANTS_FIELD` in\n * core): one grant per rule and one per hand-shared target, and `added`,\n * the tokens the grants put on `visibleTo` that were not there already.\n * The record's own scope — its HELD tokens — is `visibleTo` without\n * `added`, so taking a grant away removes only what grants alone\n * contributed, and never a consent-group token. A token a site later\n * captures the person on is held from then on: the capturing sites\n * (`hostId`, `capturedByHostIds`) are never counted as added.\n *\n * ## Visibility is not consent, and not write access\n *\n * Nothing here reads or writes a consent entry. A site a record is shared\n * with sees it; it holds no basis to market to the person, and the\n * audience paths read `heldByHost`, not `visibleTo`. Write access is its\n * own field, `writeTo`, present only on a shared record: the held tokens\n * and the tokens of grants whose access is `edit`. The Firestore rules and\n * the CRM's routes check it; a read-only grant is the default.\n */\n\nimport type { ScopeToken } from '@aglyn/aglyn'\nimport {\n heldScopeTokens,\n hostIdsFromScope,\n hostScopeToken,\n isScopeToken,\n MAX_SCOPE_HOSTS,\n ORG_SCOPE_TOKEN,\n parseScopeToken,\n SCOPE_GRANTS_FIELD,\n} from '@aglyn/aglyn/app-utils/scope-tokens'\n\n/*==========================================\n * WHAT IS SHARED.\n *=========================================*/\n\n/** The four record types a share or a rule applies to, by their collection. */\nexport const CRM_SHARING_OBJECTS = ['leads', 'contacts', 'companies', 'deals'] as const\nexport type CrmSharingObject = (typeof CRM_SHARING_OBJECTS)[number]\n\nexport function isCrmSharingObject(value: unknown): value is CrmSharingObject {\n return (\n typeof value === 'string' && (CRM_SHARING_OBJECTS as readonly string[]).includes(value)\n )\n}\n\n/** How each object reads in a sentence. */\nexport const CRM_SHARING_OBJECT_LABELS: Readonly<\n Record<CrmSharingObject, { one: string; many: string }>\n> = {\n leads: { one: 'lead', many: 'Leads' },\n contacts: { one: 'contact', many: 'Contacts' },\n companies: { one: 'company', many: 'Companies' },\n deals: { one: 'deal', many: 'Deals' },\n}\n\n/** What a target site may do with a shared record. Read-only unless a grant says edit. */\nexport type CrmShareAccess = 'read' | 'edit'\n\nexport function isCrmShareAccess(value: unknown): value is CrmShareAccess {\n return value === 'read' || value === 'edit'\n}\n\n/** Where a grant came from. */\nexport type CrmShareSource = 'manual' | 'rule'\n\n/** One grant on one record. */\nexport interface CrmShareGrant {\n source: CrmShareSource\n /** The sites it shares with: `['org']` for all sites, else host tokens. */\n tokens: ScopeToken[]\n access: CrmShareAccess\n /** The rule behind a `rule` grant. */\n ruleId?: string\n /** Who shared it by hand, as they were named at the time. */\n byUid?: string\n byName?: string\n /** When the grant was made. */\n atMs: number\n}\n\n/** `sharing` on a record — see the module note. */\nexport interface CrmRecordSharing {\n /** Keyed by {@link manualGrantKey} or {@link ruleGrantKey}. */\n grants: Record<string, CrmShareGrant>\n /** Every token any grant names — the record's shares, however held. */\n tokens: ScopeToken[]\n /** The tokens the grants ADDED to `visibleTo` — see `heldScopeTokens`. */\n added: ScopeToken[]\n /** The rules holding a grant here, so a rule's removal can find its records. */\n ruleIds: string[]\n /**\n * The held tokens of a record shared with all sites, and nothing\n * otherwise. `array-contains` on it answers \"is this record shared with\n * everyone AND held by site X\" — the one question `added` alone cannot,\n * since a query takes a single array clause.\n */\n orgWideHeldBy: ScopeToken[]\n}\n\n/** The record field its grants live under — the core's name for it. */\nexport const CRM_SHARING_FIELD = SCOPE_GRANTS_FIELD\n\n/** The record field naming who may WRITE it, present only while it is shared. */\nexport const CRM_WRITE_SCOPE_FIELD = 'writeTo'\n\n/** The array field a rule's removal queries. */\nexport const CRM_SHARING_RULE_IDS_PATH = `${CRM_SHARING_FIELD}.ruleIds`\n\n/** A hand share is one grant per target, so each is unshared on its own. */\nexport function manualGrantKey(token: string): string {\n const parsed = parseScopeToken(token)\n if (!parsed) throw new Error(`not a scope token: ${token}`)\n return parsed.kind === 'org' ? 'manual_org' : `manual_${parsed.hostId}`\n}\n\nexport function ruleGrantKey(ruleId: string): string {\n return `rule_${ruleId}`\n}\n\nfunction tokenList(raw: unknown): ScopeToken[] {\n if (!Array.isArray(raw)) return []\n return [...new Set(raw.filter(isScopeToken))]\n}\n\nfunction readGrant(raw: unknown): CrmShareGrant | null {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null\n const value = raw as Record<string, unknown>\n const tokens = tokenList(value['tokens'])\n if (!tokens.length) return null\n const source = value['source'] === 'rule' ? 'rule' : value['source'] === 'manual' ? 'manual' : null\n if (!source) return null\n const ruleId = typeof value['ruleId'] === 'string' ? value['ruleId'] : ''\n if (source === 'rule' && !ruleId) return null\n const byUid = typeof value['byUid'] === 'string' ? value['byUid'] : ''\n const byName = typeof value['byName'] === 'string' ? value['byName'] : ''\n const atMs = typeof value['atMs'] === 'number' && Number.isFinite(value['atMs']) ? value['atMs'] : 0\n return {\n source,\n tokens,\n access: value['access'] === 'edit' ? 'edit' : 'read',\n ...(ruleId ? { ruleId } : {}),\n ...(byUid ? { byUid } : {}),\n ...(byName ? { byName } : {}),\n atMs,\n }\n}\n\n/** A record's grants, cleaned; `{}` for a record never shared. */\nexport function readRecordGrants(\n record: Readonly<Record<string, unknown>> | null | undefined,\n): Record<string, CrmShareGrant> {\n const sharing = (record ?? {})[CRM_SHARING_FIELD]\n if (!sharing || typeof sharing !== 'object' || Array.isArray(sharing)) return {}\n const grants = (sharing as Record<string, unknown>)['grants']\n if (!grants || typeof grants !== 'object' || Array.isArray(grants)) return {}\n const out: Record<string, CrmShareGrant> = {}\n for (const [key, raw] of Object.entries(grants as Record<string, unknown>)) {\n const grant = readGrant(raw)\n if (grant) out[key] = grant\n }\n return out\n}\n\n/*==========================================\n * THE PLAN: grants in, the three fields out.\n *=========================================*/\n\n/**\n * The sites that captured or created the record. Their tokens are HELD,\n * whatever `added` says: a person shared to site B and later captured on B\n * is B's own from then on, and unsharing must not hide them from B.\n */\nexport function recordHolderHostIds(record: Readonly<Record<string, unknown>>): string[] {\n const ids = new Set<string>()\n const hostId = record['hostId']\n if (typeof hostId === 'string' && hostId) ids.add(hostId)\n const captured = record['capturedByHostIds']\n if (Array.isArray(captured)) {\n for (const id of captured) if (typeof id === 'string' && id) ids.add(id)\n }\n return [...ids]\n}\n\n/**\n * The record's own scope: `visibleTo` less what grants added, its capturing\n * sites kept — the core's `heldScopeTokens`, as scope tokens.\n */\nexport function recordHeldScope(record: Readonly<Record<string, unknown>>): ScopeToken[] {\n return tokenList(heldScopeTokens(record))\n}\n\nexport interface CrmSharingPlan {\n visibleTo: ScopeToken[]\n /** `null` removes the field: an unshared record writes by `visibleTo`. */\n writeTo: ScopeToken[] | null\n /** `null` removes the field. */\n sharing: CrmRecordSharing | null\n}\n\n/**\n * The fields a record carries under `grants`.\n *\n * A grant naming a token the record already holds adds nothing, and a\n * record held org-wide adds nothing at all — every site sees it already, so\n * a share there would only make a later unshare look like it hid something.\n * The token order is the held scope first and the additions after it, so an\n * unchanged record plans the `visibleTo` it has.\n */\nexport function planRecordSharing(\n record: Readonly<Record<string, unknown>>,\n grants: Readonly<Record<string, CrmShareGrant>>,\n): CrmSharingPlan {\n const held = recordHeldScope(record)\n const entries = Object.entries(grants)\n if (!entries.length) return { visibleTo: held, writeTo: null, sharing: null }\n const heldSet = new Set<string>(held)\n const heldOrgWide = heldSet.has(ORG_SCOPE_TOKEN)\n const tokens: ScopeToken[] = []\n const editTokens: ScopeToken[] = []\n const ruleIds: string[] = []\n for (const [, grant] of entries.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))) {\n for (const token of grant.tokens) {\n if (!tokens.includes(token)) tokens.push(token)\n if (grant.access === 'edit' && !editTokens.includes(token)) editTokens.push(token)\n }\n if (grant.source === 'rule' && grant.ruleId && !ruleIds.includes(grant.ruleId)) {\n ruleIds.push(grant.ruleId)\n }\n }\n const added = heldOrgWide ? [] : tokens.filter((token) => !heldSet.has(token))\n const writeTo = [...held]\n for (const token of editTokens) if (!writeTo.includes(token)) writeTo.push(token)\n return {\n visibleTo: [...held, ...added],\n writeTo,\n sharing: {\n grants: Object.fromEntries(entries),\n tokens,\n added,\n ruleIds,\n orgWideHeldBy: added.includes(ORG_SCOPE_TOKEN) ? [...held] : [],\n },\n }\n}\n\nconst sameList = (a: readonly unknown[] | null | undefined, b: readonly unknown[] | null | undefined) =>\n (a ?? null) === null\n ? (b ?? null) === null\n : (b ?? null) !== null && a!.length === b!.length && a!.every((entry, at) => entry === b![at])\n\nconst sameGrant = (a: CrmShareGrant | undefined, b: CrmShareGrant | undefined) =>\n !!a &&\n !!b &&\n a.source === b.source &&\n a.access === b.access &&\n (a.ruleId ?? '') === (b.ruleId ?? '') &&\n (a.byUid ?? '') === (b.byUid ?? '') &&\n (a.byName ?? '') === (b.byName ?? '') &&\n a.atMs === b.atMs &&\n sameList(a.tokens, b.tokens)\n\n/** Whether writing `plan` would change what the record stores. */\nexport function sharingPlanChanges(\n record: Readonly<Record<string, unknown>>,\n plan: CrmSharingPlan,\n): boolean {\n if (!sameList(tokenList(record['visibleTo']), plan.visibleTo)) return true\n const storedWrite = record[CRM_WRITE_SCOPE_FIELD]\n if (!sameList(Array.isArray(storedWrite) ? tokenList(storedWrite) : null, plan.writeTo)) return true\n const stored = record[CRM_SHARING_FIELD]\n if (!plan.sharing) return stored !== undefined && stored !== null\n const grants = readRecordGrants(record)\n const keys = Object.keys(plan.sharing.grants)\n if (keys.length !== Object.keys(grants).length) return true\n if (!keys.every((key) => sameGrant(grants[key], plan.sharing!.grants[key]))) return true\n const sharing = (stored ?? {}) as Record<string, unknown>\n return (\n !sameList(tokenList(sharing['tokens']), plan.sharing.tokens) ||\n !sameList(tokenList(sharing['added']), plan.sharing.added) ||\n !sameList(\n Array.isArray(sharing['ruleIds']) ? (sharing['ruleIds'] as unknown[]) : [],\n plan.sharing.ruleIds,\n ) ||\n !sameList(tokenList(sharing['orgWideHeldBy']), plan.sharing.orgWideHeldBy)\n )\n}\n\n/*==========================================\n * SHARING RULES: `crm.sharingRules` on the org document.\n *=========================================*/\n\n/** The org document path the rules are stored under. */\nexport const CRM_SHARING_RULES_PATH = 'crm.sharingRules'\n\n/** A workspace keeps at most this many. */\nexport const CRM_SHARING_RULES_MAX = 20\n\n/** At most this many values per criterion. */\nexport const CRM_SHARING_CRITERION_MAX = 20\n\n/**\n * Which records a rule matches, beyond the object and the capturing site.\n * AND across the keys, OR within one; an absent or empty key matches\n * everything.\n */\nexport interface CrmSharingCriteria {\n /** A lead's or a contact's lead source, by label. */\n leadSources?: string[]\n /** Any of these tags (lead, contact, company). */\n tags?: string[]\n /** Filed under any of these campaigns (lead, contact). */\n campaignIds?: string[]\n /** A lead's status, a contact's lifecycle stage, or a deal's stage id. */\n stages?: string[]\n /** Owned by any of these members. */\n ownerUids?: string[]\n}\n\nexport const CRM_SHARING_CRITERIA_KEYS = [\n 'leadSources',\n 'tags',\n 'campaignIds',\n 'stages',\n 'ownerUids',\n] as const satisfies readonly (keyof CrmSharingCriteria)[]\n\n/** Which criteria an object's records can answer. */\nexport const CRM_SHARING_CRITERIA_FOR: Readonly<\n Record<CrmSharingObject, readonly (keyof CrmSharingCriteria)[]>\n> = {\n leads: ['leadSources', 'tags', 'campaignIds', 'stages', 'ownerUids'],\n contacts: ['leadSources', 'tags', 'campaignIds', 'stages', 'ownerUids'],\n companies: ['tags', 'ownerUids'],\n deals: ['stages', 'ownerUids'],\n}\n\n/** Where a rule's recompute stands. */\nexport interface CrmSharingRuleRun {\n status: 'running' | 'done' | 'failed'\n /** What the run does: apply the rule everywhere, or take it away. */\n mode: 'apply' | 'remove'\n /** Records examined. */\n processed: number\n /** Records whose sharing changed. */\n changed: number\n /** The last record id an `apply` run examined, to continue after. */\n cursor?: string | null\n startedAtMs: number\n finishedAtMs?: number\n error?: string\n}\n\nexport interface CrmSharingRule {\n id: string\n name: string\n object: CrmSharingObject\n /** Off keeps the rule and takes away what it granted. */\n enabled: boolean\n /** Records captured or created on any of these sites; empty is every site. */\n sourceHostIds: string[]\n criteria: CrmSharingCriteria\n /** `['org']` for all sites, including sites added later; else host tokens. */\n targets: ScopeToken[]\n access: CrmShareAccess\n /** Being deleted: its grants are being removed, and it matches nothing. */\n deleting?: boolean\n createdByUid?: string\n createdAtMs: number\n updatedAtMs: number\n run?: CrmSharingRuleRun\n}\n\nconst RULE_ID = /^[A-Za-z0-9_-]{1,64}$/\n\nfunction stringList(raw: unknown, max = CRM_SHARING_CRITERION_MAX): string[] {\n if (!Array.isArray(raw)) return []\n const out: string[] = []\n for (const entry of raw) {\n const value = String(entry ?? '').trim().slice(0, 200)\n if (value && !out.includes(value)) out.push(value)\n if (out.length >= max) break\n }\n return out\n}\n\n/** One rule as stored, cleaned, or null when it cannot be applied. */\nexport function readCrmSharingRule(raw: unknown): CrmSharingRule | null {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null\n const value = raw as Record<string, unknown>\n const id = String(value['id'] ?? '')\n if (!RULE_ID.test(id)) return null\n if (!isCrmSharingObject(value['object'])) return null\n const targets = normalizeShareTargets(tokenList(value['targets']))\n if (!targets) return null\n const rawCriteria = (value['criteria'] ?? {}) as Record<string, unknown>\n const criteria: CrmSharingCriteria = {}\n for (const key of CRM_SHARING_CRITERIA_FOR[value['object']]) {\n const list = stringList(rawCriteria[key])\n if (list.length) criteria[key] = key === 'tags' ? list.map((tag) => tag.toLowerCase()) : list\n }\n const run = value['run'] as Record<string, unknown> | undefined\n return {\n id,\n name: String(value['name'] ?? '').trim().slice(0, 120) || 'Sharing rule',\n object: value['object'],\n enabled: value['enabled'] !== false,\n sourceHostIds: stringList(value['sourceHostIds'], MAX_SCOPE_HOSTS),\n criteria,\n targets,\n access: value['access'] === 'edit' ? 'edit' : 'read',\n ...(value['deleting'] === true ? { deleting: true } : {}),\n ...(typeof value['createdByUid'] === 'string' ? { createdByUid: value['createdByUid'] } : {}),\n createdAtMs: Number(value['createdAtMs']) || 0,\n updatedAtMs: Number(value['updatedAtMs']) || 0,\n ...(run && typeof run === 'object'\n ? {\n run: {\n status:\n run['status'] === 'done' || run['status'] === 'failed' ? run['status'] : 'running',\n mode: run['mode'] === 'remove' ? 'remove' : 'apply',\n processed: Number(run['processed']) || 0,\n changed: Number(run['changed']) || 0,\n cursor: typeof run['cursor'] === 'string' ? run['cursor'] : null,\n startedAtMs: Number(run['startedAtMs']) || 0,\n ...(typeof run['finishedAtMs'] === 'number' ? { finishedAtMs: run['finishedAtMs'] } : {}),\n ...(typeof run['error'] === 'string' ? { error: run['error'] } : {}),\n },\n }\n : {}),\n }\n}\n\n/** The org's sharing rules, in their stored order. */\nexport function readCrmSharingRules(\n org: Readonly<Record<string, unknown>> | null | undefined,\n): CrmSharingRule[] {\n const crm = (org ?? {})['crm']\n const raw =\n crm && typeof crm === 'object' && !Array.isArray(crm)\n ? (crm as Record<string, unknown>)['sharingRules']\n : undefined\n if (!Array.isArray(raw)) return []\n const out: CrmSharingRule[] = []\n for (const entry of raw) {\n const rule = readCrmSharingRule(entry)\n if (rule && !out.some((existing) => existing.id === rule.id)) out.push(rule)\n }\n return out\n}\n\n/** The rules that grant anything now: enabled, and not being deleted. */\nexport function activeSharingRules(\n rules: readonly CrmSharingRule[],\n object?: CrmSharingObject,\n): CrmSharingRule[] {\n return rules.filter(\n (rule) => rule.enabled && !rule.deleting && (!object || rule.object === object),\n )\n}\n\n/**\n * A share's targets, cleaned for storage: `['org']` when all sites are\n * named, else the host tokens, capped. `null` when nothing is left.\n */\nexport function normalizeShareTargets(tokens: readonly string[]): ScopeToken[] | null {\n const clean = tokenList(tokens)\n if (!clean.length) return null\n if (clean.includes(ORG_SCOPE_TOKEN)) return [ORG_SCOPE_TOKEN]\n if (clean.length > MAX_SCOPE_HOSTS) return null\n return clean\n}\n\n/** Targets from a request: `'all'`, or a list of host ids. */\nexport function shareTargetsFrom(input: unknown): ScopeToken[] | null {\n if (input === 'all') return [ORG_SCOPE_TOKEN]\n if (!Array.isArray(input)) return null\n return normalizeShareTargets(\n input\n .map((id) => String(id ?? '').trim())\n .filter((id) => id && !id.includes('/') && !id.includes(':'))\n .map(hostScopeToken),\n )\n}\n\n/*==========================================\n * WHAT A RULE READS OFF A RECORD.\n *=========================================*/\n\nexport interface CrmSharingFacts {\n hostIds: string[]\n leadSources: string[]\n tags: string[]\n campaignIds: string[]\n stages: string[]\n ownerUids: string[]\n}\n\nconst str = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\nconst strs = (value: unknown): string[] =>\n Array.isArray(value) ? value.map(str).filter(Boolean) : []\n\n/**\n * The facts a rule's criteria are matched against. A contact keeps each\n * holder's working fields in its own facet, so a contact answers with the\n * union of its facets: a rule about \"contacts tagged vip\" matches a person\n * any holder tagged.\n */\nexport function crmSharingFacts(\n object: CrmSharingObject,\n record: Readonly<Record<string, unknown>>,\n): CrmSharingFacts {\n const facts: CrmSharingFacts = {\n hostIds: recordHolderHostIds(record),\n leadSources: [],\n tags: [],\n campaignIds: [],\n stages: [],\n ownerUids: [],\n }\n const take = (source: Readonly<Record<string, unknown>>, stageField: string | null) => {\n const leadSource = str(source['leadSource'])\n if (leadSource) facts.leadSources.push(leadSource)\n facts.tags.push(...strs(source['tags']).map((tag) => tag.toLowerCase()))\n facts.campaignIds.push(...strs(source['campaignIds']))\n const stage = stageField ? str(source[stageField]) : ''\n if (stage) facts.stages.push(stage)\n const owner = str(source['ownerUid'])\n if (owner) facts.ownerUids.push(owner)\n }\n if (object === 'leads') take(record, 'status')\n else if (object === 'companies') take(record, null)\n else if (object === 'deals') take(record, 'stageId')\n else {\n const facets = record['facets']\n if (facets && typeof facets === 'object' && !Array.isArray(facets)) {\n for (const facet of Object.values(facets as Record<string, unknown>)) {\n if (facet && typeof facet === 'object' && !Array.isArray(facet)) {\n take(facet as Record<string, unknown>, 'lifecycleStage')\n }\n }\n }\n }\n // A lead with no status is New, as every lead reader answers it.\n if (object === 'leads' && !facts.stages.length) facts.stages.push('new')\n return facts\n}\n\nconst anyOf = (wanted: readonly string[] | undefined, have: readonly string[], fold = false) => {\n if (!wanted?.length) return true\n const set = new Set(fold ? have.map((value) => value.toLowerCase()) : have)\n return wanted.some((value) => set.has(fold ? value.toLowerCase() : value))\n}\n\n/** Whether a rule shares this record. */\nexport function crmSharingRuleMatches(\n rule: CrmSharingRule,\n object: CrmSharingObject,\n record: Readonly<Record<string, unknown>>,\n): boolean {\n if (!rule.enabled || rule.deleting || rule.object !== object) return false\n const facts = crmSharingFacts(object, record)\n if (rule.sourceHostIds.length && !facts.hostIds.some((id) => rule.sourceHostIds.includes(id))) {\n return false\n }\n const { criteria } = rule\n return (\n anyOf(criteria.leadSources, facts.leadSources, true) &&\n anyOf(criteria.tags, facts.tags, true) &&\n anyOf(criteria.campaignIds, facts.campaignIds) &&\n anyOf(criteria.stages, facts.stages) &&\n anyOf(criteria.ownerUids, facts.ownerUids)\n )\n}\n\n/**\n * The grants a record should carry: its hand shares as they are, and one\n * grant per active rule that matches it now. A rule grant already present\n * keeps its date, so re-evaluating an unchanged record writes nothing.\n *\n * A rule grant whose rule is not among `rules` is dropped — the caller\n * passes every active rule of the org, so a rule absent from them has been\n * switched off or deleted.\n */\nexport function evaluateRecordGrants(\n object: CrmSharingObject,\n record: Readonly<Record<string, unknown>>,\n rules: readonly CrmSharingRule[],\n atMs: number,\n): Record<string, CrmShareGrant> {\n const current = readRecordGrants(record)\n const next: Record<string, CrmShareGrant> = {}\n for (const [key, grant] of Object.entries(current)) {\n if (grant.source === 'manual') next[key] = grant\n }\n for (const rule of activeSharingRules(rules, object)) {\n if (!crmSharingRuleMatches(rule, object, record)) continue\n const key = ruleGrantKey(rule.id)\n const before = current[key]\n next[key] = {\n source: 'rule',\n ruleId: rule.id,\n tokens: [...rule.targets],\n access: rule.access,\n atMs: before && sameList(before.tokens, rule.targets) && before.access === rule.access\n ? before.atMs\n : atMs,\n }\n }\n return next\n}\n\n/** The hand shares after sharing with `targets`, by `by`. */\nexport function withManualShares(\n grants: Readonly<Record<string, CrmShareGrant>>,\n targets: readonly ScopeToken[],\n access: CrmShareAccess,\n by: { uid: string; name?: string | null },\n atMs: number,\n): Record<string, CrmShareGrant> {\n const next = { ...grants }\n for (const token of targets) {\n const name = String(by.name ?? '').trim()\n next[manualGrantKey(token)] = {\n source: 'manual',\n tokens: [token],\n access,\n byUid: by.uid,\n ...(name ? { byName: name } : {}),\n atMs,\n }\n }\n return next\n}\n\n/** The hand shares after unsharing `targets`. Rule grants are the rule's to take away. */\nexport function withoutManualShares(\n grants: Readonly<Record<string, CrmShareGrant>>,\n targets: readonly string[],\n): Record<string, CrmShareGrant> {\n const drop = new Set(targets.filter(isScopeToken).map(manualGrantKey))\n return Object.fromEntries(Object.entries(grants).filter(([key]) => !drop.has(key)))\n}\n\n/*==========================================\n * WHAT A READER SEES.\n *=========================================*/\n\n/**\n * Whether a caller holding `tokens` may write the record: `writeTo` on a\n * shared record, else `visibleTo` — the Firestore rules' own predicate.\n */\nexport function crmRecordWritableBy(\n record: Readonly<Record<string, unknown>>,\n tokens: readonly string[],\n): boolean {\n const writeTo = record[CRM_WRITE_SCOPE_FIELD]\n const scope = Array.isArray(writeTo) ? writeTo : record['visibleTo']\n if (!Array.isArray(scope) || !tokens.length) return false\n return scope.some((token) => tokens.includes(token as string))\n}\n\n/** How a record reached a site that sees it only through a share. */\nexport interface CrmShareChip {\n source: CrmShareSource\n access: CrmShareAccess\n /** The member who shared it by hand. */\n byName?: string\n /** The rule that shared it. */\n ruleId?: string\n /** Shared with every site. */\n allSites: boolean\n}\n\n/**\n * Why a site seeing this record sees it, when the answer is a share — or\n * `null` when the site holds it (or cannot see it at all). `hostIds` are\n * the viewing group's sites; at the organization level there is none.\n */\nexport function crmShareChipFor(\n record: Readonly<Record<string, unknown>> | null | undefined,\n hostIds: readonly string[],\n): CrmShareChip | null {\n if (!record || !hostIds.length) return null\n const held = recordHeldScope(record)\n if (held.includes(ORG_SCOPE_TOKEN)) return null\n const viewing = new Set(hostIds.map(hostScopeToken))\n if (held.some((token) => viewing.has(token))) return null\n const grants = Object.values(readRecordGrants(record))\n const reaching = grants.filter((grant) =>\n grant.tokens.some((token) => token === ORG_SCOPE_TOKEN || viewing.has(token)),\n )\n if (!reaching.length) return null\n // A hand share names a person, so it leads; an edit grant outranks read.\n const pick =\n reaching.find((grant) => grant.source === 'manual' && grant.access === 'edit') ??\n reaching.find((grant) => grant.source === 'manual') ??\n reaching.find((grant) => grant.access === 'edit') ??\n reaching[0]\n return {\n source: pick.source,\n access: reaching.some((grant) => grant.access === 'edit') ? 'edit' : 'read',\n ...(pick.byName ? { byName: pick.byName } : {}),\n ...(pick.ruleId ? { ruleId: pick.ruleId } : {}),\n allSites: pick.tokens.includes(ORG_SCOPE_TOKEN),\n }\n}\n\n/** The chip's words: \"Shared by Dana\", \"Shared by rule Brand A leads\". */\nexport function crmShareChipLabel(\n chip: CrmShareChip,\n rules: readonly Pick<CrmSharingRule, 'id' | 'name'>[] = [],\n): string {\n if (chip.source === 'manual') return chip.byName ? `Shared by ${chip.byName}` : 'Shared by hand'\n const rule = rules.find((entry) => entry.id === chip.ruleId)\n return rule ? `Shared by rule ${rule.name}` : 'Shared by a rule'\n}\n\n/** One line of a record's Sharing card. */\nexport type CrmSharingLine =\n | { kind: 'held'; hostIds: string[]; allSites: boolean }\n | {\n kind: 'manual'\n key: string\n token: ScopeToken\n access: CrmShareAccess\n byName?: string\n atMs: number\n }\n | { kind: 'rule'; key: string; ruleId: string; tokens: ScopeToken[]; access: CrmShareAccess; atMs: number }\n\n/**\n * Where a record is visible and why: the sites that hold it (its consent\n * group or its creating site), then one line per hand share, then one per\n * rule. The held line is never unsharable; a hand share is, in one click.\n */\nexport function describeRecordSharing(\n record: Readonly<Record<string, unknown>> | null | undefined,\n): CrmSharingLine[] {\n if (!record) return []\n const held = recordHeldScope(record)\n const lines: CrmSharingLine[] = [\n { kind: 'held', hostIds: hostIdsFromScope(held), allSites: held.includes(ORG_SCOPE_TOKEN) },\n ]\n const grants = Object.entries(readRecordGrants(record))\n for (const [key, grant] of grants) {\n if (grant.source !== 'manual') continue\n lines.push({\n kind: 'manual',\n key,\n token: grant.tokens[0],\n access: grant.access,\n ...(grant.byName ? { byName: grant.byName } : {}),\n atMs: grant.atMs,\n })\n }\n for (const [key, grant] of grants) {\n if (grant.source !== 'rule' || !grant.ruleId) continue\n lines.push({\n kind: 'rule',\n key,\n ruleId: grant.ruleId,\n tokens: grant.tokens,\n access: grant.access,\n atMs: grant.atMs,\n })\n }\n return lines\n}\n\n/** \"All sites\", a site's name, or \"N sites\". */\nexport function describeShareTargets(\n tokens: readonly string[],\n siteName: (hostId: string) => string,\n): string {\n if (tokens.includes(ORG_SCOPE_TOKEN)) return 'All sites'\n const ids = hostIdsFromScope(tokens)\n if (ids.length === 1) return siteName(ids[0])\n if (ids.length <= 3) return ids.map(siteName).join(', ')\n return `${ids.length} sites`\n}\n\n/** A rule as one sentence of its \"which records\" half. */\nexport function describeSharingRuleScope(\n rule: Pick<CrmSharingRule, 'object' | 'sourceHostIds' | 'criteria'>,\n siteName: (hostId: string) => string,\n labels: { owner?: (uid: string) => string; stage?: (id: string) => string } = {},\n): string {\n const parts: string[] = [CRM_SHARING_OBJECT_LABELS[rule.object].many]\n if (rule.sourceHostIds.length) {\n parts.push(`captured on ${rule.sourceHostIds.map(siteName).join(' or ')}`)\n }\n const { criteria } = rule\n if (criteria.leadSources?.length) parts.push(`from ${criteria.leadSources.join(' or ')}`)\n if (criteria.tags?.length) parts.push(`tagged ${criteria.tags.join(' or ')}`)\n if (criteria.campaignIds?.length) {\n parts.push(`in ${criteria.campaignIds.length === 1 ? 'a campaign' : `${criteria.campaignIds.length} campaigns`}`)\n }\n if (criteria.stages?.length) {\n parts.push(`at ${criteria.stages.map((id) => labels.stage?.(id) ?? id).join(' or ')}`)\n }\n if (criteria.ownerUids?.length) {\n parts.push(`owned by ${criteria.ownerUids.map((uid) => labels.owner?.(uid) ?? uid).join(' or ')}`)\n }\n return parts.join(', ')\n}\n\n/*==========================================\n * THE ROUTE.\n *=========================================*/\n\n/** `POST /api/crm/sharing` — every sharing act, by `action`. */\nexport const CRM_SHARING_ROUTE = 'crm/sharing' as const\n\nexport const crmSharingRouteUrl = (): string => `/api/${CRM_SHARING_ROUTE}`\n\n/** Records one share or unshare may name. */\nexport const CRM_SHARING_IDS_MAX = 200\n\nexport type CrmSharingAction =\n | 'sites'\n | 'share'\n | 'unshare'\n | 'evaluate'\n | 'rule-save'\n | 'rule-delete'\n | 'rule-continue'\n\nexport interface CrmSharingSite {\n id: string\n name: string\n}\n\nexport interface CrmSharingResponse {\n ok: true\n /** `sites`: the org's sites, by name. */\n sites?: CrmSharingSite[]\n /** `share`/`unshare`/`evaluate`: records whose sharing changed. */\n changed?: number\n /** Records named that could not be found. */\n missing?: number\n /** The rule a rule action saved or ran. */\n rule?: CrmSharingRule | null\n /** Whether a rule run has more to do (`rule-continue` again). */\n more?: boolean\n}\n"],"names":["heldScopeTokens","hostIdsFromScope","hostScopeToken","isScopeToken","MAX_SCOPE_HOSTS","ORG_SCOPE_TOKEN","parseScopeToken","SCOPE_GRANTS_FIELD","CRM_SHARING_OBJECTS","isCrmSharingObject","value","includes","CRM_SHARING_OBJECT_LABELS","leads","one","many","contacts","companies","deals","isCrmShareAccess","CRM_SHARING_FIELD","CRM_WRITE_SCOPE_FIELD","CRM_SHARING_RULE_IDS_PATH","manualGrantKey","token","parsed","Error","kind","hostId","ruleGrantKey","ruleId","tokenList","raw","Array","isArray","Set","filter","readGrant","tokens","length","source","byUid","byName","atMs","Number","isFinite","access","readRecordGrants","record","sharing","grants","out","key","Object","entries","grant","recordHolderHostIds","ids","add","captured","id","recordHeldScope","planRecordSharing","held","visibleTo","writeTo","heldSet","heldOrgWide","has","editTokens","ruleIds","sort","a","b","push","added","fromEntries","orgWideHeldBy","sameList","every","entry","at","sameGrant","sharingPlanChanges","plan","storedWrite","stored","undefined","keys","CRM_SHARING_RULES_PATH","CRM_SHARING_RULES_MAX","CRM_SHARING_CRITERION_MAX","CRM_SHARING_CRITERIA_KEYS","CRM_SHARING_CRITERIA_FOR","RULE_ID","stringList","max","String","trim","slice","readCrmSharingRule","test","targets","normalizeShareTargets","rawCriteria","criteria","list","map","tag","toLowerCase","run","name","object","enabled","sourceHostIds","deleting","createdByUid","createdAtMs","updatedAtMs","status","mode","processed","changed","cursor","startedAtMs","finishedAtMs","error","readCrmSharingRules","org","crm","rule","some","existing","activeSharingRules","rules","clean","shareTargetsFrom","input","str","strs","Boolean","crmSharingFacts","facts","hostIds","leadSources","tags","campaignIds","stages","ownerUids","take","stageField","leadSource","stage","owner","facets","facet","values","anyOf","wanted","have","fold","set","crmSharingRuleMatches","evaluateRecordGrants","current","next","before","withManualShares","by","uid","withoutManualShares","drop","crmRecordWritableBy","scope","crmShareChipFor","reaching","viewing","pick","find","allSites","crmShareChipLabel","chip","describeRecordSharing","lines","describeShareTargets","siteName","join","describeSharingRuleScope","labels","parts","CRM_SHARING_ROUTE","crmSharingRouteUrl","CRM_SHARING_IDS_MAX"],"mappings":"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"}
@@ -29,4 +29,4 @@
29
29
  * There is no deals import, so the header is free to read well; it stays
30
30
  * in the table's column order so the file and the screen agree.
31
31
  */
32
- export { csvAmount, csvInstant, DEAL_CSV_COLUMNS, dealsCsv, type DealCsvOptions, type DealCsvRow, } from '@aglyn/aglyn';
32
+ export { csvAmount, csvInstant, DEAL_CSV_COLUMNS, dealsCsv, type DealCsvOptions, type DealCsvRow, } from './crm-csv';
@@ -28,10 +28,9 @@
28
28
  * There is no deals import, so the header is free to read well; it stays
29
29
  * in the table's column order so the file and the screen agree.
30
30
  */ /*
31
- * The columns and the cells moved to `@aglyn/aglyn` under AGL-2662 so the
32
- * server's whole-collection export can write the same file: the console
33
- * app may not import a feature plugin, and one feature must not have two
34
- * file formats. This module keeps its name and its callers.
35
- */ export { csvAmount, csvInstant, DEAL_CSV_COLUMNS, dealsCsv } from "@aglyn/aglyn";
31
+ * The columns and the cells are `crm-csv.ts`, which the server's
32
+ * whole-collection export writes too (AGL-2662): one feature, one file
33
+ * format. This module keeps its name and its callers.
34
+ */ export { csvAmount, csvInstant, DEAL_CSV_COLUMNS, dealsCsv } from "./crm-csv.js";
36
35
 
37
36
  //# sourceMappingURL=deals-csv.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/deals-csv.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 deals CSV — one file from the table's Export button and the bulk\n * bar's (AGL-2621).\n *\n * A deal names its stage, its owner, its contact and its company by id, and\n * an id is nothing a spreadsheet can read: the stage and the pipeline are\n * written by NAME, resolved through the caller's pipelines, the owner by\n * address, and the contact and the company by the names the deal already\n * carries as captions. The amount is written in major units (`1250.00`),\n * with the currency beside it, because a column of cents is a column\n * somebody will sum wrongly.\n *\n * There is no deals import, so the header is free to read well; it stays\n * in the table's column order so the file and the screen agree.\n */\n\n/*\n * The columns and the cells moved to `@aglyn/aglyn` under AGL-2662 so the\n * server's whole-collection export can write the same file: the console\n * app may not import a feature plugin, and one feature must not have two\n * file formats. This module keeps its name and its callers.\n */\nexport {\n csvAmount,\n csvInstant,\n DEAL_CSV_COLUMNS,\n dealsCsv,\n type DealCsvOptions,\n type DealCsvRow,\n} from '@aglyn/aglyn'\n"],"names":["csvAmount","csvInstant","DEAL_CSV_COLUMNS","dealsCsv"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;CAcC,GAED;;;;;CAKC,GACD,SACEA,SAAS,EACTC,UAAU,EACVC,gBAAgB,EAChBC,QAAQ,QAGH,eAAc"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/deals-csv.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 deals CSV — one file from the table's Export button and the bulk\n * bar's (AGL-2621).\n *\n * A deal names its stage, its owner, its contact and its company by id, and\n * an id is nothing a spreadsheet can read: the stage and the pipeline are\n * written by NAME, resolved through the caller's pipelines, the owner by\n * address, and the contact and the company by the names the deal already\n * carries as captions. The amount is written in major units (`1250.00`),\n * with the currency beside it, because a column of cents is a column\n * somebody will sum wrongly.\n *\n * There is no deals import, so the header is free to read well; it stays\n * in the table's column order so the file and the screen agree.\n */\n\n/*\n * The columns and the cells are `crm-csv.ts`, which the server's\n * whole-collection export writes too (AGL-2662): one feature, one file\n * format. This module keeps its name and its callers.\n */\nexport {\n csvAmount,\n csvInstant,\n DEAL_CSV_COLUMNS,\n dealsCsv,\n type DealCsvOptions,\n type DealCsvRow,\n} from './crm-csv'\n"],"names":["csvAmount","csvInstant","DEAL_CSV_COLUMNS","dealsCsv"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;CAcC,GAED;;;;CAIC,GACD,SACEA,SAAS,EACTC,UAAU,EACVC,gBAAgB,EAChBC,QAAQ,QAGH,eAAW"}
@@ -23,7 +23,8 @@ import { type CrmClauseAsked } from './crm-list-query';
23
23
  /**
24
24
  * What the Leads section's `Show` control offers (AGL-2608): the two
25
25
  * aggregate views and each status on its own. `open` is the default — the
26
- * list opens on the work — asked of the query as `status in [new, working]`,
26
+ * list opens on the work — asked of the query as
27
+ * `status in [new, nurturing, working]`,
27
28
  * which every lead answers because every lead writer stores a status, `new`
28
29
  * for one nobody has touched (`crmLeadListFields`, AGL-3321).
29
30
  */
@@ -50,14 +51,14 @@ export declare const LEAD_SOURCE_FILTER_NONE = "\0none";
50
51
  export declare const LEAD_LIST_FILTER_FIELDS: readonly ListFilterField[];
51
52
  /** What each filter field reads as on a chip. */
52
53
  export declare const LEAD_LIST_FILTER_HEADERS: Readonly<Record<string, string>>;
53
- /** The choices for Status: Open (new or working) and each status on its own. */
54
+ /** The choices for Status: Open (new, nurturing or working) and each status on its own. */
54
55
  export declare const LEAD_STATUS_FILTER_OPTIONS: {
55
- value: "new" | "working" | "qualified" | "unqualified" | "open";
56
+ value: "new" | "nurturing" | "working" | "qualified" | "unqualified" | "open";
56
57
  label: string;
57
58
  }[];
58
59
  /** The choices for Email: every verdict, plus the two aggregates. */
59
60
  export declare const LEAD_EMAIL_FILTER_OPTIONS: {
60
- value: "none" | "bounced" | "complained" | "ok" | "blocked" | "undeliverable" | "unsubscribed" | "do_not_contact" | "problem";
61
+ value: "none" | "ok" | "bounced" | "complained" | "blocked" | "undeliverable" | "unsubscribed" | "do_not_contact" | "problem";
61
62
  label: string;
62
63
  }[];
63
64
  /**
@@ -14,13 +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 { EMAIL_STATE_LABELS, EMAIL_STATE_STATUSES, CRM_LEAD_STATUS_LABELS, CRM_LEAD_SCOPED_CAMPAIGNS_FIELD, CRM_LEAD_STATUSES, crmLeadSourceKey } from "@aglyn/aglyn";
17
+ */ import { EMAIL_STATE_LABELS, EMAIL_STATE_STATUSES, CRM_LEAD_STATUS_LABELS, CRM_LEAD_OPEN_STATUSES, CRM_LEAD_SCOPED_CAMPAIGNS_FIELD, CRM_LEAD_STATUSES, crmLeadSourceKey } from "@aglyn/aglyn";
18
18
  import { SCOPED_SEARCH_JOIN } from "@aglyn/aglyn/app-utils/name-search";
19
19
  import { listSelectCodec } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
20
20
  import { CRM_LIST_SEARCH, crmAnyOf, crmClauseValues, crmSelectField } from "./crm-list-query.js";
21
21
  export const LEAD_FILTERS = [
22
22
  'open',
23
23
  'new',
24
+ 'nurturing',
24
25
  'working',
25
26
  'qualified',
26
27
  'unqualified',
@@ -30,6 +31,7 @@ export const LEAD_FILTER_LABELS = {
30
31
  open: 'Open',
31
32
  all: 'All',
32
33
  new: CRM_LEAD_STATUS_LABELS.new,
34
+ nurturing: CRM_LEAD_STATUS_LABELS.nurturing,
33
35
  working: CRM_LEAD_STATUS_LABELS.working,
34
36
  qualified: CRM_LEAD_STATUS_LABELS.qualified,
35
37
  unqualified: CRM_LEAD_STATUS_LABELS.unqualified
@@ -117,9 +119,9 @@ export const LEAD_EMAIL_FILTER_LABELS = _extends({
117
119
  leadSource: 'Lead source',
118
120
  campaignIds: 'Campaign'
119
121
  };
120
- /** The choices for Status: Open (new or working) and each status on its own. */ export const LEAD_STATUS_FILTER_OPTIONS = LEAD_FILTERS.filter((option)=>option !== 'all').map((option)=>({
122
+ /** The choices for Status: Open (new, nurturing or working) and each status on its own. */ export const LEAD_STATUS_FILTER_OPTIONS = LEAD_FILTERS.filter((option)=>option !== 'all').map((option)=>({
121
123
  value: option,
122
- label: option === 'open' ? 'Open (new or working)' : LEAD_FILTER_LABELS[option]
124
+ label: option === 'open' ? 'Open (new, nurturing or working)' : LEAD_FILTER_LABELS[option]
123
125
  }));
124
126
  /** The choices for Email: every verdict, plus the two aggregates. */ export const LEAD_EMAIL_FILTER_OPTIONS = LEAD_EMAIL_FILTERS.filter((option)=>option !== 'any').map((option)=>({
125
127
  value: option,
@@ -207,7 +209,7 @@ const STATUS_ALL = {
207
209
  * lead writer stores (`crmLeadListFields` in `@aglyn/aglyn`):
208
210
  *
209
211
  * status `status`, stored `new` on a lead nobody has touched, so
210
- * Open is `status in [new, working]`
212
+ * Open is `status in [new, nurturing, working]`
211
213
  * emailState `emailStatus`, the verdict's status or `none`; "Cannot be
212
214
  * emailed" is every verdict but `ok`
213
215
  * leadSource `leadSourceKey`, the label as the picklist compares it, or
@@ -304,7 +306,7 @@ const ORG_READER = {
304
306
  {
305
307
  const statuses = [];
306
308
  for (const value of values){
307
- if (value === 'open') statuses.push('new', 'working');
309
+ if (value === 'open') statuses.push(...CRM_LEAD_OPEN_STATUSES);
308
310
  else if (CRM_LEAD_STATUSES.includes(value)) statuses.push(value);
309
311
  else return {
310
312
  refused: `${value} is not a lead status`
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/lead-filters.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 EMAIL_STATE_LABELS,\n EMAIL_STATE_STATUSES,\n CRM_LEAD_STATUS_LABELS,\n CRM_LEAD_SCOPED_CAMPAIGNS_FIELD,\n CRM_LEAD_STATUSES,\n type EmailStateStatus,\n type CrmLeadStatus,\n crmLeadSourceKey,\n} from '@aglyn/aglyn'\nimport type { CrmViewFilterClause } from '@aglyn/aglyn'\nimport { SCOPED_SEARCH_JOIN } from '@aglyn/aglyn/app-utils/name-search'\nimport type { ListFilterField, ListFilterRequest } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport { type ListGridFilterCodec, listSelectCodec } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport type { ListQueryDeclaration, ListQuerySort } from '@aglyn/shared-ui-jsx/const/list-query-plan'\nimport {\n CRM_LIST_SEARCH,\n type CrmClauseAsked,\n crmAnyOf,\n crmClauseValues,\n crmSelectField,\n} from './crm-list-query'\n\n/**\n * What the Leads section's `Show` control offers (AGL-2608): the two\n * aggregate views and each status on its own. `open` is the default — the\n * list opens on the work — asked of the query as `status in [new, working]`,\n * which every lead answers because every lead writer stores a status, `new`\n * for one nobody has touched (`crmLeadListFields`, AGL-3321).\n */\nexport type LeadFilter = 'open' | 'all' | CrmLeadStatus\n\nexport const LEAD_FILTERS: readonly LeadFilter[] = [\n 'open',\n 'new',\n 'working',\n 'qualified',\n 'unqualified',\n 'all',\n]\n\nexport const LEAD_FILTER_LABELS: Record<LeadFilter, string> = {\n open: 'Open',\n all: 'All',\n new: CRM_LEAD_STATUS_LABELS.new,\n working: CRM_LEAD_STATUS_LABELS.working,\n qualified: CRM_LEAD_STATUS_LABELS.qualified,\n unqualified: CRM_LEAD_STATUS_LABELS.unqualified,\n}\n\n/**\n * The Leads section's `Email` control (AGL-3245): every lead, the ones a\n * member must not email — any verdict but `ok` — the ones nothing has been\n * said about, or one verdict on its own. Beside `Show`, not inside it: a\n * bounce does not move a lead out of Open, and a queue of bounced leads is\n * a queue of people to reach some other way.\n */\nexport type LeadEmailFilter = 'any' | 'problem' | 'none' | EmailStateStatus\n\nexport const LEAD_EMAIL_FILTERS: readonly LeadEmailFilter[] = [\n 'any',\n 'problem',\n ...EMAIL_STATE_STATUSES,\n 'none',\n]\n\nexport const LEAD_EMAIL_FILTER_LABELS: Record<LeadEmailFilter, string> = {\n any: 'Any',\n problem: 'Cannot be emailed',\n none: 'Nothing known',\n ...EMAIL_STATE_LABELS,\n}\n\n/**\n * The `Lead source` control's \"no lead source\" choice (AGL-3298). Held in\n * the saved view as an `isEmpty` clause, never as this string; it is only\n * the select's own value for the choice.\n */\nexport const LEAD_SOURCE_FILTER_NONE = '\\u0000none'\n\n/*==========================================\n * THE LEADS GRID'S FILTERS (AGL-3313).\n *\n * The Show, Email, Campaign and Lead source dropdowns became columns of the\n * grid's own Filters panel. Their clauses keep the shape the dropdowns\n * stored, so every saved view of leads reads as it did:\n *\n * status `equals <filter>`; none at all is Open, `all` is every lead\n * emailState `equals <filter>`\n * campaignIds `contains <campaign id>`\n * leadSource `equals <label>`, or `isEmpty` for the leads holding none\n *\n * Every one is asked of the Leads query (AGL-3321) through the field a\n * writer stores for it — see {@link leadQueryClause}.\n *=========================================*/\n\n/** The fields the grid's panel offers on Leads. */\nexport const LEAD_LIST_FILTER_FIELDS: readonly ListFilterField[] = [\n { column: 'status', kind: 'exact', path: 'status', operators: ['equals', 'isAnyOf'] },\n { column: 'emailState', kind: 'exact', path: 'emailState', operators: ['equals', 'isAnyOf'] },\n { column: 'ownerUid', kind: 'exact', path: 'ownerUid', operators: ['equals', 'isAnyOf'] },\n { column: 'leadSource', kind: 'exact', path: 'leadSource', operators: ['equals', 'isAnyOf'] },\n { column: 'campaignIds', kind: 'exact', path: 'campaignIds', operators: ['equals'] },\n]\n\n/** What each filter field reads as on a chip. */\nexport const LEAD_LIST_FILTER_HEADERS: Readonly<Record<string, string>> = {\n status: 'Status',\n emailState: 'Email',\n ownerUid: 'Owner',\n leadSource: 'Lead source',\n campaignIds: 'Campaign',\n}\n\n/** The choices for Status: Open (new or working) and each status on its own. */\nexport const LEAD_STATUS_FILTER_OPTIONS = LEAD_FILTERS.filter(\n (option) => option !== 'all',\n).map((option) => ({\n value: option,\n label: option === 'open' ? 'Open (new or working)' : LEAD_FILTER_LABELS[option],\n}))\n\n/** The choices for Email: every verdict, plus the two aggregates. */\nexport const LEAD_EMAIL_FILTER_OPTIONS = LEAD_EMAIL_FILTERS.filter(\n (option) => option !== 'any',\n).map((option) => ({ value: option, label: LEAD_EMAIL_FILTER_LABELS[option] }))\n\nconst STATUS_OPEN: CrmViewFilterClause = { field: 'status', op: 'equals', value: 'open' }\nconst STATUS_ALL: CrmViewFilterClause = { field: 'status', op: 'equals', value: 'all' }\n\n/**\n * The clauses the grid shows for a view's stored ones. A stored list with\n * no status clause is Open, which the list opened on before views existed,\n * so it is shown as the clause it means; `status equals all` is every lead\n * and so no clause at all.\n */\nexport function leadClausesForGrid(\n stored: readonly CrmViewFilterClause[],\n): CrmViewFilterClause[] {\n const status = stored.find((clause) => clause.field === 'status')\n if (!status) return [STATUS_OPEN, ...stored]\n if (status.op === 'equals' && status.value === 'all') {\n return stored.filter((clause) => clause.field !== 'status')\n }\n return [...stored]\n}\n\n/** The inverse of {@link leadClausesForGrid}: what the view stores. */\nexport function leadClausesToStore(\n shown: readonly CrmViewFilterClause[],\n): CrmViewFilterClause[] {\n const status = shown.find((clause) => clause.field === 'status')\n if (!status) return [...shown, STATUS_ALL]\n if (status.op === 'equals' && status.value === 'open') {\n return shown.filter((clause) => clause.field !== 'status')\n }\n return [...shown]\n}\n\n/** Campaign clauses were stored as `contains`; the panel's select says `is`. */\nexport const LEAD_CAMPAIGN_CODEC: ListGridFilterCodec = {\n toItem: (clause) =>\n clause.op === 'contains' || clause.op === 'equals'\n ? { operator: 'is', value: clause.value }\n : null,\n toClause: (item) => {\n const clause = listSelectCodec.toClause(item)\n return clause && clause.op === 'equals' ? { ...clause, op: 'contains' } : null\n },\n}\n\n/** \"No lead source\" is an `isEmpty` clause, shown as a choice of the select. */\nexport const LEAD_SOURCE_CODEC: ListGridFilterCodec = {\n toItem: (clause) =>\n clause.op === 'isEmpty'\n ? { operator: 'is', value: LEAD_SOURCE_FILTER_NONE }\n : listSelectCodec.toItem(clause),\n toClause: (item) => {\n const clause = listSelectCodec.toClause(item)\n if (clause?.op === 'equals' && clause.value === LEAD_SOURCE_FILTER_NONE) {\n return { field: clause.field, op: 'isEmpty', value: '' }\n }\n return clause\n },\n}\n\n/** The Leads fields whose stored clauses translate their own way. */\nexport const LEAD_FILTER_CODECS: Readonly<Record<string, ListGridFilterCodec>> = {\n campaignIds: LEAD_CAMPAIGN_CODEC,\n leadSource: LEAD_SOURCE_CODEC,\n}\n\n/*==========================================\n * THE LEADS QUERY (AGL-3321).\n *\n * `orgs/{orgId}/leads`, newest seen first, under a site narrowed by the\n * `visibleTo` scope clause. Each grid clause is asked through a field every\n * lead writer stores (`crmLeadListFields` in `@aglyn/aglyn`):\n *\n * status `status`, stored `new` on a lead nobody has touched, so\n * Open is `status in [new, working]`\n * emailState `emailStatus`, the verdict's status or `none`; \"Cannot be\n * emailed\" is every verdict but `ok`\n * leadSource `leadSourceKey`, the label as the picklist compares it, or\n * `null` for none\n * ownerUid as stored\n * campaignIds `array-contains` on the lead's own campaigns; under a site,\n * `scopedCampaignIds` — the campaign behind each of the\n * site's scope tokens — which stands in the scope clause's\n * place, so the one array clause answers both\n *\n * and the search box reads `searchTokens` — word prefixes of the name,\n * address, company, title and tags. Every equality rides one\n * `(field, lastSeenAtMs DESC)` composite; the campaign and the search are\n * array clauses, so the two do not stand together (nor under a site, where\n * the scope clause is the query's one array clause, save the search, which\n * folds into it for a reader who may drop it).\n *=========================================*/\n\n/** The one order the Leads list takes: newest seen first. */\nexport const LEAD_LIST_SORTS: readonly ListQuerySort[] = [{ path: 'lastSeenAtMs', direction: 'desc' }]\n\n/** The fields the Leads query asks — each a stored field, as named. */\nexport const LEAD_QUERY_FIELDS: readonly ListFilterField[] = [\n crmSelectField('status'),\n crmSelectField('emailStatus'),\n crmSelectField('ownerUid'),\n {\n column: 'leadSourceKey',\n kind: 'exact',\n path: 'leadSourceKey',\n presence: 'nullable',\n operators: ['equals', 'isAnyOf', 'isEmpty'],\n },\n {\n column: 'campaignIds',\n kind: 'exact',\n path: 'campaignIds',\n tokensPath: 'campaignIds',\n operators: ['contains'],\n },\n {\n // A campaign under a site: the lead's campaigns behind its scope.\n column: CRM_LEAD_SCOPED_CAMPAIGNS_FIELD,\n kind: 'exact',\n path: CRM_LEAD_SCOPED_CAMPAIGNS_FIELD,\n tokensPath: CRM_LEAD_SCOPED_CAMPAIGNS_FIELD,\n operators: ['isAnyOf'],\n },\n]\n\nexport const LEAD_LIST_DECLARATION: ListQueryDeclaration = {\n fields: LEAD_QUERY_FIELDS,\n sorts: LEAD_LIST_SORTS,\n search: CRM_LIST_SEARCH,\n}\n\n/**\n * A collaborator's search (see `prefixSearch` in `useCrmListQuery`): the\n * start of the address, beside the scope clause — the `(visibleTo, email)`\n * composite the Inbox's leads list reads too.\n */\nexport const LEAD_PREFIX_SEARCH = {\n field: {\n column: 'email',\n kind: 'text',\n path: 'email',\n lowerPath: 'email',\n presence: 'always',\n operators: ['startsWith'],\n } satisfies ListFilterField,\n notice: 'Search matches the start of a lead’s address for access limited to specific sites.',\n}\n\n/** Whether an asked clause stands in for the scope clause: a campaign behind the site's scope. */\nexport const leadClauseImpliesScope = (clause: ListFilterRequest): boolean =>\n clause.field === CRM_LEAD_SCOPED_CAMPAIGNS_FIELD\n\n/** The verdicts a member must not email to: every one but `ok`. */\nconst EMAIL_PROBLEMS = EMAIL_STATE_STATUSES.filter((status) => status !== 'ok')\n\n/** Who is asking, for the one clause whose shape depends on it: the Campaign. */\nexport interface LeadQueryReader {\n /** The site's scope tokens, or `null` at the organization level. */\n scopeTokens: readonly string[] | null\n /** Whether the reader may run a query without the scope clause (`useCrmFoldsScope`). */\n foldsScope: boolean\n}\n\nconst ORG_READER: LeadQueryReader = { scopeTokens: null, foldsScope: true }\n\n/**\n * One grid clause as the clause the Leads query asks — see the block above.\n * A value the list does not know is refused by name rather than dropped, so\n * a stored view that names a retired choice says so.\n */\nexport function leadQueryClause(\n clause: CrmViewFilterClause | ListFilterRequest,\n reader: LeadQueryReader = ORG_READER,\n): CrmClauseAsked {\n const values = crmClauseValues(clause)\n switch (clause.field) {\n case 'status': {\n const statuses: string[] = []\n for (const value of values) {\n if (value === 'open') statuses.push('new', 'working')\n else if ((CRM_LEAD_STATUSES as readonly string[]).includes(value)) statuses.push(value)\n else return { refused: `${value} is not a lead status` }\n }\n return crmAnyOf('status', statuses)\n }\n case 'emailState': {\n const keys: string[] = []\n for (const value of values) {\n if (value === 'problem') keys.push(...EMAIL_PROBLEMS)\n else if (value === 'none') keys.push('none')\n else if ((EMAIL_STATE_STATUSES as readonly string[]).includes(value)) keys.push(value)\n else return { refused: `${value} is not an email verdict` }\n }\n return crmAnyOf('emailStatus', keys)\n }\n case 'leadSource': {\n if (clause.op === 'isEmpty' || (values.length === 1 && values[0] === LEAD_SOURCE_FILTER_NONE)) {\n return { field: 'leadSourceKey', op: 'isEmpty', value: '' }\n }\n if (values.includes(LEAD_SOURCE_FILTER_NONE)) {\n return { refused: '\"No lead source\" cannot be picked beside a value' }\n }\n const keys = values.map((value) => crmLeadSourceKey(value)).filter((key): key is string => Boolean(key))\n return crmAnyOf('leadSourceKey', keys)\n }\n case 'ownerUid':\n return crmAnyOf('ownerUid', values)\n case 'campaignIds': {\n if (values.length !== 1) return { refused: 'pick one campaign' }\n if (!reader.scopeTokens) return { field: 'campaignIds', op: 'contains', value: values[0] }\n if (!reader.foldsScope) {\n return {\n refused:\n 'your access is limited to specific sites, which this filter cannot be combined with — an organization administrator can use it',\n }\n }\n return {\n field: CRM_LEAD_SCOPED_CAMPAIGNS_FIELD,\n op: 'isAnyOf',\n value: reader.scopeTokens.map((token) => `${token}${SCOPED_SEARCH_JOIN}${values[0]}`).join(','),\n }\n }\n default:\n return { refused: 'this list does not filter by that' }\n }\n}\n\n/**\n * The query shape the Leads list can send beyond its own declaration, for\n * the index pin (AGL-3321): a collaborator's address prefix beside the\n * equalities they may set.\n */\nexport function leadIndexShapes(): ListQueryDeclaration[] {\n const equalities = LEAD_QUERY_FIELDS.filter((field) => !field.tokensPath)\n return [{ fields: [LEAD_PREFIX_SEARCH.field, ...equalities], sorts: [] }]\n}\n"],"names":["EMAIL_STATE_LABELS","EMAIL_STATE_STATUSES","CRM_LEAD_STATUS_LABELS","CRM_LEAD_SCOPED_CAMPAIGNS_FIELD","CRM_LEAD_STATUSES","crmLeadSourceKey","SCOPED_SEARCH_JOIN","listSelectCodec","CRM_LIST_SEARCH","crmAnyOf","crmClauseValues","crmSelectField","LEAD_FILTERS","LEAD_FILTER_LABELS","open","all","new","working","qualified","unqualified","LEAD_EMAIL_FILTERS","LEAD_EMAIL_FILTER_LABELS","any","problem","none","LEAD_SOURCE_FILTER_NONE","LEAD_LIST_FILTER_FIELDS","column","kind","path","operators","LEAD_LIST_FILTER_HEADERS","status","emailState","ownerUid","leadSource","campaignIds","LEAD_STATUS_FILTER_OPTIONS","filter","option","map","value","label","LEAD_EMAIL_FILTER_OPTIONS","STATUS_OPEN","field","op","STATUS_ALL","leadClausesForGrid","stored","find","clause","leadClausesToStore","shown","LEAD_CAMPAIGN_CODEC","toItem","operator","toClause","item","LEAD_SOURCE_CODEC","LEAD_FILTER_CODECS","LEAD_LIST_SORTS","direction","LEAD_QUERY_FIELDS","presence","tokensPath","LEAD_LIST_DECLARATION","fields","sorts","search","LEAD_PREFIX_SEARCH","lowerPath","notice","leadClauseImpliesScope","EMAIL_PROBLEMS","ORG_READER","scopeTokens","foldsScope","leadQueryClause","reader","values","statuses","push","includes","refused","keys","length","key","Boolean","token","join","leadIndexShapes","equalities"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/lead-filters.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 EMAIL_STATE_LABELS,\n EMAIL_STATE_STATUSES,\n CRM_LEAD_STATUS_LABELS,\n CRM_LEAD_OPEN_STATUSES,\n CRM_LEAD_SCOPED_CAMPAIGNS_FIELD,\n CRM_LEAD_STATUSES,\n type EmailStateStatus,\n type CrmLeadStatus,\n crmLeadSourceKey,\n} from '@aglyn/aglyn'\nimport type { CrmViewFilterClause } from '@aglyn/aglyn'\nimport { SCOPED_SEARCH_JOIN } from '@aglyn/aglyn/app-utils/name-search'\nimport type { ListFilterField, ListFilterRequest } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport { type ListGridFilterCodec, listSelectCodec } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport type { ListQueryDeclaration, ListQuerySort } from '@aglyn/shared-ui-jsx/const/list-query-plan'\nimport {\n CRM_LIST_SEARCH,\n type CrmClauseAsked,\n crmAnyOf,\n crmClauseValues,\n crmSelectField,\n} from './crm-list-query'\n\n/**\n * What the Leads section's `Show` control offers (AGL-2608): the two\n * aggregate views and each status on its own. `open` is the default — the\n * list opens on the work — asked of the query as\n * `status in [new, nurturing, working]`,\n * which every lead answers because every lead writer stores a status, `new`\n * for one nobody has touched (`crmLeadListFields`, AGL-3321).\n */\nexport type LeadFilter = 'open' | 'all' | CrmLeadStatus\n\nexport const LEAD_FILTERS: readonly LeadFilter[] = [\n 'open',\n 'new',\n 'nurturing',\n 'working',\n 'qualified',\n 'unqualified',\n 'all',\n]\n\nexport const LEAD_FILTER_LABELS: Record<LeadFilter, string> = {\n open: 'Open',\n all: 'All',\n new: CRM_LEAD_STATUS_LABELS.new,\n nurturing: CRM_LEAD_STATUS_LABELS.nurturing,\n working: CRM_LEAD_STATUS_LABELS.working,\n qualified: CRM_LEAD_STATUS_LABELS.qualified,\n unqualified: CRM_LEAD_STATUS_LABELS.unqualified,\n}\n\n/**\n * The Leads section's `Email` control (AGL-3245): every lead, the ones a\n * member must not email — any verdict but `ok` — the ones nothing has been\n * said about, or one verdict on its own. Beside `Show`, not inside it: a\n * bounce does not move a lead out of Open, and a queue of bounced leads is\n * a queue of people to reach some other way.\n */\nexport type LeadEmailFilter = 'any' | 'problem' | 'none' | EmailStateStatus\n\nexport const LEAD_EMAIL_FILTERS: readonly LeadEmailFilter[] = [\n 'any',\n 'problem',\n ...EMAIL_STATE_STATUSES,\n 'none',\n]\n\nexport const LEAD_EMAIL_FILTER_LABELS: Record<LeadEmailFilter, string> = {\n any: 'Any',\n problem: 'Cannot be emailed',\n none: 'Nothing known',\n ...EMAIL_STATE_LABELS,\n}\n\n/**\n * The `Lead source` control's \"no lead source\" choice (AGL-3298). Held in\n * the saved view as an `isEmpty` clause, never as this string; it is only\n * the select's own value for the choice.\n */\nexport const LEAD_SOURCE_FILTER_NONE = '\\u0000none'\n\n/*==========================================\n * THE LEADS GRID'S FILTERS (AGL-3313).\n *\n * The Show, Email, Campaign and Lead source dropdowns became columns of the\n * grid's own Filters panel. Their clauses keep the shape the dropdowns\n * stored, so every saved view of leads reads as it did:\n *\n * status `equals <filter>`; none at all is Open, `all` is every lead\n * emailState `equals <filter>`\n * campaignIds `contains <campaign id>`\n * leadSource `equals <label>`, or `isEmpty` for the leads holding none\n *\n * Every one is asked of the Leads query (AGL-3321) through the field a\n * writer stores for it — see {@link leadQueryClause}.\n *=========================================*/\n\n/** The fields the grid's panel offers on Leads. */\nexport const LEAD_LIST_FILTER_FIELDS: readonly ListFilterField[] = [\n { column: 'status', kind: 'exact', path: 'status', operators: ['equals', 'isAnyOf'] },\n { column: 'emailState', kind: 'exact', path: 'emailState', operators: ['equals', 'isAnyOf'] },\n { column: 'ownerUid', kind: 'exact', path: 'ownerUid', operators: ['equals', 'isAnyOf'] },\n { column: 'leadSource', kind: 'exact', path: 'leadSource', operators: ['equals', 'isAnyOf'] },\n { column: 'campaignIds', kind: 'exact', path: 'campaignIds', operators: ['equals'] },\n]\n\n/** What each filter field reads as on a chip. */\nexport const LEAD_LIST_FILTER_HEADERS: Readonly<Record<string, string>> = {\n status: 'Status',\n emailState: 'Email',\n ownerUid: 'Owner',\n leadSource: 'Lead source',\n campaignIds: 'Campaign',\n}\n\n/** The choices for Status: Open (new, nurturing or working) and each status on its own. */\nexport const LEAD_STATUS_FILTER_OPTIONS = LEAD_FILTERS.filter(\n (option) => option !== 'all',\n).map((option) => ({\n value: option,\n label: option === 'open' ? 'Open (new, nurturing or working)' : LEAD_FILTER_LABELS[option],\n}))\n\n/** The choices for Email: every verdict, plus the two aggregates. */\nexport const LEAD_EMAIL_FILTER_OPTIONS = LEAD_EMAIL_FILTERS.filter(\n (option) => option !== 'any',\n).map((option) => ({ value: option, label: LEAD_EMAIL_FILTER_LABELS[option] }))\n\nconst STATUS_OPEN: CrmViewFilterClause = { field: 'status', op: 'equals', value: 'open' }\nconst STATUS_ALL: CrmViewFilterClause = { field: 'status', op: 'equals', value: 'all' }\n\n/**\n * The clauses the grid shows for a view's stored ones. A stored list with\n * no status clause is Open, which the list opened on before views existed,\n * so it is shown as the clause it means; `status equals all` is every lead\n * and so no clause at all.\n */\nexport function leadClausesForGrid(\n stored: readonly CrmViewFilterClause[],\n): CrmViewFilterClause[] {\n const status = stored.find((clause) => clause.field === 'status')\n if (!status) return [STATUS_OPEN, ...stored]\n if (status.op === 'equals' && status.value === 'all') {\n return stored.filter((clause) => clause.field !== 'status')\n }\n return [...stored]\n}\n\n/** The inverse of {@link leadClausesForGrid}: what the view stores. */\nexport function leadClausesToStore(\n shown: readonly CrmViewFilterClause[],\n): CrmViewFilterClause[] {\n const status = shown.find((clause) => clause.field === 'status')\n if (!status) return [...shown, STATUS_ALL]\n if (status.op === 'equals' && status.value === 'open') {\n return shown.filter((clause) => clause.field !== 'status')\n }\n return [...shown]\n}\n\n/** Campaign clauses were stored as `contains`; the panel's select says `is`. */\nexport const LEAD_CAMPAIGN_CODEC: ListGridFilterCodec = {\n toItem: (clause) =>\n clause.op === 'contains' || clause.op === 'equals'\n ? { operator: 'is', value: clause.value }\n : null,\n toClause: (item) => {\n const clause = listSelectCodec.toClause(item)\n return clause && clause.op === 'equals' ? { ...clause, op: 'contains' } : null\n },\n}\n\n/** \"No lead source\" is an `isEmpty` clause, shown as a choice of the select. */\nexport const LEAD_SOURCE_CODEC: ListGridFilterCodec = {\n toItem: (clause) =>\n clause.op === 'isEmpty'\n ? { operator: 'is', value: LEAD_SOURCE_FILTER_NONE }\n : listSelectCodec.toItem(clause),\n toClause: (item) => {\n const clause = listSelectCodec.toClause(item)\n if (clause?.op === 'equals' && clause.value === LEAD_SOURCE_FILTER_NONE) {\n return { field: clause.field, op: 'isEmpty', value: '' }\n }\n return clause\n },\n}\n\n/** The Leads fields whose stored clauses translate their own way. */\nexport const LEAD_FILTER_CODECS: Readonly<Record<string, ListGridFilterCodec>> = {\n campaignIds: LEAD_CAMPAIGN_CODEC,\n leadSource: LEAD_SOURCE_CODEC,\n}\n\n/*==========================================\n * THE LEADS QUERY (AGL-3321).\n *\n * `orgs/{orgId}/leads`, newest seen first, under a site narrowed by the\n * `visibleTo` scope clause. Each grid clause is asked through a field every\n * lead writer stores (`crmLeadListFields` in `@aglyn/aglyn`):\n *\n * status `status`, stored `new` on a lead nobody has touched, so\n * Open is `status in [new, nurturing, working]`\n * emailState `emailStatus`, the verdict's status or `none`; \"Cannot be\n * emailed\" is every verdict but `ok`\n * leadSource `leadSourceKey`, the label as the picklist compares it, or\n * `null` for none\n * ownerUid as stored\n * campaignIds `array-contains` on the lead's own campaigns; under a site,\n * `scopedCampaignIds` — the campaign behind each of the\n * site's scope tokens — which stands in the scope clause's\n * place, so the one array clause answers both\n *\n * and the search box reads `searchTokens` — word prefixes of the name,\n * address, company, title and tags. Every equality rides one\n * `(field, lastSeenAtMs DESC)` composite; the campaign and the search are\n * array clauses, so the two do not stand together (nor under a site, where\n * the scope clause is the query's one array clause, save the search, which\n * folds into it for a reader who may drop it).\n *=========================================*/\n\n/** The one order the Leads list takes: newest seen first. */\nexport const LEAD_LIST_SORTS: readonly ListQuerySort[] = [{ path: 'lastSeenAtMs', direction: 'desc' }]\n\n/** The fields the Leads query asks — each a stored field, as named. */\nexport const LEAD_QUERY_FIELDS: readonly ListFilterField[] = [\n crmSelectField('status'),\n crmSelectField('emailStatus'),\n crmSelectField('ownerUid'),\n {\n column: 'leadSourceKey',\n kind: 'exact',\n path: 'leadSourceKey',\n presence: 'nullable',\n operators: ['equals', 'isAnyOf', 'isEmpty'],\n },\n {\n column: 'campaignIds',\n kind: 'exact',\n path: 'campaignIds',\n tokensPath: 'campaignIds',\n operators: ['contains'],\n },\n {\n // A campaign under a site: the lead's campaigns behind its scope.\n column: CRM_LEAD_SCOPED_CAMPAIGNS_FIELD,\n kind: 'exact',\n path: CRM_LEAD_SCOPED_CAMPAIGNS_FIELD,\n tokensPath: CRM_LEAD_SCOPED_CAMPAIGNS_FIELD,\n operators: ['isAnyOf'],\n },\n]\n\nexport const LEAD_LIST_DECLARATION: ListQueryDeclaration = {\n fields: LEAD_QUERY_FIELDS,\n sorts: LEAD_LIST_SORTS,\n search: CRM_LIST_SEARCH,\n}\n\n/**\n * A collaborator's search (see `prefixSearch` in `useCrmListQuery`): the\n * start of the address, beside the scope clause — the `(visibleTo, email)`\n * composite the Inbox's leads list reads too.\n */\nexport const LEAD_PREFIX_SEARCH = {\n field: {\n column: 'email',\n kind: 'text',\n path: 'email',\n lowerPath: 'email',\n presence: 'always',\n operators: ['startsWith'],\n } satisfies ListFilterField,\n notice: 'Search matches the start of a lead’s address for access limited to specific sites.',\n}\n\n/** Whether an asked clause stands in for the scope clause: a campaign behind the site's scope. */\nexport const leadClauseImpliesScope = (clause: ListFilterRequest): boolean =>\n clause.field === CRM_LEAD_SCOPED_CAMPAIGNS_FIELD\n\n/** The verdicts a member must not email to: every one but `ok`. */\nconst EMAIL_PROBLEMS = EMAIL_STATE_STATUSES.filter((status) => status !== 'ok')\n\n/** Who is asking, for the one clause whose shape depends on it: the Campaign. */\nexport interface LeadQueryReader {\n /** The site's scope tokens, or `null` at the organization level. */\n scopeTokens: readonly string[] | null\n /** Whether the reader may run a query without the scope clause (`useCrmFoldsScope`). */\n foldsScope: boolean\n}\n\nconst ORG_READER: LeadQueryReader = { scopeTokens: null, foldsScope: true }\n\n/**\n * One grid clause as the clause the Leads query asks — see the block above.\n * A value the list does not know is refused by name rather than dropped, so\n * a stored view that names a retired choice says so.\n */\nexport function leadQueryClause(\n clause: CrmViewFilterClause | ListFilterRequest,\n reader: LeadQueryReader = ORG_READER,\n): CrmClauseAsked {\n const values = crmClauseValues(clause)\n switch (clause.field) {\n case 'status': {\n const statuses: string[] = []\n for (const value of values) {\n if (value === 'open') statuses.push(...CRM_LEAD_OPEN_STATUSES)\n else if ((CRM_LEAD_STATUSES as readonly string[]).includes(value)) statuses.push(value)\n else return { refused: `${value} is not a lead status` }\n }\n return crmAnyOf('status', statuses)\n }\n case 'emailState': {\n const keys: string[] = []\n for (const value of values) {\n if (value === 'problem') keys.push(...EMAIL_PROBLEMS)\n else if (value === 'none') keys.push('none')\n else if ((EMAIL_STATE_STATUSES as readonly string[]).includes(value)) keys.push(value)\n else return { refused: `${value} is not an email verdict` }\n }\n return crmAnyOf('emailStatus', keys)\n }\n case 'leadSource': {\n if (clause.op === 'isEmpty' || (values.length === 1 && values[0] === LEAD_SOURCE_FILTER_NONE)) {\n return { field: 'leadSourceKey', op: 'isEmpty', value: '' }\n }\n if (values.includes(LEAD_SOURCE_FILTER_NONE)) {\n return { refused: '\"No lead source\" cannot be picked beside a value' }\n }\n const keys = values.map((value) => crmLeadSourceKey(value)).filter((key): key is string => Boolean(key))\n return crmAnyOf('leadSourceKey', keys)\n }\n case 'ownerUid':\n return crmAnyOf('ownerUid', values)\n case 'campaignIds': {\n if (values.length !== 1) return { refused: 'pick one campaign' }\n if (!reader.scopeTokens) return { field: 'campaignIds', op: 'contains', value: values[0] }\n if (!reader.foldsScope) {\n return {\n refused:\n 'your access is limited to specific sites, which this filter cannot be combined with — an organization administrator can use it',\n }\n }\n return {\n field: CRM_LEAD_SCOPED_CAMPAIGNS_FIELD,\n op: 'isAnyOf',\n value: reader.scopeTokens.map((token) => `${token}${SCOPED_SEARCH_JOIN}${values[0]}`).join(','),\n }\n }\n default:\n return { refused: 'this list does not filter by that' }\n }\n}\n\n/**\n * The query shape the Leads list can send beyond its own declaration, for\n * the index pin (AGL-3321): a collaborator's address prefix beside the\n * equalities they may set.\n */\nexport function leadIndexShapes(): ListQueryDeclaration[] {\n const equalities = LEAD_QUERY_FIELDS.filter((field) => !field.tokensPath)\n return [{ fields: [LEAD_PREFIX_SEARCH.field, ...equalities], sorts: [] }]\n}\n"],"names":["EMAIL_STATE_LABELS","EMAIL_STATE_STATUSES","CRM_LEAD_STATUS_LABELS","CRM_LEAD_OPEN_STATUSES","CRM_LEAD_SCOPED_CAMPAIGNS_FIELD","CRM_LEAD_STATUSES","crmLeadSourceKey","SCOPED_SEARCH_JOIN","listSelectCodec","CRM_LIST_SEARCH","crmAnyOf","crmClauseValues","crmSelectField","LEAD_FILTERS","LEAD_FILTER_LABELS","open","all","new","nurturing","working","qualified","unqualified","LEAD_EMAIL_FILTERS","LEAD_EMAIL_FILTER_LABELS","any","problem","none","LEAD_SOURCE_FILTER_NONE","LEAD_LIST_FILTER_FIELDS","column","kind","path","operators","LEAD_LIST_FILTER_HEADERS","status","emailState","ownerUid","leadSource","campaignIds","LEAD_STATUS_FILTER_OPTIONS","filter","option","map","value","label","LEAD_EMAIL_FILTER_OPTIONS","STATUS_OPEN","field","op","STATUS_ALL","leadClausesForGrid","stored","find","clause","leadClausesToStore","shown","LEAD_CAMPAIGN_CODEC","toItem","operator","toClause","item","LEAD_SOURCE_CODEC","LEAD_FILTER_CODECS","LEAD_LIST_SORTS","direction","LEAD_QUERY_FIELDS","presence","tokensPath","LEAD_LIST_DECLARATION","fields","sorts","search","LEAD_PREFIX_SEARCH","lowerPath","notice","leadClauseImpliesScope","EMAIL_PROBLEMS","ORG_READER","scopeTokens","foldsScope","leadQueryClause","reader","values","statuses","push","includes","refused","keys","length","key","Boolean","token","join","leadIndexShapes","equalities"],"mappings":"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"}
@@ -37,4 +37,4 @@
37
37
  * capture door's own record (the sources, the two timestamps, the capture
38
38
  * count) and the conversion's stamp on "Do not import".
39
39
  */
40
- export { LEAD_CSV_COLUMNS, leadCsvHeader, leadsCsv, type LeadCsvOptions, type LeadCsvRow, } from '@aglyn/aglyn';
40
+ export { LEAD_CSV_COLUMNS, leadCsvHeader, leadsCsv, type LeadCsvOptions, type LeadCsvRow, } from './crm-csv';
@@ -35,6 +35,6 @@
35
35
  * name, and the team's status, owner, reason and notes — and leaves the
36
36
  * capture door's own record (the sources, the two timestamps, the capture
37
37
  * count) and the conversion's stamp on "Do not import".
38
- */ /* The writer moved to `@aglyn/aglyn` under AGL-2662 — see `deals-csv.ts`. */ export { LEAD_CSV_COLUMNS, leadCsvHeader, leadsCsv } from "@aglyn/aglyn";
38
+ */ /* The writer is `crm-csv.ts`, shared with the server's export — see `deals-csv.ts`. */ export { LEAD_CSV_COLUMNS, leadCsvHeader, leadsCsv } from "./crm-csv.js";
39
39
 
40
40
  //# sourceMappingURL=leads-csv.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/leads-csv.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 leads CSV — one file from the Leads section's Export button and the\n * bulk bar's (AGL-2662).\n *\n * A lead is a capture the CRM has annotated: what the door wrote (the\n * address, the name, every surface that met the person, when, how often)\n * and what the team wrote on top (a status, an owner, a reason, notes).\n * The file carries both. The status is written by its LABEL and an absent\n * one as `New`, the way the list reads it, and the owner by ADDRESS rather\n * than uid, the way every other CRM file writes one. The sources are joined\n * with `|` as the contacts file joins its multi-valued cells.\n *\n * At the organization level the same file gains a `Site` column, because a\n * row there is some site's lead and the file must say which; under a site\n * every row is the site's own and the column is not written.\n *\n * The header is written to READ well rather than to round-trip, and the\n * import (`lead-import-drawer.tsx`) is built to that header rather than the\n * other way round: it maps the columns a file may set — the address, the\n * name, and the team's status, owner, reason and notes — and leaves the\n * capture door's own record (the sources, the two timestamps, the capture\n * count) and the conversion's stamp on \"Do not import\".\n */\n\n/* The writer moved to `@aglyn/aglyn` under AGL-2662 — see `deals-csv.ts`. */\nexport {\n LEAD_CSV_COLUMNS,\n leadCsvHeader,\n leadsCsv,\n type LeadCsvOptions,\n type LeadCsvRow,\n} from '@aglyn/aglyn'\n"],"names":["LEAD_CSV_COLUMNS","leadCsvHeader","leadsCsv"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;CAsBC,GAED,2EAA2E,GAC3E,SACEA,gBAAgB,EAChBC,aAAa,EACbC,QAAQ,QAGH,eAAc"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/leads-csv.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 leads CSV — one file from the Leads section's Export button and the\n * bulk bar's (AGL-2662).\n *\n * A lead is a capture the CRM has annotated: what the door wrote (the\n * address, the name, every surface that met the person, when, how often)\n * and what the team wrote on top (a status, an owner, a reason, notes).\n * The file carries both. The status is written by its LABEL and an absent\n * one as `New`, the way the list reads it, and the owner by ADDRESS rather\n * than uid, the way every other CRM file writes one. The sources are joined\n * with `|` as the contacts file joins its multi-valued cells.\n *\n * At the organization level the same file gains a `Site` column, because a\n * row there is some site's lead and the file must say which; under a site\n * every row is the site's own and the column is not written.\n *\n * The header is written to READ well rather than to round-trip, and the\n * import (`lead-import-drawer.tsx`) is built to that header rather than the\n * other way round: it maps the columns a file may set — the address, the\n * name, and the team's status, owner, reason and notes — and leaves the\n * capture door's own record (the sources, the two timestamps, the capture\n * count) and the conversion's stamp on \"Do not import\".\n */\n\n/* The writer is `crm-csv.ts`, shared with the server's export — see `deals-csv.ts`. */\nexport {\n LEAD_CSV_COLUMNS,\n leadCsvHeader,\n leadsCsv,\n type LeadCsvOptions,\n type LeadCsvRow,\n} from './crm-csv'\n"],"names":["LEAD_CSV_COLUMNS","leadCsvHeader","leadsCsv"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;CAsBC,GAED,qFAAqF,GACrF,SACEA,gBAAgB,EAChBC,aAAa,EACbC,QAAQ,QAGH,eAAW"}
@@ -25,4 +25,4 @@
25
25
  * at nine in the assignee's zone — so it is written as an ISO timestamp,
26
26
  * and a spreadsheet renders it in whatever zone it is opened in.
27
27
  */
28
- export { TASK_CSV_COLUMNS, tasksCsv, type TaskCsvOptions, type TaskCsvRow, } from '@aglyn/aglyn';
28
+ export { TASK_CSV_COLUMNS, tasksCsv, type TaskCsvOptions, type TaskCsvRow, } from './crm-csv';
@@ -23,6 +23,6 @@
23
23
  * for the same reason. A due date is an INSTANT — a task due at nine is due
24
24
  * at nine in the assignee's zone — so it is written as an ISO timestamp,
25
25
  * and a spreadsheet renders it in whatever zone it is opened in.
26
- */ /* The writer moved to `@aglyn/aglyn` under AGL-2662 — see `deals-csv.ts`. */ export { TASK_CSV_COLUMNS, tasksCsv } from "@aglyn/aglyn";
26
+ */ /* The writer is `crm-csv.ts`, shared with the server's export — see `deals-csv.ts`. */ export { TASK_CSV_COLUMNS, tasksCsv } from "./crm-csv.js";
27
27
 
28
28
  //# sourceMappingURL=tasks-csv.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/tasks-csv.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 tasks CSV — one file from the section's Export button and the bulk\n * bar's (AGL-2621).\n *\n * A task hangs off a contact, a company or a deal by id; the file names\n * each by the name the list already resolved for the row, so a spreadsheet\n * reads \"Acme\" and not a document id. The assignee is written by address\n * for the same reason. A due date is an INSTANT — a task due at nine is due\n * at nine in the assignee's zone — so it is written as an ISO timestamp,\n * and a spreadsheet renders it in whatever zone it is opened in.\n */\n\n/* The writer moved to `@aglyn/aglyn` under AGL-2662 — see `deals-csv.ts`. */\nexport {\n TASK_CSV_COLUMNS,\n tasksCsv,\n type TaskCsvOptions,\n type TaskCsvRow,\n} from '@aglyn/aglyn'\n"],"names":["TASK_CSV_COLUMNS","tasksCsv"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;CAUC,GAED,2EAA2E,GAC3E,SACEA,gBAAgB,EAChBC,QAAQ,QAGH,eAAc"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/tasks-csv.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 tasks CSV — one file from the section's Export button and the bulk\n * bar's (AGL-2621).\n *\n * A task hangs off a contact, a company or a deal by id; the file names\n * each by the name the list already resolved for the row, so a spreadsheet\n * reads \"Acme\" and not a document id. The assignee is written by address\n * for the same reason. A due date is an INSTANT — a task due at nine is due\n * at nine in the assignee's zone — so it is written as an ISO timestamp,\n * and a spreadsheet renders it in whatever zone it is opened in.\n */\n\n/* The writer is `crm-csv.ts`, shared with the server's export — see `deals-csv.ts`. */\nexport {\n TASK_CSV_COLUMNS,\n tasksCsv,\n type TaskCsvOptions,\n type TaskCsvRow,\n} from './crm-csv'\n"],"names":["TASK_CSV_COLUMNS","tasksCsv"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;CAUC,GAED,qFAAqF,GACrF,SACEA,gBAAgB,EAChBC,QAAQ,QAGH,eAAW"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import type { OrgPlan } from '@aglyn/aglyn/foundation/definitions/org-billing.types';
18
+ import type { PluginPlanEntitlementsDeclaration } from '@aglyn/aglyn/plugin-manager/plugin-plan-entitlements';
19
+ /**
20
+ * THE CRM'S PLAN BAND (AGL-3080): the one-to-one email pace each plan
21
+ * includes. Compiled into core's `PLAN_ENTITLEMENTS` by the manifest
22
+ * generator (`register.planEntitlements`); this is where the figure is
23
+ * written, beside the reasoning that sized it.
24
+ */
25
+ declare module '@aglyn/aglyn/plugin-manager/plugin-entitlement-keys' {
26
+ interface PluginEntitlementQuotas {
27
+ /**
28
+ * One-to-one emails a workspace may send from CRM records per UTC day —
29
+ * the message a rep writes to one person from their contact page, as
30
+ * against a campaign (`emailSendsPerMonth`) or transactional mail.
31
+ *
32
+ * A HARD cap on every tier and a daily one, because it is the only send
33
+ * class a person can produce by hand at volume: a campaign is one act
34
+ * over a metered audience, and a receipt follows an order somebody paid
35
+ * for, but a rep with a template and a list can put a thousand messages
36
+ * onto the platform's sending reputation in an afternoon. The day
37
+ * boundary is what makes the cap a pace rather than a wall — tomorrow the
38
+ * count is zero again — and the number is sized so that every tier holds
39
+ * its margin with the whole day spent, which is the arithmetic the Drive
40
+ * pricing decision of 2026-09-05 records. `checkCrmEmailQuota` reads it;
41
+ * the counter it is enforced against is
42
+ * `orgs/{orgId}/crmEmailUsage/{day}`.
43
+ *
44
+ * Every send still counts on the org's `emailSends` cost meter, like any
45
+ * other message the provider charged for. 0 on Free, which has no CRM,
46
+ * and finite on every paid tier, Enterprise's 2,000 included.
47
+ *
48
+ * ## This ladder and `emailSendsPerMonth` are NOT the same ladder
49
+ *
50
+ * They answer to different principles, deliberately, and a tier's two
51
+ * numbers are not expected to look like each other. `emailSendsPerMonth`
52
+ * is sized against SHARED-DOMAIN REPUTATION: campaign mail is bulk on a
53
+ * pool every tier without its own verified sending domain rides under
54
+ * `p=reject`, so what one workspace sends is a cost every other workspace
55
+ * pays. This one is sized against COGS SHARE: one-to-one mail is a
56
+ * per-tier margin question, which is why it is a daily pace rather than a
57
+ * monthly band.
58
+ *
59
+ * The two are far apart at the entry tier — a band of 0 beside a real
60
+ * daily allowance — and that reads as an inconsistency until the
61
+ * principles are named. It is not one. One-to-one mail is addressed to a
62
+ * single person, usually inside a relationship the recipient started, and
63
+ * it reaches the provider through a per-user pace limit, both suppression
64
+ * lists, a `declined` consent basis as a hard stop, and an atomic daily
65
+ * reservation. Complaint rates on it are structurally unlike bulk.
66
+ * Reconciling the two ladders would price a reply to one customer as
67
+ * though it carried a campaign's reputation risk.
68
+ */
69
+ crmEmailsPerDay?: number;
70
+ }
71
+ }
72
+ /** The one-to-one emails each plan may send a UTC day — see the key's docblock. */
73
+ export declare const CRM_EMAILS_PER_DAY_BY_PLAN: Readonly<Record<OrgPlan, number>>;
74
+ /** The CRM's plan figures, for the manifest generator. */
75
+ export declare function crmPlanEntitlements(): PluginPlanEntitlementsDeclaration;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ // Types only: the manifest generator loads this module on its own to compile
17
+ // the plan figures, so nothing here may reach a core module at runtime.
18
+ /** The one-to-one emails each plan may send a UTC day — see the key's docblock. */ export const CRM_EMAILS_PER_DAY_BY_PLAN = {
19
+ // No one-to-one email on Free, which has no CRM: the send lives on a
20
+ // record's page, and Free opens no record. Zero here is what
21
+ // `checkCrmEmailQuota` refuses against, so a per-org grant of
22
+ // `features.crm` alone still sends nothing until the band is raised with
23
+ // it.
24
+ free: 0,
25
+ // One-to-one email from a CRM record, per UTC day (AGL-2611). A hard daily
26
+ // pace rather than a monthly meter, sized so the whole day spent at $0.0009
27
+ // a message leaves the tier's CRM axis inside the 20% cost share the Drive
28
+ // pricing decision of 2026-09-05 sets. 50 a day is $1.35 a month against a
29
+ // $16 annual price. Every send still lands on the `emailSends` cost meter
30
+ // beside the transactional mail this tier already sends.
31
+ starter: 50,
32
+ pro: 150,
33
+ business: 200,
34
+ scale: 300,
35
+ advanced: 500,
36
+ agency: 1000,
37
+ // Finite like the campaign allowance beside it, and for the same reason
38
+ // one-to-one mail is capped on every other tier: a rep with a template and
39
+ // a list can put thousands of messages onto the platform's sending
40
+ // reputation in an afternoon. A negotiated agreement whose reps need more
41
+ // than 2,000 a day says so, and the override is what carries it.
42
+ enterprise: 2000
43
+ };
44
+ /** The CRM's plan figures, for the manifest generator. */ export function crmPlanEntitlements() {
45
+ return {
46
+ quotas: [
47
+ {
48
+ key: 'crmEmailsPerDay',
49
+ label: 'One-to-one emails / day',
50
+ byPlan: CRM_EMAILS_PER_DAY_BY_PLAN
51
+ }
52
+ ]
53
+ };
54
+ }
55
+
56
+ //# sourceMappingURL=plan-entitlements.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/plugins/crm/src/lib/plan-entitlements.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// Types only: the manifest generator loads this module on its own to compile\n// the plan figures, so nothing here may reach a core module at runtime.\nimport type { OrgPlan } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport type { PluginPlanEntitlementsDeclaration } from '@aglyn/aglyn/plugin-manager/plugin-plan-entitlements'\n\n/**\n * THE CRM'S PLAN BAND (AGL-3080): the one-to-one email pace each plan\n * includes. Compiled into core's `PLAN_ENTITLEMENTS` by the manifest\n * generator (`register.planEntitlements`); this is where the figure is\n * written, beside the reasoning that sized it.\n */\ndeclare module '@aglyn/aglyn/plugin-manager/plugin-entitlement-keys' {\n interface PluginEntitlementQuotas {\n /**\n * One-to-one emails a workspace may send from CRM records per UTC day —\n * the message a rep writes to one person from their contact page, as\n * against a campaign (`emailSendsPerMonth`) or transactional mail.\n *\n * A HARD cap on every tier and a daily one, because it is the only send\n * class a person can produce by hand at volume: a campaign is one act\n * over a metered audience, and a receipt follows an order somebody paid\n * for, but a rep with a template and a list can put a thousand messages\n * onto the platform's sending reputation in an afternoon. The day\n * boundary is what makes the cap a pace rather than a wall — tomorrow the\n * count is zero again — and the number is sized so that every tier holds\n * its margin with the whole day spent, which is the arithmetic the Drive\n * pricing decision of 2026-09-05 records. `checkCrmEmailQuota` reads it;\n * the counter it is enforced against is\n * `orgs/{orgId}/crmEmailUsage/{day}`.\n *\n * Every send still counts on the org's `emailSends` cost meter, like any\n * other message the provider charged for. 0 on Free, which has no CRM,\n * and finite on every paid tier, Enterprise's 2,000 included.\n *\n * ## This ladder and `emailSendsPerMonth` are NOT the same ladder\n *\n * They answer to different principles, deliberately, and a tier's two\n * numbers are not expected to look like each other. `emailSendsPerMonth`\n * is sized against SHARED-DOMAIN REPUTATION: campaign mail is bulk on a\n * pool every tier without its own verified sending domain rides under\n * `p=reject`, so what one workspace sends is a cost every other workspace\n * pays. This one is sized against COGS SHARE: one-to-one mail is a\n * per-tier margin question, which is why it is a daily pace rather than a\n * monthly band.\n *\n * The two are far apart at the entry tier — a band of 0 beside a real\n * daily allowance — and that reads as an inconsistency until the\n * principles are named. It is not one. One-to-one mail is addressed to a\n * single person, usually inside a relationship the recipient started, and\n * it reaches the provider through a per-user pace limit, both suppression\n * lists, a `declined` consent basis as a hard stop, and an atomic daily\n * reservation. Complaint rates on it are structurally unlike bulk.\n * Reconciling the two ladders would price a reply to one customer as\n * though it carried a campaign's reputation risk.\n */\n crmEmailsPerDay?: number\n }\n}\n\n/** The one-to-one emails each plan may send a UTC day — see the key's docblock. */\nexport const CRM_EMAILS_PER_DAY_BY_PLAN: Readonly<Record<OrgPlan, number>> = {\n // No one-to-one email on Free, which has no CRM: the send lives on a\n // record's page, and Free opens no record. Zero here is what\n // `checkCrmEmailQuota` refuses against, so a per-org grant of\n // `features.crm` alone still sends nothing until the band is raised with\n // it.\n free: 0,\n // One-to-one email from a CRM record, per UTC day (AGL-2611). A hard daily\n // pace rather than a monthly meter, sized so the whole day spent at $0.0009\n // a message leaves the tier's CRM axis inside the 20% cost share the Drive\n // pricing decision of 2026-09-05 sets. 50 a day is $1.35 a month against a\n // $16 annual price. Every send still lands on the `emailSends` cost meter\n // beside the transactional mail this tier already sends.\n starter: 50,\n pro: 150,\n business: 200,\n scale: 300,\n advanced: 500,\n agency: 1000,\n // Finite like the campaign allowance beside it, and for the same reason\n // one-to-one mail is capped on every other tier: a rep with a template and\n // a list can put thousands of messages onto the platform's sending\n // reputation in an afternoon. A negotiated agreement whose reps need more\n // than 2,000 a day says so, and the override is what carries it.\n enterprise: 2_000,\n}\n\n/** The CRM's plan figures, for the manifest generator. */\nexport function crmPlanEntitlements(): PluginPlanEntitlementsDeclaration {\n return {\n quotas: [\n {\n key: 'crmEmailsPerDay',\n label: 'One-to-one emails / day',\n byPlan: CRM_EMAILS_PER_DAY_BY_PLAN,\n },\n ],\n }\n}\n"],"names":["CRM_EMAILS_PER_DAY_BY_PLAN","free","starter","pro","business","scale","advanced","agency","enterprise","crmPlanEntitlements","quotas","key","label","byPlan"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,6EAA6E;AAC7E,wEAAwE;AA0DxE,iFAAiF,GACjF,OAAO,MAAMA,6BAAgE;IAC3E,qEAAqE;IACrE,6DAA6D;IAC7D,8DAA8D;IAC9D,yEAAyE;IACzE,MAAM;IACNC,MAAM;IACN,2EAA2E;IAC3E,4EAA4E;IAC5E,2EAA2E;IAC3E,2EAA2E;IAC3E,0EAA0E;IAC1E,yDAAyD;IACzDC,SAAS;IACTC,KAAK;IACLC,UAAU;IACVC,OAAO;IACPC,UAAU;IACVC,QAAQ;IACR,wEAAwE;IACxE,2EAA2E;IAC3E,mEAAmE;IACnE,0EAA0E;IAC1E,iEAAiE;IACjEC,YAAY;AACd,EAAC;AAED,wDAAwD,GACxD,OAAO,SAASC;IACd,OAAO;QACLC,QAAQ;YACN;gBACEC,KAAK;gBACLC,OAAO;gBACPC,QAAQb;YACV;SACD;IACH;AACF"}