@antelopejs/dms-mailing 0.2.2

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 (550) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +162 -0
  3. package/dist/antelope-modules.d.ts +32 -0
  4. package/dist/antelope-modules.js +36 -0
  5. package/dist/antelope-modules.js.map +1 -0
  6. package/dist/automation/email-event-trigger.d.ts +16 -0
  7. package/dist/automation/email-event-trigger.js +70 -0
  8. package/dist/automation/email-event-trigger.js.map +1 -0
  9. package/dist/automation/index.d.ts +4 -0
  10. package/dist/automation/index.js +39 -0
  11. package/dist/automation/index.js.map +1 -0
  12. package/dist/automation/send-template-action.d.ts +13 -0
  13. package/dist/automation/send-template-action.js +71 -0
  14. package/dist/automation/send-template-action.js.map +1 -0
  15. package/dist/config.d.ts +5 -0
  16. package/dist/config.js +13 -0
  17. package/dist/config.js.map +1 -0
  18. package/dist/constants.d.ts +28 -0
  19. package/dist/constants.js +32 -0
  20. package/dist/constants.js.map +1 -0
  21. package/dist/crons/index.d.ts +1 -0
  22. package/dist/crons/index.js +18 -0
  23. package/dist/crons/index.js.map +1 -0
  24. package/dist/crons/retention.d.ts +18 -0
  25. package/dist/crons/retention.js +107 -0
  26. package/dist/crons/retention.js.map +1 -0
  27. package/dist/data/body.d.ts +1 -0
  28. package/dist/data/body.js +18 -0
  29. package/dist/data/body.js.map +1 -0
  30. package/dist/data/index.d.ts +3 -0
  31. package/dist/data/index.js +20 -0
  32. package/dist/data/index.js.map +1 -0
  33. package/dist/data/sends-table.d.ts +28 -0
  34. package/dist/data/sends-table.js +161 -0
  35. package/dist/data/sends-table.js.map +1 -0
  36. package/dist/data/templates-table.d.ts +31 -0
  37. package/dist/data/templates-table.js +173 -0
  38. package/dist/data/templates-table.js.map +1 -0
  39. package/dist/data-types/categories-type.d.ts +35 -0
  40. package/dist/data-types/categories-type.js +37 -0
  41. package/dist/data-types/categories-type.js.map +1 -0
  42. package/dist/data-types/category-type.d.ts +8 -0
  43. package/dist/data-types/category-type.js +43 -0
  44. package/dist/data-types/category-type.js.map +1 -0
  45. package/dist/data-types/index.d.ts +2 -0
  46. package/dist/data-types/index.js +19 -0
  47. package/dist/data-types/index.js.map +1 -0
  48. package/dist/db/index.d.ts +2 -0
  49. package/dist/db/index.js +19 -0
  50. package/dist/db/index.js.map +1 -0
  51. package/dist/db/models/index.d.ts +4 -0
  52. package/dist/db/models/index.js +21 -0
  53. package/dist/db/models/index.js.map +1 -0
  54. package/dist/db/models/send-events.model.d.ts +27 -0
  55. package/dist/db/models/send-events.model.js +35 -0
  56. package/dist/db/models/send-events.model.js.map +1 -0
  57. package/dist/db/models/sends.model.d.ts +32 -0
  58. package/dist/db/models/sends.model.js +115 -0
  59. package/dist/db/models/sends.model.js.map +1 -0
  60. package/dist/db/models/settings.model.d.ts +23 -0
  61. package/dist/db/models/settings.model.js +13 -0
  62. package/dist/db/models/settings.model.js.map +1 -0
  63. package/dist/db/models/templates.model.d.ts +23 -0
  64. package/dist/db/models/templates.model.js +13 -0
  65. package/dist/db/models/templates.model.js.map +1 -0
  66. package/dist/db/tables/index.d.ts +4 -0
  67. package/dist/db/tables/index.js +21 -0
  68. package/dist/db/tables/index.js.map +1 -0
  69. package/dist/db/tables/send-events.table.d.ts +10 -0
  70. package/dist/db/tables/send-events.table.js +44 -0
  71. package/dist/db/tables/send-events.table.js.map +1 -0
  72. package/dist/db/tables/sends.table.d.ts +26 -0
  73. package/dist/db/tables/sends.table.js +113 -0
  74. package/dist/db/tables/sends.table.js.map +1 -0
  75. package/dist/db/tables/settings.table.d.ts +15 -0
  76. package/dist/db/tables/settings.table.js +68 -0
  77. package/dist/db/tables/settings.table.js.map +1 -0
  78. package/dist/db/tables/templates.table.d.ts +18 -0
  79. package/dist/db/tables/templates.table.js +81 -0
  80. package/dist/db/tables/templates.table.js.map +1 -0
  81. package/dist/engine/conditions.d.ts +2 -0
  82. package/dist/engine/conditions.js +20 -0
  83. package/dist/engine/conditions.js.map +1 -0
  84. package/dist/engine/coverage.d.ts +4 -0
  85. package/dist/engine/coverage.js +45 -0
  86. package/dist/engine/coverage.js.map +1 -0
  87. package/dist/engine/fallback-html.d.ts +4 -0
  88. package/dist/engine/fallback-html.js +84 -0
  89. package/dist/engine/fallback-html.js.map +1 -0
  90. package/dist/engine/index.d.ts +7 -0
  91. package/dist/engine/index.js +24 -0
  92. package/dist/engine/index.js.map +1 -0
  93. package/dist/engine/paths.d.ts +3 -0
  94. package/dist/engine/paths.js +32 -0
  95. package/dist/engine/paths.js.map +1 -0
  96. package/dist/engine/resolve.d.ts +4 -0
  97. package/dist/engine/resolve.js +139 -0
  98. package/dist/engine/resolve.js.map +1 -0
  99. package/dist/engine/tokens.d.ts +7 -0
  100. package/dist/engine/tokens.js +29 -0
  101. package/dist/engine/tokens.js.map +1 -0
  102. package/dist/engine/walk.d.ts +3 -0
  103. package/dist/engine/walk.js +14 -0
  104. package/dist/engine/walk.js.map +1 -0
  105. package/dist/hooks/index.d.ts +1 -0
  106. package/dist/hooks/index.js +18 -0
  107. package/dist/hooks/index.js.map +1 -0
  108. package/dist/hooks/tenant-export.d.ts +14 -0
  109. package/dist/hooks/tenant-export.js +84 -0
  110. package/dist/hooks/tenant-export.js.map +1 -0
  111. package/dist/implementations/dms-mailing/index.d.ts +2 -0
  112. package/dist/implementations/dms-mailing/index.js +8 -0
  113. package/dist/implementations/dms-mailing/index.js.map +1 -0
  114. package/dist/index.d.ts +13 -0
  115. package/dist/index.js +95 -0
  116. package/dist/index.js.map +1 -0
  117. package/dist/pages/editor.d.ts +9 -0
  118. package/dist/pages/editor.js +30 -0
  119. package/dist/pages/editor.js.map +1 -0
  120. package/dist/pages/index.d.ts +7 -0
  121. package/dist/pages/index.js +24 -0
  122. package/dist/pages/index.js.map +1 -0
  123. package/dist/pages/module.d.ts +1 -0
  124. package/dist/pages/module.js +12 -0
  125. package/dist/pages/module.js.map +1 -0
  126. package/dist/pages/overview.d.ts +14 -0
  127. package/dist/pages/overview.js +92 -0
  128. package/dist/pages/overview.js.map +1 -0
  129. package/dist/pages/sends.d.ts +13 -0
  130. package/dist/pages/sends.js +125 -0
  131. package/dist/pages/sends.js.map +1 -0
  132. package/dist/pages/settings/form.d.ts +1 -0
  133. package/dist/pages/settings/form.js +68 -0
  134. package/dist/pages/settings/form.js.map +1 -0
  135. package/dist/pages/settings/index.d.ts +2 -0
  136. package/dist/pages/settings/index.js +19 -0
  137. package/dist/pages/settings/index.js.map +1 -0
  138. package/dist/pages/settings/page.d.ts +10 -0
  139. package/dist/pages/settings/page.js +35 -0
  140. package/dist/pages/settings/page.js.map +1 -0
  141. package/dist/pages/templates.d.ts +10 -0
  142. package/dist/pages/templates.js +140 -0
  143. package/dist/pages/templates.js.map +1 -0
  144. package/dist/permissions.d.ts +1 -0
  145. package/dist/permissions.js +37 -0
  146. package/dist/permissions.js.map +1 -0
  147. package/dist/quick-actions.d.ts +2 -0
  148. package/dist/quick-actions.js +18 -0
  149. package/dist/quick-actions.js.map +1 -0
  150. package/dist/realtime.d.ts +4 -0
  151. package/dist/realtime.js +44 -0
  152. package/dist/realtime.js.map +1 -0
  153. package/dist/routes/events.d.ts +9 -0
  154. package/dist/routes/events.js +63 -0
  155. package/dist/routes/events.js.map +1 -0
  156. package/dist/routes/index.d.ts +7 -0
  157. package/dist/routes/index.js +24 -0
  158. package/dist/routes/index.js.map +1 -0
  159. package/dist/routes/metrics.d.ts +54 -0
  160. package/dist/routes/metrics.js +235 -0
  161. package/dist/routes/metrics.js.map +1 -0
  162. package/dist/routes/period.d.ts +5 -0
  163. package/dist/routes/period.js +36 -0
  164. package/dist/routes/period.js.map +1 -0
  165. package/dist/routes/provider.d.ts +13 -0
  166. package/dist/routes/provider.js +41 -0
  167. package/dist/routes/provider.js.map +1 -0
  168. package/dist/routes/sends.d.ts +31 -0
  169. package/dist/routes/sends.js +178 -0
  170. package/dist/routes/sends.js.map +1 -0
  171. package/dist/routes/settings.d.ts +14 -0
  172. package/dist/routes/settings.js +72 -0
  173. package/dist/routes/settings.js.map +1 -0
  174. package/dist/routes/templates.d.ts +61 -0
  175. package/dist/routes/templates.js +230 -0
  176. package/dist/routes/templates.js.map +1 -0
  177. package/dist/services/attention.d.ts +23 -0
  178. package/dist/services/attention.js +104 -0
  179. package/dist/services/attention.js.map +1 -0
  180. package/dist/services/categories.d.ts +34 -0
  181. package/dist/services/categories.js +34 -0
  182. package/dist/services/categories.js.map +1 -0
  183. package/dist/services/events.d.ts +10 -0
  184. package/dist/services/events.js +89 -0
  185. package/dist/services/events.js.map +1 -0
  186. package/dist/services/metrics.d.ts +34 -0
  187. package/dist/services/metrics.js +103 -0
  188. package/dist/services/metrics.js.map +1 -0
  189. package/dist/services/notify.d.ts +20 -0
  190. package/dist/services/notify.js +63 -0
  191. package/dist/services/notify.js.map +1 -0
  192. package/dist/services/permissions.d.ts +2 -0
  193. package/dist/services/permissions.js +15 -0
  194. package/dist/services/permissions.js.map +1 -0
  195. package/dist/services/provider.d.ts +6 -0
  196. package/dist/services/provider.js +25 -0
  197. package/dist/services/provider.js.map +1 -0
  198. package/dist/services/render.d.ts +6 -0
  199. package/dist/services/render.js +21 -0
  200. package/dist/services/render.js.map +1 -0
  201. package/dist/services/send.d.ts +49 -0
  202. package/dist/services/send.js +259 -0
  203. package/dist/services/send.js.map +1 -0
  204. package/dist/services/settings.d.ts +4 -0
  205. package/dist/services/settings.js +61 -0
  206. package/dist/services/settings.js.map +1 -0
  207. package/dist/services/status.d.ts +3 -0
  208. package/dist/services/status.js +16 -0
  209. package/dist/services/status.js.map +1 -0
  210. package/dist/services/templates.d.ts +17 -0
  211. package/dist/services/templates.js +92 -0
  212. package/dist/services/templates.js.map +1 -0
  213. package/dist/test/antelope.test.d.ts +2 -0
  214. package/dist/test/antelope.test.js +179 -0
  215. package/dist/test/antelope.test.js.map +1 -0
  216. package/dist/test/helpers/http.d.ts +4 -0
  217. package/dist/test/helpers/http.js +28 -0
  218. package/dist/test/helpers/http.js.map +1 -0
  219. package/dist/test/helpers/owner.d.ts +5 -0
  220. package/dist/test/helpers/owner.js +30 -0
  221. package/dist/test/helpers/owner.js.map +1 -0
  222. package/dist/test/integration/events.test.d.ts +1 -0
  223. package/dist/test/integration/events.test.js +132 -0
  224. package/dist/test/integration/events.test.js.map +1 -0
  225. package/dist/test/integration/retention.test.d.ts +1 -0
  226. package/dist/test/integration/retention.test.js +291 -0
  227. package/dist/test/integration/retention.test.js.map +1 -0
  228. package/dist/test/integration/sends.test.d.ts +1 -0
  229. package/dist/test/integration/sends.test.js +157 -0
  230. package/dist/test/integration/sends.test.js.map +1 -0
  231. package/dist/test/integration/settings.test.d.ts +1 -0
  232. package/dist/test/integration/settings.test.js +45 -0
  233. package/dist/test/integration/settings.test.js.map +1 -0
  234. package/dist/test/integration/setup.test.d.ts +1 -0
  235. package/dist/test/integration/setup.test.js +32 -0
  236. package/dist/test/integration/setup.test.js.map +1 -0
  237. package/dist/test/integration/templates.test.d.ts +1 -0
  238. package/dist/test/integration/templates.test.js +167 -0
  239. package/dist/test/integration/templates.test.js.map +1 -0
  240. package/dist/test/integration/tenant-export.test.d.ts +1 -0
  241. package/dist/test/integration/tenant-export.test.js +85 -0
  242. package/dist/test/integration/tenant-export.test.js.map +1 -0
  243. package/dist/test/unit/attention.test.d.ts +1 -0
  244. package/dist/test/unit/attention.test.js +109 -0
  245. package/dist/test/unit/attention.test.js.map +1 -0
  246. package/dist/test/unit/automation.test.d.ts +1 -0
  247. package/dist/test/unit/automation.test.js +104 -0
  248. package/dist/test/unit/automation.test.js.map +1 -0
  249. package/dist/test/unit/categories.test.d.ts +1 -0
  250. package/dist/test/unit/categories.test.js +38 -0
  251. package/dist/test/unit/categories.test.js.map +1 -0
  252. package/dist/test/unit/engine/conditions.test.d.ts +1 -0
  253. package/dist/test/unit/engine/conditions.test.js +23 -0
  254. package/dist/test/unit/engine/conditions.test.js.map +1 -0
  255. package/dist/test/unit/engine/coverage.test.d.ts +1 -0
  256. package/dist/test/unit/engine/coverage.test.js +106 -0
  257. package/dist/test/unit/engine/coverage.test.js.map +1 -0
  258. package/dist/test/unit/engine/fallback-html.test.d.ts +1 -0
  259. package/dist/test/unit/engine/fallback-html.test.js +78 -0
  260. package/dist/test/unit/engine/fallback-html.test.js.map +1 -0
  261. package/dist/test/unit/engine/paths.test.d.ts +1 -0
  262. package/dist/test/unit/engine/paths.test.js +33 -0
  263. package/dist/test/unit/engine/paths.test.js.map +1 -0
  264. package/dist/test/unit/engine/resolve.test.d.ts +1 -0
  265. package/dist/test/unit/engine/resolve.test.js +157 -0
  266. package/dist/test/unit/engine/resolve.test.js.map +1 -0
  267. package/dist/test/unit/engine/tokens.test.d.ts +1 -0
  268. package/dist/test/unit/engine/tokens.test.js +26 -0
  269. package/dist/test/unit/engine/tokens.test.js.map +1 -0
  270. package/dist/test/unit/events.test.d.ts +1 -0
  271. package/dist/test/unit/events.test.js +13 -0
  272. package/dist/test/unit/events.test.js.map +1 -0
  273. package/dist/test/unit/metrics.test.d.ts +1 -0
  274. package/dist/test/unit/metrics.test.js +108 -0
  275. package/dist/test/unit/metrics.test.js.map +1 -0
  276. package/dist/test/unit/notify.test.d.ts +1 -0
  277. package/dist/test/unit/notify.test.js +49 -0
  278. package/dist/test/unit/notify.test.js.map +1 -0
  279. package/dist/test/unit/period.test.d.ts +1 -0
  280. package/dist/test/unit/period.test.js +44 -0
  281. package/dist/test/unit/period.test.js.map +1 -0
  282. package/dist/test/unit/provider.test.d.ts +1 -0
  283. package/dist/test/unit/provider.test.js +47 -0
  284. package/dist/test/unit/provider.test.js.map +1 -0
  285. package/dist/test/unit/realtime.test.d.ts +1 -0
  286. package/dist/test/unit/realtime.test.js +13 -0
  287. package/dist/test/unit/realtime.test.js.map +1 -0
  288. package/dist/test/unit/render.test.d.ts +1 -0
  289. package/dist/test/unit/render.test.js +23 -0
  290. package/dist/test/unit/render.test.js.map +1 -0
  291. package/dist/test/unit/retention.test.d.ts +1 -0
  292. package/dist/test/unit/retention.test.js +66 -0
  293. package/dist/test/unit/retention.test.js.map +1 -0
  294. package/dist/test/unit/send-batch.test.d.ts +1 -0
  295. package/dist/test/unit/send-batch.test.js +46 -0
  296. package/dist/test/unit/send-batch.test.js.map +1 -0
  297. package/dist/test/unit/send.test.d.ts +1 -0
  298. package/dist/test/unit/send.test.js +120 -0
  299. package/dist/test/unit/send.test.js.map +1 -0
  300. package/dist/test/unit/smoke.test.d.ts +1 -0
  301. package/dist/test/unit/smoke.test.js +10 -0
  302. package/dist/test/unit/smoke.test.js.map +1 -0
  303. package/dist/test/unit/status.test.d.ts +1 -0
  304. package/dist/test/unit/status.test.js +16 -0
  305. package/dist/test/unit/status.test.js.map +1 -0
  306. package/dist/test/unit/templates.test.d.ts +1 -0
  307. package/dist/test/unit/templates.test.js +48 -0
  308. package/dist/test/unit/templates.test.js.map +1 -0
  309. package/dist/test/unit/tenant-export.test.d.ts +1 -0
  310. package/dist/test/unit/tenant-export.test.js +59 -0
  311. package/dist/test/unit/tenant-export.test.js.map +1 -0
  312. package/dist/test/unit/webhook-secret.test.d.ts +1 -0
  313. package/dist/test/unit/webhook-secret.test.js +42 -0
  314. package/dist/test/unit/webhook-secret.test.js.map +1 -0
  315. package/dist/types/blocks.d.ts +2 -0
  316. package/dist/types/blocks.js +3 -0
  317. package/dist/types/blocks.js.map +1 -0
  318. package/dist/types/index.d.ts +4 -0
  319. package/dist/types/index.js +21 -0
  320. package/dist/types/index.js.map +1 -0
  321. package/dist/types/resolved.d.ts +68 -0
  322. package/dist/types/resolved.js +3 -0
  323. package/dist/types/resolved.js.map +1 -0
  324. package/dist/types/sends.d.ts +14 -0
  325. package/dist/types/sends.js +19 -0
  326. package/dist/types/sends.js.map +1 -0
  327. package/dist/types/settings.d.ts +16 -0
  328. package/dist/types/settings.js +11 -0
  329. package/dist/types/settings.js.map +1 -0
  330. package/dist/validation/settings.schema.d.ts +63 -0
  331. package/dist/validation/settings.schema.js +29 -0
  332. package/dist/validation/settings.schema.js.map +1 -0
  333. package/dist/validation/templates.schema.d.ts +248 -0
  334. package/dist/validation/templates.schema.js +126 -0
  335. package/dist/validation/templates.schema.js.map +1 -0
  336. package/frontend-vue/.editorconfig +12 -0
  337. package/frontend-vue/.prettierrc +11 -0
  338. package/frontend-vue/app/components/AttentionCard.vue +78 -0
  339. package/frontend-vue/app/components/CategoriesInput.vue +126 -0
  340. package/frontend-vue/app/components/CategoryInput.vue +67 -0
  341. package/frontend-vue/app/components/Editor.vue +63 -0
  342. package/frontend-vue/app/components/FunnelCard.vue +66 -0
  343. package/frontend-vue/app/components/HtmlPreviewModal.vue +20 -0
  344. package/frontend-vue/app/components/NewTemplateModal.vue +158 -0
  345. package/frontend-vue/app/components/ProviderChip.vue +39 -0
  346. package/frontend-vue/app/components/SendDrawer.vue +298 -0
  347. package/frontend-vue/app/components/TemplateCard.vue +127 -0
  348. package/frontend-vue/app/components/TemplateDrawer.vue +394 -0
  349. package/frontend-vue/app/components/TemplateGallery.vue +112 -0
  350. package/frontend-vue/app/components/TemplatePreviewFrame.vue +103 -0
  351. package/frontend-vue/app/components/TestSendModal.vue +162 -0
  352. package/frontend-vue/app/components/TokenText.vue +26 -0
  353. package/frontend-vue/app/components/blocks/Button.vue +19 -0
  354. package/frontend-vue/app/components/blocks/Code.vue +17 -0
  355. package/frontend-vue/app/components/blocks/Divider.vue +3 -0
  356. package/frontend-vue/app/components/blocks/Footer.vue +22 -0
  357. package/frontend-vue/app/components/blocks/Heading.vue +18 -0
  358. package/frontend-vue/app/components/blocks/Hero.vue +32 -0
  359. package/frontend-vue/app/components/blocks/List.vue +33 -0
  360. package/frontend-vue/app/components/blocks/Paragraph.vue +18 -0
  361. package/frontend-vue/app/components/blocks/Total.vue +18 -0
  362. package/frontend-vue/app/components/editor/EditorBlock.vue +121 -0
  363. package/frontend-vue/app/components/editor/EditorBlockList.vue +44 -0
  364. package/frontend-vue/app/components/editor/EditorBlockSettings.vue +186 -0
  365. package/frontend-vue/app/components/editor/EditorCanvas.vue +91 -0
  366. package/frontend-vue/app/components/editor/EditorConditionFields.vue +88 -0
  367. package/frontend-vue/app/components/editor/EditorDataPanel.vue +128 -0
  368. package/frontend-vue/app/components/editor/EditorDraftBar.vue +35 -0
  369. package/frontend-vue/app/components/editor/EditorIfBlock.vue +120 -0
  370. package/frontend-vue/app/components/editor/EditorMissingLocale.vue +62 -0
  371. package/frontend-vue/app/components/editor/EditorPalette.vue +62 -0
  372. package/frontend-vue/app/components/editor/EditorRail.vue +58 -0
  373. package/frontend-vue/app/components/editor/EditorSubjectSettings.vue +37 -0
  374. package/frontend-vue/app/components/editor/EditorTemplateSettings.vue +87 -0
  375. package/frontend-vue/app/components/editor/EditorTestSendModal.vue +66 -0
  376. package/frontend-vue/app/components/editor/EditorToolbar.vue +155 -0
  377. package/frontend-vue/app/components/editor/EditorVariables.vue +163 -0
  378. package/frontend-vue/app/components/editor/EditorWorkspace.vue +189 -0
  379. package/frontend-vue/app/composables/useEditorContext.ts +44 -0
  380. package/frontend-vue/app/composables/useMailingApi.ts +197 -0
  381. package/frontend-vue/app/composables/useMailingPeriodData.ts +39 -0
  382. package/frontend-vue/app/composables/useTemplateDrawer.ts +90 -0
  383. package/frontend-vue/app/composables/useTemplateEditor.ts +228 -0
  384. package/frontend-vue/app/emails/EmailMailingTemplate.vue +24 -0
  385. package/frontend-vue/app/emails/components/MailingBlocks.vue +117 -0
  386. package/frontend-vue/app/plugins/template-gallery-display.client.ts +14 -0
  387. package/frontend-vue/app/types/component.ts +29 -0
  388. package/frontend-vue/app/types/mailing.ts +365 -0
  389. package/frontend-vue/app/utils/blocks/components.ts +14 -0
  390. package/frontend-vue/app/utils/blocks/dnd.ts +15 -0
  391. package/frontend-vue/app/utils/blocks/factory.ts +100 -0
  392. package/frontend-vue/app/utils/blocks/fields.ts +58 -0
  393. package/frontend-vue/app/utils/blocks/index.ts +5 -0
  394. package/frontend-vue/app/utils/blocks/tree.ts +74 -0
  395. package/frontend-vue/app/utils/categories.ts +54 -0
  396. package/frontend-vue/app/utils/clone.ts +3 -0
  397. package/frontend-vue/app/utils/conditions.ts +50 -0
  398. package/frontend-vue/app/utils/editor-route.ts +18 -0
  399. package/frontend-vue/app/utils/gallery.ts +41 -0
  400. package/frontend-vue/app/utils/locales.ts +41 -0
  401. package/frontend-vue/app/utils/paths.ts +84 -0
  402. package/frontend-vue/app/utils/period.ts +23 -0
  403. package/frontend-vue/app/utils/testData.ts +10 -0
  404. package/frontend-vue/app/utils/timeline.ts +68 -0
  405. package/frontend-vue/app/utils/tokens.ts +24 -0
  406. package/frontend-vue/app/utils/variables.ts +48 -0
  407. package/frontend-vue/dms.email.ts +9 -0
  408. package/frontend-vue/dms.frontend.ts +29 -0
  409. package/frontend-vue/eslint.config.mjs +13 -0
  410. package/frontend-vue/i18n/locales/mailing-en-GB.json +397 -0
  411. package/frontend-vue/i18n/locales/mailing-fr-FR.json +397 -0
  412. package/frontend-vue/package.json +32 -0
  413. package/frontend-vue/pnpm-lock.yaml +6446 -0
  414. package/frontend-vue/pnpm-workspace.yaml +2 -0
  415. package/frontend-vue/tests/block-components.test.ts +14 -0
  416. package/frontend-vue/tests/block-fields.test.ts +23 -0
  417. package/frontend-vue/tests/blocks-factory.test.ts +28 -0
  418. package/frontend-vue/tests/blocks-tree.test.ts +49 -0
  419. package/frontend-vue/tests/categories.test.ts +64 -0
  420. package/frontend-vue/tests/conditions.test.ts +36 -0
  421. package/frontend-vue/tests/dnd.test.ts +11 -0
  422. package/frontend-vue/tests/editor-block-list.test.ts +54 -0
  423. package/frontend-vue/tests/editor-route.test.ts +23 -0
  424. package/frontend-vue/tests/email-render-cases.json +77 -0
  425. package/frontend-vue/tests/gallery.test.ts +39 -0
  426. package/frontend-vue/tests/i18n-editor.test.ts +21 -0
  427. package/frontend-vue/tests/i18n.test.ts +83 -0
  428. package/frontend-vue/tests/locales.test.ts +38 -0
  429. package/frontend-vue/tests/mailing-api.test.ts +10 -0
  430. package/frontend-vue/tests/paths.test.ts +128 -0
  431. package/frontend-vue/tests/period.test.ts +13 -0
  432. package/frontend-vue/tests/smoke.test.ts +8 -0
  433. package/frontend-vue/tests/template-editor.test.ts +65 -0
  434. package/frontend-vue/tests/test-data.test.ts +15 -0
  435. package/frontend-vue/tests/timeline.test.ts +29 -0
  436. package/frontend-vue/tests/tokens.test.ts +12 -0
  437. package/frontend-vue/tests/variables.test.ts +46 -0
  438. package/frontend-vue/vitest.config.ts +10 -0
  439. package/package.json +94 -0
  440. package/src/antelope-modules.ts +48 -0
  441. package/src/automation/email-event-trigger.ts +112 -0
  442. package/src/automation/index.ts +33 -0
  443. package/src/automation/send-template-action.ts +100 -0
  444. package/src/config.ts +15 -0
  445. package/src/constants.ts +42 -0
  446. package/src/crons/index.ts +1 -0
  447. package/src/crons/retention.ts +126 -0
  448. package/src/data/body.ts +14 -0
  449. package/src/data/index.ts +3 -0
  450. package/src/data/sends-table.ts +154 -0
  451. package/src/data/templates-table.ts +202 -0
  452. package/src/data-types/categories-type.ts +29 -0
  453. package/src/data-types/category-type.ts +44 -0
  454. package/src/data-types/index.ts +2 -0
  455. package/src/db/index.ts +2 -0
  456. package/src/db/models/index.ts +4 -0
  457. package/src/db/models/send-events.model.ts +46 -0
  458. package/src/db/models/sends.model.ts +147 -0
  459. package/src/db/models/settings.model.ts +12 -0
  460. package/src/db/models/templates.model.ts +12 -0
  461. package/src/db/tables/index.ts +4 -0
  462. package/src/db/tables/send-events.table.ts +31 -0
  463. package/src/db/tables/sends.table.ts +85 -0
  464. package/src/db/tables/settings.table.ts +49 -0
  465. package/src/db/tables/templates.table.ts +70 -0
  466. package/src/engine/conditions.ts +30 -0
  467. package/src/engine/coverage.ts +67 -0
  468. package/src/engine/fallback-html.ts +116 -0
  469. package/src/engine/index.ts +7 -0
  470. package/src/engine/paths.ts +33 -0
  471. package/src/engine/resolve.ts +178 -0
  472. package/src/engine/tokens.ts +35 -0
  473. package/src/engine/walk.ts +18 -0
  474. package/src/hooks/index.ts +1 -0
  475. package/src/hooks/tenant-export.ts +127 -0
  476. package/src/implementations/dms-mailing/index.ts +2 -0
  477. package/src/index.ts +62 -0
  478. package/src/pages/editor.ts +23 -0
  479. package/src/pages/index.ts +11 -0
  480. package/src/pages/module.ts +9 -0
  481. package/src/pages/overview.ts +175 -0
  482. package/src/pages/sends.ts +174 -0
  483. package/src/pages/settings/form.ts +67 -0
  484. package/src/pages/settings/index.ts +2 -0
  485. package/src/pages/settings/page.ts +34 -0
  486. package/src/pages/templates.ts +141 -0
  487. package/src/permissions.ts +44 -0
  488. package/src/quick-actions.ts +20 -0
  489. package/src/realtime.ts +53 -0
  490. package/src/routes/events.ts +57 -0
  491. package/src/routes/index.ts +7 -0
  492. package/src/routes/metrics.ts +243 -0
  493. package/src/routes/period.ts +46 -0
  494. package/src/routes/provider.ts +31 -0
  495. package/src/routes/sends.ts +215 -0
  496. package/src/routes/settings.ts +66 -0
  497. package/src/routes/templates.ts +256 -0
  498. package/src/services/attention.ts +151 -0
  499. package/src/services/categories.ts +43 -0
  500. package/src/services/events.ts +121 -0
  501. package/src/services/metrics.ts +175 -0
  502. package/src/services/notify.ts +90 -0
  503. package/src/services/permissions.ts +42 -0
  504. package/src/services/provider.ts +46 -0
  505. package/src/services/render.ts +33 -0
  506. package/src/services/send.ts +409 -0
  507. package/src/services/settings.ts +74 -0
  508. package/src/services/status.ts +16 -0
  509. package/src/services/templates.ts +157 -0
  510. package/src/test/antelope.test.ts +188 -0
  511. package/src/test/helpers/http.ts +23 -0
  512. package/src/test/helpers/owner.ts +37 -0
  513. package/src/test/integration/events.test.ts +136 -0
  514. package/src/test/integration/retention.test.ts +363 -0
  515. package/src/test/integration/sends.test.ts +200 -0
  516. package/src/test/integration/settings.test.ts +50 -0
  517. package/src/test/integration/setup.test.ts +35 -0
  518. package/src/test/integration/templates.test.ts +229 -0
  519. package/src/test/integration/tenant-export.test.ts +125 -0
  520. package/src/test/unit/attention.test.ts +129 -0
  521. package/src/test/unit/automation.test.ts +130 -0
  522. package/src/test/unit/categories.test.ts +51 -0
  523. package/src/test/unit/engine/conditions.test.ts +35 -0
  524. package/src/test/unit/engine/coverage.test.ts +118 -0
  525. package/src/test/unit/engine/fallback-html.test.ts +86 -0
  526. package/src/test/unit/engine/paths.test.ts +39 -0
  527. package/src/test/unit/engine/resolve.test.ts +176 -0
  528. package/src/test/unit/engine/tokens.test.ts +26 -0
  529. package/src/test/unit/events.test.ts +19 -0
  530. package/src/test/unit/metrics.test.ts +170 -0
  531. package/src/test/unit/notify.test.ts +71 -0
  532. package/src/test/unit/period.test.ts +64 -0
  533. package/src/test/unit/provider.test.ts +57 -0
  534. package/src/test/unit/realtime.test.ts +11 -0
  535. package/src/test/unit/render.test.ts +23 -0
  536. package/src/test/unit/retention.test.ts +91 -0
  537. package/src/test/unit/send-batch.test.ts +72 -0
  538. package/src/test/unit/send.test.ts +161 -0
  539. package/src/test/unit/smoke.test.ts +8 -0
  540. package/src/test/unit/status.test.ts +14 -0
  541. package/src/test/unit/templates.test.ts +52 -0
  542. package/src/test/unit/tenant-export.test.ts +88 -0
  543. package/src/test/unit/webhook-secret.test.ts +50 -0
  544. package/src/types/blocks.ts +31 -0
  545. package/src/types/index.ts +4 -0
  546. package/src/types/resolved.ts +89 -0
  547. package/src/types/sends.ts +44 -0
  548. package/src/types/settings.ts +24 -0
  549. package/src/validation/settings.schema.ts +33 -0
  550. package/src/validation/templates.schema.ts +145 -0
@@ -0,0 +1,363 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { mock } from "node:test";
3
+ import { expect } from "chai";
4
+ import { GetModel } from "@antelopejs/interface-database-decorators";
5
+ import { AtomicMutationQuery } from "@antelopejs/interface-database/atomic";
6
+ import {
7
+ purgeInPages,
8
+ purgeOrphanEvents,
9
+ purgeSend,
10
+ } from "../../crons/retention";
11
+ import { type MailingSend, SendEventModel, SendModel } from "../../db";
12
+ import { recordEmailEvent } from "../../services/events";
13
+
14
+ const OLD = new Date("2020-01-01T00:00:00Z");
15
+ const CUTOFF = new Date("2021-01-01T00:00:00Z");
16
+ const PAGE_SIZE = 2;
17
+ const MAX_ROWS = 10;
18
+
19
+ interface Fixture {
20
+ tenantId: string;
21
+ sends: SendModel;
22
+ events: SendEventModel;
23
+ }
24
+
25
+ function fixture(): Fixture {
26
+ const tenantId = randomUUID();
27
+ return {
28
+ tenantId,
29
+ sends: GetModel(SendModel, tenantId),
30
+ events: GetModel(SendEventModel, tenantId),
31
+ };
32
+ }
33
+
34
+ async function seed(
35
+ f: Fixture,
36
+ changes: Partial<MailingSend> = {},
37
+ ): Promise<string> {
38
+ const [id] = await f.sends.insertSend({
39
+ templateId: "template",
40
+ templateSlug: "retention",
41
+ locale: "en",
42
+ recipientEmail: "retention@example.test",
43
+ status: "delivered",
44
+ providerMessageId: randomUUID(),
45
+ latencyMs: 1,
46
+ json_variables: "{}",
47
+ opens: 0,
48
+ clicks: 0,
49
+ isTest: true,
50
+ });
51
+ await f.sends.table
52
+ .get(id)
53
+ .update({ createdAt: OLD, ...changes })
54
+ .run();
55
+ await f.events.insert({
56
+ sendId: id,
57
+ type: "delivered",
58
+ at: OLD,
59
+ json_details: "{}",
60
+ });
61
+ return id;
62
+ }
63
+
64
+ function sweep(f: Fixture): Promise<number> {
65
+ return purgeInPages(
66
+ {
67
+ async loadPage(size) {
68
+ return (await f.sends.listOlderThan(CUTOFF, size)).map(
69
+ (send) => send._id,
70
+ );
71
+ },
72
+ purgeSend: (id) => purgeSend(f.tenantId, id, CUTOFF),
73
+ },
74
+ { pageSize: PAGE_SIZE, maxRows: MAX_ROWS },
75
+ );
76
+ }
77
+
78
+ async function loseAcknowledgement(
79
+ action: () => Promise<unknown>,
80
+ ): Promise<number> {
81
+ const original: AtomicMutationQuery["run"] = Object.getOwnPropertyDescriptor(
82
+ AtomicMutationQuery.prototype,
83
+ "run",
84
+ )?.value;
85
+ let calls = 0;
86
+ const run = mock.method(AtomicMutationQuery.prototype, "run");
87
+ run.mock.mockImplementation(async function (this: AtomicMutationQuery) {
88
+ calls++;
89
+ await original.call(this);
90
+ return "unknown";
91
+ });
92
+ try {
93
+ await action().then(
94
+ () => {
95
+ throw new Error("expected unknown outcome failure");
96
+ },
97
+ (error: Error) =>
98
+ expect(error.message).to.include("outcome is unknown; not retrying"),
99
+ );
100
+ } finally {
101
+ run.mock.restore();
102
+ }
103
+ return calls;
104
+ }
105
+
106
+ describe("[integration] replay-safe Mongo retention", () => {
107
+ it("initializes a fresh identity and revision on every send", async () => {
108
+ const f = fixture();
109
+ const sourceId = await seed(f);
110
+ const source = await f.sends.getSnapshot(sourceId);
111
+ expect(source?.revision).to.be.a("string");
112
+ const [first] = await f.sends.insertSend(source!);
113
+ const [second] = await f.sends.insertSend(source!);
114
+ const firstSend = await f.sends.getSnapshot(first);
115
+ const secondSend = await f.sends.getSnapshot(second);
116
+ expect(new Set([sourceId, first, second]).size).to.equal(3);
117
+ expect(firstSend?.revision)
118
+ .to.be.a("string")
119
+ .and.not.equal(source?.revision);
120
+ expect(secondSend?.revision)
121
+ .to.be.a("string")
122
+ .and.not.equal(firstSend?.revision);
123
+ expect(firstSend!.createdAt.getTime()).to.be.greaterThan(CUTOFF.getTime());
124
+ expect(await f.sends.retire(sourceId, CUTOFF)).to.equal(true);
125
+ expect((await f.sends.getSnapshot(sourceId))?.revision)
126
+ .to.be.a("string")
127
+ .and.not.equal(source?.revision);
128
+ expect(await sweep(f)).to.equal(1);
129
+ expect(await f.sends.getAll()).to.have.length(2);
130
+ });
131
+
132
+ it("overlapping paged sweeps count each deleted send once", async () => {
133
+ const f = fixture();
134
+ const ids = await Promise.all(Array.from({ length: 5 }, () => seed(f)));
135
+ const counts = await Promise.all([sweep(f), sweep(f)]);
136
+ expect(counts[0] + counts[1]).to.equal(ids.length);
137
+ expect(await f.sends.getAll()).to.deep.equal([]);
138
+ expect(await f.events.getAll()).to.deep.equal([]);
139
+ expect(await sweep(f)).to.equal(0);
140
+ });
141
+
142
+ it("preserves new, queued, cutoff-boundary and recently active sends and their events", async () => {
143
+ const f = fixture();
144
+ const changes: Partial<MailingSend>[] = [
145
+ { createdAt: new Date() },
146
+ { createdAt: CUTOFF },
147
+ { status: "queued" },
148
+ { lastEventAt: CUTOFF },
149
+ { lastActivityAt: CUTOFF },
150
+ ];
151
+ const ids = await Promise.all(changes.map((change) => seed(f, change)));
152
+ expect(await f.sends.listOlderThan(CUTOFF, MAX_ROWS)).to.deep.equal([]);
153
+ for (const id of ids)
154
+ expect(await purgeSend(f.tenantId, id, CUTOFF)).to.equal(0);
155
+ expect(await f.sends.getAll()).to.have.length(ids.length);
156
+ expect(await f.events.getAll()).to.have.length(ids.length);
157
+ });
158
+
159
+ it("rechecks stale candidates after a backdated webhook activates the send", async () => {
160
+ const f = fixture();
161
+ const id = await seed(f);
162
+ const [candidate] = await f.sends.listOlderThan(CUTOFF, PAGE_SIZE);
163
+ expect(candidate._id).to.equal(id);
164
+ expect(
165
+ await recordEmailEvent(f.tenantId, {
166
+ messageId: candidate.providerMessageId!,
167
+ provider: "fixture",
168
+ type: "opened",
169
+ at: OLD,
170
+ }),
171
+ ).to.equal(true);
172
+ expect(await purgeSend(f.tenantId, id, CUTOFF)).to.equal(0);
173
+ expect((await f.sends.get(id))?.opens).to.equal(1);
174
+ expect(await f.events.getBySend(id)).to.have.length(2);
175
+ });
176
+
177
+ it("serializes retirement against concurrent activity at the send document", async () => {
178
+ const f = fixture();
179
+ const id = await seed(f);
180
+ const candidate = await f.sends.get(id);
181
+ const [retired, accepted] = await Promise.all([
182
+ f.sends.retire(id, CUTOFF),
183
+ f.sends.recordActivity(candidate!, {
184
+ status: "opened",
185
+ opens: 1,
186
+ clicks: 0,
187
+ lastEventAt: OLD,
188
+ }),
189
+ ]);
190
+ expect(Number(retired) + Number(accepted)).to.equal(1);
191
+ const send = await f.sends.get(id);
192
+ expect(send?.isRetiring === true).to.equal(retired);
193
+ expect(send?.opens).to.equal(Number(accepted));
194
+ });
195
+
196
+ it("retries known conflicts and counts both concurrent opened webhooks", async () => {
197
+ const f = fixture();
198
+ const id = await seed(f);
199
+ const send = await f.sends.get(id);
200
+ const accepted = await Promise.all(
201
+ Array.from({ length: 2 }, () =>
202
+ recordEmailEvent(f.tenantId, {
203
+ messageId: send!.providerMessageId!,
204
+ provider: "fixture",
205
+ type: "opened",
206
+ }),
207
+ ),
208
+ );
209
+ expect(accepted).to.deep.equal([true, true]);
210
+ expect((await f.sends.get(id))?.opens).to.equal(2);
211
+ expect(await f.events.getBySend(id)).to.have.length(3);
212
+ expect(await purgeSend(f.tenantId, id, CUTOFF)).to.equal(0);
213
+ });
214
+
215
+ it("does not retry an uncertain webhook write or emit an unacknowledged event", async () => {
216
+ const f = fixture();
217
+ const id = await seed(f);
218
+ const send = await f.sends.get(id);
219
+ const calls = await loseAcknowledgement(() =>
220
+ recordEmailEvent(f.tenantId, {
221
+ messageId: send!.providerMessageId!,
222
+ provider: "fixture",
223
+ type: "opened",
224
+ }),
225
+ );
226
+ expect(calls).to.equal(1);
227
+ expect((await f.sends.get(id))?.opens).to.equal(1);
228
+ expect(await f.events.getBySend(id)).to.have.length(1);
229
+ });
230
+
231
+ it("leaves uncertain retirement replayable and never counts an uncertain deletion", async () => {
232
+ const f = fixture();
233
+ const id = await seed(f);
234
+ expect(
235
+ await loseAcknowledgement(() => purgeSend(f.tenantId, id, CUTOFF)),
236
+ ).to.equal(1);
237
+ expect((await f.sends.get(id))?.isRetiring).to.equal(true);
238
+ expect(await f.events.getBySend(id)).to.have.length(1);
239
+ expect(
240
+ await loseAcknowledgement(() => purgeSend(f.tenantId, id, CUTOFF)),
241
+ ).to.equal(1);
242
+ expect(await f.sends.get(id)).to.equal(undefined);
243
+ expect(await f.events.getBySend(id)).to.deep.equal([]);
244
+ expect(await sweep(f)).to.equal(0);
245
+ });
246
+
247
+ it("rejects a webhook after retirement and removes an already-paused writer's late event", async () => {
248
+ const f = fixture();
249
+ const id = await seed(f);
250
+ const send = await f.sends.get(id);
251
+ expect(await f.sends.retire(id, CUTOFF)).to.equal(true);
252
+ expect(
253
+ await recordEmailEvent(f.tenantId, {
254
+ messageId: send!.providerMessageId!,
255
+ provider: "fixture",
256
+ type: "opened",
257
+ }),
258
+ ).to.equal(false);
259
+ expect(
260
+ await f.events.insertForActiveSend({
261
+ sendId: id,
262
+ type: "opened",
263
+ at: new Date(),
264
+ json_details: "{}",
265
+ }),
266
+ ).to.equal(false);
267
+ expect(await f.events.getBySend(id)).to.have.length(1);
268
+ expect(await purgeSend(f.tenantId, id, CUTOFF)).to.equal(1);
269
+ expect(
270
+ await f.events.insertForActiveSend({
271
+ sendId: id,
272
+ type: "clicked",
273
+ at: new Date(),
274
+ json_details: "{}",
275
+ }),
276
+ ).to.equal(false);
277
+ expect(await f.events.getBySend(id)).to.deep.equal([]);
278
+ });
279
+
280
+ it("retries event cleanup failures after marking", async () => {
281
+ const f = fixture();
282
+ const id = await seed(f);
283
+ const deletion = mock.method(
284
+ SendEventModel.prototype,
285
+ "deleteBySend",
286
+ async () => {
287
+ throw new Error("cleanup unavailable");
288
+ },
289
+ );
290
+ try {
291
+ await purgeSend(f.tenantId, id, CUTOFF).then(
292
+ () => {
293
+ throw new Error("expected cleanup failure");
294
+ },
295
+ (error: Error) => expect(error.message).to.equal("cleanup unavailable"),
296
+ );
297
+ } finally {
298
+ deletion.mock.restore();
299
+ }
300
+ expect((await f.sends.get(id))?.isRetiring).to.equal(true);
301
+ expect(await f.events.getBySend(id)).to.have.length(1);
302
+ expect(await sweep(f)).to.equal(1);
303
+ expect(await sweep(f)).to.equal(0);
304
+ });
305
+
306
+ it("retries parent deletion failures after the events are gone", async () => {
307
+ const f = fixture();
308
+ const id = await seed(f);
309
+ const deletion = mock.method(
310
+ SendModel.prototype,
311
+ "deleteRetired",
312
+ async () => {
313
+ throw new Error("delete unavailable");
314
+ },
315
+ );
316
+ try {
317
+ await purgeSend(f.tenantId, id, CUTOFF).then(
318
+ () => {
319
+ throw new Error("expected delete failure");
320
+ },
321
+ (error: Error) => expect(error.message).to.equal("delete unavailable"),
322
+ );
323
+ } finally {
324
+ deletion.mock.restore();
325
+ }
326
+ expect((await f.sends.get(id))?.isRetiring).to.equal(true);
327
+ expect(await f.events.getBySend(id)).to.deep.equal([]);
328
+ expect(await sweep(f)).to.equal(1);
329
+ expect(await sweep(f)).to.equal(0);
330
+ });
331
+
332
+ it("reconciles crash-orphaned events across pages without removing active history", async () => {
333
+ const f = fixture();
334
+ const active = await seed(f, { createdAt: new Date() });
335
+ const retired = await seed(f);
336
+ expect(await purgeSend(f.tenantId, retired, CUTOFF)).to.equal(1);
337
+ await f.events.insert(
338
+ Array.from({ length: 5 }, (_value, index) => ({
339
+ sendId: index === 0 ? retired : randomUUID(),
340
+ type: "opened" as const,
341
+ at: new Date(),
342
+ json_details: "{}",
343
+ })),
344
+ );
345
+ await Promise.all([
346
+ purgeOrphanEvents(f.tenantId, PAGE_SIZE),
347
+ purgeOrphanEvents(f.tenantId, PAGE_SIZE),
348
+ ]);
349
+ expect(await f.events.getBySend(active)).to.have.length(1);
350
+ expect(await f.events.getAll()).to.have.length(1);
351
+ expect(await f.events.getBySend(retired)).to.deep.equal([]);
352
+ await f.events.insert({
353
+ sendId: retired,
354
+ type: "clicked",
355
+ at: new Date(),
356
+ json_details: "{}",
357
+ });
358
+ await purgeOrphanEvents(f.tenantId, PAGE_SIZE);
359
+ await purgeOrphanEvents(f.tenantId, PAGE_SIZE);
360
+ expect(await f.events.getBySend(active)).to.have.length(1);
361
+ expect(await f.events.getAll()).to.have.length(1);
362
+ });
363
+ });
@@ -0,0 +1,200 @@
1
+ import { expect } from "chai";
2
+ import { authorizedClient } from "../helpers/http";
3
+ import { ensureOwnerSession } from "../helpers/owner";
4
+
5
+ const HTTP_OK = 200;
6
+ const HTTP_UNPROCESSABLE = 422;
7
+ const HTTP_NOT_FOUND = 404;
8
+ const DAY_MS = 86_400_000;
9
+ const TABLE = "/api/mailing/tables/templates";
10
+
11
+ interface TemplateRow {
12
+ _id: string;
13
+ slug: string;
14
+ }
15
+
16
+ interface SendResult {
17
+ sendId: string;
18
+ }
19
+
20
+ describe("[integration] sends", () => {
21
+ it("sends a test e-mail through the SMTP fixture and logs it", async () => {
22
+ const session = await ensureOwnerSession();
23
+ const client = authorizedClient(session.accessToken);
24
+ const created = await client.post(`${TABLE}/new`, {
25
+ slug: "test-send",
26
+ name: "Test send",
27
+ category: "orders",
28
+ });
29
+ expect(created.status, JSON.stringify(created.data)).to.equal(HTTP_OK);
30
+ const id = created.data[0] as string;
31
+ const sent = await client.post(`/api/mailing/templates/${id}/test-send`, {
32
+ to: ["sofie@example.test"],
33
+ locale: "en",
34
+ data: { customer: { firstName: "Sofie" }, detailsUrl: "https://x" },
35
+ });
36
+ expect(sent.status, JSON.stringify(sent.data)).to.equal(HTTP_OK);
37
+ expect(sent.data.results[0].status).to.be.oneOf(["sent", "queued"]);
38
+
39
+ const detail = await client.get(
40
+ `/api/mailing/sends/${sent.data.results[0].sendId}`,
41
+ );
42
+ expect(detail.status).to.equal(HTTP_OK);
43
+ expect(detail.data.send.isTest).to.equal(true);
44
+ expect(detail.data.send.templateSlug).to.equal("test-send");
45
+ expect(detail.data.events[0].type).to.equal(detail.data.send.status);
46
+ });
47
+
48
+ it("refuses a real send on a draft template", async () => {
49
+ const session = await ensureOwnerSession();
50
+ const client = authorizedClient(session.accessToken);
51
+ const created = await client.post(`${TABLE}/new`, {
52
+ slug: "real-draft",
53
+ name: "Real draft",
54
+ category: "orders",
55
+ });
56
+ const id = created.data[0] as string;
57
+ const sent = await client.post(`/api/mailing/templates/${id}/send`, {
58
+ to: ["sofie@example.test"],
59
+ locale: "en",
60
+ });
61
+ expect(sent.status).to.equal(HTTP_UNPROCESSABLE);
62
+ expect(JSON.stringify(sent.data)).to.contain("template_not_live");
63
+ });
64
+
65
+ it("sends for real from a live template and counts it in the metrics", async () => {
66
+ const session = await ensureOwnerSession();
67
+ const client = authorizedClient(session.accessToken);
68
+ const created = await client.post(`${TABLE}/new`, {
69
+ slug: "real-send",
70
+ name: "Real send",
71
+ category: "orders",
72
+ });
73
+ const id = created.data[0] as string;
74
+ await client.post(`/api/mailing/templates/${id}/publish`, {});
75
+ const sent = await client.post(`/api/mailing/templates/${id}/send`, {
76
+ to: ["sofie@example.test"],
77
+ locale: "en",
78
+ data: { customer: { firstName: "Sofie" }, detailsUrl: "https://x" },
79
+ });
80
+ expect(sent.status, JSON.stringify(sent.data)).to.equal(HTTP_OK);
81
+ const detail = await client.get(
82
+ `/api/mailing/sends/${sent.data.results[0].sendId}`,
83
+ );
84
+ expect(detail.data.send.isTest).to.equal(false);
85
+ expect(detail.data.send.source).to.equal("dms-mailing:manual");
86
+ });
87
+
88
+ it("records one send per recipient of a multi-recipient test send", async () => {
89
+ const session = await ensureOwnerSession();
90
+ const client = authorizedClient(session.accessToken);
91
+ const list = await client.get(`${TABLE}/list`);
92
+ const id = (
93
+ list.data.results.find(
94
+ (item: TemplateRow) => item.slug === "test-send",
95
+ ) as TemplateRow
96
+ )._id;
97
+ const sent = await client.post(`/api/mailing/templates/${id}/test-send`, {
98
+ to: ["first@example.test", "second@example.test"],
99
+ locale: "en",
100
+ data: { customer: { firstName: "Sofie" }, detailsUrl: "https://x" },
101
+ });
102
+ expect(sent.status, JSON.stringify(sent.data)).to.equal(HTTP_OK);
103
+ expect(sent.data.results).to.have.length(2);
104
+ const sendIds = sent.data.results.map(
105
+ (result: SendResult) => result.sendId,
106
+ );
107
+ expect(new Set(sendIds).size).to.equal(2);
108
+ });
109
+
110
+ it("refuses to send when required variables are missing and blocking is on", async () => {
111
+ const session = await ensureOwnerSession();
112
+ const client = authorizedClient(session.accessToken);
113
+ const settings = await client.get("/api/mailing/settings");
114
+ await client.post("/api/mailing/settings", {
115
+ ...settings.data,
116
+ blockOnMissingVariables: true,
117
+ });
118
+ const list = await client.get(`${TABLE}/list`);
119
+ const id = (
120
+ list.data.results.find(
121
+ (item: TemplateRow) => item.slug === "test-send",
122
+ ) as TemplateRow
123
+ )._id;
124
+ // Blank content references nothing, so give it a token to miss.
125
+ await client.post(`/api/mailing/templates/${id}/content`, {
126
+ content: {
127
+ locales: {
128
+ en: {
129
+ subject: "Hi {{customer.firstName}}",
130
+ preheader: "",
131
+ blocks: [],
132
+ },
133
+ },
134
+ },
135
+ });
136
+ await client.post(`/api/mailing/templates/${id}/variables`, {
137
+ variables: [
138
+ { path: "customer.firstName", type: "string", required: true },
139
+ ],
140
+ });
141
+ const blocked = await client.post(
142
+ `/api/mailing/templates/${id}/test-send`,
143
+ { to: ["sofie@example.test"], locale: "en", data: {} },
144
+ );
145
+ expect(blocked.status).to.equal(HTTP_UNPROCESSABLE);
146
+ await client.post("/api/mailing/settings", {
147
+ ...settings.data,
148
+ blockOnMissingVariables: false,
149
+ });
150
+ });
151
+
152
+ it("replays a send and lists it in the sends table", async () => {
153
+ const session = await ensureOwnerSession();
154
+ const client = authorizedClient(session.accessToken);
155
+ const list = await client.get("/api/mailing/tables/sends/list");
156
+ expect(list.status, JSON.stringify(list.data)).to.equal(HTTP_OK);
157
+ const original = list.data.results[0] as TemplateRow;
158
+ const replay = await client.post(
159
+ `/api/mailing/sends/${original._id}/replay`,
160
+ {},
161
+ );
162
+ expect(replay.status, JSON.stringify(replay.data)).to.equal(HTTP_OK);
163
+ expect(replay.data.sendId).to.not.equal(original._id);
164
+ const html = await client.get(`/api/mailing/sends/${original._id}/html`);
165
+ expect(html.status).to.equal(HTTP_OK);
166
+ expect(html.data.html).to.include("<!DOCTYPE html>");
167
+ });
168
+
169
+ it("exposes metrics and the provider status", async () => {
170
+ const session = await ensureOwnerSession();
171
+ const client = authorizedClient(session.accessToken);
172
+ const from = new Date(Date.now() - DAY_MS).toISOString();
173
+ const to = new Date(Date.now() + DAY_MS).toISOString();
174
+ const kpiUrl = `/api/mailing/metrics/kpi/sends?from=${from}&to=${to}`;
175
+ const kpi = await client.get(kpiUrl);
176
+ expect(kpi.status, JSON.stringify(kpi.data)).to.equal(HTTP_OK);
177
+ expect(kpi.data.value).to.be.a("number");
178
+
179
+ const volume = await client.get(
180
+ `/api/mailing/metrics/volume?from=${from}&to=${to}`,
181
+ );
182
+ expect(volume.status).to.equal(HTTP_OK);
183
+ expect(volume.data.series[0].data).to.not.have.length(0);
184
+
185
+ const unknown = await client.get("/api/mailing/metrics/kpi/nope");
186
+ expect(unknown.status).to.equal(HTTP_NOT_FOUND);
187
+
188
+ const provider = await client.get("/api/mailing/provider");
189
+ expect(provider.status).to.equal(HTTP_OK);
190
+ expect(provider.data.connected).to.equal(true);
191
+
192
+ const funnel = await client.get("/api/mailing/metrics/funnel");
193
+ expect(funnel.status).to.equal(HTTP_OK);
194
+ expect(funnel.data.steps).to.have.length(5);
195
+
196
+ const attention = await client.get("/api/mailing/metrics/attention");
197
+ expect(attention.status).to.equal(HTTP_OK);
198
+ expect(attention.data.items).to.be.an("array");
199
+ });
200
+ });
@@ -0,0 +1,50 @@
1
+ import { expect } from "chai";
2
+ import { authorizedClient } from "../helpers/http";
3
+ import { ensureOwnerSession } from "../helpers/owner";
4
+
5
+ const HTTP_OK = 200;
6
+ const HTTP_BAD_REQUEST = 400;
7
+ const DEFAULT_CATEGORY_COUNT = 5;
8
+ const DEFAULT_RETENTION_DAYS = 90;
9
+ const UPDATED_RETENTION_DAYS = 30;
10
+ const INVALID_RETENTION_DAYS = 0;
11
+
12
+ describe("[integration] settings", () => {
13
+ it("returns defaults, accepts an update and rejects an invalid retention", async () => {
14
+ const session = await ensureOwnerSession();
15
+ const client = authorizedClient(session.accessToken);
16
+ const initial = await client.get("/api/mailing/settings");
17
+ expect(initial.status).to.equal(HTTP_OK);
18
+ expect(initial.data.fallbackLocale).to.equal("en");
19
+ expect(initial.data.logRetentionDays).to.equal(DEFAULT_RETENTION_DAYS);
20
+ expect(initial.data.categories).to.have.length(DEFAULT_CATEGORY_COUNT);
21
+
22
+ const update = await client.post("/api/mailing/settings", {
23
+ ...initial.data,
24
+ fallbackLocale: "fr",
25
+ logRetentionDays: UPDATED_RETENTION_DAYS,
26
+ blockOnMissingVariables: true,
27
+ });
28
+ expect(update.status).to.equal(HTTP_OK);
29
+ const after = await client.get("/api/mailing/settings");
30
+ expect(after.data.fallbackLocale).to.equal("fr");
31
+ expect(after.data.blockOnMissingVariables).to.equal(true);
32
+
33
+ const invalid = await client.post("/api/mailing/settings", {
34
+ ...after.data,
35
+ logRetentionDays: INVALID_RETENTION_DAYS,
36
+ });
37
+ expect(invalid.status).to.equal(HTTP_BAD_REQUEST);
38
+
39
+ // Blanking the secret would leave the unauthenticated event endpoint with
40
+ // no gate, so the write has to be refused rather than silently accepted.
41
+ const blankSecret = await client.post("/api/mailing/settings", {
42
+ ...after.data,
43
+ webhookSecret: "",
44
+ });
45
+ expect(blankSecret.status).to.equal(HTTP_BAD_REQUEST);
46
+
47
+ const restored = await client.post("/api/mailing/settings", initial.data);
48
+ expect(restored.status).to.equal(HTTP_OK);
49
+ });
50
+ });
@@ -0,0 +1,35 @@
1
+ import { createClient } from "../helpers/http";
2
+
3
+ const READY_TIMEOUT_MS = 30_000;
4
+ const POLL_INTERVAL_MS = 100;
5
+ const HTTP_OK = 200;
6
+
7
+ function delay(ms: number): Promise<void> {
8
+ return new Promise((resolve) => setTimeout(resolve, ms));
9
+ }
10
+
11
+ async function waitFor(
12
+ description: string,
13
+ check: () => Promise<boolean>,
14
+ ): Promise<void> {
15
+ const deadline = Date.now() + READY_TIMEOUT_MS;
16
+ while (Date.now() < deadline) {
17
+ if (await check()) return;
18
+ await delay(POLL_INTERVAL_MS);
19
+ }
20
+ throw new Error(`Timed out waiting for ${description}`);
21
+ }
22
+
23
+ before(async function () {
24
+ this.timeout(READY_TIMEOUT_MS + 5_000);
25
+
26
+ const client = createClient();
27
+ await waitFor("the DMS runtime to accept onboarding calls", async () => {
28
+ try {
29
+ const response = await client.get("/api/onboarding/informations");
30
+ return response.status === HTTP_OK;
31
+ } catch {
32
+ return false;
33
+ }
34
+ });
35
+ });