@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,227 @@
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 CrmLeadFields, type CrmTask } from '@aglyn/aglyn/app-utils/crm';
18
+ /**
19
+ * The zone the digest's "today" is read in when nothing else names one.
20
+ *
21
+ * The schedule fires at 13:00 UTC, which is 08:00 here, so the window this
22
+ * zone draws is the morning the mail arrives in for the people the product
23
+ * is built with. A member elsewhere reads a window that is at most a few
24
+ * hours off their own, which is what a morning summary can tolerate and a
25
+ * "due at 9:00" reminder could not — that reminder is the assignment
26
+ * notification's job, not this one's.
27
+ */
28
+ export declare const CRM_DIGEST_DEFAULT_TIME_ZONE = "America/Chicago";
29
+ /**
30
+ * The zone the digest's "today" is drawn in, and the zone every CRM mail
31
+ * and notification prints a task's time in.
32
+ *
33
+ * `CRM_DIGEST_TIME_ZONE` overrides the default for a self-hosted console
34
+ * whose team wakes up elsewhere; a value `Intl` does not know falls back to
35
+ * the default with a warning rather than throwing the whole run away.
36
+ */
37
+ export declare function digestTimeZone(env?: NodeJS.ProcessEnv): string;
38
+ /** How long a `new` lead may sit untouched before the digest names it. */
39
+ export declare const CRM_DIGEST_LEAD_AGE_MS: number;
40
+ /**
41
+ * The most due tasks one org's sweep reads.
42
+ *
43
+ * One query per org, ordered soonest-due first, so an org with more than this
44
+ * many overdue-or-today tasks digests the oldest five hundred and reports the
45
+ * window as full rather than reading without bound. Five hundred is an org
46
+ * whose problem is not that nobody reminded them.
47
+ */
48
+ export declare const CRM_DIGEST_TASK_CEILING = 500;
49
+ /**
50
+ * The most old leads one site's sweep reads.
51
+ *
52
+ * The same shape the Leads section uses, for the same reason it gives: a
53
+ * lead the capture door wrote carries no `status` field, and Firestore
54
+ * cannot select on a field's absence, so "unworked" is a filter over a
55
+ * loaded window rather than a query. The window is the newest old leads by
56
+ * first sighting, which is where the ones still worth a call are.
57
+ */
58
+ export declare const CRM_DIGEST_LEAD_WINDOW = 200;
59
+ /** How many items one section of the email lists before "and N more". */
60
+ export declare const CRM_DIGEST_LIST_MAX = 10;
61
+ /** Midnight at the start of the calendar day `ms` falls in, in `timeZone`. */
62
+ export declare function startOfDayInZone(ms: number, timeZone: string): number;
63
+ /**
64
+ * Midnight at the start of the NEXT calendar day in `timeZone` — built from
65
+ * the calendar rather than by adding 24 hours, for the reason the tasks
66
+ * section gives: a day a clock change falls in is 23 or 25 hours long.
67
+ */
68
+ export declare function startOfNextDayInZone(ms: number, timeZone: string): number;
69
+ /** `YYYY-MM-DD` of the UTC day `ms` falls in — the digest's idempotence key. */
70
+ export declare function utcDayKey(ms: number): string;
71
+ /** The half-open window `[start of today, start of tomorrow)` in a zone. */
72
+ export interface CrmDigestWindow {
73
+ startMs: number;
74
+ endMs: number;
75
+ }
76
+ export declare function crmDigestWindow(nowMs: number, timeZone: string): CrmDigestWindow;
77
+ /** A due task as the digest carries it: the fields a line and a link need. */
78
+ export interface CrmDigestTask extends Pick<CrmTask, 'title' | 'kind' | 'contactId' | 'companyId' | 'dealId'> {
79
+ id: string;
80
+ dueAtMs: number;
81
+ assigneeUid: string;
82
+ /** The site the task was created on — where its link opens. */
83
+ hostId: string;
84
+ }
85
+ /** An unworked lead as the digest carries it. */
86
+ export interface CrmDigestLead {
87
+ id: string;
88
+ hostId: string;
89
+ email: string;
90
+ name?: string;
91
+ ownerUid?: string;
92
+ firstSeenAtMs: number;
93
+ }
94
+ export interface CrmDigestCounts {
95
+ overdue: number;
96
+ today: number;
97
+ leads: number;
98
+ /**
99
+ * The member's leads automated email is reaching (AGL-3446) — open, but
100
+ * not owed, so a figure the email states and never a reason to send one.
101
+ */
102
+ nurturing: number;
103
+ }
104
+ /** One member's morning. */
105
+ export interface CrmMemberDigest {
106
+ overdue: CrmDigestTask[];
107
+ today: CrmDigestTask[];
108
+ leads: CrmDigestLead[];
109
+ /** Leads in `nurturing`, by the same owner-or-site rule as `leads`. */
110
+ nurturing: CrmDigestLead[];
111
+ }
112
+ /**
113
+ * Where a due task stands against the digest's window. Only tasks due
114
+ * BEFORE the window's end are read at all, so `later` names a task the
115
+ * query should not have returned and the bucketing refuses to count.
116
+ */
117
+ export declare function crmDigestTaskState(dueAtMs: number, window: CrmDigestWindow): 'overdue' | 'today' | 'later';
118
+ /**
119
+ * Whether a lead is one the digest should name: still `new` — which an
120
+ * absent status reads as, because the capture door stamps none — and first
121
+ * seen at least {@link CRM_DIGEST_LEAD_AGE_MS} ago.
122
+ */
123
+ export declare function isUnworkedLead(lead: Pick<CrmLeadFields, 'status'> & {
124
+ firstSeenAtMs?: unknown;
125
+ }, nowMs: number, ageMs?: number): boolean;
126
+ /**
127
+ * Whether a lead is one the digest counts as being nurtured (AGL-3446):
128
+ * a sequence step or a campaign email has reached it and no person has
129
+ * engaged yet. The same age as an unworked lead, so the two figures are
130
+ * read over the same leads — the old ones that would otherwise be owed.
131
+ */
132
+ export declare function isNurturingLead(lead: Pick<CrmLeadFields, 'status'> & {
133
+ firstSeenAtMs?: unknown;
134
+ }, nowMs: number, ageMs?: number): boolean;
135
+ /**
136
+ * Whether a lead is THIS member's to work: theirs by owner, or nobody's on
137
+ * a site they reach. An owned lead is one person's; an unowned one is on
138
+ * everybody's list until somebody takes it, which is what the digest is for.
139
+ */
140
+ export declare function leadIsForMember(lead: Pick<CrmDigestLead, 'ownerUid' | 'hostId'>, member: {
141
+ uid: string;
142
+ reachesHost: (hostId: string) => boolean;
143
+ }): boolean;
144
+ /**
145
+ * Whether the org's plan carries the CRM (AGL-2611's `features.crm`).
146
+ *
147
+ * Read by key rather than through the typed flag map so this module does not
148
+ * depend on the key existing: the flag lands with the CRM's gate, and a
149
+ * plan that has not declared it is a plan that does not include it — a
150
+ * digest about a surface the plan cannot open would be a reminder to pay.
151
+ */
152
+ export declare function crmDigestEntitled(features: Record<string, boolean | undefined> | null | undefined): boolean;
153
+ /**
154
+ * Every member's digest, from what the org's sweep read.
155
+ *
156
+ * A task goes to its assignee and to nobody else — an unassigned task is a
157
+ * deliberate choice in the drawer, and an assignee no longer on the roster
158
+ * is a task nobody owes. A lead goes to its owner, or to every member who
159
+ * reaches the site when it has none. Members with nothing owed are simply
160
+ * absent from the result, which is what makes "per member with open work"
161
+ * cost nothing for everyone else — and a nurturing lead is not owed, so it
162
+ * joins a digest that exists and never starts one.
163
+ */
164
+ export declare function buildMemberDigests(input: {
165
+ tasks: readonly CrmDigestTask[];
166
+ leads: readonly CrmDigestLead[];
167
+ nurturing?: readonly CrmDigestLead[];
168
+ members: ReadonlyArray<{
169
+ uid: string;
170
+ reachesHost: (hostId: string) => boolean;
171
+ }>;
172
+ window: CrmDigestWindow;
173
+ }): Map<string, CrmMemberDigest>;
174
+ export declare function crmDigestCounts(digest: CrmMemberDigest): CrmDigestCounts;
175
+ /**
176
+ * The line that says the nurturing figure (AGL-3446), or empty when there
177
+ * is none: the leads the unworked list leaves out, and why.
178
+ */
179
+ export declare function composeCrmDigestNurturingLine(counts: Pick<CrmDigestCounts, 'nurturing'>): string;
180
+ /**
181
+ * The one line the notification is: "3 tasks due today, 2 overdue, 4
182
+ * unworked leads". Segments that are zero are left out rather than read as
183
+ * "0 overdue" — the line is what is owed, and nothing owed is no segment.
184
+ */
185
+ export declare function composeCrmDigestSummary(counts: Pick<CrmDigestCounts, 'overdue' | 'today' | 'leads'>): string;
186
+ /**
187
+ * The digest's subject. It names the workspace when it has one: the digest
188
+ * is sent per workspace, and a member of two would otherwise get two mails
189
+ * that read the same.
190
+ */
191
+ export declare function composeCrmDigestSubject(counts: Pick<CrmDigestCounts, 'overdue' | 'today' | 'leads'>, workspaceName?: string): string;
192
+ /** What the digest email needs from the route besides the digest itself. */
193
+ export interface CrmDigestEmailInput {
194
+ digest: CrmMemberDigest;
195
+ nowMs: number;
196
+ timeZone: string;
197
+ /** The product name the mail reads as — the org's brand when white-labeled. */
198
+ productName: string;
199
+ /**
200
+ * The workspace the digest is for. The system email shows only the body,
201
+ * so the body names it: a member of two workspaces gets one digest each.
202
+ */
203
+ workspaceName?: string;
204
+ /** The Tasks section, on the site the member's first task belongs to. */
205
+ tasksUrl: string;
206
+ /** The Leads section of a site, for the sites that have unworked leads. */
207
+ leadsUrl: (hostId: string) => string;
208
+ /** Account settings → Notifications, where the digest can be switched off. */
209
+ settingsUrl: string;
210
+ /** A site's name for a lead line; the id when the site is unknown. */
211
+ hostName: (hostId: string) => string;
212
+ /** The brand's support line, already prefixed, or empty. */
213
+ supportLine?: string;
214
+ /**
215
+ * Leave out the closing paragraph and the support line (AGL-3367), for the
216
+ * copy that goes inside a system email: its footer says why the mail came
217
+ * and where to get help, and its body links the settings page.
218
+ */
219
+ omitClosing?: boolean;
220
+ }
221
+ /**
222
+ * The plain-text email. Text rather than a designed template because the
223
+ * body is a list, and `sendEmail` synthesizes the HTML part that makes its
224
+ * links live. Overdue first — it is the section a reader acts on — then
225
+ * today, then the leads, each capped and counted, and the links last.
226
+ */
227
+ export declare function composeCrmDigestEmailText(input: CrmDigestEmailInput): string;
@@ -0,0 +1,336 @@
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
+ * The daily CRM digest (AGL-2619): what one member is owed this morning,
18
+ * computed from the tasks and leads a scheduled route has read.
19
+ *
20
+ * "Overdue" and "due today" are read off the clock at read time by design
21
+ * (AGL-2599) — nothing stamps them — which is why nothing ever told anybody.
22
+ * The digest is the one reader that runs on a schedule: once a day it
23
+ * counts, per org and per member, the open tasks whose due date has passed
24
+ * or falls today and the leads nobody has worked, and says so once, in the
25
+ * console and by mail. Every judgment here is pure so the route that sends
26
+ * and the spec that proves it read the same arithmetic.
27
+ *
28
+ * ## Whose day
29
+ *
30
+ * A task's "today" on screen is the READER's calendar day, from their own
31
+ * clock. A server has no reader, so the digest reads the day in ONE zone —
32
+ * {@link CRM_DIGEST_DEFAULT_TIME_ZONE}, the zone the schedule was chosen for
33
+ * (08:00 there) — taken as a parameter so an org-level setting can supply
34
+ * its own the day one exists. The idempotence key is the UTC day
35
+ * ({@link utcDayKey}), which is a fact about the run and not about anybody's
36
+ * morning.
37
+ */ import { startOfNextZonedDay, startOfZonedDay } from "@aglyn/shared-util-timestamp/zoned-time";
38
+ import { crmLeadStatus } from "@aglyn/aglyn/app-utils/crm";
39
+ /**
40
+ * The zone the digest's "today" is read in when nothing else names one.
41
+ *
42
+ * The schedule fires at 13:00 UTC, which is 08:00 here, so the window this
43
+ * zone draws is the morning the mail arrives in for the people the product
44
+ * is built with. A member elsewhere reads a window that is at most a few
45
+ * hours off their own, which is what a morning summary can tolerate and a
46
+ * "due at 9:00" reminder could not — that reminder is the assignment
47
+ * notification's job, not this one's.
48
+ */ export const CRM_DIGEST_DEFAULT_TIME_ZONE = 'America/Chicago';
49
+ /**
50
+ * The zone the digest's "today" is drawn in, and the zone every CRM mail
51
+ * and notification prints a task's time in.
52
+ *
53
+ * `CRM_DIGEST_TIME_ZONE` overrides the default for a self-hosted console
54
+ * whose team wakes up elsewhere; a value `Intl` does not know falls back to
55
+ * the default with a warning rather than throwing the whole run away.
56
+ */ export function digestTimeZone(env = process.env) {
57
+ var _env_CRM_DIGEST_TIME_ZONE;
58
+ const configured = String((_env_CRM_DIGEST_TIME_ZONE = env['CRM_DIGEST_TIME_ZONE']) != null ? _env_CRM_DIGEST_TIME_ZONE : '').trim();
59
+ if (!configured) return CRM_DIGEST_DEFAULT_TIME_ZONE;
60
+ try {
61
+ new Intl.DateTimeFormat('en-US', {
62
+ timeZone: configured
63
+ });
64
+ return configured;
65
+ } catch (unused) {
66
+ console.warn(`crm-daily-digest: CRM_DIGEST_TIME_ZONE "${configured}" is not a zone — ` + `using ${CRM_DIGEST_DEFAULT_TIME_ZONE}`);
67
+ return CRM_DIGEST_DEFAULT_TIME_ZONE;
68
+ }
69
+ }
70
+ /** How long a `new` lead may sit untouched before the digest names it. */ export const CRM_DIGEST_LEAD_AGE_MS = 2 * 24 * 60 * 60 * 1000;
71
+ /**
72
+ * The most due tasks one org's sweep reads.
73
+ *
74
+ * One query per org, ordered soonest-due first, so an org with more than this
75
+ * many overdue-or-today tasks digests the oldest five hundred and reports the
76
+ * window as full rather than reading without bound. Five hundred is an org
77
+ * whose problem is not that nobody reminded them.
78
+ */ export const CRM_DIGEST_TASK_CEILING = 500;
79
+ /**
80
+ * The most old leads one site's sweep reads.
81
+ *
82
+ * The same shape the Leads section uses, for the same reason it gives: a
83
+ * lead the capture door wrote carries no `status` field, and Firestore
84
+ * cannot select on a field's absence, so "unworked" is a filter over a
85
+ * loaded window rather than a query. The window is the newest old leads by
86
+ * first sighting, which is where the ones still worth a call are.
87
+ */ export const CRM_DIGEST_LEAD_WINDOW = 200;
88
+ /** How many items one section of the email lists before "and N more". */ export const CRM_DIGEST_LIST_MAX = 10;
89
+ /*==========================================
90
+ * CALENDAR DAYS IN A NAMED ZONE
91
+ *
92
+ * The zone math lives in `@aglyn/shared-util-timestamp/zoned-time`, where
93
+ * booking slots and sending windows read it as well; the digest route and
94
+ * its spec call it by the names below.
95
+ *=========================================*/ /** Midnight at the start of the calendar day `ms` falls in, in `timeZone`. */ export function startOfDayInZone(ms, timeZone) {
96
+ return startOfZonedDay(ms, timeZone);
97
+ }
98
+ /**
99
+ * Midnight at the start of the NEXT calendar day in `timeZone` — built from
100
+ * the calendar rather than by adding 24 hours, for the reason the tasks
101
+ * section gives: a day a clock change falls in is 23 or 25 hours long.
102
+ */ export function startOfNextDayInZone(ms, timeZone) {
103
+ return startOfNextZonedDay(ms, timeZone);
104
+ }
105
+ /** `YYYY-MM-DD` of the UTC day `ms` falls in — the digest's idempotence key. */ export function utcDayKey(ms) {
106
+ return new Date(ms).toISOString().slice(0, 10);
107
+ }
108
+ export function crmDigestWindow(nowMs, timeZone) {
109
+ return {
110
+ startMs: startOfDayInZone(nowMs, timeZone),
111
+ endMs: startOfNextDayInZone(nowMs, timeZone)
112
+ };
113
+ }
114
+ /**
115
+ * Where a due task stands against the digest's window. Only tasks due
116
+ * BEFORE the window's end are read at all, so `later` names a task the
117
+ * query should not have returned and the bucketing refuses to count.
118
+ */ export function crmDigestTaskState(dueAtMs, window) {
119
+ if (dueAtMs < window.startMs) return 'overdue';
120
+ if (dueAtMs < window.endMs) return 'today';
121
+ return 'later';
122
+ }
123
+ /**
124
+ * Whether a lead is one the digest should name: still `new` — which an
125
+ * absent status reads as, because the capture door stamps none — and first
126
+ * seen at least {@link CRM_DIGEST_LEAD_AGE_MS} ago.
127
+ */ export function isUnworkedLead(lead, nowMs, ageMs = CRM_DIGEST_LEAD_AGE_MS) {
128
+ if (crmLeadStatus(lead) !== 'new') return false;
129
+ return seenBefore(lead, nowMs - ageMs);
130
+ }
131
+ /**
132
+ * Whether a lead is one the digest counts as being nurtured (AGL-3446):
133
+ * a sequence step or a campaign email has reached it and no person has
134
+ * engaged yet. The same age as an unworked lead, so the two figures are
135
+ * read over the same leads — the old ones that would otherwise be owed.
136
+ */ export function isNurturingLead(lead, nowMs, ageMs = CRM_DIGEST_LEAD_AGE_MS) {
137
+ return crmLeadStatus(lead) === 'nurturing' && seenBefore(lead, nowMs - ageMs);
138
+ }
139
+ function seenBefore(lead, cutoffMs) {
140
+ const firstSeen = Number(lead.firstSeenAtMs);
141
+ return Number.isFinite(firstSeen) && firstSeen > 0 && firstSeen <= cutoffMs;
142
+ }
143
+ /**
144
+ * Whether a lead is THIS member's to work: theirs by owner, or nobody's on
145
+ * a site they reach. An owned lead is one person's; an unowned one is on
146
+ * everybody's list until somebody takes it, which is what the digest is for.
147
+ */ export function leadIsForMember(lead, member) {
148
+ if (lead.ownerUid) return lead.ownerUid === member.uid;
149
+ return member.reachesHost(lead.hostId);
150
+ }
151
+ /**
152
+ * Whether the org's plan carries the CRM (AGL-2611's `features.crm`).
153
+ *
154
+ * Read by key rather than through the typed flag map so this module does not
155
+ * depend on the key existing: the flag lands with the CRM's gate, and a
156
+ * plan that has not declared it is a plan that does not include it — a
157
+ * digest about a surface the plan cannot open would be a reminder to pay.
158
+ */ export function crmDigestEntitled(features) {
159
+ return (features == null ? void 0 : features['crm']) === true;
160
+ }
161
+ /**
162
+ * Every member's digest, from what the org's sweep read.
163
+ *
164
+ * A task goes to its assignee and to nobody else — an unassigned task is a
165
+ * deliberate choice in the drawer, and an assignee no longer on the roster
166
+ * is a task nobody owes. A lead goes to its owner, or to every member who
167
+ * reaches the site when it has none. Members with nothing owed are simply
168
+ * absent from the result, which is what makes "per member with open work"
169
+ * cost nothing for everyone else — and a nurturing lead is not owed, so it
170
+ * joins a digest that exists and never starts one.
171
+ */ export function buildMemberDigests(input) {
172
+ const { tasks, leads, nurturing = [], members, window } = input;
173
+ const digests = new Map();
174
+ const digestFor = (uid)=>{
175
+ let digest = digests.get(uid);
176
+ if (!digest) {
177
+ digest = {
178
+ overdue: [],
179
+ today: [],
180
+ leads: [],
181
+ nurturing: []
182
+ };
183
+ digests.set(uid, digest);
184
+ }
185
+ return digest;
186
+ };
187
+ const roster = new Map(members.map((member)=>[
188
+ member.uid,
189
+ member
190
+ ]));
191
+ for (const task of tasks){
192
+ if (!task.assigneeUid || !roster.has(task.assigneeUid)) continue;
193
+ const state = crmDigestTaskState(task.dueAtMs, window);
194
+ if (state === 'later') continue;
195
+ digestFor(task.assigneeUid)[state].push(task);
196
+ }
197
+ for (const lead of leads){
198
+ for (const member of members){
199
+ if (leadIsForMember(lead, member)) digestFor(member.uid).leads.push(lead);
200
+ }
201
+ }
202
+ for (const lead of nurturing){
203
+ for (const member of members){
204
+ const digest = digests.get(member.uid);
205
+ if (digest && leadIsForMember(lead, member)) digest.nurturing.push(lead);
206
+ }
207
+ }
208
+ for (const digest of digests.values()){
209
+ digest.overdue.sort((a, b)=>a.dueAtMs - b.dueAtMs);
210
+ digest.today.sort((a, b)=>a.dueAtMs - b.dueAtMs);
211
+ digest.leads.sort((a, b)=>a.firstSeenAtMs - b.firstSeenAtMs);
212
+ }
213
+ return digests;
214
+ }
215
+ export function crmDigestCounts(digest) {
216
+ return {
217
+ overdue: digest.overdue.length,
218
+ today: digest.today.length,
219
+ leads: digest.leads.length,
220
+ nurturing: digest.nurturing.length
221
+ };
222
+ }
223
+ /**
224
+ * The line that says the nurturing figure (AGL-3446), or empty when there
225
+ * is none: the leads the unworked list leaves out, and why.
226
+ */ export function composeCrmDigestNurturingLine(counts) {
227
+ if (counts.nurturing <= 0) return '';
228
+ return counts.nurturing === 1 ? '1 lead is in sequences or campaigns.' : `${counts.nurturing} leads are in sequences or campaigns.`;
229
+ }
230
+ const plural = (count, noun)=>`${count} ${noun}${count === 1 ? '' : 's'}`;
231
+ /**
232
+ * The one line the notification is: "3 tasks due today, 2 overdue, 4
233
+ * unworked leads". Segments that are zero are left out rather than read as
234
+ * "0 overdue" — the line is what is owed, and nothing owed is no segment.
235
+ */ export function composeCrmDigestSummary(counts) {
236
+ const segments = [];
237
+ if (counts.today > 0) segments.push(`${plural(counts.today, 'task')} due today`);
238
+ if (counts.overdue > 0) {
239
+ segments.push(counts.today > 0 ? `${counts.overdue} overdue` : `${plural(counts.overdue, 'task')} overdue`);
240
+ }
241
+ if (counts.leads > 0) segments.push(`${plural(counts.leads, 'unworked lead')}`);
242
+ return segments.join(', ');
243
+ }
244
+ /**
245
+ * The digest's subject. It names the workspace when it has one: the digest
246
+ * is sent per workspace, and a member of two would otherwise get two mails
247
+ * that read the same.
248
+ */ export function composeCrmDigestSubject(counts, workspaceName) {
249
+ const workspace = workspaceName == null ? void 0 : workspaceName.trim();
250
+ return `Your ${workspace ? `${workspace} ` : ''}CRM today: ${composeCrmDigestSummary(counts)}`;
251
+ }
252
+ /** A task's due time, with the zone named: it is the digest's, not the reader's. */ function whenInZone(ms, timeZone) {
253
+ return new Date(ms).toLocaleString('en-US', {
254
+ timeZone,
255
+ weekday: 'short',
256
+ month: 'short',
257
+ day: 'numeric',
258
+ hour: 'numeric',
259
+ minute: '2-digit',
260
+ timeZoneName: 'short'
261
+ });
262
+ }
263
+ function dateInZone(ms, timeZone) {
264
+ return new Date(ms).toLocaleDateString('en-US', {
265
+ timeZone,
266
+ month: 'short',
267
+ day: 'numeric'
268
+ });
269
+ }
270
+ function section(heading, lines) {
271
+ const shown = lines.slice(0, CRM_DIGEST_LIST_MAX);
272
+ const rest = lines.length - shown.length;
273
+ return [
274
+ `${heading} (${lines.length})`,
275
+ ...shown.map((line)=>`- ${line}`),
276
+ ...rest > 0 ? [
277
+ ` and ${rest} more`
278
+ ] : [],
279
+ ''
280
+ ];
281
+ }
282
+ /**
283
+ * The plain-text email. Text rather than a designed template because the
284
+ * body is a list, and `sendEmail` synthesizes the HTML part that makes its
285
+ * links live. Overdue first — it is the section a reader acts on — then
286
+ * today, then the leads, each capped and counted, and the links last.
287
+ */ export function composeCrmDigestEmailText(input) {
288
+ var _input_supportLine;
289
+ var _input_workspaceName;
290
+ const { digest, nowMs, timeZone, productName } = input;
291
+ const counts = crmDigestCounts(digest);
292
+ const day = new Date(nowMs).toLocaleDateString('en-US', {
293
+ timeZone,
294
+ weekday: 'long',
295
+ month: 'long',
296
+ day: 'numeric'
297
+ });
298
+ const workspace = (_input_workspaceName = input.workspaceName) == null ? void 0 : _input_workspaceName.trim();
299
+ const lines = [
300
+ `Here is your ${productName} CRM${workspace ? ` in ${workspace}` : ''} for ${day}: ` + `${composeCrmDigestSummary(counts)}.`,
301
+ ''
302
+ ];
303
+ // A task's site, as the lead lines name theirs; an organization task
304
+ // (AGL-2637) has none.
305
+ const taskLine = (task)=>[
306
+ task.title,
307
+ task.hostId ? input.hostName(task.hostId) : '',
308
+ whenInZone(task.dueAtMs, timeZone)
309
+ ].filter(Boolean).join(' · ');
310
+ if (digest.overdue.length) {
311
+ lines.push(...section('Overdue', digest.overdue.map(taskLine)));
312
+ }
313
+ if (digest.today.length) {
314
+ lines.push(...section('Due today', digest.today.map(taskLine)));
315
+ }
316
+ if (digest.leads.length) {
317
+ lines.push(...section('Unworked leads', digest.leads.map((lead)=>{
318
+ const who = lead.name ? `${lead.name} <${lead.email}>` : lead.email;
319
+ return `${who} · ${input.hostName(lead.hostId)} · first seen ${dateInZone(lead.firstSeenAtMs, timeZone)}`;
320
+ })));
321
+ }
322
+ const nurturingLine = composeCrmDigestNurturingLine(counts);
323
+ if (nurturingLine) lines.push(nurturingLine, '');
324
+ if (counts.overdue + counts.today > 0) lines.push(`Open your tasks: ${input.tasksUrl}`);
325
+ const leadHosts = [
326
+ ...new Set(digest.leads.map((lead)=>lead.hostId))
327
+ ].slice(0, 3);
328
+ for (const hostId of leadHosts){
329
+ lines.push(`Leads on ${input.hostName(hostId)}: ${input.leadsUrl(hostId)}`);
330
+ }
331
+ if (input.omitClosing) return lines.join('\n').trimEnd();
332
+ lines.push('', 'You get this each morning because the Daily CRM digest is on in your ' + `notification settings: ${input.settingsUrl}`);
333
+ return lines.join('\n') + ((_input_supportLine = input.supportLine) != null ? _input_supportLine : '');
334
+ }
335
+
336
+ //# sourceMappingURL=crm-digest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/crm-digest.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 daily CRM digest (AGL-2619): what one member is owed this morning,\n * computed from the tasks and leads a scheduled route has read.\n *\n * \"Overdue\" and \"due today\" are read off the clock at read time by design\n * (AGL-2599) — nothing stamps them — which is why nothing ever told anybody.\n * The digest is the one reader that runs on a schedule: once a day it\n * counts, per org and per member, the open tasks whose due date has passed\n * or falls today and the leads nobody has worked, and says so once, in the\n * console and by mail. Every judgment here is pure so the route that sends\n * and the spec that proves it read the same arithmetic.\n *\n * ## Whose day\n *\n * A task's \"today\" on screen is the READER's calendar day, from their own\n * clock. A server has no reader, so the digest reads the day in ONE zone —\n * {@link CRM_DIGEST_DEFAULT_TIME_ZONE}, the zone the schedule was chosen for\n * (08:00 there) — taken as a parameter so an org-level setting can supply\n * its own the day one exists. The idempotence key is the UTC day\n * ({@link utcDayKey}), which is a fact about the run and not about anybody's\n * morning.\n */\n\nimport {\n startOfNextZonedDay,\n startOfZonedDay,\n} from '@aglyn/shared-util-timestamp/zoned-time'\nimport { type CrmLeadFields, type CrmTask, crmLeadStatus } from '@aglyn/aglyn/app-utils/crm'\n\n/**\n * The zone the digest's \"today\" is read in when nothing else names one.\n *\n * The schedule fires at 13:00 UTC, which is 08:00 here, so the window this\n * zone draws is the morning the mail arrives in for the people the product\n * is built with. A member elsewhere reads a window that is at most a few\n * hours off their own, which is what a morning summary can tolerate and a\n * \"due at 9:00\" reminder could not — that reminder is the assignment\n * notification's job, not this one's.\n */\nexport const CRM_DIGEST_DEFAULT_TIME_ZONE = 'America/Chicago'\n\n/**\n * The zone the digest's \"today\" is drawn in, and the zone every CRM mail\n * and notification prints a task's time in.\n *\n * `CRM_DIGEST_TIME_ZONE` overrides the default for a self-hosted console\n * whose team wakes up elsewhere; a value `Intl` does not know falls back to\n * the default with a warning rather than throwing the whole run away.\n */\nexport function digestTimeZone(env: NodeJS.ProcessEnv = process.env): string {\n const configured = String(env['CRM_DIGEST_TIME_ZONE'] ?? '').trim()\n if (!configured) return CRM_DIGEST_DEFAULT_TIME_ZONE\n try {\n new Intl.DateTimeFormat('en-US', { timeZone: configured })\n return configured\n } catch {\n console.warn(\n `crm-daily-digest: CRM_DIGEST_TIME_ZONE \"${configured}\" is not a zone — ` +\n `using ${CRM_DIGEST_DEFAULT_TIME_ZONE}`,\n )\n return CRM_DIGEST_DEFAULT_TIME_ZONE\n }\n}\n\n/** How long a `new` lead may sit untouched before the digest names it. */\nexport const CRM_DIGEST_LEAD_AGE_MS = 2 * 24 * 60 * 60 * 1000\n\n/**\n * The most due tasks one org's sweep reads.\n *\n * One query per org, ordered soonest-due first, so an org with more than this\n * many overdue-or-today tasks digests the oldest five hundred and reports the\n * window as full rather than reading without bound. Five hundred is an org\n * whose problem is not that nobody reminded them.\n */\nexport const CRM_DIGEST_TASK_CEILING = 500\n\n/**\n * The most old leads one site's sweep reads.\n *\n * The same shape the Leads section uses, for the same reason it gives: a\n * lead the capture door wrote carries no `status` field, and Firestore\n * cannot select on a field's absence, so \"unworked\" is a filter over a\n * loaded window rather than a query. The window is the newest old leads by\n * first sighting, which is where the ones still worth a call are.\n */\nexport const CRM_DIGEST_LEAD_WINDOW = 200\n\n/** How many items one section of the email lists before \"and N more\". */\nexport const CRM_DIGEST_LIST_MAX = 10\n\n/*==========================================\n * CALENDAR DAYS IN A NAMED ZONE\n *\n * The zone math lives in `@aglyn/shared-util-timestamp/zoned-time`, where\n * booking slots and sending windows read it as well; the digest route and\n * its spec call it by the names below.\n *=========================================*/\n\n/** Midnight at the start of the calendar day `ms` falls in, in `timeZone`. */\nexport function startOfDayInZone(ms: number, timeZone: string): number {\n return startOfZonedDay(ms, timeZone)\n}\n\n/**\n * Midnight at the start of the NEXT calendar day in `timeZone` — built from\n * the calendar rather than by adding 24 hours, for the reason the tasks\n * section gives: a day a clock change falls in is 23 or 25 hours long.\n */\nexport function startOfNextDayInZone(ms: number, timeZone: string): number {\n return startOfNextZonedDay(ms, timeZone)\n}\n\n/** `YYYY-MM-DD` of the UTC day `ms` falls in — the digest's idempotence key. */\nexport function utcDayKey(ms: number): string {\n return new Date(ms).toISOString().slice(0, 10)\n}\n\n/** The half-open window `[start of today, start of tomorrow)` in a zone. */\nexport interface CrmDigestWindow {\n startMs: number\n endMs: number\n}\n\nexport function crmDigestWindow(nowMs: number, timeZone: string): CrmDigestWindow {\n return {\n startMs: startOfDayInZone(nowMs, timeZone),\n endMs: startOfNextDayInZone(nowMs, timeZone),\n }\n}\n\n/*==========================================\n * WHAT A DIGEST IS MADE OF\n *=========================================*/\n\n/** A due task as the digest carries it: the fields a line and a link need. */\nexport interface CrmDigestTask\n extends Pick<CrmTask, 'title' | 'kind' | 'contactId' | 'companyId' | 'dealId'> {\n id: string\n dueAtMs: number\n assigneeUid: string\n /** The site the task was created on — where its link opens. */\n hostId: string\n}\n\n/** An unworked lead as the digest carries it. */\nexport interface CrmDigestLead {\n id: string\n hostId: string\n email: string\n name?: string\n ownerUid?: string\n firstSeenAtMs: number\n}\n\nexport interface CrmDigestCounts {\n overdue: number\n today: number\n leads: number\n /**\n * The member's leads automated email is reaching (AGL-3446) — open, but\n * not owed, so a figure the email states and never a reason to send one.\n */\n nurturing: number\n}\n\n/** One member's morning. */\nexport interface CrmMemberDigest {\n overdue: CrmDigestTask[]\n today: CrmDigestTask[]\n leads: CrmDigestLead[]\n /** Leads in `nurturing`, by the same owner-or-site rule as `leads`. */\n nurturing: CrmDigestLead[]\n}\n\n/**\n * Where a due task stands against the digest's window. Only tasks due\n * BEFORE the window's end are read at all, so `later` names a task the\n * query should not have returned and the bucketing refuses to count.\n */\nexport function crmDigestTaskState(\n dueAtMs: number,\n window: CrmDigestWindow,\n): 'overdue' | 'today' | 'later' {\n if (dueAtMs < window.startMs) return 'overdue'\n if (dueAtMs < window.endMs) return 'today'\n return 'later'\n}\n\n/**\n * Whether a lead is one the digest should name: still `new` — which an\n * absent status reads as, because the capture door stamps none — and first\n * seen at least {@link CRM_DIGEST_LEAD_AGE_MS} ago.\n */\nexport function isUnworkedLead(\n lead: Pick<CrmLeadFields, 'status'> & { firstSeenAtMs?: unknown },\n nowMs: number,\n ageMs: number = CRM_DIGEST_LEAD_AGE_MS,\n): boolean {\n if (crmLeadStatus(lead) !== 'new') return false\n return seenBefore(lead, nowMs - ageMs)\n}\n\n/**\n * Whether a lead is one the digest counts as being nurtured (AGL-3446):\n * a sequence step or a campaign email has reached it and no person has\n * engaged yet. The same age as an unworked lead, so the two figures are\n * read over the same leads — the old ones that would otherwise be owed.\n */\nexport function isNurturingLead(\n lead: Pick<CrmLeadFields, 'status'> & { firstSeenAtMs?: unknown },\n nowMs: number,\n ageMs: number = CRM_DIGEST_LEAD_AGE_MS,\n): boolean {\n return crmLeadStatus(lead) === 'nurturing' && seenBefore(lead, nowMs - ageMs)\n}\n\nfunction seenBefore(lead: { firstSeenAtMs?: unknown }, cutoffMs: number): boolean {\n const firstSeen = Number(lead.firstSeenAtMs)\n return Number.isFinite(firstSeen) && firstSeen > 0 && firstSeen <= cutoffMs\n}\n\n/**\n * Whether a lead is THIS member's to work: theirs by owner, or nobody's on\n * a site they reach. An owned lead is one person's; an unowned one is on\n * everybody's list until somebody takes it, which is what the digest is for.\n */\nexport function leadIsForMember(\n lead: Pick<CrmDigestLead, 'ownerUid' | 'hostId'>,\n member: { uid: string; reachesHost: (hostId: string) => boolean },\n): boolean {\n if (lead.ownerUid) return lead.ownerUid === member.uid\n return member.reachesHost(lead.hostId)\n}\n\n/**\n * Whether the org's plan carries the CRM (AGL-2611's `features.crm`).\n *\n * Read by key rather than through the typed flag map so this module does not\n * depend on the key existing: the flag lands with the CRM's gate, and a\n * plan that has not declared it is a plan that does not include it — a\n * digest about a surface the plan cannot open would be a reminder to pay.\n */\nexport function crmDigestEntitled(\n features: Record<string, boolean | undefined> | null | undefined,\n): boolean {\n return features?.['crm'] === true\n}\n\n/**\n * Every member's digest, from what the org's sweep read.\n *\n * A task goes to its assignee and to nobody else — an unassigned task is a\n * deliberate choice in the drawer, and an assignee no longer on the roster\n * is a task nobody owes. A lead goes to its owner, or to every member who\n * reaches the site when it has none. Members with nothing owed are simply\n * absent from the result, which is what makes \"per member with open work\"\n * cost nothing for everyone else — and a nurturing lead is not owed, so it\n * joins a digest that exists and never starts one.\n */\nexport function buildMemberDigests(input: {\n tasks: readonly CrmDigestTask[]\n leads: readonly CrmDigestLead[]\n nurturing?: readonly CrmDigestLead[]\n members: ReadonlyArray<{ uid: string; reachesHost: (hostId: string) => boolean }>\n window: CrmDigestWindow\n}): Map<string, CrmMemberDigest> {\n const { tasks, leads, nurturing = [], members, window } = input\n const digests = new Map<string, CrmMemberDigest>()\n const digestFor = (uid: string): CrmMemberDigest => {\n let digest = digests.get(uid)\n if (!digest) {\n digest = { overdue: [], today: [], leads: [], nurturing: [] }\n digests.set(uid, digest)\n }\n return digest\n }\n const roster = new Map(members.map((member) => [member.uid, member]))\n for (const task of tasks) {\n if (!task.assigneeUid || !roster.has(task.assigneeUid)) continue\n const state = crmDigestTaskState(task.dueAtMs, window)\n if (state === 'later') continue\n digestFor(task.assigneeUid)[state].push(task)\n }\n for (const lead of leads) {\n for (const member of members) {\n if (leadIsForMember(lead, member)) digestFor(member.uid).leads.push(lead)\n }\n }\n for (const lead of nurturing) {\n for (const member of members) {\n const digest = digests.get(member.uid)\n if (digest && leadIsForMember(lead, member)) digest.nurturing.push(lead)\n }\n }\n for (const digest of digests.values()) {\n digest.overdue.sort((a, b) => a.dueAtMs - b.dueAtMs)\n digest.today.sort((a, b) => a.dueAtMs - b.dueAtMs)\n digest.leads.sort((a, b) => a.firstSeenAtMs - b.firstSeenAtMs)\n }\n return digests\n}\n\nexport function crmDigestCounts(digest: CrmMemberDigest): CrmDigestCounts {\n return {\n overdue: digest.overdue.length,\n today: digest.today.length,\n leads: digest.leads.length,\n nurturing: digest.nurturing.length,\n }\n}\n\n/**\n * The line that says the nurturing figure (AGL-3446), or empty when there\n * is none: the leads the unworked list leaves out, and why.\n */\nexport function composeCrmDigestNurturingLine(counts: Pick<CrmDigestCounts, 'nurturing'>): string {\n if (counts.nurturing <= 0) return ''\n return counts.nurturing === 1\n ? '1 lead is in sequences or campaigns.'\n : `${counts.nurturing} leads are in sequences or campaigns.`\n}\n\nconst plural = (count: number, noun: string): string =>\n `${count} ${noun}${count === 1 ? '' : 's'}`\n\n/**\n * The one line the notification is: \"3 tasks due today, 2 overdue, 4\n * unworked leads\". Segments that are zero are left out rather than read as\n * \"0 overdue\" — the line is what is owed, and nothing owed is no segment.\n */\nexport function composeCrmDigestSummary(\n counts: Pick<CrmDigestCounts, 'overdue' | 'today' | 'leads'>,\n): string {\n const segments: string[] = []\n if (counts.today > 0) segments.push(`${plural(counts.today, 'task')} due today`)\n if (counts.overdue > 0) {\n segments.push(\n counts.today > 0 ? `${counts.overdue} overdue` : `${plural(counts.overdue, 'task')} overdue`,\n )\n }\n if (counts.leads > 0) segments.push(`${plural(counts.leads, 'unworked lead')}`)\n return segments.join(', ')\n}\n\n/**\n * The digest's subject. It names the workspace when it has one: the digest\n * is sent per workspace, and a member of two would otherwise get two mails\n * that read the same.\n */\nexport function composeCrmDigestSubject(\n counts: Pick<CrmDigestCounts, 'overdue' | 'today' | 'leads'>,\n workspaceName?: string,\n): string {\n const workspace = workspaceName?.trim()\n return `Your ${workspace ? `${workspace} ` : ''}CRM today: ${composeCrmDigestSummary(counts)}`\n}\n\n/** What the digest email needs from the route besides the digest itself. */\nexport interface CrmDigestEmailInput {\n digest: CrmMemberDigest\n nowMs: number\n timeZone: string\n /** The product name the mail reads as — the org's brand when white-labeled. */\n productName: string\n /**\n * The workspace the digest is for. The system email shows only the body,\n * so the body names it: a member of two workspaces gets one digest each.\n */\n workspaceName?: string\n /** The Tasks section, on the site the member's first task belongs to. */\n tasksUrl: string\n /** The Leads section of a site, for the sites that have unworked leads. */\n leadsUrl: (hostId: string) => string\n /** Account settings → Notifications, where the digest can be switched off. */\n settingsUrl: string\n /** A site's name for a lead line; the id when the site is unknown. */\n hostName: (hostId: string) => string\n /** The brand's support line, already prefixed, or empty. */\n supportLine?: string\n /**\n * Leave out the closing paragraph and the support line (AGL-3367), for the\n * copy that goes inside a system email: its footer says why the mail came\n * and where to get help, and its body links the settings page.\n */\n omitClosing?: boolean\n}\n\n/** A task's due time, with the zone named: it is the digest's, not the reader's. */\nfunction whenInZone(ms: number, timeZone: string): string {\n return new Date(ms).toLocaleString('en-US', {\n timeZone,\n weekday: 'short',\n month: 'short',\n day: 'numeric',\n hour: 'numeric',\n minute: '2-digit',\n timeZoneName: 'short',\n })\n}\n\nfunction dateInZone(ms: number, timeZone: string): string {\n return new Date(ms).toLocaleDateString('en-US', {\n timeZone,\n month: 'short',\n day: 'numeric',\n })\n}\n\nfunction section(heading: string, lines: string[]): string[] {\n const shown = lines.slice(0, CRM_DIGEST_LIST_MAX)\n const rest = lines.length - shown.length\n return [\n `${heading} (${lines.length})`,\n ...shown.map((line) => `- ${line}`),\n ...(rest > 0 ? [` and ${rest} more`] : []),\n '',\n ]\n}\n\n/**\n * The plain-text email. Text rather than a designed template because the\n * body is a list, and `sendEmail` synthesizes the HTML part that makes its\n * links live. Overdue first — it is the section a reader acts on — then\n * today, then the leads, each capped and counted, and the links last.\n */\nexport function composeCrmDigestEmailText(input: CrmDigestEmailInput): string {\n const { digest, nowMs, timeZone, productName } = input\n const counts = crmDigestCounts(digest)\n const day = new Date(nowMs).toLocaleDateString('en-US', {\n timeZone,\n weekday: 'long',\n month: 'long',\n day: 'numeric',\n })\n const workspace = input.workspaceName?.trim()\n const lines: string[] = [\n `Here is your ${productName} CRM${workspace ? ` in ${workspace}` : ''} for ${day}: ` +\n `${composeCrmDigestSummary(counts)}.`,\n '',\n ]\n // A task's site, as the lead lines name theirs; an organization task\n // (AGL-2637) has none.\n const taskLine = (task: CrmDigestTask) =>\n [task.title, task.hostId ? input.hostName(task.hostId) : '', whenInZone(task.dueAtMs, timeZone)]\n .filter(Boolean)\n .join(' · ')\n if (digest.overdue.length) {\n lines.push(...section('Overdue', digest.overdue.map(taskLine)))\n }\n if (digest.today.length) {\n lines.push(...section('Due today', digest.today.map(taskLine)))\n }\n if (digest.leads.length) {\n lines.push(\n ...section(\n 'Unworked leads',\n digest.leads.map((lead) => {\n const who = lead.name ? `${lead.name} <${lead.email}>` : lead.email\n return `${who} · ${input.hostName(lead.hostId)} · first seen ${dateInZone(\n lead.firstSeenAtMs,\n timeZone,\n )}`\n }),\n ),\n )\n }\n const nurturingLine = composeCrmDigestNurturingLine(counts)\n if (nurturingLine) lines.push(nurturingLine, '')\n if (counts.overdue + counts.today > 0) lines.push(`Open your tasks: ${input.tasksUrl}`)\n const leadHosts = [...new Set(digest.leads.map((lead) => lead.hostId))].slice(0, 3)\n for (const hostId of leadHosts) {\n lines.push(`Leads on ${input.hostName(hostId)}: ${input.leadsUrl(hostId)}`)\n }\n if (input.omitClosing) return lines.join('\\n').trimEnd()\n lines.push(\n '',\n 'You get this each morning because the Daily CRM digest is on in your ' +\n `notification settings: ${input.settingsUrl}`,\n )\n return lines.join('\\n') + (input.supportLine ?? '')\n}\n"],"names":["startOfNextZonedDay","startOfZonedDay","crmLeadStatus","CRM_DIGEST_DEFAULT_TIME_ZONE","digestTimeZone","env","process","configured","String","trim","Intl","DateTimeFormat","timeZone","console","warn","CRM_DIGEST_LEAD_AGE_MS","CRM_DIGEST_TASK_CEILING","CRM_DIGEST_LEAD_WINDOW","CRM_DIGEST_LIST_MAX","startOfDayInZone","ms","startOfNextDayInZone","utcDayKey","Date","toISOString","slice","crmDigestWindow","nowMs","startMs","endMs","crmDigestTaskState","dueAtMs","window","isUnworkedLead","lead","ageMs","seenBefore","isNurturingLead","cutoffMs","firstSeen","Number","firstSeenAtMs","isFinite","leadIsForMember","member","ownerUid","uid","reachesHost","hostId","crmDigestEntitled","features","buildMemberDigests","input","tasks","leads","nurturing","members","digests","Map","digestFor","digest","get","overdue","today","set","roster","map","task","assigneeUid","has","state","push","values","sort","a","b","crmDigestCounts","length","composeCrmDigestNurturingLine","counts","plural","count","noun","composeCrmDigestSummary","segments","join","composeCrmDigestSubject","workspaceName","workspace","whenInZone","toLocaleString","weekday","month","day","hour","minute","timeZoneName","dateInZone","toLocaleDateString","section","heading","lines","shown","rest","line","composeCrmDigestEmailText","productName","taskLine","title","hostName","filter","Boolean","who","name","email","nurturingLine","tasksUrl","leadHosts","Set","leadsUrl","omitClosing","trimEnd","settingsUrl","supportLine"],"mappings":"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"}
@@ -138,7 +138,13 @@ export declare function mapContactImportRow(cells: readonly string[], mapping: C
138
138
  /** Why one row was not stored. */
139
139
  export type ContactImportSkipReason = 'invalid-email' | 'duplicate' | 'audience-band'
140
140
  /** The person was erased from this workspace at their request (AGL-2623). */
141
- | 'erased' | 'write-failed';
141
+ | 'erased' | 'write-failed'
142
+ /**
143
+ * The request ran out of time before it reached the row (AGL-3423). Nothing
144
+ * was written for it, and importing the skipped file again is safe: a row
145
+ * that did land merges into its record rather than creating a second one.
146
+ */
147
+ | 'not-reached';
142
148
  /** How a skip reason reads on screen and in the downloaded file. */
143
149
  export declare const CONTACT_IMPORT_SKIP_LABELS: Record<ContactImportSkipReason, string>;
144
150
  /** A cell the file carried that could not be read as the field it was mapped to. */
@@ -260,7 +260,8 @@ const CUSTOM_TEXT_MAX = 1000;
260
260
  duplicate: 'Appears earlier in the file',
261
261
  'audience-band': 'Contact limit reached',
262
262
  erased: 'Erased from this workspace at their request',
263
- 'write-failed': 'Could not be saved'
263
+ 'write-failed': 'Could not be saved',
264
+ 'not-reached': 'Not reached in time; import the skipped rows again'
264
265
  };
265
266
  /**
266
267
  * A tag cell as the tags it names.