@aglyn/plugins-outreach 1.0.0-beta.143

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 (282) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +93 -0
  3. package/package.json +52 -0
  4. package/src/index.d.ts +22 -0
  5. package/src/index.js +29 -0
  6. package/src/index.js.map +1 -0
  7. package/src/lib/components/compliance-section.d.ts +20 -0
  8. package/src/lib/components/compliance-section.js +291 -0
  9. package/src/lib/components/compliance-section.js.map +1 -0
  10. package/src/lib/components/enroll-dialog.d.ts +36 -0
  11. package/src/lib/components/enroll-dialog.js +656 -0
  12. package/src/lib/components/enroll-dialog.js.map +1 -0
  13. package/src/lib/components/enrollments-table.d.ts +28 -0
  14. package/src/lib/components/enrollments-table.js +375 -0
  15. package/src/lib/components/enrollments-table.js.map +1 -0
  16. package/src/lib/components/mailbox-card.d.ts +46 -0
  17. package/src/lib/components/mailbox-card.js +517 -0
  18. package/src/lib/components/mailbox-card.js.map +1 -0
  19. package/src/lib/components/mailboxes-section.d.ts +24 -0
  20. package/src/lib/components/mailboxes-section.js +258 -0
  21. package/src/lib/components/mailboxes-section.js.map +1 -0
  22. package/src/lib/components/outreach-console-page.d.ts +17 -0
  23. package/src/lib/components/outreach-console-page.js +81 -0
  24. package/src/lib/components/outreach-console-page.js.map +1 -0
  25. package/src/lib/components/outreach-console-sections.d.ts +43 -0
  26. package/src/lib/components/outreach-console-sections.js +55 -0
  27. package/src/lib/components/outreach-console-sections.js.map +1 -0
  28. package/src/lib/components/outreach-ui.d.ts +51 -0
  29. package/src/lib/components/outreach-ui.js +171 -0
  30. package/src/lib/components/outreach-ui.js.map +1 -0
  31. package/src/lib/components/send-window-fields.d.ts +25 -0
  32. package/src/lib/components/send-window-fields.js +178 -0
  33. package/src/lib/components/send-window-fields.js.map +1 -0
  34. package/src/lib/components/sequence-detail.d.ts +54 -0
  35. package/src/lib/components/sequence-detail.js +435 -0
  36. package/src/lib/components/sequence-detail.js.map +1 -0
  37. package/src/lib/components/sequence-editor.d.ts +37 -0
  38. package/src/lib/components/sequence-editor.js +485 -0
  39. package/src/lib/components/sequence-editor.js.map +1 -0
  40. package/src/lib/components/sequence-mailbox-picker.d.ts +44 -0
  41. package/src/lib/components/sequence-mailbox-picker.js +137 -0
  42. package/src/lib/components/sequence-mailbox-picker.js.map +1 -0
  43. package/src/lib/components/sequence-preview.d.ts +27 -0
  44. package/src/lib/components/sequence-preview.js +146 -0
  45. package/src/lib/components/sequence-preview.js.map +1 -0
  46. package/src/lib/components/sequence-step-card.d.ts +31 -0
  47. package/src/lib/components/sequence-step-card.js +326 -0
  48. package/src/lib/components/sequence-step-card.js.map +1 -0
  49. package/src/lib/components/sequences-section.d.ts +40 -0
  50. package/src/lib/components/sequences-section.js +310 -0
  51. package/src/lib/components/sequences-section.js.map +1 -0
  52. package/src/lib/components/use-outreach-api.d.ts +38 -0
  53. package/src/lib/components/use-outreach-api.js +139 -0
  54. package/src/lib/components/use-outreach-api.js.map +1 -0
  55. package/src/lib/components/use-outreach-crm.d.ts +50 -0
  56. package/src/lib/components/use-outreach-crm.js +200 -0
  57. package/src/lib/components/use-outreach-crm.js.map +1 -0
  58. package/src/lib/components/use-outreach-data.d.ts +55 -0
  59. package/src/lib/components/use-outreach-data.js +199 -0
  60. package/src/lib/components/use-outreach-data.js.map +1 -0
  61. package/src/lib/components/use-outreach-mailbox-api.d.ts +30 -0
  62. package/src/lib/components/use-outreach-mailbox-api.js +101 -0
  63. package/src/lib/components/use-outreach-mailbox-api.js.map +1 -0
  64. package/src/lib/components/use-outreach-mailboxes.d.ts +22 -0
  65. package/src/lib/components/use-outreach-mailboxes.js +68 -0
  66. package/src/lib/components/use-outreach-mailboxes.js.map +1 -0
  67. package/src/lib/components/use-outreach-settings.d.ts +17 -0
  68. package/src/lib/components/use-outreach-settings.js +64 -0
  69. package/src/lib/components/use-outreach-settings.js.map +1 -0
  70. package/src/lib/constants/api-routes.d.ts +68 -0
  71. package/src/lib/constants/api-routes.js +57 -0
  72. package/src/lib/constants/api-routes.js.map +1 -0
  73. package/src/lib/constants/bundle-common.d.ts +46 -0
  74. package/src/lib/constants/bundle-common.js +55 -0
  75. package/src/lib/constants/bundle-common.js.map +1 -0
  76. package/src/lib/constants/runtime-jobs.d.ts +24 -0
  77. package/src/lib/constants/runtime-jobs.js +24 -0
  78. package/src/lib/constants/runtime-jobs.js.map +1 -0
  79. package/src/lib/declarations.console-server.d.ts +38 -0
  80. package/src/lib/declarations.console-server.js +99 -0
  81. package/src/lib/declarations.console-server.js.map +1 -0
  82. package/src/lib/engine/compose.d.ts +86 -0
  83. package/src/lib/engine/compose.js +212 -0
  84. package/src/lib/engine/compose.js.map +1 -0
  85. package/src/lib/engine/delivery-status.d.ts +53 -0
  86. package/src/lib/engine/delivery-status.js +184 -0
  87. package/src/lib/engine/delivery-status.js.map +1 -0
  88. package/src/lib/engine/do-not-contact.d.ts +18 -0
  89. package/src/lib/engine/do-not-contact.js +34 -0
  90. package/src/lib/engine/do-not-contact.js.map +1 -0
  91. package/src/lib/engine/enrollment-state.d.ts +156 -0
  92. package/src/lib/engine/enrollment-state.js +303 -0
  93. package/src/lib/engine/enrollment-state.js.map +1 -0
  94. package/src/lib/engine/gates.d.ts +125 -0
  95. package/src/lib/engine/gates.js +232 -0
  96. package/src/lib/engine/gates.js.map +1 -0
  97. package/src/lib/engine/index.d.ts +28 -0
  98. package/src/lib/engine/index.js +33 -0
  99. package/src/lib/engine/index.js.map +1 -0
  100. package/src/lib/engine/mailbox-health.d.ts +49 -0
  101. package/src/lib/engine/mailbox-health.js +83 -0
  102. package/src/lib/engine/mailbox-health.js.map +1 -0
  103. package/src/lib/engine/opt-out-intent.d.ts +41 -0
  104. package/src/lib/engine/opt-out-intent.js +220 -0
  105. package/src/lib/engine/opt-out-intent.js.map +1 -0
  106. package/src/lib/engine/recipient-country.d.ts +41 -0
  107. package/src/lib/engine/recipient-country.js +171 -0
  108. package/src/lib/engine/recipient-country.js.map +1 -0
  109. package/src/lib/engine/schedule.d.ts +70 -0
  110. package/src/lib/engine/schedule.js +115 -0
  111. package/src/lib/engine/schedule.js.map +1 -0
  112. package/src/lib/engine/sending-capacity.d.ts +96 -0
  113. package/src/lib/engine/sending-capacity.js +172 -0
  114. package/src/lib/engine/sending-capacity.js.map +1 -0
  115. package/src/lib/engine/sequence-validation.d.ts +90 -0
  116. package/src/lib/engine/sequence-validation.js +291 -0
  117. package/src/lib/engine/sequence-validation.js.map +1 -0
  118. package/src/lib/engine/thread-classification.d.ts +78 -0
  119. package/src/lib/engine/thread-classification.js +224 -0
  120. package/src/lib/engine/thread-classification.js.map +1 -0
  121. package/src/lib/engine/thread-message.d.ts +80 -0
  122. package/src/lib/engine/thread-message.js +143 -0
  123. package/src/lib/engine/thread-message.js.map +1 -0
  124. package/src/lib/enrollment/enroll-people.d.ts +81 -0
  125. package/src/lib/enrollment/enroll-people.js +250 -0
  126. package/src/lib/enrollment/enroll-people.js.map +1 -0
  127. package/src/lib/enrollment/gate-lookups.d.ts +32 -0
  128. package/src/lib/enrollment/gate-lookups.js +154 -0
  129. package/src/lib/enrollment/gate-lookups.js.map +1 -0
  130. package/src/lib/mailboxes/mailbox-api.d.ts +126 -0
  131. package/src/lib/mailboxes/mailbox-api.js +66 -0
  132. package/src/lib/mailboxes/mailbox-api.js.map +1 -0
  133. package/src/lib/mailboxes/mailbox-credentials.d.ts +95 -0
  134. package/src/lib/mailboxes/mailbox-credentials.js +102 -0
  135. package/src/lib/mailboxes/mailbox-credentials.js.map +1 -0
  136. package/src/lib/mailboxes/mailbox-erasure.d.ts +69 -0
  137. package/src/lib/mailboxes/mailbox-erasure.js +148 -0
  138. package/src/lib/mailboxes/mailbox-erasure.js.map +1 -0
  139. package/src/lib/mailboxes/mailbox-gate.d.ts +75 -0
  140. package/src/lib/mailboxes/mailbox-gate.js +98 -0
  141. package/src/lib/mailboxes/mailbox-gate.js.map +1 -0
  142. package/src/lib/mailboxes/mailbox-revoke.d.ts +59 -0
  143. package/src/lib/mailboxes/mailbox-revoke.js +41 -0
  144. package/src/lib/mailboxes/mailbox-revoke.js.map +1 -0
  145. package/src/lib/mailboxes/mailbox-routes.d.ts +119 -0
  146. package/src/lib/mailboxes/mailbox-routes.js +605 -0
  147. package/src/lib/mailboxes/mailbox-routes.js.map +1 -0
  148. package/src/lib/mailboxes/mailbox-settings.d.ts +110 -0
  149. package/src/lib/mailboxes/mailbox-settings.js +212 -0
  150. package/src/lib/mailboxes/mailbox-settings.js.map +1 -0
  151. package/src/lib/mailboxes/mailbox-transport.d.ts +64 -0
  152. package/src/lib/mailboxes/mailbox-transport.js +94 -0
  153. package/src/lib/mailboxes/mailbox-transport.js.map +1 -0
  154. package/src/lib/mailboxes/oauth-redirect.d.ts +41 -0
  155. package/src/lib/mailboxes/oauth-redirect.js +63 -0
  156. package/src/lib/mailboxes/oauth-redirect.js.map +1 -0
  157. package/src/lib/mailboxes/oauth-state.d.ts +152 -0
  158. package/src/lib/mailboxes/oauth-state.js +212 -0
  159. package/src/lib/mailboxes/oauth-state.js.map +1 -0
  160. package/src/lib/mailboxes/outreach-config.d.ts +64 -0
  161. package/src/lib/mailboxes/outreach-config.js +77 -0
  162. package/src/lib/mailboxes/outreach-config.js.map +1 -0
  163. package/src/lib/mailboxes/register-mailbox-routes.d.ts +47 -0
  164. package/src/lib/mailboxes/register-mailbox-routes.js +135 -0
  165. package/src/lib/mailboxes/register-mailbox-routes.js.map +1 -0
  166. package/src/lib/model/compliance-settings.d.ts +78 -0
  167. package/src/lib/model/compliance-settings.js +155 -0
  168. package/src/lib/model/compliance-settings.js.map +1 -0
  169. package/src/lib/model/outreach-api.d.ts +210 -0
  170. package/src/lib/model/outreach-api.js +28 -0
  171. package/src/lib/model/outreach-api.js.map +1 -0
  172. package/src/lib/model/outreach.types.d.ts +563 -0
  173. package/src/lib/model/outreach.types.js +225 -0
  174. package/src/lib/model/outreach.types.js.map +1 -0
  175. package/src/lib/model/sequence-draft.d.ts +84 -0
  176. package/src/lib/model/sequence-draft.js +239 -0
  177. package/src/lib/model/sequence-draft.js.map +1 -0
  178. package/src/lib/model/step-preview.d.ts +45 -0
  179. package/src/lib/model/step-preview.js +98 -0
  180. package/src/lib/model/step-preview.js.map +1 -0
  181. package/src/lib/model/stored-records.d.ts +23 -0
  182. package/src/lib/model/stored-records.js +84 -0
  183. package/src/lib/model/stored-records.js.map +1 -0
  184. package/src/lib/plugin.d.ts +46 -0
  185. package/src/lib/plugin.js +86 -0
  186. package/src/lib/plugin.js.map +1 -0
  187. package/src/lib/routes/enroll-routes.d.ts +64 -0
  188. package/src/lib/routes/enroll-routes.js +350 -0
  189. package/src/lib/routes/enroll-routes.js.map +1 -0
  190. package/src/lib/routes/enrollment-routes.d.ts +19 -0
  191. package/src/lib/routes/enrollment-routes.js +171 -0
  192. package/src/lib/routes/enrollment-routes.js.map +1 -0
  193. package/src/lib/routes/platform-deps.d.ts +51 -0
  194. package/src/lib/routes/platform-deps.js +66 -0
  195. package/src/lib/routes/platform-deps.js.map +1 -0
  196. package/src/lib/routes/preview-routes.d.ts +33 -0
  197. package/src/lib/routes/preview-routes.js +117 -0
  198. package/src/lib/routes/preview-routes.js.map +1 -0
  199. package/src/lib/routes/register-routes.d.ts +22 -0
  200. package/src/lib/routes/register-routes.js +92 -0
  201. package/src/lib/routes/register-routes.js.map +1 -0
  202. package/src/lib/routes/route-deps.d.ts +49 -0
  203. package/src/lib/routes/route-deps.js +27 -0
  204. package/src/lib/routes/route-deps.js.map +1 -0
  205. package/src/lib/routes/route-gate.d.ts +94 -0
  206. package/src/lib/routes/route-gate.js +87 -0
  207. package/src/lib/routes/route-gate.js.map +1 -0
  208. package/src/lib/routes/route-http.d.ts +32 -0
  209. package/src/lib/routes/route-http.js +62 -0
  210. package/src/lib/routes/route-http.js.map +1 -0
  211. package/src/lib/routes/sequence-routes.d.ts +32 -0
  212. package/src/lib/routes/sequence-routes.js +359 -0
  213. package/src/lib/routes/sequence-routes.js.map +1 -0
  214. package/src/lib/routes/settings-routes.d.ts +30 -0
  215. package/src/lib/routes/settings-routes.js +73 -0
  216. package/src/lib/routes/settings-routes.js.map +1 -0
  217. package/src/lib/runtime/enrollment-events.d.ts +86 -0
  218. package/src/lib/runtime/enrollment-events.js +141 -0
  219. package/src/lib/runtime/enrollment-events.js.map +1 -0
  220. package/src/lib/runtime/fixtures/fake-gmail.d.ts +121 -0
  221. package/src/lib/runtime/fixtures/fake-gmail.js +312 -0
  222. package/src/lib/runtime/fixtures/fake-gmail.js.map +1 -0
  223. package/src/lib/runtime/mailbox-health-store.d.ts +47 -0
  224. package/src/lib/runtime/mailbox-health-store.js +168 -0
  225. package/src/lib/runtime/mailbox-health-store.js.map +1 -0
  226. package/src/lib/runtime/person-erasure.d.ts +39 -0
  227. package/src/lib/runtime/person-erasure.js +57 -0
  228. package/src/lib/runtime/person-erasure.js.map +1 -0
  229. package/src/lib/runtime/platform-runtime-deps.d.ts +29 -0
  230. package/src/lib/runtime/platform-runtime-deps.js +98 -0
  231. package/src/lib/runtime/platform-runtime-deps.js.map +1 -0
  232. package/src/lib/runtime/runtime-deps.d.ts +77 -0
  233. package/src/lib/runtime/runtime-deps.js +18 -0
  234. package/src/lib/runtime/runtime-deps.js.map +1 -0
  235. package/src/lib/runtime/send-job.d.ts +42 -0
  236. package/src/lib/runtime/send-job.js +780 -0
  237. package/src/lib/runtime/send-job.js.map +1 -0
  238. package/src/lib/runtime/sync-job.d.ts +41 -0
  239. package/src/lib/runtime/sync-job.js +480 -0
  240. package/src/lib/runtime/sync-job.js.map +1 -0
  241. package/src/lib/runtime/timeline.d.ts +67 -0
  242. package/src/lib/runtime/timeline.js +93 -0
  243. package/src/lib/runtime/timeline.js.map +1 -0
  244. package/src/lib/runtime/unsubscribe-link.d.ts +75 -0
  245. package/src/lib/runtime/unsubscribe-link.js +121 -0
  246. package/src/lib/runtime/unsubscribe-link.js.map +1 -0
  247. package/src/lib/runtime/unsubscribe-route.d.ts +24 -0
  248. package/src/lib/runtime/unsubscribe-route.js +103 -0
  249. package/src/lib/runtime/unsubscribe-route.js.map +1 -0
  250. package/src/lib/server.d.ts +53 -0
  251. package/src/lib/server.js +94 -0
  252. package/src/lib/server.js.map +1 -0
  253. package/src/lib/storage/compliance-settings-store.d.ts +44 -0
  254. package/src/lib/storage/compliance-settings-store.js +78 -0
  255. package/src/lib/storage/compliance-settings-store.js.map +1 -0
  256. package/src/lib/storage/do-not-contact-store.d.ts +60 -0
  257. package/src/lib/storage/do-not-contact-store.js +168 -0
  258. package/src/lib/storage/do-not-contact-store.js.map +1 -0
  259. package/src/lib/storage/outreach-records.d.ts +33 -0
  260. package/src/lib/storage/outreach-records.js +35 -0
  261. package/src/lib/storage/outreach-records.js.map +1 -0
  262. package/src/lib/subprocessors.d.ts +48 -0
  263. package/src/lib/subprocessors.js +70 -0
  264. package/src/lib/subprocessors.js.map +1 -0
  265. package/src/lib/transport/gmail-client.d.ts +145 -0
  266. package/src/lib/transport/gmail-client.js +229 -0
  267. package/src/lib/transport/gmail-client.js.map +1 -0
  268. package/src/lib/transport/gmail-errors.d.ts +84 -0
  269. package/src/lib/transport/gmail-errors.js +138 -0
  270. package/src/lib/transport/gmail-errors.js.map +1 -0
  271. package/src/lib/transport/google-oauth.d.ts +138 -0
  272. package/src/lib/transport/google-oauth.js +247 -0
  273. package/src/lib/transport/google-oauth.js.map +1 -0
  274. package/src/lib/transport/http.d.ts +53 -0
  275. package/src/lib/transport/http.js +79 -0
  276. package/src/lib/transport/http.js.map +1 -0
  277. package/src/lib/transport/rfc5322.d.ts +116 -0
  278. package/src/lib/transport/rfc5322.js +330 -0
  279. package/src/lib/transport/rfc5322.js.map +1 -0
  280. package/src/lib/transport/send-message.d.ts +69 -0
  281. package/src/lib/transport/send-message.js +77 -0
  282. package/src/lib/transport/send-message.js.map +1 -0
@@ -0,0 +1,303 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ /*==========================================
18
+ * ONE PERSON'S RUN THROUGH A SEQUENCE (AGL-2979).
19
+ *
20
+ * ## The state machine
21
+ *
22
+ * active ──► paused ──► active
23
+ * │ │
24
+ * ├──────────┴──► replied · bounced · opted_out · stopped
25
+ * └─► finished · failed
26
+ *
27
+ * An enrollment sends only while `active`. Everything that ends the sending
28
+ * records why (`stopReason`, `stopDetail`, when, and who). Ending the
29
+ * sending does not end what can still be learned from the thread: a reply,
30
+ * a bounce or an opt-out that arrives after the last step, after a member
31
+ * stopped it or after a send failed is still recorded, and a person who
32
+ * replied and then asked to be left alone moves on to `opted_out` — an
33
+ * opt-out is the one fact every later sequence has to be able to find.
34
+ * `bounced` and `opted_out` are final.
35
+ *
36
+ * Every function returns a PATCH — the fields to write — and never the
37
+ * whole document, so a writer applies it with its own SDK and its own
38
+ * timestamps.
39
+ *==========================================*/ import { OUTREACH_ATTESTATION_KINDS, OUTREACH_STOP_REASONS_BY_STATUS } from "../model/outreach.types.js";
40
+ import { normalizeOutreachPersonalLine } from "./gates.js";
41
+ import { effectiveOutreachWindow, scheduleOutreachDue } from "./schedule.js";
42
+ import { isInThreadEmailStep } from "./sequence-validation.js";
43
+ /** Where each status may go next. */ export const OUTREACH_ENROLLMENT_TRANSITIONS = {
44
+ active: [
45
+ 'paused',
46
+ 'finished',
47
+ 'replied',
48
+ 'bounced',
49
+ 'opted_out',
50
+ 'stopped',
51
+ 'failed'
52
+ ],
53
+ paused: [
54
+ 'active',
55
+ 'replied',
56
+ 'bounced',
57
+ 'opted_out',
58
+ 'stopped'
59
+ ],
60
+ finished: [
61
+ 'replied',
62
+ 'bounced',
63
+ 'opted_out'
64
+ ],
65
+ stopped: [
66
+ 'replied',
67
+ 'bounced',
68
+ 'opted_out'
69
+ ],
70
+ failed: [
71
+ 'replied',
72
+ 'bounced',
73
+ 'opted_out'
74
+ ],
75
+ replied: [
76
+ 'opted_out'
77
+ ],
78
+ bounced: [],
79
+ opted_out: []
80
+ };
81
+ export function canTransitionOutreachEnrollment(from, to) {
82
+ var _OUTREACH_ENROLLMENT_TRANSITIONS_from;
83
+ return ((_OUTREACH_ENROLLMENT_TRANSITIONS_from = OUTREACH_ENROLLMENT_TRANSITIONS[from]) == null ? void 0 : _OUTREACH_ENROLLMENT_TRANSITIONS_from.includes(to)) === true;
84
+ }
85
+ const STATUS_WORDS = {
86
+ active: 'is active',
87
+ paused: 'is paused',
88
+ finished: 'finished',
89
+ replied: 'got a reply',
90
+ bounced: 'bounced',
91
+ opted_out: 'opted out',
92
+ stopped: 'was stopped',
93
+ failed: 'failed'
94
+ };
95
+ const EVENT_TARGET = {
96
+ pause: 'paused',
97
+ resume: 'active',
98
+ stop: 'stopped',
99
+ reply: 'replied',
100
+ opt_out: 'opted_out',
101
+ bounce: 'bounced',
102
+ send_failed: 'failed'
103
+ };
104
+ const detailOf = (value)=>{
105
+ const text = typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : '';
106
+ return text ? text.slice(0, 500) : null;
107
+ };
108
+ /** What an event does to an enrollment's status — see the module note. */ export function applyOutreachEnrollmentEvent(enrollment, event) {
109
+ var _event_reason;
110
+ const from = enrollment.status;
111
+ const to = EVENT_TARGET[event.type];
112
+ if (!to) return {
113
+ patch: null,
114
+ error: 'That is not something that happens to an enrollment.'
115
+ };
116
+ if (from === to) return {
117
+ patch: null,
118
+ error: null
119
+ };
120
+ if (!canTransitionOutreachEnrollment(from, to)) {
121
+ return {
122
+ patch: null,
123
+ error: `This enrollment ${STATUS_WORDS[from]}, so it can't be marked as one that ${STATUS_WORDS[to]}.`
124
+ };
125
+ }
126
+ if (event.type === 'resume') {
127
+ return {
128
+ patch: {
129
+ status: 'active',
130
+ stopReason: null,
131
+ stopDetail: null,
132
+ stoppedAtMs: null,
133
+ stoppedByUid: null,
134
+ nextDueAtMs: enrollment.nextDueAtMs
135
+ },
136
+ error: null
137
+ };
138
+ }
139
+ const reason = event.type === 'pause' ? 'manual' : event.type === 'stop' ? (_event_reason = event.reason) != null ? _event_reason : 'manual' : event.type === 'reply' ? 'reply' : event.type === 'opt_out' ? event.reason : event.type === 'bounce' ? 'hard_bounce' : 'send_failed';
140
+ const allowed = OUTREACH_STOP_REASONS_BY_STATUS[to];
141
+ if (!(allowed == null ? void 0 : allowed.includes(reason))) {
142
+ return {
143
+ patch: null,
144
+ error: `"${reason}" isn't a reason an enrollment ${STATUS_WORDS[to]}.`
145
+ };
146
+ }
147
+ const byUid = 'byUid' in event && typeof event.byUid === 'string' && event.byUid ? event.byUid : null;
148
+ return {
149
+ patch: {
150
+ status: to,
151
+ stopReason: reason,
152
+ stopDetail: detailOf(event.detail),
153
+ stoppedAtMs: event.atMs,
154
+ stoppedByUid: byUid,
155
+ // A paused enrollment keeps its place, so resuming it picks up where it
156
+ // stopped; every other status has nothing left waiting.
157
+ nextDueAtMs: to === 'paused' ? enrollment.nextDueAtMs : null
158
+ },
159
+ error: null
160
+ };
161
+ }
162
+ /** When a new enrollment's first step comes due, or `null` when it cannot be placed. */ export function planOutreachFirstDue(input) {
163
+ var _input_sequence_steps, _input_sequence_settings;
164
+ const first = (_input_sequence_steps = input.sequence.steps) == null ? void 0 : _input_sequence_steps[0];
165
+ if (!first) return null;
166
+ return scheduleOutreachDue({
167
+ fromMs: input.enrolledAtMs,
168
+ delayBusinessDays: first.delayBusinessDays,
169
+ timeZone: input.mailbox.timezone,
170
+ window: effectiveOutreachWindow((_input_sequence_settings = input.sequence.settings) == null ? void 0 : _input_sequence_settings.window, input.mailbox.window),
171
+ random: input.random
172
+ });
173
+ }
174
+ /** Stamps the attestations a rep ticked with who ticked them and when; unknown kinds are dropped. */ export function outreachAttestationsFrom(kinds, uid, atMs) {
175
+ const attestations = {};
176
+ for (const kind of kinds != null ? kinds : []){
177
+ if (OUTREACH_ATTESTATION_KINDS.includes(kind)) {
178
+ attestations[kind] = {
179
+ uid,
180
+ atMs
181
+ };
182
+ }
183
+ }
184
+ return attestations;
185
+ }
186
+ /**
187
+ * A new enrollment document, first step scheduled — one builder for every
188
+ * door that enrolls, so an enrollment made from a contact and one made from
189
+ * a list are the same shape. Call it after the gates allowed the person.
190
+ */ export function buildOutreachEnrollment(draft) {
191
+ var _draft_contactName, _draft_email, _draft_attestations;
192
+ return {
193
+ id: draft.id,
194
+ sequenceId: draft.sequence.id,
195
+ contactId: draft.contactId,
196
+ contactName: String((_draft_contactName = draft.contactName) != null ? _draft_contactName : '').replace(/\s+/g, ' ').trim().slice(0, 200),
197
+ email: String((_draft_email = draft.email) != null ? _draft_email : '').trim().toLowerCase(),
198
+ hostId: draft.sequence.hostId,
199
+ mailboxId: draft.sequence.mailboxId,
200
+ stepIndex: 0,
201
+ nextDueAtMs: planOutreachFirstDue({
202
+ sequence: draft.sequence,
203
+ mailbox: draft.mailbox,
204
+ enrolledAtMs: draft.nowMs,
205
+ random: draft.random
206
+ }),
207
+ status: 'active',
208
+ stopReason: null,
209
+ stopDetail: null,
210
+ stoppedAtMs: null,
211
+ stoppedByUid: null,
212
+ personalLine: normalizeOutreachPersonalLine(draft.personalLine),
213
+ cold: draft.cold === true,
214
+ attestations: _extends({}, (_draft_attestations = draft.attestations) != null ? _draft_attestations : {}),
215
+ enrolledByUid: draft.enrolledByUid,
216
+ gmailThreadId: null,
217
+ gmailThreadIds: [],
218
+ threadSubject: null,
219
+ messageIds: [],
220
+ lastSentAtMs: null,
221
+ createdAtMs: draft.nowMs,
222
+ updatedAtMs: draft.nowMs
223
+ };
224
+ }
225
+ /**
226
+ * What running the current step writes: the send recorded on the thread
227
+ * fields, and the next step scheduled — or the enrollment `finished` when
228
+ * that was the last.
229
+ */ export function planOutreachStepCompletion(input) {
230
+ var _sequence_steps;
231
+ var _sequence_settings;
232
+ const { enrollment, sequence } = input;
233
+ if (enrollment.status !== 'active') {
234
+ return {
235
+ patch: null,
236
+ error: 'Only an active enrollment runs its steps.'
237
+ };
238
+ }
239
+ const steps = (_sequence_steps = sequence.steps) != null ? _sequence_steps : [];
240
+ const step = steps[enrollment.stepIndex];
241
+ if (!step) return {
242
+ patch: null,
243
+ error: 'This enrollment has no step at its position.'
244
+ };
245
+ const patch = {
246
+ status: 'active',
247
+ stepIndex: enrollment.stepIndex + 1,
248
+ nextDueAtMs: null,
249
+ lastSentAtMs: input.completedAtMs
250
+ };
251
+ if (step.kind === 'email') {
252
+ var _ref, _ref1, _enrollment_gmailThreadIds;
253
+ const sent = input.sent;
254
+ const messageId = String((_ref = sent == null ? void 0 : sent.messageId) != null ? _ref : '').trim();
255
+ const threadId = String((_ref1 = sent == null ? void 0 : sent.threadId) != null ? _ref1 : '').trim();
256
+ if (!messageId || !threadId) {
257
+ return {
258
+ patch: null,
259
+ error: 'An email step needs the sent message and thread ids.'
260
+ };
261
+ }
262
+ const threadIds = [
263
+ ...(_enrollment_gmailThreadIds = enrollment.gmailThreadIds) != null ? _enrollment_gmailThreadIds : []
264
+ ];
265
+ if (!threadIds.includes(threadId)) threadIds.push(threadId);
266
+ patch.gmailThreadId = threadId;
267
+ patch.gmailThreadIds = threadIds;
268
+ if (isInThreadEmailStep(steps, enrollment.stepIndex) && enrollment.threadSubject) {
269
+ var _enrollment_messageIds;
270
+ patch.messageIds = [
271
+ ...(_enrollment_messageIds = enrollment.messageIds) != null ? _enrollment_messageIds : [],
272
+ messageId
273
+ ];
274
+ } else {
275
+ var _sent_subject;
276
+ patch.threadSubject = String((_sent_subject = sent.subject) != null ? _sent_subject : '').trim();
277
+ patch.messageIds = [
278
+ messageId
279
+ ];
280
+ }
281
+ }
282
+ const next = steps[patch.stepIndex];
283
+ if (!next) {
284
+ patch.status = 'finished';
285
+ return {
286
+ patch,
287
+ error: null
288
+ };
289
+ }
290
+ patch.nextDueAtMs = scheduleOutreachDue({
291
+ fromMs: input.completedAtMs,
292
+ delayBusinessDays: next.delayBusinessDays,
293
+ timeZone: input.mailbox.timezone,
294
+ window: effectiveOutreachWindow((_sequence_settings = sequence.settings) == null ? void 0 : _sequence_settings.window, input.mailbox.window),
295
+ random: input.random
296
+ });
297
+ return {
298
+ patch,
299
+ error: patch.nextDueAtMs === null ? "The next step couldn't be scheduled: check the sending window and the mailbox's timezone." : null
300
+ };
301
+ }
302
+
303
+ //# sourceMappingURL=enrollment-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/engine/enrollment-state.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 * ONE PERSON'S RUN THROUGH A SEQUENCE (AGL-2979).\n *\n * ## The state machine\n *\n * active ──► paused ──► active\n * │ │\n * ├──────────┴──► replied · bounced · opted_out · stopped\n * └─► finished · failed\n *\n * An enrollment sends only while `active`. Everything that ends the sending\n * records why (`stopReason`, `stopDetail`, when, and who). Ending the\n * sending does not end what can still be learned from the thread: a reply,\n * a bounce or an opt-out that arrives after the last step, after a member\n * stopped it or after a send failed is still recorded, and a person who\n * replied and then asked to be left alone moves on to `opted_out` — an\n * opt-out is the one fact every later sequence has to be able to find.\n * `bounced` and `opted_out` are final.\n *\n * Every function returns a PATCH — the fields to write — and never the\n * whole document, so a writer applies it with its own SDK and its own\n * timestamps.\n *==========================================*/\n\nimport {\n OUTREACH_ATTESTATION_KINDS,\n OUTREACH_STOP_REASONS_BY_STATUS,\n type OutreachAttestationKind,\n type OutreachAttestations,\n type OutreachEnrollment,\n type OutreachEnrollmentStatus,\n type OutreachMailbox,\n type OutreachSequence,\n type OutreachStopReason,\n} from '../model/outreach.types'\nimport { normalizeOutreachPersonalLine } from './gates'\nimport { effectiveOutreachWindow, type OutreachRandom, scheduleOutreachDue } from './schedule'\nimport { isInThreadEmailStep } from './sequence-validation'\n\n/** Where each status may go next. */\nexport const OUTREACH_ENROLLMENT_TRANSITIONS: Readonly<\n Record<OutreachEnrollmentStatus, readonly OutreachEnrollmentStatus[]>\n> = {\n active: ['paused', 'finished', 'replied', 'bounced', 'opted_out', 'stopped', 'failed'],\n paused: ['active', 'replied', 'bounced', 'opted_out', 'stopped'],\n finished: ['replied', 'bounced', 'opted_out'],\n stopped: ['replied', 'bounced', 'opted_out'],\n failed: ['replied', 'bounced', 'opted_out'],\n replied: ['opted_out'],\n bounced: [],\n opted_out: [],\n}\n\nexport function canTransitionOutreachEnrollment(\n from: OutreachEnrollmentStatus,\n to: OutreachEnrollmentStatus,\n): boolean {\n return OUTREACH_ENROLLMENT_TRANSITIONS[from]?.includes(to) === true\n}\n\n/** Something that happened to an enrollment. `atMs` is when it happened. */\nexport type OutreachEnrollmentEvent =\n | { type: 'pause'; atMs: number; byUid: string | null; detail?: string | null }\n | { type: 'resume'; atMs: number; byUid: string | null }\n | {\n type: 'stop'\n atMs: number\n byUid: string | null\n reason?: Extract<OutreachStopReason, 'manual' | 'gate' | 'sequence_archived'>\n detail?: string | null\n }\n | { type: 'reply'; atMs: number; detail?: string | null }\n | {\n type: 'opt_out'\n atMs: number\n reason: Extract<OutreachStopReason, 'opt_out_reply' | 'unsubscribe' | 'do_not_contact'>\n detail?: string | null\n }\n | { type: 'bounce'; atMs: number; detail?: string | null }\n | { type: 'send_failed'; atMs: number; detail?: string | null }\n\n/** The status fields an event writes. */\nexport interface OutreachEnrollmentStatusPatch {\n status: OutreachEnrollmentStatus\n stopReason: OutreachStopReason | null\n stopDetail: string | null\n stoppedAtMs: number | null\n stoppedByUid: string | null\n nextDueAtMs: number | null\n}\n\n/**\n * `patch` is what to write, or `null` when there is nothing to write — the\n * enrollment is already there, or the event is not allowed, in which case\n * `error` says why.\n */\nexport interface OutreachEnrollmentEventResult {\n patch: OutreachEnrollmentStatusPatch | null\n error: string | null\n}\n\nconst STATUS_WORDS: Record<OutreachEnrollmentStatus, string> = {\n active: 'is active',\n paused: 'is paused',\n finished: 'finished',\n replied: 'got a reply',\n bounced: 'bounced',\n opted_out: 'opted out',\n stopped: 'was stopped',\n failed: 'failed',\n}\n\nconst EVENT_TARGET: Record<OutreachEnrollmentEvent['type'], OutreachEnrollmentStatus> = {\n pause: 'paused',\n resume: 'active',\n stop: 'stopped',\n reply: 'replied',\n opt_out: 'opted_out',\n bounce: 'bounced',\n send_failed: 'failed',\n}\n\nconst detailOf = (value: unknown): string | null => {\n const text = typeof value === 'string' ? value.replace(/\\s+/g, ' ').trim() : ''\n return text ? text.slice(0, 500) : null\n}\n\n/** What an event does to an enrollment's status — see the module note. */\nexport function applyOutreachEnrollmentEvent(\n enrollment: Pick<OutreachEnrollment, 'status' | 'nextDueAtMs'>,\n event: OutreachEnrollmentEvent,\n): OutreachEnrollmentEventResult {\n const from = enrollment.status\n const to = EVENT_TARGET[event.type]\n if (!to) return { patch: null, error: 'That is not something that happens to an enrollment.' }\n if (from === to) return { patch: null, error: null }\n if (!canTransitionOutreachEnrollment(from, to)) {\n return {\n patch: null,\n error: `This enrollment ${STATUS_WORDS[from]}, so it can't be marked as one that ${STATUS_WORDS[to]}.`,\n }\n }\n if (event.type === 'resume') {\n return {\n patch: {\n status: 'active',\n stopReason: null,\n stopDetail: null,\n stoppedAtMs: null,\n stoppedByUid: null,\n nextDueAtMs: enrollment.nextDueAtMs,\n },\n error: null,\n }\n }\n const reason: OutreachStopReason =\n event.type === 'pause'\n ? 'manual'\n : event.type === 'stop'\n ? (event.reason ?? 'manual')\n : event.type === 'reply'\n ? 'reply'\n : event.type === 'opt_out'\n ? event.reason\n : event.type === 'bounce'\n ? 'hard_bounce'\n : 'send_failed'\n const allowed = OUTREACH_STOP_REASONS_BY_STATUS[to as keyof typeof OUTREACH_STOP_REASONS_BY_STATUS]\n if (!allowed?.includes(reason)) {\n return { patch: null, error: `\"${reason}\" isn't a reason an enrollment ${STATUS_WORDS[to]}.` }\n }\n const byUid = 'byUid' in event && typeof event.byUid === 'string' && event.byUid ? event.byUid : null\n return {\n patch: {\n status: to,\n stopReason: reason,\n stopDetail: detailOf(event.detail),\n stoppedAtMs: event.atMs,\n stoppedByUid: byUid,\n // A paused enrollment keeps its place, so resuming it picks up where it\n // stopped; every other status has nothing left waiting.\n nextDueAtMs: to === 'paused' ? enrollment.nextDueAtMs : null,\n },\n error: null,\n }\n}\n\ntype ScheduleMailbox = Pick<OutreachMailbox, 'timezone' | 'window'>\ntype ScheduleSequence = Pick<OutreachSequence, 'steps' | 'settings'>\n\n/** When a new enrollment's first step comes due, or `null` when it cannot be placed. */\nexport function planOutreachFirstDue(input: {\n sequence: ScheduleSequence\n mailbox: ScheduleMailbox\n enrolledAtMs: number\n random: OutreachRandom\n}): number | null {\n const first = input.sequence.steps?.[0]\n if (!first) return null\n return scheduleOutreachDue({\n fromMs: input.enrolledAtMs,\n delayBusinessDays: first.delayBusinessDays,\n timeZone: input.mailbox.timezone,\n window: effectiveOutreachWindow(input.sequence.settings?.window, input.mailbox.window),\n random: input.random,\n })\n}\n\n/** Stamps the attestations a rep ticked with who ticked them and when; unknown kinds are dropped. */\nexport function outreachAttestationsFrom(\n kinds: readonly unknown[],\n uid: string,\n atMs: number,\n): OutreachAttestations {\n const attestations: OutreachAttestations = {}\n for (const kind of kinds ?? []) {\n if ((OUTREACH_ATTESTATION_KINDS as readonly unknown[]).includes(kind)) {\n attestations[kind as OutreachAttestationKind] = { uid, atMs }\n }\n }\n return attestations\n}\n\nexport interface OutreachEnrollmentDraft {\n id: string\n sequence: Pick<OutreachSequence, 'id' | 'hostId' | 'mailboxId' | 'steps' | 'settings'>\n mailbox: ScheduleMailbox\n contactId: string\n /** The contact's name as the sending site knows it; `''` for none. */\n contactName?: string | null\n /** The address, as the gates normalized it. */\n email: string\n /** The gates' `cold`. */\n cold: boolean\n personalLine: string\n attestations: OutreachAttestations\n enrolledByUid: string\n nowMs: number\n random: OutreachRandom\n}\n\n/**\n * A new enrollment document, first step scheduled — one builder for every\n * door that enrolls, so an enrollment made from a contact and one made from\n * a list are the same shape. Call it after the gates allowed the person.\n */\nexport function buildOutreachEnrollment(draft: OutreachEnrollmentDraft): OutreachEnrollment {\n return {\n id: draft.id,\n sequenceId: draft.sequence.id,\n contactId: draft.contactId,\n contactName: String(draft.contactName ?? '').replace(/\\s+/g, ' ').trim().slice(0, 200),\n email: String(draft.email ?? '').trim().toLowerCase(),\n hostId: draft.sequence.hostId,\n mailboxId: draft.sequence.mailboxId,\n stepIndex: 0,\n nextDueAtMs: planOutreachFirstDue({\n sequence: draft.sequence,\n mailbox: draft.mailbox,\n enrolledAtMs: draft.nowMs,\n random: draft.random,\n }),\n status: 'active',\n stopReason: null,\n stopDetail: null,\n stoppedAtMs: null,\n stoppedByUid: null,\n personalLine: normalizeOutreachPersonalLine(draft.personalLine),\n cold: draft.cold === true,\n attestations: { ...(draft.attestations ?? {}) },\n enrolledByUid: draft.enrolledByUid,\n gmailThreadId: null,\n gmailThreadIds: [],\n threadSubject: null,\n messageIds: [],\n lastSentAtMs: null,\n createdAtMs: draft.nowMs,\n updatedAtMs: draft.nowMs,\n }\n}\n\n/** What the provider reported for an email step that was sent. */\nexport interface OutreachSentEmail {\n /** The `Message-ID` header the message went out with, angle brackets included. */\n messageId: string\n /** The provider's thread id. */\n threadId: string\n /** The subject as sent. */\n subject: string\n}\n\nexport interface OutreachStepCompletionInput {\n enrollment: Pick<\n OutreachEnrollment,\n 'status' | 'stepIndex' | 'gmailThreadId' | 'gmailThreadIds' | 'messageIds' | 'threadSubject'\n >\n sequence: ScheduleSequence\n mailbox: ScheduleMailbox\n /** When the step ran. */\n completedAtMs: number\n /** For an email step, what was sent; not read for a task. */\n sent?: OutreachSentEmail | null\n random: OutreachRandom\n}\n\n/** The fields a completed step writes. */\nexport interface OutreachStepCompletionPatch {\n status: 'active' | 'finished'\n stepIndex: number\n nextDueAtMs: number | null\n lastSentAtMs: number\n gmailThreadId?: string\n gmailThreadIds?: string[]\n threadSubject?: string\n messageIds?: string[]\n}\n\nexport interface OutreachStepCompletionResult {\n patch: OutreachStepCompletionPatch | null\n /**\n * Why there is no patch, or — beside one — that the next step could not\n * be placed on the calendar (a window that never opens, a zone `Intl` does\n * not know), which leaves the enrollment with nothing due until a member\n * fixes the window and resumes it.\n */\n error: string | null\n}\n\n/**\n * What running the current step writes: the send recorded on the thread\n * fields, and the next step scheduled — or the enrollment `finished` when\n * that was the last.\n */\nexport function planOutreachStepCompletion(\n input: OutreachStepCompletionInput,\n): OutreachStepCompletionResult {\n const { enrollment, sequence } = input\n if (enrollment.status !== 'active') {\n return { patch: null, error: 'Only an active enrollment runs its steps.' }\n }\n const steps = sequence.steps ?? []\n const step = steps[enrollment.stepIndex]\n if (!step) return { patch: null, error: 'This enrollment has no step at its position.' }\n const patch: OutreachStepCompletionPatch = {\n status: 'active',\n stepIndex: enrollment.stepIndex + 1,\n nextDueAtMs: null,\n lastSentAtMs: input.completedAtMs,\n }\n if (step.kind === 'email') {\n const sent = input.sent\n const messageId = String(sent?.messageId ?? '').trim()\n const threadId = String(sent?.threadId ?? '').trim()\n if (!messageId || !threadId) {\n return { patch: null, error: 'An email step needs the sent message and thread ids.' }\n }\n const threadIds = [...(enrollment.gmailThreadIds ?? [])]\n if (!threadIds.includes(threadId)) threadIds.push(threadId)\n patch.gmailThreadId = threadId\n patch.gmailThreadIds = threadIds\n if (isInThreadEmailStep(steps, enrollment.stepIndex) && enrollment.threadSubject) {\n patch.messageIds = [...(enrollment.messageIds ?? []), messageId]\n } else {\n patch.threadSubject = String(sent.subject ?? '').trim()\n patch.messageIds = [messageId]\n }\n }\n const next = steps[patch.stepIndex]\n if (!next) {\n patch.status = 'finished'\n return { patch, error: null }\n }\n patch.nextDueAtMs = scheduleOutreachDue({\n fromMs: input.completedAtMs,\n delayBusinessDays: next.delayBusinessDays,\n timeZone: input.mailbox.timezone,\n window: effectiveOutreachWindow(sequence.settings?.window, input.mailbox.window),\n random: input.random,\n })\n return {\n patch,\n error:\n patch.nextDueAtMs === null\n ? \"The next step couldn't be scheduled: check the sending window and the mailbox's timezone.\"\n : null,\n }\n}\n"],"names":["OUTREACH_ATTESTATION_KINDS","OUTREACH_STOP_REASONS_BY_STATUS","normalizeOutreachPersonalLine","effectiveOutreachWindow","scheduleOutreachDue","isInThreadEmailStep","OUTREACH_ENROLLMENT_TRANSITIONS","active","paused","finished","stopped","failed","replied","bounced","opted_out","canTransitionOutreachEnrollment","from","to","includes","STATUS_WORDS","EVENT_TARGET","pause","resume","stop","reply","opt_out","bounce","send_failed","detailOf","value","text","replace","trim","slice","applyOutreachEnrollmentEvent","enrollment","event","status","type","patch","error","stopReason","stopDetail","stoppedAtMs","stoppedByUid","nextDueAtMs","reason","allowed","byUid","detail","atMs","planOutreachFirstDue","input","first","sequence","steps","fromMs","enrolledAtMs","delayBusinessDays","timeZone","mailbox","timezone","window","settings","random","outreachAttestationsFrom","kinds","uid","attestations","kind","buildOutreachEnrollment","draft","id","sequenceId","contactId","contactName","String","email","toLowerCase","hostId","mailboxId","stepIndex","nowMs","personalLine","cold","enrolledByUid","gmailThreadId","gmailThreadIds","threadSubject","messageIds","lastSentAtMs","createdAtMs","updatedAtMs","planOutreachStepCompletion","step","completedAtMs","sent","messageId","threadId","threadIds","push","subject","next"],"mappings":"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"}
@@ -0,0 +1,125 @@
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 ContactSource } from '@aglyn/aglyn/app-utils/contacts';
18
+ import type { TopicSubscriptionState } from '@aglyn/aglyn/app-utils/email-topics';
19
+ import { type MemberAddresses } from '@aglyn/aglyn/app-utils/member-email-aliases';
20
+ import { type OutreachAttestationKind, type OutreachAttestations, type OutreachEnrollmentStatus, type OutreachSequenceSettings } from '../model/outreach.types';
21
+ import { type OutreachRecipientCountry } from './recipient-country';
22
+ /**
23
+ * The capture doors that mean a person came to the business: a form, a
24
+ * sign-up to the site or its newsletter, an order, a booking. `api`,
25
+ * `import` and `manual` are how a business adds people it found, which is
26
+ * exactly what cold means.
27
+ */
28
+ export declare const OUTREACH_INBOUND_CONTACT_SOURCES: readonly ContactSource[];
29
+ /** The enrollment statuses that hold a person: one sequence at a time. */
30
+ export declare const OUTREACH_OPEN_ENROLLMENT_STATUSES: readonly OutreachEnrollmentStatus[];
31
+ /**
32
+ * What the runtime looked up about the address before asking. Every field is
33
+ * `null` when its read failed, and a `null` refuses.
34
+ */
35
+ export interface OutreachGateLookups {
36
+ /**
37
+ * `emailSuppressions/{key}` holds a record in force — a hard bounce or a
38
+ * spam complaint anywhere on the platform (`isEmailSuppressed`).
39
+ */
40
+ platformSuppressed: boolean | null;
41
+ /** `hosts/{hostId}/suppressions/{key}` exists for the sequence's site. */
42
+ hostSuppressed: boolean | null;
43
+ /**
44
+ * The address's standing on the site's `sales` topic —
45
+ * `readTopicSubscriptionState` of the `sales` entry in
46
+ * `hosts/{hostId}/topicOptOuts/{key}`. Only `opted-out` refuses: a pending
47
+ * confirmation is about a subscription, and a sales email does not rest on
48
+ * one.
49
+ */
50
+ salesTopicState: TopicSubscriptionState | null;
51
+ /** The address is on the organization's Outreach do-not-contact list. */
52
+ doNotContact: boolean | null;
53
+ /**
54
+ * The organization's roster: each member's sign-in address and confirmed
55
+ * aliases, as `crmInboundRoster` builds it.
56
+ */
57
+ workspaceMembers: readonly MemberAddresses[] | null;
58
+ /**
59
+ * The person's enrollments that are `active` or `paused`, found by contact
60
+ * id or by address. Other statuses are ignored, so passing every
61
+ * enrollment is safe too.
62
+ */
63
+ openEnrollments: ReadonlyArray<{
64
+ id: string;
65
+ sequenceId: string;
66
+ status: OutreachEnrollmentStatus;
67
+ }> | null;
68
+ /**
69
+ * An email the person wrote (`direction: 'inbound'`) is filed on the
70
+ * contact. `null` reads as `false`, which only makes the contact cold and
71
+ * the rules stricter.
72
+ */
73
+ hasInboundEmail: boolean | null;
74
+ }
75
+ export interface OutreachGateInput {
76
+ /** The address the steps go to. */
77
+ email: string;
78
+ /** The contact document as stored (`orgs/{orgId}/contacts/{id}`), or `null` when it is gone. */
79
+ contact: Record<string, unknown> | null | undefined;
80
+ /** The consent group of the sequence's site: whose facet holds stage, sources and address. */
81
+ contactGroupId: string;
82
+ /** The company document the contact is filed under, when there is one. */
83
+ company?: Record<string, unknown> | null;
84
+ /** The sequence's settings as stored; a missing field reads as its default. */
85
+ settings: Partial<OutreachSequenceSettings> | null | undefined;
86
+ /** The rep's signal sentence. */
87
+ personalLine: string | null | undefined;
88
+ attestations: OutreachAttestations | null | undefined;
89
+ /** The enrollment being re-checked before a send, so it does not count as another. */
90
+ enrollmentId?: string | null;
91
+ lookups: OutreachGateLookups;
92
+ }
93
+ export type OutreachGateCode = 'contact_missing' | 'invalid_email' | 'email_not_on_contact' | 'free_mail' | 'country_not_allowed' | 'country_unknown' | 'platform_suppressed' | 'host_suppressed' | 'sales_opted_out' | 'do_not_contact' | 'workspace_member' | 'customer' | 'already_enrolled' | 'attestations_missing' | 'personal_line_missing' | 'personal_line_too_long';
94
+ export interface OutreachGateBlock {
95
+ code: OutreachGateCode;
96
+ /** A sentence a rep can act on. */
97
+ reason: string;
98
+ }
99
+ export interface OutreachGateResult {
100
+ allowed: boolean;
101
+ /** The address, normalized, or `null` when it is not one. */
102
+ email: string | null;
103
+ cold: boolean;
104
+ country: OutreachRecipientCountry;
105
+ /** Every gate that refuses, in gate order. */
106
+ blocks: OutreachGateBlock[];
107
+ /** The attestations a cold contact still needs, in the order they are asked. */
108
+ missingAttestations: OutreachAttestationKind[];
109
+ }
110
+ /**
111
+ * Whether a contact came to the sending site's business through no inbound
112
+ * door — see the module note.
113
+ */
114
+ export declare function isColdOutreachContact(contact: Record<string, unknown> | null | undefined, contactGroupId: string, hasInboundEmail: boolean): boolean;
115
+ /**
116
+ * Whether the sending site's business counts the contact as a customer: a
117
+ * `customer` stage, the `evangelist` stage past it, or an order on record.
118
+ */
119
+ export declare function isOutreachCustomer(contact: Record<string, unknown> | null | undefined, contactGroupId: string): boolean;
120
+ /** A confirmation counts only when it says who confirmed it and when. */
121
+ export declare function outreachAttestationHolds(attestations: OutreachAttestations | null | undefined, kind: OutreachAttestationKind): boolean;
122
+ /** The personal line as stored: one line, whitespace settled. */
123
+ export declare function normalizeOutreachPersonalLine(value: unknown): string;
124
+ /** Every gate an address and a contact must pass — see the module note. */
125
+ export declare function evaluateOutreachGates(input: OutreachGateInput): OutreachGateResult;