@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,118 @@
1
+ import { expect } from "chai";
2
+ import {
3
+ collectContentVariablePaths,
4
+ collectVariablePaths,
5
+ missingRequiredVariables,
6
+ } from "../../../engine";
7
+ import type {
8
+ LocaleContent,
9
+ TemplateContent,
10
+ VariableDefinition,
11
+ } from "../../../types";
12
+
13
+ const CONTENT: LocaleContent = {
14
+ subject: "Order {{order.ref}}",
15
+ preheader: "",
16
+ blocks: [
17
+ {
18
+ id: "h",
19
+ type: "heading",
20
+ text: "Hi {{customer.firstName}}",
21
+ align: "left",
22
+ size: 23,
23
+ visibleIf: null,
24
+ },
25
+ {
26
+ id: "l",
27
+ type: "list",
28
+ source: "order.lines",
29
+ labelPath: "label",
30
+ valuePath: "total",
31
+ visibleIf: null,
32
+ },
33
+ {
34
+ id: "i",
35
+ type: "if",
36
+ condition: { path: "order.hasGift", operator: "truthy", value: "" },
37
+ visibleIf: null,
38
+ children: [
39
+ {
40
+ id: "p",
41
+ type: "paragraph",
42
+ text: "{{gift.note}}",
43
+ align: "left",
44
+ visibleIf: null,
45
+ },
46
+ ],
47
+ elseChildren: null,
48
+ },
49
+ {
50
+ id: "b",
51
+ type: "button",
52
+ text: "Track",
53
+ href: "{{order.trackUrl}}",
54
+ align: "left",
55
+ visibleIf: { path: "order.shipped", operator: "truthy", value: "" },
56
+ },
57
+ {
58
+ id: "f",
59
+ type: "footer",
60
+ text: "",
61
+ unsubscribeLabel: "u",
62
+ preferencesLabel: "p",
63
+ visibleIf: null,
64
+ },
65
+ ],
66
+ };
67
+
68
+ describe("[unit] engine/coverage", () => {
69
+ it("collects every referenced path once, sorted, ignoring list item paths and system paths", () => {
70
+ expect(collectVariablePaths(CONTENT)).to.deep.equal([
71
+ "customer.firstName",
72
+ "gift.note",
73
+ "order.hasGift",
74
+ "order.lines",
75
+ "order.ref",
76
+ "order.shipped",
77
+ "order.trackUrl",
78
+ ]);
79
+ });
80
+ it("lists declared required variables missing from a data set", () => {
81
+ const declared: VariableDefinition[] = [
82
+ { path: "order.ref", type: "string", required: true },
83
+ { path: "order.lines[]", type: "array", required: true },
84
+ { path: "customer.firstName", type: "string", required: false },
85
+ ];
86
+ expect(
87
+ missingRequiredVariables(declared, { order: { ref: "X" } }),
88
+ ).to.deep.equal(["order.lines"]);
89
+ });
90
+ });
91
+
92
+ describe("[unit] engine/coverage across locales", () => {
93
+ it("unions the paths of every locale, sorted and deduplicated", () => {
94
+ const french: LocaleContent = {
95
+ subject: "Commande {{order.ref}}",
96
+ preheader: "",
97
+ blocks: [
98
+ {
99
+ id: "p",
100
+ type: "paragraph",
101
+ text: "Bonjour {{customer.lastName}}",
102
+ align: "left",
103
+ visibleIf: null,
104
+ },
105
+ ],
106
+ };
107
+ const content: TemplateContent = { locales: { en: CONTENT, fr: french } };
108
+ const paths = collectContentVariablePaths(content);
109
+ expect(paths).to.include("customer.lastName");
110
+ expect(paths).to.include("order.ref");
111
+ expect(paths.filter((path) => path === "order.ref")).to.have.lengthOf(1);
112
+ expect(paths).to.deep.equal([...paths].sort());
113
+ });
114
+
115
+ it("answers an empty list for content with no locale", () => {
116
+ expect(collectContentVariablePaths({ locales: {} })).to.deep.equal([]);
117
+ });
118
+ });
@@ -0,0 +1,86 @@
1
+ import { expect } from "chai";
2
+ import { renderFallbackHtml } from "../../../engine";
3
+ import type { ResolvedEmail } from "../../../types";
4
+
5
+ const EMAIL: ResolvedEmail = {
6
+ subject: "Hello",
7
+ preheader: "Pre",
8
+ missing: [],
9
+ hiddenBlockIds: [],
10
+ blocks: [
11
+ { id: "h", type: "heading", text: "Hi <Sofie>", align: "left", size: 23 },
12
+ {
13
+ id: "b",
14
+ type: "button",
15
+ text: "Go",
16
+ href: "https://x.test/?a=1&b=2",
17
+ align: "center",
18
+ },
19
+ { id: "l", type: "list", rows: [{ label: "Chair", value: "64" }] },
20
+ {
21
+ id: "f",
22
+ type: "footer",
23
+ text: "Antelope",
24
+ unsubscribeLabel: "Unsub",
25
+ preferencesLabel: "Prefs",
26
+ unsubscribeUrl: "https://u",
27
+ preferencesUrl: "",
28
+ },
29
+ ],
30
+ };
31
+
32
+ describe("[unit] engine/fallback-html", () => {
33
+ const html = renderFallbackHtml(EMAIL, "en");
34
+ it("produces a full document with the subject as title and escaped content", () => {
35
+ expect(html).to.match(/^<!DOCTYPE html>/);
36
+ expect(html).to.include("<title>Hello</title>");
37
+ expect(html).to.include("Hi &lt;Sofie&gt;");
38
+ expect(html).to.include('href="https://x.test/?a=1&amp;b=2"');
39
+ });
40
+ it("renders list rows and only the footer links that have a url", () => {
41
+ expect(html).to.include("Chair");
42
+ expect(html).to.include('href="https://u"');
43
+ expect(html).to.not.include("Prefs");
44
+ });
45
+ });
46
+
47
+ const withHref = (href: string): ResolvedEmail => ({
48
+ subject: "S",
49
+ preheader: "P",
50
+ missing: [],
51
+ hiddenBlockIds: [],
52
+ blocks: [{ id: "b", type: "button", text: "Go", href, align: "left" }],
53
+ });
54
+
55
+ describe("[unit] engine/fallback-html link schemes", () => {
56
+ // A link target can come straight from an interpolated variable, so it is
57
+ // recipient-supplied data. Escaping the entities is not enough: the scheme
58
+ // itself has to be one a mail client may follow.
59
+ it("keeps the schemes an e-mail legitimately uses", () => {
60
+ for (const href of [
61
+ "https://x.test/a?b=1",
62
+ "http://x.test",
63
+ "mailto:a@b.test",
64
+ "/relative/path",
65
+ "{{order.trackUrl}}",
66
+ ]) {
67
+ expect(renderFallbackHtml(withHref(href), "en")).to.contain("href=");
68
+ expect(renderFallbackHtml(withHref(href), "en")).to.not.contain(
69
+ 'href="#"',
70
+ );
71
+ }
72
+ });
73
+
74
+ it("neutralises a scheme that carries code", () => {
75
+ for (const href of [
76
+ "javascript:alert(1)",
77
+ "JavaScript:alert(1)",
78
+ " javascript:alert(1)",
79
+ "data:text/html,<script>alert(1)</script>",
80
+ "vbscript:msgbox",
81
+ ]) {
82
+ expect(renderFallbackHtml(withHref(href), "en")).to.contain('href="#"');
83
+ expect(renderFallbackHtml(withHref(href), "en")).to.not.contain("alert");
84
+ }
85
+ });
86
+ });
@@ -0,0 +1,39 @@
1
+ import { expect } from "chai";
2
+ import { getPath, normalizePath } from "../../../engine";
3
+
4
+ const DATA = {
5
+ order: { ref: "CMD-1", lines: [{ label: "A" }] },
6
+ customer: { firstName: "Sofie" },
7
+ };
8
+
9
+ describe("[unit] engine/paths", () => {
10
+ it("reads nested values", () => {
11
+ expect(getPath(DATA, "order.ref")).to.equal("CMD-1");
12
+ expect(getPath(DATA, "order.lines")).to.deep.equal([{ label: "A" }]);
13
+ });
14
+ it("returns undefined for missing segments without throwing", () => {
15
+ expect(getPath(DATA, "order.missing.deep")).to.equal(undefined);
16
+ expect(getPath(null, "a")).to.equal(undefined);
17
+ });
18
+ it("strips the array suffix used in declarations", () => {
19
+ expect(normalizePath("order.lines[]")).to.equal("order.lines");
20
+ });
21
+ });
22
+
23
+ describe("[unit] engine/paths reserved keys", () => {
24
+ // A token path is authored, not recipient-supplied, but `{{constructor}}`
25
+ // still resolved to the Object constructor and String()'d its source into the
26
+ // e-mail. Nothing legitimate walks through these.
27
+ it("refuses to walk the prototype chain", () => {
28
+ expect(getPath({ a: 1 }, "constructor")).to.equal(undefined);
29
+ expect(getPath({ a: 1 }, "__proto__")).to.equal(undefined);
30
+ expect(getPath({ a: 1 }, "prototype")).to.equal(undefined);
31
+ expect(
32
+ getPath({ a: { __proto__: { leaked: 1 } } }, "a.__proto__"),
33
+ ).to.equal(undefined);
34
+ });
35
+
36
+ it("still reads an ordinary path", () => {
37
+ expect(getPath({ a: { b: 2 } }, "a.b")).to.equal(2);
38
+ });
39
+ });
@@ -0,0 +1,176 @@
1
+ import { expect } from "chai";
2
+ import { resolveEmail } from "../../../engine";
3
+ import type { LocaleContent } from "../../../types";
4
+
5
+ const CONTENT: LocaleContent = {
6
+ subject: "Order {{order.ref}}",
7
+ preheader: "Thanks {{customer.firstName}}",
8
+ blocks: [
9
+ {
10
+ id: "h",
11
+ type: "heading",
12
+ text: "Thanks {{customer.firstName}}",
13
+ align: "left",
14
+ size: 23,
15
+ visibleIf: null,
16
+ },
17
+ {
18
+ id: "l",
19
+ type: "list",
20
+ source: "order.lines",
21
+ labelPath: "label",
22
+ valuePath: "total",
23
+ visibleIf: null,
24
+ },
25
+ {
26
+ id: "t",
27
+ type: "total",
28
+ label: "Total",
29
+ value: "{{order.total}}",
30
+ visibleIf: null,
31
+ },
32
+ {
33
+ id: "i",
34
+ type: "if",
35
+ condition: { path: "order.hasGift", operator: "truthy", value: "" },
36
+ visibleIf: null,
37
+ children: [
38
+ {
39
+ id: "gift",
40
+ type: "paragraph",
41
+ text: "Gift inside",
42
+ align: "left",
43
+ visibleIf: null,
44
+ },
45
+ ],
46
+ elseChildren: [
47
+ {
48
+ id: "nogift",
49
+ type: "paragraph",
50
+ text: "No gift",
51
+ align: "left",
52
+ visibleIf: null,
53
+ },
54
+ ],
55
+ },
56
+ {
57
+ id: "b",
58
+ type: "button",
59
+ text: "Track",
60
+ href: "{{order.trackUrl}}",
61
+ align: "center",
62
+ visibleIf: { path: "order.shipped", operator: "truthy", value: "" },
63
+ },
64
+ {
65
+ id: "f",
66
+ type: "footer",
67
+ text: "Antelope",
68
+ unsubscribeLabel: "Unsubscribe",
69
+ preferencesLabel: "Preferences",
70
+ visibleIf: null,
71
+ },
72
+ ],
73
+ };
74
+
75
+ const DATA = {
76
+ customer: { firstName: "Sofie" },
77
+ order: {
78
+ ref: "CMD-1",
79
+ total: "70,50 €",
80
+ hasGift: true,
81
+ lines: [
82
+ { label: "Chair", total: "64" },
83
+ { label: "Shipping", total: "6,50" },
84
+ ],
85
+ },
86
+ unsubscribeUrl: "https://u",
87
+ };
88
+
89
+ describe("[unit] engine/resolve", () => {
90
+ const email = resolveEmail(CONTENT, DATA);
91
+ it("interpolates subject and preheader", () => {
92
+ expect(email.subject).to.equal("Order CMD-1");
93
+ expect(email.preheader).to.equal("Thanks Sofie");
94
+ });
95
+ it("expands lists, picks the if branch and flattens it", () => {
96
+ expect(email.blocks.map((block) => block.id)).to.deep.equal([
97
+ "h",
98
+ "l",
99
+ "t",
100
+ "gift",
101
+ "f",
102
+ ]);
103
+ const list = email.blocks[1]!;
104
+ expect(list.type === "list" && list.rows).to.deep.equal([
105
+ { label: "Chair", value: "64" },
106
+ { label: "Shipping", value: "6,50" },
107
+ ]);
108
+ });
109
+ it("hides blocks whose visibleIf is false and reports them", () => {
110
+ expect(email.hiddenBlockIds).to.deep.equal(["nogift", "b"]);
111
+ });
112
+ // The only unresolved user path in this fixture is the tracking URL of the
113
+ // button, which `order.shipped` hides — so it is not reported. The two
114
+ // directions of that rule are covered below.
115
+ it("resolves the footer's system urls without reporting them missing", () => {
116
+ expect(email.missing).to.deep.equal([]);
117
+ const footer = email.blocks[4]!;
118
+ expect(footer.type === "footer" && footer.unsubscribeUrl).to.equal(
119
+ "https://u",
120
+ );
121
+ expect(footer.type === "footer" && footer.preferencesUrl).to.equal("");
122
+ });
123
+ });
124
+
125
+ describe("[unit] engine/resolve hidden blocks", () => {
126
+ const hiddenParagraph = {
127
+ subject: "S",
128
+ preheader: "",
129
+ blocks: [
130
+ {
131
+ id: "p1",
132
+ type: "paragraph" as const,
133
+ align: "left" as const,
134
+ text: "A gift: {{order.giftMessage}}",
135
+ visibleIf: {
136
+ path: "order.hasGift",
137
+ operator: "truthy" as const,
138
+ value: "",
139
+ },
140
+ },
141
+ ],
142
+ };
143
+
144
+ // `blockOnMissingVariables` refuses a send whose resolved e-mail reports a
145
+ // missing variable. A block the condition hides is never rendered, so what it
146
+ // references must not count — otherwise conditional content makes that
147
+ // setting unusable.
148
+ it("does not report a variable only a hidden block references", () => {
149
+ const email = resolveEmail(hiddenParagraph, { order: { hasGift: false } });
150
+ expect(email.blocks).to.have.lengthOf(0);
151
+ expect(email.hiddenBlockIds).to.deep.equal(["p1"]);
152
+ expect(email.missing).to.deep.equal([]);
153
+ });
154
+
155
+ it("still reports it when the block is shown", () => {
156
+ const email = resolveEmail(hiddenParagraph, { order: { hasGift: true } });
157
+ expect(email.blocks).to.have.lengthOf(1);
158
+ expect(email.missing).to.deep.equal(["order.giftMessage"]);
159
+ });
160
+ });
161
+
162
+ describe("[unit] engine/resolve system variables", () => {
163
+ // `unsubscribeUrl` and `preferencesUrl` are filled by the runtime, so a
164
+ // template referencing one must not ask the author to declare it.
165
+ it("keeps system paths out of the missing list, unlike user paths", () => {
166
+ const email = resolveEmail(
167
+ {
168
+ subject: "{{unsubscribeUrl}} {{preferencesUrl}} {{order.ref}}",
169
+ preheader: "",
170
+ blocks: [],
171
+ },
172
+ {},
173
+ );
174
+ expect(email.missing).to.deep.equal(["order.ref"]);
175
+ });
176
+ });
@@ -0,0 +1,26 @@
1
+ import { expect } from "chai";
2
+ import { extractTokens, interpolate } from "../../../engine";
3
+
4
+ describe("[unit] engine/tokens", () => {
5
+ it("extracts unique trimmed token paths in order", () => {
6
+ expect(
7
+ extractTokens("Hi {{ customer.firstName }}, {{order.ref}} {{order.ref}}"),
8
+ ).to.deep.equal(["customer.firstName", "order.ref"]);
9
+ });
10
+ it("interpolates known paths and reports missing ones", () => {
11
+ const result = interpolate("Hi {{customer.firstName}}, ref {{order.ref}}", {
12
+ customer: { firstName: "Sofie" },
13
+ });
14
+ expect(result.text).to.equal("Hi Sofie, ref {{order.ref}}");
15
+ expect(result.missing).to.deep.equal(["order.ref"]);
16
+ });
17
+ it("renders numbers and booleans as text and treats null as missing", () => {
18
+ const result = interpolate("{{count}}-{{flag}}-{{nothing}}", {
19
+ count: 3,
20
+ flag: false,
21
+ nothing: null,
22
+ });
23
+ expect(result.text).to.equal("3-false-{{nothing}}");
24
+ expect(result.missing).to.deep.equal(["nothing"]);
25
+ });
26
+ });
@@ -0,0 +1,19 @@
1
+ import { expect } from "chai";
2
+ import { applyEvent } from "../../services/events";
3
+
4
+ describe("[unit] services/events", () => {
5
+ it("promotes the status forward only and counts opens/clicks", () => {
6
+ expect(
7
+ applyEvent({ status: "sent", opens: 0, clicks: 0 }, "opened"),
8
+ ).to.deep.equal({ status: "opened", opens: 1, clicks: 0 });
9
+ expect(
10
+ applyEvent({ status: "clicked", opens: 1, clicks: 1 }, "opened"),
11
+ ).to.deep.equal({ status: "clicked", opens: 2, clicks: 1 });
12
+ expect(
13
+ applyEvent({ status: "delivered", opens: 0, clicks: 0 }, "bounced"),
14
+ ).to.deep.equal({ status: "bounced", opens: 0, clicks: 0 });
15
+ expect(
16
+ applyEvent({ status: "bounced", opens: 0, clicks: 0 }, "delivered"),
17
+ ).to.deep.equal({ status: "bounced", opens: 0, clicks: 0 });
18
+ });
19
+ });
@@ -0,0 +1,170 @@
1
+ import { expect } from "chai";
2
+ import type { MailingSend } from "../../db";
3
+ import {
4
+ aggregate,
5
+ dailySeries,
6
+ deltaPercent,
7
+ domainOf,
8
+ forAudience,
9
+ readAudience,
10
+ } from "../../services/metrics";
11
+
12
+ const LATENCY_MS = 100;
13
+ const YEAR = 2026;
14
+ const MONTH = 8;
15
+
16
+ const send = (
17
+ status: MailingSend["status"],
18
+ email: string,
19
+ day: number,
20
+ opens = 0,
21
+ clicks = 0,
22
+ ): MailingSend =>
23
+ ({
24
+ status,
25
+ recipientEmail: email,
26
+ createdAt: new Date(Date.UTC(YEAR, MONTH, day)),
27
+ opens,
28
+ clicks,
29
+ latencyMs: LATENCY_MS,
30
+ }) as MailingSend;
31
+
32
+ const SENDS = [
33
+ send("delivered", "a@gmail.com", 1, 1),
34
+ send("opened", "b@gmail.com", 1, 1, 0),
35
+ send("clicked", "c@outlook.com", 2, 2, 1),
36
+ send("bounced", "d@bad.tld", 2),
37
+ send("failed", "e@gmail.com", 3),
38
+ ];
39
+
40
+ const TOLERANCE = 0.01;
41
+ const EXPECTED_DELIVERABILITY = 60;
42
+ const EXPECTED_OPEN_RATE = 66.67;
43
+ const EXPECTED_DELTA = 20;
44
+
45
+ describe("[unit] services/metrics", () => {
46
+ it("aggregates the funnel counters", () => {
47
+ const totals = aggregate(SENDS);
48
+ expect(totals).to.deep.include({
49
+ sends: 5,
50
+ delivered: 3,
51
+ opened: 2,
52
+ clicked: 1,
53
+ bounced: 1,
54
+ failed: 1,
55
+ unsubscribed: 0,
56
+ queued: 0,
57
+ });
58
+ expect(totals.deliverability).to.be.closeTo(
59
+ EXPECTED_DELIVERABILITY,
60
+ TOLERANCE,
61
+ );
62
+ expect(totals.openRate).to.be.closeTo(EXPECTED_OPEN_RATE, TOLERANCE);
63
+ });
64
+
65
+ it("computes deltas and daily series", () => {
66
+ expect(deltaPercent(120, LATENCY_MS)).to.equal(EXPECTED_DELTA);
67
+ expect(deltaPercent(5, 0)).to.equal(0);
68
+ expect(
69
+ dailySeries(
70
+ SENDS,
71
+ new Date(Date.UTC(YEAR, MONTH, 1)),
72
+ new Date(Date.UTC(YEAR, MONTH, 4)),
73
+ ).map((point) => point.y),
74
+ ).to.deep.equal([2, 2, 1]);
75
+ expect(domainOf("x@Gmail.com")).to.equal("gmail.com");
76
+ });
77
+ });
78
+
79
+ const testSend = (): MailingSend =>
80
+ ({
81
+ status: "sent",
82
+ recipientEmail: "t@gmail.com",
83
+ createdAt: new Date(Date.UTC(YEAR, MONTH, 1)),
84
+ opens: 0,
85
+ clicks: 0,
86
+ latencyMs: LATENCY_MS,
87
+ isTest: true,
88
+ }) as MailingSend;
89
+
90
+ const MIXED = [...SENDS, testSend()];
91
+
92
+ describe("[unit] services/metrics audience", () => {
93
+ it("reads the audience from the query, defaulting to the business view", () => {
94
+ expect(readAudience("operational")).to.equal("operational");
95
+ expect(readAudience("business")).to.equal("business");
96
+ expect(readAudience(null)).to.equal("business");
97
+ expect(readAudience("nonsense")).to.equal("business");
98
+ });
99
+
100
+ it("hides test sends from the business view", () => {
101
+ expect(forAudience(MIXED, "business")).to.have.lengthOf(SENDS.length);
102
+ expect(aggregate(forAudience(MIXED, "business")).sends).to.equal(
103
+ SENDS.length,
104
+ );
105
+ });
106
+
107
+ it("counts every send in the operational view", () => {
108
+ expect(forAudience(MIXED, "operational")).to.have.lengthOf(MIXED.length);
109
+ expect(aggregate(forAudience(MIXED, "operational")).sends).to.equal(
110
+ MIXED.length,
111
+ );
112
+ });
113
+ });
114
+
115
+ // The DMS period selector sends local day boundaries: for a UTC+2 tenant,
116
+ // "last 7 days" arrives as 2026-09-03T22:00Z → 2026-09-10T21:59:59.999Z.
117
+ const WINDOW_FROM = new Date("2026-09-03T22:00:00.000Z");
118
+ const WINDOW_TO = new Date("2026-09-10T21:59:59.999Z");
119
+ const DAYS_IN_WINDOW = 7;
120
+
121
+ const sentAt = (iso: string): MailingSend =>
122
+ ({
123
+ status: "sent",
124
+ recipientEmail: "a@gmail.com",
125
+ createdAt: new Date(iso),
126
+ opens: 0,
127
+ clicks: 0,
128
+ latencyMs: LATENCY_MS,
129
+ }) as MailingSend;
130
+
131
+ describe("[unit] services/metrics daily series over a DMS period", () => {
132
+ it("covers every day of the window, last one included", () => {
133
+ const points = dailySeries([], WINDOW_FROM, WINDOW_TO);
134
+ expect(points).to.have.lengthOf(DAYS_IN_WINDOW);
135
+ expect(points[0]?.x).to.equal("2026-09-04");
136
+ expect(points[points.length - 1]?.x).to.equal("2026-09-10");
137
+ });
138
+
139
+ // The KPI counts this send through `aggregate`; the chart must agree.
140
+ it("counts a send made on the last day of the window", () => {
141
+ const points = dailySeries(
142
+ [sentAt("2026-09-10T12:30:00.000Z")],
143
+ WINDOW_FROM,
144
+ WINDOW_TO,
145
+ );
146
+ expect(points.find((point) => point.x === "2026-09-10")?.y).to.equal(1);
147
+ expect(points.reduce((total, point) => total + point.y, 0)).to.equal(1);
148
+ });
149
+
150
+ // Boundaries are local, keys must follow them: a send just after local
151
+ // midnight belongs to the local day that just started, not the UTC one that
152
+ // is still running.
153
+ it("buckets a send by the local day the window is cut on", () => {
154
+ const points = dailySeries(
155
+ [sentAt("2026-09-06T22:30:00.000Z")],
156
+ WINDOW_FROM,
157
+ WINDOW_TO,
158
+ );
159
+ expect(points.find((point) => point.x === "2026-09-07")?.y).to.equal(1);
160
+ });
161
+
162
+ it("ignores a send outside the window rather than folding it into an edge", () => {
163
+ const points = dailySeries(
164
+ [sentAt("2026-09-03T10:00:00.000Z"), sentAt("2026-09-11T10:00:00.000Z")],
165
+ WINDOW_FROM,
166
+ WINDOW_TO,
167
+ );
168
+ expect(points.reduce((total, point) => total + point.y, 0)).to.equal(0);
169
+ });
170
+ });