@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.
- package/README.md +8 -7
- package/dist/constants.d.ts +1 -2
- package/dist/constants.js +2 -3
- package/dist/constants.js.map +1 -1
- package/dist/data/templates-table.d.ts +4 -1
- package/dist/data/templates-table.js +55 -20
- package/dist/data/templates-table.js.map +1 -1
- package/dist/data-types/categories-type.d.ts +5 -5
- package/dist/db/models/index.d.ts +0 -1
- package/dist/db/models/index.js +0 -1
- package/dist/db/models/index.js.map +1 -1
- package/dist/db/models/send-events.model.js +2 -3
- package/dist/db/models/send-events.model.js.map +1 -1
- package/dist/db/models/sends.model.js +2 -3
- package/dist/db/models/sends.model.js.map +1 -1
- package/dist/db/models/templates.model.d.ts +10 -1
- package/dist/db/models/templates.model.js +29 -10
- package/dist/db/models/templates.model.js.map +1 -1
- package/dist/db/tables/index.d.ts +0 -1
- package/dist/db/tables/index.js +0 -1
- package/dist/db/tables/index.js.map +1 -1
- package/dist/db/tables/settings.table.d.ts +0 -1
- package/dist/db/tables/settings.table.js +0 -4
- package/dist/db/tables/settings.table.js.map +1 -1
- package/dist/db/tables/templates.table.d.ts +7 -4
- package/dist/db/tables/templates.table.js +4 -14
- package/dist/db/tables/templates.table.js.map +1 -1
- package/dist/engine/formats.d.ts +13 -0
- package/dist/engine/formats.js +85 -0
- package/dist/engine/formats.js.map +1 -0
- package/dist/engine/index.d.ts +1 -0
- package/dist/engine/index.js +1 -0
- package/dist/engine/index.js.map +1 -1
- package/dist/engine/resolve.d.ts +2 -1
- package/dist/engine/resolve.js +14 -7
- package/dist/engine/resolve.js.map +1 -1
- package/dist/engine/tokens.d.ts +2 -1
- package/dist/engine/tokens.js +3 -5
- package/dist/engine/tokens.js.map +1 -1
- package/dist/frontend-module.d.ts +2 -0
- package/dist/frontend-module.js +24 -0
- package/dist/frontend-module.js.map +1 -0
- package/dist/hooks/index.d.ts +0 -1
- package/dist/hooks/index.js +0 -1
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/tenant-export.js +4 -1
- package/dist/hooks/tenant-export.js.map +1 -1
- package/dist/implementations/dms-mailing/index.d.ts +3 -3
- package/dist/implementations/dms-mailing/index.js +8 -7
- package/dist/implementations/dms-mailing/index.js.map +1 -1
- package/dist/index.js +2 -20
- package/dist/index.js.map +1 -1
- package/dist/pages/overview.d.ts +1 -1
- package/dist/pages/overview.js +11 -13
- package/dist/pages/overview.js.map +1 -1
- package/dist/pages/sends.d.ts +1 -1
- package/dist/pages/sends.js +16 -8
- package/dist/pages/sends.js.map +1 -1
- package/dist/pages/settings/form.js +8 -0
- package/dist/pages/settings/form.js.map +1 -1
- package/dist/pages/templates.js +3 -8
- package/dist/pages/templates.js.map +1 -1
- package/dist/pages/toolbar.d.ts +12 -0
- package/dist/pages/toolbar.js +41 -0
- package/dist/pages/toolbar.js.map +1 -0
- package/dist/realtime.d.ts +2 -2
- package/dist/realtime.js +8 -8
- package/dist/realtime.js.map +1 -1
- package/dist/routes/metrics.d.ts +9 -2
- package/dist/routes/metrics.js +90 -35
- package/dist/routes/metrics.js.map +1 -1
- package/dist/routes/period.d.ts +1 -0
- package/dist/routes/period.js +19 -0
- package/dist/routes/period.js.map +1 -1
- package/dist/routes/sends.js +6 -3
- package/dist/routes/sends.js.map +1 -1
- package/dist/routes/settings.js +6 -7
- package/dist/routes/settings.js.map +1 -1
- package/dist/routes/templates.d.ts +8 -4
- package/dist/routes/templates.js +41 -20
- package/dist/routes/templates.js.map +1 -1
- package/dist/services/categories.d.ts +2 -1
- package/dist/services/categories.js +9 -0
- package/dist/services/categories.js.map +1 -1
- package/dist/services/declarations.js +42 -184
- package/dist/services/declarations.js.map +1 -1
- package/dist/services/events.js +1 -1
- package/dist/services/events.js.map +1 -1
- package/dist/services/localized-text.d.ts +7 -0
- package/dist/services/localized-text.js +28 -0
- package/dist/services/localized-text.js.map +1 -0
- package/dist/services/send-import.js +0 -2
- package/dist/services/send-import.js.map +1 -1
- package/dist/services/send-stat-items.js +34 -12
- package/dist/services/send-stat-items.js.map +1 -1
- package/dist/services/send-stats.d.ts +7 -0
- package/dist/services/send-stats.js +17 -5
- package/dist/services/send-stats.js.map +1 -1
- package/dist/services/send.d.ts +2 -0
- package/dist/services/send.js +11 -12
- package/dist/services/send.js.map +1 -1
- package/dist/services/sender-declaration.d.ts +2 -0
- package/dist/services/sender-declaration.js +18 -0
- package/dist/services/sender-declaration.js.map +1 -0
- package/dist/services/settings.d.ts +1 -1
- package/dist/services/settings.js +1 -5
- package/dist/services/settings.js.map +1 -1
- package/dist/services/starters.d.ts +2 -2
- package/dist/services/starters.js +1 -10
- package/dist/services/starters.js.map +1 -1
- package/dist/services/templates.d.ts +13 -7
- package/dist/services/templates.js +72 -33
- package/dist/services/templates.js.map +1 -1
- package/dist/services/webhook-status.d.ts +2 -1
- package/dist/services/webhook-status.js +6 -5
- package/dist/services/webhook-status.js.map +1 -1
- package/dist/types/settings.d.ts +8 -0
- package/dist/types/settings.js +33 -6
- package/dist/types/settings.js.map +1 -1
- package/dist/validation/declarations.schema.d.ts +25 -7
- package/dist/validation/declarations.schema.js +16 -3
- package/dist/validation/declarations.schema.js.map +1 -1
- package/dist/validation/settings.schema.d.ts +41 -41
- package/dist/validation/settings.schema.js +30 -6
- package/dist/validation/settings.schema.js.map +1 -1
- package/dist/validation/templates.schema.d.ts +13 -0
- package/dist/validation/templates.schema.js +6 -1
- package/dist/validation/templates.schema.js.map +1 -1
- package/frontend-vue/app/components/CategoriesInput.vue +15 -13
- package/frontend-vue/app/components/FunnelCard.vue +46 -3
- package/frontend-vue/app/components/NewTemplateModal.vue +4 -2
- package/frontend-vue/app/components/OverviewGate.vue +0 -8
- package/frontend-vue/app/components/ProviderPill.vue +50 -0
- package/frontend-vue/app/components/SendDrawer.vue +206 -183
- package/frontend-vue/app/components/StarterGallery.vue +1 -1
- package/frontend-vue/app/components/TemplateCard.vue +4 -1
- package/frontend-vue/app/components/TemplateDrawer.vue +3 -5
- package/frontend-vue/app/components/TemplateGallery.vue +57 -17
- package/frontend-vue/app/components/TemplatePreviewFrame.vue +41 -11
- package/frontend-vue/app/components/editor/EditorTemplatePane.vue +1 -3
- package/frontend-vue/app/components/editor/EditorTitle.vue +1 -7
- package/frontend-vue/app/composables/useLocalizedDefaults.ts +5 -7
- package/frontend-vue/app/composables/useMailingApi.ts +3 -3
- package/frontend-vue/app/composables/useMailingReads.ts +2 -2
- package/frontend-vue/app/composables/useTemplateCategories.ts +8 -7
- package/frontend-vue/app/types/mailing.ts +18 -0
- package/frontend-vue/app/utils/categories.ts +58 -49
- package/frontend-vue/app/utils/funnel.ts +25 -0
- package/frontend-vue/app/utils/period.ts +0 -7
- package/frontend-vue/app/utils/starters.ts +3 -1
- package/frontend-vue/app/utils/tokens.ts +6 -1
- package/frontend-vue/i18n/locales/mailing-en-GB.json +19 -28
- package/frontend-vue/i18n/locales/mailing-fr-FR.json +19 -28
- package/frontend-vue/package.json +1 -1
- package/frontend-vue/tests/categories-input.test.ts +4 -10
- package/frontend-vue/tests/categories.test.ts +55 -17
- package/frontend-vue/tests/conditions.test.ts +1 -4
- package/frontend-vue/tests/funnel-card.test.ts +27 -0
- package/frontend-vue/tests/funnel.test.ts +25 -0
- package/frontend-vue/tests/localized-defaults.test.ts +0 -29
- package/frontend-vue/tests/new-template-modal.test.ts +1 -1
- package/frontend-vue/tests/overview-gate.test.ts +1 -24
- package/frontend-vue/tests/page-reads.test.ts +9 -25
- package/frontend-vue/tests/period.test.ts +0 -12
- package/frontend-vue/tests/replay-toast.test.ts +2 -1
- package/frontend-vue/tests/send-drawer.test.ts +123 -0
- package/frontend-vue/tests/template-drawer.test.ts +1 -1
- package/frontend-vue/tests/template-gallery.test.ts +90 -3
- package/frontend-vue/tests/template-preview-frame.test.ts +92 -0
- package/package.json +8 -7
- package/skills/dms-mailing/SKILL.md +25 -18
- package/src/constants.ts +2 -2
- package/src/data/sends-table.ts +1 -1
- package/src/data/templates-table.ts +125 -33
- package/src/db/models/index.ts +0 -1
- package/src/db/models/send-events.model.ts +2 -2
- package/src/db/models/sends.model.ts +7 -8
- package/src/db/models/templates.model.ts +64 -16
- package/src/db/tables/index.ts +0 -1
- package/src/db/tables/settings.table.ts +0 -8
- package/src/db/tables/templates.table.ts +12 -17
- package/src/engine/formats.ts +124 -0
- package/src/engine/index.ts +1 -0
- package/src/engine/resolve.ts +18 -7
- package/src/engine/tokens.ts +7 -5
- package/src/frontend-module.ts +36 -0
- package/src/hooks/index.ts +0 -1
- package/src/hooks/tenant-export.ts +8 -1
- package/src/implementations/dms-mailing/index.ts +5 -16
- package/src/index.ts +3 -29
- package/src/pages/overview.ts +20 -16
- package/src/pages/sends.ts +27 -11
- package/src/pages/settings/form.ts +16 -0
- package/src/pages/templates.ts +4 -8
- package/src/pages/toolbar.ts +77 -0
- package/src/realtime.ts +17 -8
- package/src/routes/metrics.ts +158 -45
- package/src/routes/period.ts +32 -0
- package/src/routes/sends.ts +13 -4
- package/src/routes/settings.ts +11 -8
- package/src/routes/templates.ts +81 -37
- package/src/services/categories.ts +14 -1
- package/src/services/declarations.ts +64 -317
- package/src/services/events.ts +1 -1
- package/src/services/localized-text.ts +53 -0
- package/src/services/send-import.ts +2 -2
- package/src/services/send-stat-items.ts +60 -13
- package/src/services/send-stats.ts +37 -5
- package/src/services/send.ts +19 -20
- package/src/services/sender-declaration.ts +27 -0
- package/src/services/settings.ts +3 -10
- package/src/services/starters.ts +3 -15
- package/src/services/templates.ts +109 -63
- package/src/services/webhook-status.ts +3 -3
- package/src/types/settings.ts +55 -5
- package/src/validation/declarations.schema.ts +29 -3
- package/src/validation/settings.schema.ts +56 -14
- package/src/validation/templates.schema.ts +10 -0
- package/dist/db/models/template-slugs.model.d.ts +0 -27
- package/dist/db/models/template-slugs.model.js +0 -43
- package/dist/db/models/template-slugs.model.js.map +0 -1
- package/dist/db/tables/template-slugs.table.d.ts +0 -7
- package/dist/db/tables/template-slugs.table.js +0 -35
- package/dist/db/tables/template-slugs.table.js.map +0 -1
- package/dist/hooks/database-ready.d.ts +0 -7
- package/dist/hooks/database-ready.js +0 -26
- package/dist/hooks/database-ready.js.map +0 -1
- package/dist/services/fingerprint.d.ts +0 -1
- package/dist/services/fingerprint.js +0 -20
- package/dist/services/fingerprint.js.map +0 -1
- package/frontend-vue/app/components/PageHeader.vue +0 -94
- package/frontend-vue/app/composables/useOverviewSetup.ts +0 -21
- package/src/db/models/template-slugs.model.ts +0 -61
- package/src/db/tables/template-slugs.table.ts +0 -29
- package/src/hooks/database-ready.ts +0 -47
- 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
|
-
|
|
43
|
-
|
|
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 ? "
|
|
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
|
-
[...
|
|
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:
|
|
86
|
-
|
|
87
|
-
|
|
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(
|
|
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:
|
|
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(
|
|
116
|
+
const real = current.filter(isRealSend);
|
|
84
117
|
const totals = aggregate(real);
|
|
85
|
-
const
|
|
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,
|
|
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
|
|
package/src/services/send.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
):
|
|
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 {
|
|
189
|
-
|
|
190
|
-
|
|
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
|
|
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
|
+
}
|
package/src/services/settings.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/services/starters.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type {
|
|
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
|
-
):
|
|
365
|
+
): TemplateFields {
|
|
378
366
|
return {
|
|
379
367
|
...blankFields(starter.content),
|
|
380
368
|
json_variables: JSON.stringify(starter.variables),
|