@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,229 @@
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 HTTP_NOT_FOUND = 404;
8
+ const TABLE = "/api/mailing/tables/templates";
9
+
10
+ interface TemplateRow {
11
+ _id: string;
12
+ slug: string;
13
+ status: string;
14
+ locales: string;
15
+ }
16
+
17
+ interface BlockRow {
18
+ type: string;
19
+ }
20
+
21
+ describe("[integration] templates table", () => {
22
+ it("creates a blank template with only a footer and lists it", async () => {
23
+ const session = await ensureOwnerSession();
24
+ const client = authorizedClient(session.accessToken);
25
+ const created = await client.post(`${TABLE}/new`, {
26
+ slug: "order-confirmed",
27
+ name: "Order confirmed",
28
+ category: "orders",
29
+ });
30
+ expect(created.status, JSON.stringify(created.data)).to.equal(HTTP_OK);
31
+
32
+ const list = await client.get(`${TABLE}/list`);
33
+ expect(list.status).to.equal(HTTP_OK);
34
+ const row = list.data.results.find(
35
+ (item: TemplateRow) => item.slug === "order-confirmed",
36
+ ) as TemplateRow;
37
+ expect(row.status).to.equal("draft");
38
+ expect(row.locales).to.equal("en");
39
+
40
+ const content = await client.get(
41
+ `/api/mailing/templates/${row._id}/content`,
42
+ );
43
+ expect(content.status).to.equal(HTTP_OK);
44
+ expect(
45
+ content.data.content.locales.en.blocks.map(
46
+ (block: BlockRow) => block.type,
47
+ ),
48
+ ).to.deep.equal(["footer"]);
49
+ expect(content.data.content.locales.en.subject).to.equal("");
50
+ expect(content.data.variables).to.deep.equal([]);
51
+ });
52
+
53
+ it("creates from an existing template by copying its content", async () => {
54
+ const session = await ensureOwnerSession();
55
+ const client = authorizedClient(session.accessToken);
56
+ const source = await client.post(`${TABLE}/new`, {
57
+ slug: "copy-source",
58
+ name: "Copy source",
59
+ category: "orders",
60
+ });
61
+ const sourceId = source.data[0] as string;
62
+ await client.post(`/api/mailing/templates/${sourceId}/content`, {
63
+ content: {
64
+ locales: {
65
+ en: {
66
+ subject: "Hello {{customer.firstName}}",
67
+ preheader: "",
68
+ blocks: [],
69
+ },
70
+ },
71
+ },
72
+ });
73
+ await client.post(`/api/mailing/templates/${sourceId}/variables`, {
74
+ variables: [
75
+ { path: "customer.firstName", type: "string", required: true },
76
+ ],
77
+ });
78
+
79
+ const copy = await client.post(`${TABLE}/new`, {
80
+ slug: "copy-target",
81
+ name: "Copy target",
82
+ sourceTemplateId: sourceId,
83
+ });
84
+ expect(copy.status, JSON.stringify(copy.data)).to.equal(HTTP_OK);
85
+ const content = await client.get(
86
+ `/api/mailing/templates/${copy.data[0] as string}/content`,
87
+ );
88
+ expect(content.data.content.locales.en.subject).to.equal(
89
+ "Hello {{customer.firstName}}",
90
+ );
91
+ expect(content.data.variables).to.deep.equal([
92
+ { path: "customer.firstName", type: "string", required: true },
93
+ ]);
94
+ });
95
+
96
+ it("keeps the slug when only the name and category are edited", async () => {
97
+ const session = await ensureOwnerSession();
98
+ const client = authorizedClient(session.accessToken);
99
+ const created = await client.post(`${TABLE}/new`, {
100
+ slug: "keeps-slug",
101
+ name: "Keeps slug",
102
+ category: "orders",
103
+ });
104
+ const id = created.data[0] as string;
105
+ // The data-api `edit` route replaces the writable fields: a body missing
106
+ // one nulls it. The editor must send the whole writable set.
107
+ const edited = await client.put(`${TABLE}/edit?id=${id}`, {
108
+ slug: "keeps-slug",
109
+ name: "Renamed",
110
+ category: "billing",
111
+ });
112
+ expect(edited.status, JSON.stringify(edited.data)).to.equal(HTTP_OK);
113
+ const row = await client.get(`${TABLE}/get?id=${id}`);
114
+ expect(row.data.slug).to.equal("keeps-slug");
115
+ expect(row.data.name).to.equal("Renamed");
116
+ expect(row.data.category).to.equal("billing");
117
+ });
118
+
119
+ it("refuses an unknown source template", async () => {
120
+ const session = await ensureOwnerSession();
121
+ const client = authorizedClient(session.accessToken);
122
+ const created = await client.post(`${TABLE}/new`, {
123
+ slug: "copy-missing",
124
+ name: "Copy missing",
125
+ sourceTemplateId: "does-not-exist",
126
+ });
127
+ expect(created.status).to.equal(HTTP_NOT_FOUND);
128
+ expect(JSON.stringify(created.data)).to.contain("template_not_found");
129
+ });
130
+
131
+ it("rejects a duplicate slug", async () => {
132
+ const session = await ensureOwnerSession();
133
+ const client = authorizedClient(session.accessToken);
134
+ const dup = await client.post(`${TABLE}/new`, {
135
+ slug: "order-confirmed",
136
+ name: "Again",
137
+ category: "orders",
138
+ });
139
+ expect(dup.status).to.be.at.least(HTTP_BAD_REQUEST);
140
+ });
141
+ });
142
+
143
+ describe("[integration] template content and status", () => {
144
+ async function createTemplate(
145
+ client: ReturnType<typeof authorizedClient>,
146
+ slug: string,
147
+ ): Promise<string> {
148
+ const created = await client.post(`${TABLE}/new`, {
149
+ slug,
150
+ name: slug,
151
+ category: "orders",
152
+ });
153
+ expect(created.status, JSON.stringify(created.data)).to.equal(HTTP_OK);
154
+ return created.data[0] as string;
155
+ }
156
+
157
+ it("replaces the content, publishes and duplicates", async () => {
158
+ const session = await ensureOwnerSession();
159
+ const client = authorizedClient(session.accessToken);
160
+ const id = await createTemplate(client, "welcome");
161
+ const before = await client.get(`/api/mailing/templates/${id}/content`);
162
+ const content = before.data.content;
163
+ content.locales.en.subject = "Welcome {{customer.firstName}}";
164
+ const saved = await client.post(`/api/mailing/templates/${id}/content`, {
165
+ content,
166
+ });
167
+ expect(saved.status, JSON.stringify(saved.data)).to.equal(HTTP_OK);
168
+ expect(saved.data.saved).to.equal(true);
169
+
170
+ const publish = await client.post(
171
+ `/api/mailing/templates/${id}/publish`,
172
+ {},
173
+ );
174
+ expect(publish.status).to.equal(HTTP_OK);
175
+ const after = await client.get(`/api/mailing/templates/${id}/content`);
176
+ expect(after.data.template.status).to.equal("live");
177
+ expect(after.data.content.locales.en.subject).to.equal(
178
+ "Welcome {{customer.firstName}}",
179
+ );
180
+
181
+ const variables = await client.post(
182
+ `/api/mailing/templates/${id}/variables`,
183
+ {
184
+ variables: [
185
+ { path: "customer.firstName", type: "string", required: false },
186
+ ],
187
+ },
188
+ );
189
+ expect(variables.status).to.equal(HTTP_OK);
190
+ const testData = await client.post(
191
+ `/api/mailing/templates/${id}/test-data`,
192
+ { data: { customer: { firstName: "Sofie" } } },
193
+ );
194
+ expect(testData.status).to.equal(HTTP_OK);
195
+
196
+ const duplicate = await client.post(
197
+ `/api/mailing/templates/${id}/duplicate`,
198
+ { slug: "welcome-copy", name: "Welcome copy" },
199
+ );
200
+ expect(duplicate.status, JSON.stringify(duplicate.data)).to.equal(HTTP_OK);
201
+ const copy = await client.get(
202
+ `/api/mailing/templates/${duplicate.data.id}/content`,
203
+ );
204
+ expect(copy.data.template.status).to.equal("draft");
205
+ expect(copy.data.content.locales.en.subject).to.equal(
206
+ "Welcome {{customer.firstName}}",
207
+ );
208
+ expect(copy.data.variables).to.have.length(1);
209
+ });
210
+
211
+ it("previews the current content with test data and reports missing variables", async () => {
212
+ const session = await ensureOwnerSession();
213
+ const client = authorizedClient(session.accessToken);
214
+ const list = await client.get(`${TABLE}/list`);
215
+ const id = (
216
+ list.data.results.find(
217
+ (item: TemplateRow) => item.slug === "welcome",
218
+ ) as TemplateRow
219
+ )._id;
220
+ const preview = await client.post(`/api/mailing/templates/${id}/preview`, {
221
+ locale: "en",
222
+ data: {},
223
+ });
224
+ expect(preview.status, JSON.stringify(preview.data)).to.equal(HTTP_OK);
225
+ expect(preview.data.html).to.include("<!DOCTYPE html>");
226
+ expect(preview.data.missing).to.include("customer.firstName");
227
+ expect(preview.data.subject).to.equal("Welcome {{customer.firstName}}");
228
+ });
229
+ });
@@ -0,0 +1,125 @@
1
+ import type { Readable } from "node:stream";
2
+ import { DEFAULT_TENANT_ID } from "@antelopejs/interface-dms/constants";
3
+ import type { TenantExportArchive } from "@antelopejs/interface-dms/hooks";
4
+ import { expect } from "chai";
5
+ import {
6
+ contributeTenantExport,
7
+ type ExportedCollection,
8
+ exportedCollections,
9
+ jsonArrayChunks,
10
+ } from "../../hooks/tenant-export";
11
+ import { authorizedClient } from "../helpers/http";
12
+ import { ensureOwnerSession } from "../helpers/owner";
13
+
14
+ const HTTP_OK = 200;
15
+ const TABLE = "/api/mailing/tables/templates";
16
+ const TEMPLATES_ENTRY = "templates.json";
17
+ const SETTINGS_ENTRY = "settings.json";
18
+ const EXPORTED_SLUG = "exported-template";
19
+ const SINGLE_ROW_PAGE = 1;
20
+ const LIST_ALL = 100;
21
+
22
+ interface TemplateRow {
23
+ slug: string;
24
+ }
25
+
26
+ class CollectingArchive implements TenantExportArchive {
27
+ readonly entries = new Map<string, string>();
28
+
29
+ addJson(path: string, data: unknown): Promise<void> {
30
+ this.entries.set(path, JSON.stringify(data));
31
+ return Promise.resolve();
32
+ }
33
+
34
+ addFile(path: string): Promise<void> {
35
+ return Promise.reject(new Error(`Unexpected file entry "${path}"`));
36
+ }
37
+
38
+ async addStream(path: string, stream: Readable): Promise<void> {
39
+ let text = "";
40
+ for await (const chunk of stream) text += String(chunk);
41
+ this.entries.set(path, text);
42
+ }
43
+ }
44
+
45
+ function parsed<T>(archive: CollectingArchive, entry: string): T {
46
+ return JSON.parse(archive.entries.get(entry) as string) as T;
47
+ }
48
+
49
+ describe("[integration] tenant export", () => {
50
+ it("streams the mailing data into the archive instead of one blob", async () => {
51
+ const session = await ensureOwnerSession();
52
+ const client = authorizedClient(session.accessToken);
53
+ const created = await client.post(`${TABLE}/new`, {
54
+ slug: EXPORTED_SLUG,
55
+ name: "Exported template",
56
+ category: "orders",
57
+ });
58
+ expect(created.status, JSON.stringify(created.data)).to.equal(HTTP_OK);
59
+
60
+ const archive = new CollectingArchive();
61
+ const contribution = await contributeTenantExport(
62
+ DEFAULT_TENANT_ID,
63
+ archive,
64
+ new AbortController().signal,
65
+ );
66
+
67
+ expect(contribution).to.equal(undefined);
68
+ expect([...archive.entries.keys()]).to.have.members([
69
+ TEMPLATES_ENTRY,
70
+ SETTINGS_ENTRY,
71
+ ]);
72
+ const templates = parsed<TemplateRow[]>(archive, TEMPLATES_ENTRY);
73
+ expect(templates.map((template) => template.slug)).to.include(
74
+ EXPORTED_SLUG,
75
+ );
76
+ expect(archive.entries.get(SETTINGS_ENTRY)).to.not.contain("webhookSecret");
77
+ });
78
+
79
+ it("stops contributing when the export is aborted", async () => {
80
+ const archive = new CollectingArchive();
81
+ const controller = new AbortController();
82
+ controller.abort();
83
+
84
+ let aborted = false;
85
+ try {
86
+ await contributeTenantExport(
87
+ DEFAULT_TENANT_ID,
88
+ archive,
89
+ controller.signal,
90
+ );
91
+ } catch {
92
+ aborted = true;
93
+ }
94
+
95
+ expect(aborted).to.equal(true);
96
+ expect(archive.entries.size).to.equal(0);
97
+ });
98
+
99
+ it("pages the reads without skipping or repeating a row", async () => {
100
+ const session = await ensureOwnerSession();
101
+ const client = authorizedClient(session.accessToken);
102
+ // The list answers 10 rows unless told otherwise; the export reads them all.
103
+ const list = await client.get(`${TABLE}/list?limit=${LIST_ALL}`);
104
+ expect(list.status, JSON.stringify(list.data)).to.equal(HTTP_OK);
105
+ const slugs = (list.data.results as TemplateRow[]).map(
106
+ (template) => template.slug,
107
+ );
108
+ const [templates] = exportedCollections(DEFAULT_TENANT_ID);
109
+
110
+ let text = "";
111
+ for await (const chunk of jsonArrayChunks(
112
+ (templates as ExportedCollection).load,
113
+ new AbortController().signal,
114
+ SINGLE_ROW_PAGE,
115
+ )) {
116
+ text += chunk;
117
+ }
118
+
119
+ const paged = (JSON.parse(text) as TemplateRow[]).map(
120
+ (template) => template.slug,
121
+ );
122
+ expect(paged).to.have.members(slugs);
123
+ expect(new Set(paged).size).to.equal(paged.length);
124
+ });
125
+ });
@@ -0,0 +1,129 @@
1
+ import { expect } from "chai";
2
+ import type { MailingSend, MailingTemplate } from "../../db";
3
+ import {
4
+ type AttentionItem,
5
+ buildAttentionItems,
6
+ } from "../../services/attention";
7
+
8
+ const send = (isTest: boolean): MailingSend =>
9
+ ({ status: "sent", templateSlug: "welcome", isTest }) as MailingSend;
10
+
11
+ const NO_TEMPLATES: MailingTemplate[] = [];
12
+
13
+ const findExcluded = (items: AttentionItem[]) =>
14
+ items.find((item) => item.id === "test-only-window");
15
+
16
+ describe("[unit] services/attention test-only window", () => {
17
+ it("explains the empty figures when every send in the window is a test", () => {
18
+ const items = buildAttentionItems({
19
+ templates: NO_TEMPLATES,
20
+ sends: [],
21
+ excluded: [send(true), send(true)],
22
+ tracking: null,
23
+ });
24
+ const item = findExcluded(items);
25
+ expect(item).to.not.equal(undefined);
26
+ expect(item).to.deep.include({
27
+ tone: "neutral",
28
+ title: "$dms_mailing.attention.test_only_window.title",
29
+ description: "$dms_mailing.attention.test_only_window.description",
30
+ });
31
+ expect(item?.params).to.deep.equal({ count: 2 });
32
+ });
33
+
34
+ it("stays quiet when the window holds real sends too", () => {
35
+ const items = buildAttentionItems({
36
+ templates: NO_TEMPLATES,
37
+ sends: [send(false)],
38
+ excluded: [send(true)],
39
+ tracking: null,
40
+ });
41
+ expect(findExcluded(items)).to.equal(undefined);
42
+ });
43
+
44
+ it("stays quiet when the window is genuinely empty", () => {
45
+ const items = buildAttentionItems({
46
+ templates: NO_TEMPLATES,
47
+ sends: [],
48
+ excluded: [],
49
+ tracking: null,
50
+ });
51
+ expect(findExcluded(items)).to.equal(undefined);
52
+ });
53
+ });
54
+
55
+ const findUntracked = (items: AttentionItem[]) =>
56
+ items.find((item) => item.id === "untracked-rates");
57
+
58
+ const TRACKS_NOTHING = {
59
+ name: "smtp",
60
+ openTracking: false,
61
+ clickTracking: false,
62
+ };
63
+ const TRACKS_EVERYTHING = {
64
+ name: "postmark",
65
+ openTracking: true,
66
+ clickTracking: true,
67
+ };
68
+
69
+ describe("[unit] services/attention untracked rates", () => {
70
+ it("explains the flat rates when the provider reports neither opens nor clicks", () => {
71
+ const items = buildAttentionItems({
72
+ templates: NO_TEMPLATES,
73
+ sends: [send(false)],
74
+ excluded: [],
75
+ tracking: TRACKS_NOTHING,
76
+ });
77
+ const item = findUntracked(items);
78
+ expect(item).to.not.equal(undefined);
79
+ expect(item).to.deep.include({
80
+ tone: "neutral",
81
+ title: "$dms_mailing.attention.untracked_rates.title",
82
+ description: "$dms_mailing.attention.untracked_rates.description",
83
+ });
84
+ expect(item?.params).to.deep.equal({ provider: "smtp" });
85
+ });
86
+
87
+ it("stays quiet when the provider tracks both", () => {
88
+ const items = buildAttentionItems({
89
+ templates: NO_TEMPLATES,
90
+ sends: [send(false)],
91
+ excluded: [],
92
+ tracking: TRACKS_EVERYTHING,
93
+ });
94
+ expect(findUntracked(items)).to.equal(undefined);
95
+ });
96
+
97
+ it("still fires when only one of the two is tracked, since a rate stays flat", () => {
98
+ const items = buildAttentionItems({
99
+ templates: NO_TEMPLATES,
100
+ sends: [send(false)],
101
+ excluded: [],
102
+ tracking: { ...TRACKS_NOTHING, openTracking: true },
103
+ });
104
+ expect(findUntracked(items)).to.not.equal(undefined);
105
+ });
106
+
107
+ // The notice explains figures the window actually shows. With nothing sent
108
+ // there are no rates on screen to explain, and it would just be noise.
109
+ it("stays quiet when the window holds no send at all", () => {
110
+ const items = buildAttentionItems({
111
+ templates: NO_TEMPLATES,
112
+ sends: [],
113
+ excluded: [],
114
+ tracking: TRACKS_NOTHING,
115
+ });
116
+ expect(findUntracked(items)).to.equal(undefined);
117
+ });
118
+
119
+ // The provider may be unreachable; that is the provider card's business.
120
+ it("stays quiet when the provider capabilities are unknown", () => {
121
+ const items = buildAttentionItems({
122
+ templates: NO_TEMPLATES,
123
+ sends: [send(false)],
124
+ excluded: [],
125
+ tracking: null,
126
+ });
127
+ expect(findUntracked(items)).to.equal(undefined);
128
+ });
129
+ });
@@ -0,0 +1,130 @@
1
+ import { expect } from "chai";
2
+ import { unregisterAutomationNodes } from "../../automation";
3
+ import {
4
+ clearEmailEventSubscriptions,
5
+ type EmailEventPayload,
6
+ emailEventTrigger,
7
+ emitEmailEvent,
8
+ } from "../../automation/email-event-trigger";
9
+ import { sendTemplateAction } from "../../automation/send-template-action";
10
+
11
+ const EVENT: EmailEventPayload = {
12
+ tenantId: "t",
13
+ sendId: "s",
14
+ templateSlug: "x",
15
+ type: "bounced",
16
+ recipientEmail: "a@b",
17
+ };
18
+
19
+ describe("[unit] automation nodes", () => {
20
+ afterEach(() => {
21
+ clearEmailEventSubscriptions();
22
+ });
23
+
24
+ it("describes the send-template action", () => {
25
+ expect(sendTemplateAction.id).to.equal("mailing.send-template");
26
+ expect(sendTemplateAction.inputSchema).to.deep.include({
27
+ required: ["tenantId", "slug", "to"],
28
+ });
29
+ });
30
+
31
+ // The automation module ships no loop node: a procedure cannot call this
32
+ // action once per contact, so the fan-out has to happen inside it. The port
33
+ // must therefore accept a list, which `sendTemplate` already handles.
34
+ it("accepts a list of recipients on the `to` port, not just one address", () => {
35
+ const properties = (
36
+ sendTemplateAction.inputSchema as {
37
+ properties: Record<string, { type?: unknown; items?: unknown }>;
38
+ }
39
+ ).properties;
40
+ const to = properties.to as { type?: unknown; items?: unknown };
41
+ expect(to.type).to.deep.equal(["string", "array"]);
42
+ expect(to.items).to.deep.equal({ type: "string" });
43
+ });
44
+
45
+ it("fans e-mail events out to active trigger subscriptions filtered by type", async () => {
46
+ const received: unknown[] = [];
47
+ const handle = await emailEventTrigger.activate(
48
+ { types: ["bounced"] },
49
+ (payload) => {
50
+ received.push(payload);
51
+ },
52
+ );
53
+ emitEmailEvent({
54
+ tenantId: "t",
55
+ sendId: "s",
56
+ templateSlug: "x",
57
+ type: "opened",
58
+ recipientEmail: "a@b",
59
+ });
60
+ emitEmailEvent({
61
+ tenantId: "t",
62
+ sendId: "s",
63
+ templateSlug: "x",
64
+ type: "bounced",
65
+ recipientEmail: "a@b",
66
+ });
67
+ await emailEventTrigger.deactivate(handle);
68
+ emitEmailEvent({
69
+ tenantId: "t",
70
+ sendId: "s",
71
+ templateSlug: "x",
72
+ type: "bounced",
73
+ recipientEmail: "a@b",
74
+ });
75
+ expect(received).to.have.length(1);
76
+ });
77
+
78
+ it("fans every event out to a subscription that filters no type", async () => {
79
+ const received: unknown[] = [];
80
+ const handle = await emailEventTrigger.activate({}, (payload) => {
81
+ received.push(payload);
82
+ });
83
+ emitEmailEvent({
84
+ tenantId: "t",
85
+ sendId: "s",
86
+ templateSlug: "x",
87
+ type: "opened",
88
+ recipientEmail: "a@b",
89
+ });
90
+ await emailEventTrigger.deactivate(handle);
91
+ expect(received).to.have.length(1);
92
+ });
93
+
94
+ it("keeps fanning out when a subscription throws", async () => {
95
+ const received: EmailEventPayload[] = [];
96
+ await emailEventTrigger.activate({}, () => {
97
+ throw new Error("subscriber exploded");
98
+ });
99
+ await emailEventTrigger.activate({}, (payload) => {
100
+ received.push(payload);
101
+ });
102
+
103
+ expect(() => emitEmailEvent(EVENT)).to.not.throw();
104
+ expect(received).to.deep.equal([EVENT]);
105
+ });
106
+
107
+ it("stops fanning out once the subscriptions are cleared", async () => {
108
+ const received: EmailEventPayload[] = [];
109
+ await emailEventTrigger.activate({}, (payload) => {
110
+ received.push(payload);
111
+ });
112
+
113
+ clearEmailEventSubscriptions();
114
+ emitEmailEvent(EVENT);
115
+
116
+ expect(received).to.have.length(0);
117
+ });
118
+
119
+ it("clears the subscriptions when the node types are unregistered", async () => {
120
+ const received: EmailEventPayload[] = [];
121
+ await emailEventTrigger.activate({}, (payload) => {
122
+ received.push(payload);
123
+ });
124
+
125
+ unregisterAutomationNodes();
126
+ emitEmailEvent(EVENT);
127
+
128
+ expect(received).to.have.length(0);
129
+ });
130
+ });
@@ -0,0 +1,51 @@
1
+ import { expect } from "chai";
2
+ import {
3
+ parseCategories,
4
+ reassignRemovedCategories,
5
+ } from "../../services/categories";
6
+ import type { MailingTemplate } from "../../db";
7
+ import type { TemplateCategory } from "../../types";
8
+
9
+ const CATEGORIES: TemplateCategory[] = [
10
+ { id: "orders", label: "Orders", icon: "i-ph-package" },
11
+ { id: "billing", label: "Billing", icon: "i-ph-file-text" },
12
+ ];
13
+
14
+ const template = (id: string, category?: string): MailingTemplate =>
15
+ ({ _id: id, category }) as MailingTemplate;
16
+
17
+ describe("[unit] services/categories parseCategories", () => {
18
+ it("reads the list the form field stores, as a string or an array", () => {
19
+ expect(parseCategories(JSON.stringify(CATEGORIES))).to.deep.equal(
20
+ CATEGORIES,
21
+ );
22
+ expect(parseCategories(CATEGORIES)).to.deep.equal(CATEGORIES);
23
+ });
24
+
25
+ it("answers an empty list for blank or broken input", () => {
26
+ expect(parseCategories("")).to.deep.equal([]);
27
+ expect(parseCategories("not json")).to.deep.equal([]);
28
+ expect(parseCategories(undefined)).to.deep.equal([]);
29
+ });
30
+ });
31
+
32
+ describe("[unit] services/categories reassignRemovedCategories", () => {
33
+ it("names the templates whose category no longer exists", () => {
34
+ const templates = [
35
+ template("a", "orders"),
36
+ template("b", "gone"),
37
+ template("c"),
38
+ template("d", "gone"),
39
+ ];
40
+ expect(reassignRemovedCategories(templates, CATEGORIES)).to.deep.equal([
41
+ "b",
42
+ "d",
43
+ ]);
44
+ });
45
+
46
+ it("names nothing when every category survives", () => {
47
+ expect(
48
+ reassignRemovedCategories([template("a", "orders")], CATEGORIES),
49
+ ).to.deep.equal([]);
50
+ });
51
+ });
@@ -0,0 +1,35 @@
1
+ import { expect } from "chai";
2
+ import { evaluateCondition } from "../../../engine";
3
+ import type { Condition } from "../../../types";
4
+
5
+ const condition = (operator: Condition["operator"], value = ""): Condition => ({
6
+ path: "order.total",
7
+ operator,
8
+ value,
9
+ });
10
+
11
+ describe("[unit] engine/conditions", () => {
12
+ it("treats truthy/falsy with JS semantics and missing as false", () => {
13
+ expect(
14
+ evaluateCondition(condition("truthy"), { order: { total: 1 } }),
15
+ ).to.equal(true);
16
+ expect(
17
+ evaluateCondition(condition("truthy"), { order: { total: 0 } }),
18
+ ).to.equal(false);
19
+ expect(evaluateCondition(condition("falsy"), {})).to.equal(true);
20
+ });
21
+ it("compares as strings for eq/ne and as numbers for gt/lt", () => {
22
+ expect(
23
+ evaluateCondition(condition("eq", "42"), { order: { total: 42 } }),
24
+ ).to.equal(true);
25
+ expect(
26
+ evaluateCondition(condition("ne", "42"), { order: { total: "42" } }),
27
+ ).to.equal(false);
28
+ expect(
29
+ evaluateCondition(condition("gt", "10"), { order: { total: 11 } }),
30
+ ).to.equal(true);
31
+ expect(
32
+ evaluateCondition(condition("lt", "10"), { order: { total: "abc" } }),
33
+ ).to.equal(false);
34
+ });
35
+ });