@antelopejs/dms-mailing 0.6.1 → 0.7.1

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 (236) hide show
  1. package/README.md +8 -7
  2. package/dist/constants.d.ts +1 -2
  3. package/dist/constants.js +2 -3
  4. package/dist/constants.js.map +1 -1
  5. package/dist/data/templates-table.d.ts +4 -1
  6. package/dist/data/templates-table.js +55 -20
  7. package/dist/data/templates-table.js.map +1 -1
  8. package/dist/data-types/categories-type.d.ts +5 -5
  9. package/dist/db/models/index.d.ts +0 -1
  10. package/dist/db/models/index.js +0 -1
  11. package/dist/db/models/index.js.map +1 -1
  12. package/dist/db/models/send-events.model.js +2 -3
  13. package/dist/db/models/send-events.model.js.map +1 -1
  14. package/dist/db/models/sends.model.js +2 -3
  15. package/dist/db/models/sends.model.js.map +1 -1
  16. package/dist/db/models/templates.model.d.ts +10 -1
  17. package/dist/db/models/templates.model.js +29 -10
  18. package/dist/db/models/templates.model.js.map +1 -1
  19. package/dist/db/tables/index.d.ts +0 -1
  20. package/dist/db/tables/index.js +0 -1
  21. package/dist/db/tables/index.js.map +1 -1
  22. package/dist/db/tables/settings.table.d.ts +0 -1
  23. package/dist/db/tables/settings.table.js +0 -4
  24. package/dist/db/tables/settings.table.js.map +1 -1
  25. package/dist/db/tables/templates.table.d.ts +7 -4
  26. package/dist/db/tables/templates.table.js +4 -14
  27. package/dist/db/tables/templates.table.js.map +1 -1
  28. package/dist/engine/formats.d.ts +13 -0
  29. package/dist/engine/formats.js +85 -0
  30. package/dist/engine/formats.js.map +1 -0
  31. package/dist/engine/index.d.ts +1 -0
  32. package/dist/engine/index.js +1 -0
  33. package/dist/engine/index.js.map +1 -1
  34. package/dist/engine/resolve.d.ts +2 -1
  35. package/dist/engine/resolve.js +14 -7
  36. package/dist/engine/resolve.js.map +1 -1
  37. package/dist/engine/tokens.d.ts +2 -1
  38. package/dist/engine/tokens.js +3 -5
  39. package/dist/engine/tokens.js.map +1 -1
  40. package/dist/frontend-module.d.ts +2 -0
  41. package/dist/frontend-module.js +24 -0
  42. package/dist/frontend-module.js.map +1 -0
  43. package/dist/hooks/index.d.ts +0 -1
  44. package/dist/hooks/index.js +0 -1
  45. package/dist/hooks/index.js.map +1 -1
  46. package/dist/hooks/tenant-export.js +4 -1
  47. package/dist/hooks/tenant-export.js.map +1 -1
  48. package/dist/implementations/dms-mailing/index.d.ts +3 -3
  49. package/dist/implementations/dms-mailing/index.js +8 -7
  50. package/dist/implementations/dms-mailing/index.js.map +1 -1
  51. package/dist/index.js +2 -20
  52. package/dist/index.js.map +1 -1
  53. package/dist/pages/overview.d.ts +1 -1
  54. package/dist/pages/overview.js +11 -13
  55. package/dist/pages/overview.js.map +1 -1
  56. package/dist/pages/sends.d.ts +1 -1
  57. package/dist/pages/sends.js +16 -8
  58. package/dist/pages/sends.js.map +1 -1
  59. package/dist/pages/settings/form.js +8 -0
  60. package/dist/pages/settings/form.js.map +1 -1
  61. package/dist/pages/templates.js +3 -8
  62. package/dist/pages/templates.js.map +1 -1
  63. package/dist/pages/toolbar.d.ts +12 -0
  64. package/dist/pages/toolbar.js +41 -0
  65. package/dist/pages/toolbar.js.map +1 -0
  66. package/dist/realtime.d.ts +2 -2
  67. package/dist/realtime.js +8 -8
  68. package/dist/realtime.js.map +1 -1
  69. package/dist/routes/metrics.d.ts +9 -2
  70. package/dist/routes/metrics.js +90 -35
  71. package/dist/routes/metrics.js.map +1 -1
  72. package/dist/routes/period.d.ts +1 -0
  73. package/dist/routes/period.js +19 -0
  74. package/dist/routes/period.js.map +1 -1
  75. package/dist/routes/sends.js +6 -3
  76. package/dist/routes/sends.js.map +1 -1
  77. package/dist/routes/settings.js +6 -7
  78. package/dist/routes/settings.js.map +1 -1
  79. package/dist/routes/templates.d.ts +8 -4
  80. package/dist/routes/templates.js +41 -20
  81. package/dist/routes/templates.js.map +1 -1
  82. package/dist/services/categories.d.ts +2 -1
  83. package/dist/services/categories.js +9 -0
  84. package/dist/services/categories.js.map +1 -1
  85. package/dist/services/declarations.js +42 -184
  86. package/dist/services/declarations.js.map +1 -1
  87. package/dist/services/events.js +1 -1
  88. package/dist/services/events.js.map +1 -1
  89. package/dist/services/localized-text.d.ts +7 -0
  90. package/dist/services/localized-text.js +28 -0
  91. package/dist/services/localized-text.js.map +1 -0
  92. package/dist/services/send-import.js +0 -2
  93. package/dist/services/send-import.js.map +1 -1
  94. package/dist/services/send-stat-items.js +34 -12
  95. package/dist/services/send-stat-items.js.map +1 -1
  96. package/dist/services/send-stats.d.ts +7 -0
  97. package/dist/services/send-stats.js +17 -5
  98. package/dist/services/send-stats.js.map +1 -1
  99. package/dist/services/send.d.ts +2 -0
  100. package/dist/services/send.js +11 -12
  101. package/dist/services/send.js.map +1 -1
  102. package/dist/services/sender-declaration.d.ts +2 -0
  103. package/dist/services/sender-declaration.js +18 -0
  104. package/dist/services/sender-declaration.js.map +1 -0
  105. package/dist/services/settings.d.ts +1 -1
  106. package/dist/services/settings.js +1 -5
  107. package/dist/services/settings.js.map +1 -1
  108. package/dist/services/starters.d.ts +2 -2
  109. package/dist/services/starters.js +1 -10
  110. package/dist/services/starters.js.map +1 -1
  111. package/dist/services/templates.d.ts +13 -7
  112. package/dist/services/templates.js +72 -33
  113. package/dist/services/templates.js.map +1 -1
  114. package/dist/services/webhook-status.d.ts +2 -1
  115. package/dist/services/webhook-status.js +6 -5
  116. package/dist/services/webhook-status.js.map +1 -1
  117. package/dist/types/settings.d.ts +8 -0
  118. package/dist/types/settings.js +33 -6
  119. package/dist/types/settings.js.map +1 -1
  120. package/dist/validation/declarations.schema.d.ts +25 -7
  121. package/dist/validation/declarations.schema.js +16 -3
  122. package/dist/validation/declarations.schema.js.map +1 -1
  123. package/dist/validation/settings.schema.d.ts +41 -41
  124. package/dist/validation/settings.schema.js +30 -6
  125. package/dist/validation/settings.schema.js.map +1 -1
  126. package/dist/validation/templates.schema.d.ts +13 -0
  127. package/dist/validation/templates.schema.js +6 -1
  128. package/dist/validation/templates.schema.js.map +1 -1
  129. package/frontend-vue/app/components/CategoriesInput.vue +15 -13
  130. package/frontend-vue/app/components/FunnelCard.vue +46 -3
  131. package/frontend-vue/app/components/NewTemplateModal.vue +4 -2
  132. package/frontend-vue/app/components/OverviewGate.vue +0 -8
  133. package/frontend-vue/app/components/ProviderPill.vue +50 -0
  134. package/frontend-vue/app/components/SendDrawer.vue +206 -183
  135. package/frontend-vue/app/components/StarterGallery.vue +1 -1
  136. package/frontend-vue/app/components/TemplateCard.vue +4 -1
  137. package/frontend-vue/app/components/TemplateDrawer.vue +3 -5
  138. package/frontend-vue/app/components/TemplateGallery.vue +57 -17
  139. package/frontend-vue/app/components/TemplatePreviewFrame.vue +41 -11
  140. package/frontend-vue/app/components/editor/EditorTemplatePane.vue +1 -3
  141. package/frontend-vue/app/components/editor/EditorTitle.vue +1 -7
  142. package/frontend-vue/app/composables/useLocalizedDefaults.ts +5 -7
  143. package/frontend-vue/app/composables/useMailingApi.ts +3 -3
  144. package/frontend-vue/app/composables/useMailingReads.ts +2 -2
  145. package/frontend-vue/app/composables/useTemplateCategories.ts +8 -7
  146. package/frontend-vue/app/types/mailing.ts +18 -0
  147. package/frontend-vue/app/utils/categories.ts +58 -49
  148. package/frontend-vue/app/utils/funnel.ts +25 -0
  149. package/frontend-vue/app/utils/period.ts +0 -7
  150. package/frontend-vue/app/utils/starters.ts +3 -1
  151. package/frontend-vue/app/utils/tokens.ts +6 -1
  152. package/frontend-vue/i18n/locales/mailing-en-GB.json +19 -28
  153. package/frontend-vue/i18n/locales/mailing-fr-FR.json +19 -28
  154. package/frontend-vue/package.json +1 -1
  155. package/frontend-vue/tests/categories-input.test.ts +4 -10
  156. package/frontend-vue/tests/categories.test.ts +55 -17
  157. package/frontend-vue/tests/conditions.test.ts +1 -4
  158. package/frontend-vue/tests/funnel-card.test.ts +27 -0
  159. package/frontend-vue/tests/funnel.test.ts +25 -0
  160. package/frontend-vue/tests/localized-defaults.test.ts +0 -29
  161. package/frontend-vue/tests/new-template-modal.test.ts +1 -1
  162. package/frontend-vue/tests/overview-gate.test.ts +1 -24
  163. package/frontend-vue/tests/page-reads.test.ts +9 -25
  164. package/frontend-vue/tests/period.test.ts +0 -12
  165. package/frontend-vue/tests/replay-toast.test.ts +2 -1
  166. package/frontend-vue/tests/send-drawer.test.ts +123 -0
  167. package/frontend-vue/tests/template-drawer.test.ts +1 -1
  168. package/frontend-vue/tests/template-gallery.test.ts +90 -3
  169. package/frontend-vue/tests/template-preview-frame.test.ts +92 -0
  170. package/package.json +8 -7
  171. package/skills/dms-mailing/SKILL.md +25 -18
  172. package/src/constants.ts +2 -2
  173. package/src/data/sends-table.ts +1 -1
  174. package/src/data/templates-table.ts +125 -33
  175. package/src/db/models/index.ts +0 -1
  176. package/src/db/models/send-events.model.ts +2 -2
  177. package/src/db/models/sends.model.ts +7 -8
  178. package/src/db/models/templates.model.ts +64 -16
  179. package/src/db/tables/index.ts +0 -1
  180. package/src/db/tables/settings.table.ts +0 -8
  181. package/src/db/tables/templates.table.ts +12 -17
  182. package/src/engine/formats.ts +124 -0
  183. package/src/engine/index.ts +1 -0
  184. package/src/engine/resolve.ts +18 -7
  185. package/src/engine/tokens.ts +7 -5
  186. package/src/frontend-module.ts +36 -0
  187. package/src/hooks/index.ts +0 -1
  188. package/src/hooks/tenant-export.ts +8 -1
  189. package/src/implementations/dms-mailing/index.ts +5 -16
  190. package/src/index.ts +3 -29
  191. package/src/pages/overview.ts +20 -16
  192. package/src/pages/sends.ts +27 -11
  193. package/src/pages/settings/form.ts +16 -0
  194. package/src/pages/templates.ts +4 -8
  195. package/src/pages/toolbar.ts +77 -0
  196. package/src/realtime.ts +17 -8
  197. package/src/routes/metrics.ts +158 -45
  198. package/src/routes/period.ts +32 -0
  199. package/src/routes/sends.ts +13 -4
  200. package/src/routes/settings.ts +11 -8
  201. package/src/routes/templates.ts +81 -37
  202. package/src/services/categories.ts +14 -1
  203. package/src/services/declarations.ts +64 -317
  204. package/src/services/events.ts +1 -1
  205. package/src/services/localized-text.ts +53 -0
  206. package/src/services/send-import.ts +2 -2
  207. package/src/services/send-stat-items.ts +60 -13
  208. package/src/services/send-stats.ts +37 -5
  209. package/src/services/send.ts +19 -20
  210. package/src/services/sender-declaration.ts +27 -0
  211. package/src/services/settings.ts +3 -10
  212. package/src/services/starters.ts +3 -15
  213. package/src/services/templates.ts +109 -63
  214. package/src/services/webhook-status.ts +3 -3
  215. package/src/types/settings.ts +55 -5
  216. package/src/validation/declarations.schema.ts +29 -3
  217. package/src/validation/settings.schema.ts +56 -14
  218. package/src/validation/templates.schema.ts +10 -0
  219. package/dist/db/models/template-slugs.model.d.ts +0 -27
  220. package/dist/db/models/template-slugs.model.js +0 -43
  221. package/dist/db/models/template-slugs.model.js.map +0 -1
  222. package/dist/db/tables/template-slugs.table.d.ts +0 -7
  223. package/dist/db/tables/template-slugs.table.js +0 -35
  224. package/dist/db/tables/template-slugs.table.js.map +0 -1
  225. package/dist/hooks/database-ready.d.ts +0 -7
  226. package/dist/hooks/database-ready.js +0 -26
  227. package/dist/hooks/database-ready.js.map +0 -1
  228. package/dist/services/fingerprint.d.ts +0 -1
  229. package/dist/services/fingerprint.js +0 -20
  230. package/dist/services/fingerprint.js.map +0 -1
  231. package/frontend-vue/app/components/PageHeader.vue +0 -94
  232. package/frontend-vue/app/composables/useOverviewSetup.ts +0 -21
  233. package/src/db/models/template-slugs.model.ts +0 -61
  234. package/src/db/tables/template-slugs.table.ts +0 -29
  235. package/src/hooks/database-ready.ts +0 -47
  236. package/src/services/fingerprint.ts +0 -22
@@ -3,6 +3,7 @@ import type {
3
3
  BlockText,
4
4
  ComposedText,
5
5
  } from "@antelopejs/interface-dms/base/types";
6
+ import { deltaPercent } from "./metrics";
6
7
  import type { SendStats } from "./send-stats";
7
8
 
8
9
  /** Past this wait the oldest queued send is worth a warning. */
@@ -39,11 +40,24 @@ function joined(parts: ComposedText[], fallback: ComposedText): BlockText {
39
40
  return parts.reduce((left, right) => text("joined", { left, right }));
40
41
  }
41
42
 
42
- function deltaText(delta: number | null): ComposedText[] {
43
- if (!delta) return [];
43
+ /** A figure of the comparison window, as its cell writes it. */
44
+ type ComparedValue = NonNullable<ComposedText["params"]>[string];
45
+
46
+ /**
47
+ * The change and the figure it is measured against ("▲ 8 % vs 120"), or the
48
+ * figure alone when no change can be stated (an equal or a zero figure).
49
+ * Nothing when the comparison is off or the window has no such figure.
50
+ */
51
+ function comparedText(
52
+ delta: number | null,
53
+ previous: ComparedValue | null,
54
+ ): ComposedText[] {
55
+ if (previous === null) return [];
56
+ if (!delta) return [text("versus", { previous })];
44
57
  return [
45
- text(delta > 0 ? "delta_up" : "delta_down", {
58
+ text(delta > 0 ? "delta_up_versus" : "delta_down_versus", {
46
59
  delta: percent(Math.abs(delta)),
60
+ previous,
47
61
  }),
48
62
  ];
49
63
  }
@@ -52,18 +66,29 @@ function sendsItem(stats: SendStats): StatGroupItem {
52
66
  const tests = stats.tests
53
67
  ? [text("tests_excluded", { count: count(stats.tests) })]
54
68
  : [];
69
+ const previous = stats.compared?.sends ?? null;
55
70
  return {
56
71
  id: "sends",
57
72
  icon: "i-ph-paper-plane-tilt",
58
73
  eyebrow: key("sends"),
59
74
  value: stats.sends,
60
75
  detail: joined(
61
- [...deltaText(stats.sendsDelta), ...tests],
76
+ [...comparedText(stats.sendsDelta, previous), ...tests],
62
77
  text("no_tests"),
63
78
  ),
64
79
  };
65
80
  }
66
81
 
82
+ /** The deliverability's change; none without a figure on either side. */
83
+ function deliverabilityCompared(stats: SendStats): ComposedText[] {
84
+ const previous = stats.compared?.deliverability ?? null;
85
+ if (!stats.sends || previous === null) return [];
86
+ return comparedText(
87
+ deltaPercent(stats.deliverability, previous),
88
+ percent(previous),
89
+ );
90
+ }
91
+
67
92
  function deliveredItem(stats: SendStats, provider: string): StatGroupItem {
68
93
  if (stats.isDeliveryUntracked) {
69
94
  return {
@@ -74,6 +99,10 @@ function deliveredItem(stats: SendStats, provider: string): StatGroupItem {
74
99
  detail: text("not_tracked", { provider: provider || THE_PROVIDER }),
75
100
  };
76
101
  }
102
+ const deliveredOf = text("delivered_of", {
103
+ delivered: stats.delivered,
104
+ sends: stats.sends,
105
+ });
77
106
  return {
78
107
  id: "delivered",
79
108
  icon: "i-ph-check-circle",
@@ -82,10 +111,10 @@ function deliveredItem(stats: SendStats, provider: string): StatGroupItem {
82
111
  stats.sends && stats.deliverability !== null
83
112
  ? text("rate", { rate: percent(stats.deliverability) })
84
113
  : EMPTY_FIGURE,
85
- detail: text("delivered_of", {
86
- delivered: stats.delivered,
87
- sends: stats.sends,
88
- }),
114
+ detail: joined(
115
+ [...deliverabilityCompared(stats), deliveredOf],
116
+ deliveredOf,
117
+ ),
89
118
  };
90
119
  }
91
120
 
@@ -93,13 +122,19 @@ function problemsItem(stats: SendStats): StatGroupItem {
93
122
  const parts = PROBLEM_PARTS.filter((part) => stats[part]).map((part) =>
94
123
  text(`breakdown.${part}`, { count: count(stats[part]) }),
95
124
  );
125
+ const previous = stats.compared?.problems ?? null;
126
+ const compared = comparedText(
127
+ deltaPercent(stats.problems, previous),
128
+ previous,
129
+ );
130
+ const breakdown = parts.length ? parts : [text("no_problems")];
96
131
  return {
97
132
  id: "problems",
98
133
  icon: "i-ph-warning-octagon",
99
134
  tone: stats.problems ? "error" : undefined,
100
135
  eyebrow: key("problems"),
101
136
  value: stats.problems,
102
- detail: joined(parts, text("no_problems")),
137
+ detail: joined([...compared, ...breakdown], text("no_problems")),
103
138
  to: stats.problems ? PROBLEMS_PAGE : undefined,
104
139
  };
105
140
  }
@@ -127,15 +162,26 @@ export function latencyText(ms: number): ComposedText {
127
162
  });
128
163
  }
129
164
 
165
+ /** The latency's change; none without a timed send on either side. */
166
+ function latencyCompared(stats: SendStats): ComposedText[] {
167
+ const previous = stats.compared?.latencyMedian ?? null;
168
+ if (!stats.sends || previous === null) return [];
169
+ return comparedText(
170
+ deltaPercent(stats.latencyMedian, previous),
171
+ latencyText(previous),
172
+ );
173
+ }
174
+
130
175
  function latencyItem(stats: SendStats, provider: string): StatGroupItem {
176
+ const call = provider
177
+ ? text("latency_detail", { provider })
178
+ : text("latency_detail_generic");
131
179
  return {
132
180
  id: "latency",
133
181
  icon: "i-ph-timer",
134
182
  eyebrow: key("latency"),
135
183
  value: stats.sends ? latencyText(stats.latencyMedian) : EMPTY_FIGURE,
136
- detail: provider
137
- ? text("latency_detail", { provider })
138
- : text("latency_detail_generic"),
184
+ detail: joined([...latencyCompared(stats), call], call),
139
185
  };
140
186
  }
141
187
 
@@ -143,7 +189,8 @@ function latencyItem(stats: SendStats, provider: string): StatGroupItem {
143
189
  * The five cells of the send log's stat strip, as `StatGroup` items whose
144
190
  * texts the DMS composes in the reader's language: sends with the tests left
145
191
  * out, deliverability, problems with their breakdown, the queue with its
146
- * oldest wait, and the median API latency.
192
+ * oldest wait, and the median API latency. Every cell but the queue, which is
193
+ * what waits now, states its change against the comparison window.
147
194
  */
148
195
  export function buildSendStatItems(
149
196
  stats: SendStats,
@@ -35,6 +35,21 @@ export interface SendStats {
35
35
  /** When the oldest send still queued was made; `null` with none queued. */
36
36
  oldestQueuedAt: string | null;
37
37
  latencyMedian: number;
38
+ /** The same figures over the comparison window; `null` with it turned off. */
39
+ compared: ComparedSendStats | null;
40
+ }
41
+
42
+ /**
43
+ * The figures of the strip a comparison window states. The queue is left out:
44
+ * it is what waits now, and what waited back then has long left.
45
+ */
46
+ export interface ComparedSendStats {
47
+ sends: number;
48
+ /** `null` without a real send, or when nothing reported deliveries. */
49
+ deliverability: number | null;
50
+ problems: number;
51
+ /** `null` without a real send to time. */
52
+ latencyMedian: number | null;
38
53
  }
39
54
 
40
55
  const isQueued = (send: SendSummary): boolean => send.status === "queued";
@@ -74,20 +89,36 @@ function problemBreakdown(real: SendSummary[]): ProblemBreakdown {
74
89
  return { problems: bounced + failed + spam, bounced, failed, spam };
75
90
  }
76
91
 
92
+ const isRealSend = (send: SendSummary): boolean => !send.isTest;
93
+
94
+ function comparedStats(
95
+ previous: SendSummary[],
96
+ tracking: ProviderTracking | null,
97
+ ): ComparedSendStats {
98
+ const real = previous.filter(isRealSend);
99
+ const totals = aggregate(real);
100
+ return {
101
+ sends: totals.sends,
102
+ deliverability: isDeliveryUntracked(real, tracking)
103
+ ? null
104
+ : totals.deliverability,
105
+ problems: problemBreakdown(real).problems,
106
+ latencyMedian: totals.sends ? totals.latencyMedian : null,
107
+ };
108
+ }
109
+
77
110
  /** `previous` is `null` when the user turned the comparison off. */
78
111
  export function buildSendStats(
79
112
  current: SendSummary[],
80
113
  previous: SendSummary[] | null,
81
114
  tracking: ProviderTracking | null = null,
82
115
  ): SendStats {
83
- const real = current.filter((send) => !send.isTest);
116
+ const real = current.filter(isRealSend);
84
117
  const totals = aggregate(real);
85
- const previousTotals = previous
86
- ? aggregate(previous.filter((send) => !send.isTest))
87
- : null;
118
+ const compared = previous ? comparedStats(previous, tracking) : null;
88
119
  return {
89
120
  sends: totals.sends,
90
- sendsDelta: deltaPercent(totals.sends, previousTotals?.sends ?? null),
121
+ sendsDelta: deltaPercent(totals.sends, compared?.sends ?? null),
91
122
  tests: current.length - real.length,
92
123
  delivered: totals.delivered,
93
124
  deliverability: totals.deliverability,
@@ -96,6 +127,7 @@ export function buildSendStats(
96
127
  queued: totals.queued,
97
128
  oldestQueuedAt: oldestQueuedAt(real),
98
129
  latencyMedian: totals.latencyMedian,
130
+ compared,
99
131
  };
100
132
  }
101
133
 
@@ -22,6 +22,8 @@ import {
22
22
  missingSystemVariables,
23
23
  renderPlainText,
24
24
  resolveEmail,
25
+ type ValueFormat,
26
+ valueFormatOf,
25
27
  } from "../engine";
26
28
  import type {
27
29
  SendTemplateParams,
@@ -43,11 +45,12 @@ import { refreshAttentionIfFlagged } from "./template-attention";
43
45
  import {
44
46
  hasDraft,
45
47
  localeContentOf,
46
- parseContent,
47
48
  parseVariables,
48
49
  pickLocale,
50
+ publishedSnapshot,
49
51
  publishedVersionOf,
50
- workingContent,
52
+ type VersionedSnapshot,
53
+ workingSnapshot,
51
54
  } from "./templates";
52
55
 
53
56
  export class SendError extends Error {
@@ -66,6 +69,8 @@ export interface PreparedSend {
66
69
  requestedLocale: string;
67
70
  /** Template version the content belongs to; 0 for a draft. */
68
71
  version: number;
72
+ /** How its typed values are written: the locale, the version's `date` paths. */
73
+ format: ValueFormat;
69
74
  templateId: string;
70
75
  slug: string;
71
76
  tenantId: string;
@@ -165,11 +170,6 @@ const nameOf = (to: EmailAddress): string | undefined =>
165
170
  const toRecipients = (to: EmailAddress | EmailAddress[]): EmailAddress[] =>
166
171
  Array.isArray(to) ? to : [to];
167
172
 
168
- interface ResolvedContent {
169
- content: TemplateContent;
170
- version: number;
171
- }
172
-
173
173
  /**
174
174
  * The content a send renders and the version it belongs to. A real send gets
175
175
  * what customers receive; a test send gets the draft when there is one, so a
@@ -178,20 +178,17 @@ interface ResolvedContent {
178
178
  function resolveContent(
179
179
  template: MailingTemplate,
180
180
  params: ModuleSendParams,
181
- ): ResolvedContent {
181
+ ): VersionedSnapshot {
182
182
  if (params.content)
183
183
  return {
184
+ ...workingSnapshot(template),
184
185
  content: params.content,
185
186
  version: params.version ?? DRAFT_VERSION,
186
187
  };
187
- if (params.isTest && hasDraft(template))
188
- return { content: workingContent(template), version: DRAFT_VERSION };
189
- if (params.isTest && !publishedVersionOf(template))
190
- return { content: workingContent(template), version: DRAFT_VERSION };
191
- return {
192
- content: parseContent(template),
193
- version: publishedVersionOf(template),
194
- };
188
+ if (params.isTest && (hasDraft(template) || !publishedVersionOf(template)))
189
+ return { ...workingSnapshot(template), version: DRAFT_VERSION };
190
+ const version = publishedVersionOf(template);
191
+ return { ...publishedSnapshot(template), version };
195
192
  }
196
193
 
197
194
  const isManualRealSend = (params: ModuleSendParams): boolean =>
@@ -247,7 +244,7 @@ async function renderForSend(
247
244
  );
248
245
  }
249
246
  const variables = params.variables ?? {};
250
- const email = resolveEmail(localeContent, variables, "send");
247
+ const email = resolveEmail(localeContent, variables, "send", prepared.format);
251
248
  const refused = refusedMissing(
252
249
  params,
253
250
  isBlocking,
@@ -279,13 +276,15 @@ export async function prepare(
279
276
  if (missing.length) {
280
277
  throw new SendError("missing_variables", missing.join(", "));
281
278
  }
282
- const { content, version } = resolveContent(template, params);
279
+ const { content, version, variables } = resolveContent(template, params);
283
280
  const settings = await getSettings(tenantId);
281
+ const locale = pickLocale(content, params.locale, settings.fallbackLocale);
284
282
  return {
285
283
  content,
286
- locale: pickLocale(content, params.locale, settings.fallbackLocale),
284
+ locale,
287
285
  requestedLocale: params.locale ?? "",
288
286
  version,
287
+ format: valueFormatOf(locale, variables),
289
288
  templateId: template._id,
290
289
  slug,
291
290
  tenantId,
@@ -476,7 +475,7 @@ async function record(
476
475
  outcome.error ? { reason: outcome.error } : {},
477
476
  ),
478
477
  });
479
- publishSendCreated(sendId as string);
478
+ publishSendCreated(tenantId, sendId as string);
480
479
  return sendId as string;
481
480
  }
482
481
 
@@ -0,0 +1,27 @@
1
+ import type { SenderDeclaration } from "@antelopejs/interface-dms-mailing";
2
+ import { senderDeclarationSchema } from "../validation/declarations.schema";
3
+ import { parseDeclared } from "./declaration-error";
4
+ import { getSettings, saveSettings } from "./settings";
5
+
6
+ /**
7
+ * Writes the workspace's sender (from name, address, reply-to) a project
8
+ * sets up when it provisions the workspace. A plain write: the declared
9
+ * sender replaces the stored one, and later edits in the settings are the
10
+ * user's.
11
+ */
12
+ export async function declareSender(input: SenderDeclaration): Promise<void> {
13
+ const declaration = parseDeclared(
14
+ senderDeclarationSchema,
15
+ input,
16
+ "invalid_sender",
17
+ "sender declaration",
18
+ );
19
+ const { tenantId } = declaration;
20
+ const settings = await getSettings(tenantId);
21
+ await saveSettings(tenantId, {
22
+ ...settings,
23
+ senderName: declaration.name,
24
+ senderEmail: declaration.email,
25
+ replyTo: declaration.replyTo,
26
+ });
27
+ }
@@ -1,12 +1,8 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
  import { GetModel } from "@antelopejs/interface-database-decorators";
3
- import {
4
- DEFAULT_FALLBACK_LOCALE,
5
- DEFAULT_LOG_RETENTION_DAYS,
6
- } from "../constants";
7
3
  import { type MailingSettings, SettingsModel } from "../db";
8
4
  import {
9
- DEFAULT_CATEGORIES,
5
+ MODULE_DEFAULT_SETTINGS,
10
6
  type MailingSettingsValues,
11
7
  type TemplateCategory,
12
8
  } from "../types";
@@ -15,14 +11,11 @@ export const WEBHOOK_SECRET_BYTES = 24;
15
11
 
16
12
  function defaults(): MailingSettingsValues {
17
13
  return {
18
- fallbackLocale: DEFAULT_FALLBACK_LOCALE,
19
- logRetentionDays: DEFAULT_LOG_RETENTION_DAYS,
20
- blockOnMissingVariables: false,
14
+ ...MODULE_DEFAULT_SETTINGS,
21
15
  senderName: "",
22
16
  senderEmail: "",
23
17
  replyTo: "",
24
18
  webhookSecret: randomBytes(WEBHOOK_SECRET_BYTES).toString("hex"),
25
- categories: DEFAULT_CATEGORIES,
26
19
  };
27
20
  }
28
21
 
@@ -106,7 +99,7 @@ const REFUSE_POLICY: MissingVariablesPolicy = "refuse";
106
99
  const SEND_POLICY: MissingVariablesPolicy = "send";
107
100
 
108
101
  export function missingVariablesPolicy(
109
- values: MailingSettingsValues,
102
+ values: Pick<MailingSettingsValues, "blockOnMissingVariables">,
110
103
  ): MissingVariablesPolicy {
111
104
  return values.blockOnMissingVariables ? REFUSE_POLICY : SEND_POLICY;
112
105
  }
@@ -1,11 +1,11 @@
1
- import type { MailingTemplate } from "../db";
1
+ import type { TemplateFields } from "../db";
2
2
  import type {
3
3
  Block,
4
4
  LocaleContent,
5
5
  TemplateContent,
6
6
  VariableDefinition,
7
7
  } from "../types";
8
- import { footerForCategory } from "../engine";
8
+ import { footerForCategory, formattingLocale } from "../engine";
9
9
  import { blankFields, footerBlock } from "./templates";
10
10
 
11
11
  /**
@@ -31,18 +31,6 @@ type StarterSource = Omit<StarterTemplate, "testData">;
31
31
  const DEFAULT_TEST_DATA_LOCALE = "en";
32
32
  const STARTER_CURRENCY = "EUR";
33
33
 
34
- /** `locale` when the runtime can format for it, English otherwise. */
35
- function formattingLocale(locale: string): string {
36
- try {
37
- return (
38
- Intl.NumberFormat.supportedLocalesOf(locale)[0] ??
39
- DEFAULT_TEST_DATA_LOCALE
40
- );
41
- } catch {
42
- return DEFAULT_TEST_DATA_LOCALE;
43
- }
44
- }
45
-
46
34
  /** The ordered item of the order starter's test data, by language. */
47
35
  const ORDER_LINE_NAMES: Record<string, string> = {
48
36
  en: "Ergo chair · Graphite × 4",
@@ -374,7 +362,7 @@ export function findStarter(id: string): StarterTemplate | undefined {
374
362
  export function starterFields(
375
363
  starter: StarterTemplate,
376
364
  locale: string = DEFAULT_TEST_DATA_LOCALE,
377
- ): Partial<MailingTemplate> {
365
+ ): TemplateFields {
378
366
  return {
379
367
  ...blankFields(starter.content),
380
368
  json_variables: JSON.stringify(starter.variables),