@aglyn/shared-ui-email-campaigns 1.0.0-beta.143

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 (33) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +73 -0
  3. package/package.json +39 -0
  4. package/src/index.d.ts +17 -0
  5. package/src/index.js +23 -0
  6. package/src/index.js.map +1 -0
  7. package/src/lib/components/campaign-picker.component.d.ts +54 -0
  8. package/src/lib/components/campaign-picker.component.js +121 -0
  9. package/src/lib/components/campaign-picker.component.js.map +1 -0
  10. package/src/lib/components/report-figures.d.ts +51 -0
  11. package/src/lib/components/report-figures.js +120 -0
  12. package/src/lib/components/report-figures.js.map +1 -0
  13. package/src/lib/model/campaign-container.d.ts +359 -0
  14. package/src/lib/model/campaign-container.js +355 -0
  15. package/src/lib/model/campaign-container.js.map +1 -0
  16. package/src/lib/model/campaign-conversions.d.ts +286 -0
  17. package/src/lib/model/campaign-conversions.js +249 -0
  18. package/src/lib/model/campaign-conversions.js.map +1 -0
  19. package/src/lib/model/campaign-report.d.ts +304 -0
  20. package/src/lib/model/campaign-report.js +326 -0
  21. package/src/lib/model/campaign-report.js.map +1 -0
  22. package/src/lib/model/campaign-revenue.d.ts +327 -0
  23. package/src/lib/model/campaign-revenue.js +332 -0
  24. package/src/lib/model/campaign-revenue.js.map +1 -0
  25. package/src/lib/model/campaign-send-time.d.ts +74 -0
  26. package/src/lib/model/campaign-send-time.js +120 -0
  27. package/src/lib/model/campaign-send-time.js.map +1 -0
  28. package/src/lib/model/email-record.d.ts +175 -0
  29. package/src/lib/model/email-record.js +198 -0
  30. package/src/lib/model/email-record.js.map +1 -0
  31. package/src/lib/model/index.d.ts +53 -0
  32. package/src/lib/model/index.js +48 -0
  33. package/src/lib/model/index.js.map +1 -0
@@ -0,0 +1,355 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ /**
17
+ * A CAMPAIGN IS A CONTAINER; A SEND IS ONE MESSAGE INSIDE IT.
18
+ *
19
+ * ## The two collections, and why there are two
20
+ *
21
+ * `hosts/{hostId}/campaigns/{sendId}` holds a SEND: one subject, one body,
22
+ * one audience, one set of counters. That is what the collection has always
23
+ * held, and it is why the container could not simply be that document grown
24
+ * new fields.
25
+ *
26
+ * **Its ids are load-bearing outside this repo.** Every unsubscribe link that
27
+ * has ever gone out carries `cid={sendId}`, those emails sit in inboxes
28
+ * forever, and the `cid` is inside the link's HMAC — so a send id that stops
29
+ * resolving is an opt-out that stops working, which is a compliance failure
30
+ * rather than a broken page. `/marketing/campaigns/{sendId}` is likewise
31
+ * linkable by design: a merchant pastes it into a message about last week's
32
+ * send.
33
+ *
34
+ * So the send collection is left exactly where it is, under exactly its
35
+ * existing ids, and the container is a new collection above it:
36
+ * `hosts/{hostId}/emailCampaigns/{campaignId}`. A send joins one by carrying
37
+ * {@link CAMPAIGN_SEND_CONTAINER_FIELD}; a send written before containers
38
+ * existed carries nothing, and {@link campaignListRows} presents it as a
39
+ * container of one rather than hiding it.
40
+ *
41
+ * That last property is what makes this migration-free. There is no backfill
42
+ * to run, no window in which a merchant's history is missing, and no id
43
+ * rewritten anywhere.
44
+ *
45
+ * ## Why the arithmetic is here
46
+ *
47
+ * The same reason `campaign-report.ts` gives for the per-send rates: a
48
+ * denominator chosen in JSX is a denominator nobody tests. Aggregating across
49
+ * sends adds one problem the single-send report does not have — some sends
50
+ * recorded a field and others never did — and summing those into one number
51
+ * silently reports a partial total as a complete one. Every aggregate here
52
+ * therefore reports how many sends it could measure.
53
+ */ import { campaignRate } from "./campaign-report.js";
54
+ /**
55
+ * The field on a SEND naming the campaign it belongs to.
56
+ *
57
+ * Not `campaignId`: on a send document that name already means the send's own
58
+ * id — it is what `cid` carries and what the report route addresses — and one
59
+ * word meaning both would be read into the other on the first edit.
60
+ */ export const CAMPAIGN_SEND_CONTAINER_FIELD = 'emailCampaignId';
61
+ /** A stored count as a non-negative integer, or 0. */ function progressCount(raw) {
62
+ const value = Math.floor(Number(raw));
63
+ return Number.isFinite(value) && value > 0 ? value : 0;
64
+ }
65
+ export function campaignSendProgress(send) {
66
+ var _ref;
67
+ var _send_stats, _send_stats1;
68
+ const status = String((_ref = send == null ? void 0 : send.status) != null ? _ref : 'sent');
69
+ const reached = progressCount(send == null ? void 0 : (_send_stats = send.stats) == null ? void 0 : _send_stats.sent);
70
+ const rawAudience = send == null ? void 0 : (_send_stats1 = send.stats) == null ? void 0 : _send_stats1.audienceSize;
71
+ const audience = rawAudience === undefined || rawAudience === null ? null : progressCount(rawAudience);
72
+ const resume = send == null ? void 0 : send.resume;
73
+ const remaining = progressCount(resume == null ? void 0 : resume.remaining);
74
+ const batch = progressCount(resume == null ? void 0 : resume.batch);
75
+ const nextAtMs = progressCount(resume == null ? void 0 : resume.nextAtMs);
76
+ const of = audience !== null ? ` of ${audience.toLocaleString()}` : '';
77
+ // Still going: more to address, and a run that will address it. A campaign
78
+ // a merchant CANCELED is not still going however much is left, which is why
79
+ // the status is read before the remainder.
80
+ if (remaining > 0 && nextAtMs > 0 && (status === 'scheduled' || status === 'sending')) {
81
+ return {
82
+ state: 'sending',
83
+ reached,
84
+ audience,
85
+ remaining,
86
+ batch,
87
+ nextAtMs,
88
+ label: `Sending — reached ${reached.toLocaleString()}${of}`
89
+ };
90
+ }
91
+ // Waiting for its time, with nothing delivered. The pre-batching meaning of
92
+ // `scheduled`, and still the common one.
93
+ if ((status === 'scheduled' || status === 'sending') && reached === 0) {
94
+ return {
95
+ state: 'pending',
96
+ reached: 0,
97
+ audience,
98
+ remaining,
99
+ batch,
100
+ nextAtMs: 0,
101
+ label: status === 'sending' ? 'Sending' : 'Scheduled'
102
+ };
103
+ }
104
+ if (remaining > 0) {
105
+ const why = status === 'canceled' ? 'canceled' : status === 'failed' ? 'stopped by an error' : 'stopped';
106
+ return {
107
+ state: 'stopped',
108
+ reached,
109
+ audience,
110
+ remaining,
111
+ batch,
112
+ nextAtMs: 0,
113
+ label: `Reached ${reached.toLocaleString()}${of} — ${why} with ` + `${remaining.toLocaleString()} not addressed`
114
+ };
115
+ }
116
+ return {
117
+ state: 'sent',
118
+ reached,
119
+ audience,
120
+ remaining: 0,
121
+ batch,
122
+ nextAtMs: 0,
123
+ label: batch > 1 ? `Sent to ${reached.toLocaleString()}${of} over ${batch} runs` : `Sent to ${reached.toLocaleString()}${of}`
124
+ };
125
+ }
126
+ export function campaignSendDisplay(send) {
127
+ var _ref;
128
+ const progress = campaignSendProgress(send);
129
+ if (String((_ref = send == null ? void 0 : send.status) != null ? _ref : '') === 'draft') {
130
+ return {
131
+ state: 'draft',
132
+ label: 'Draft',
133
+ progress
134
+ };
135
+ }
136
+ return {
137
+ state: progress.state,
138
+ label: progress.label,
139
+ progress
140
+ };
141
+ }
142
+ /**
143
+ * Whether this email is between batches, with more of its audience to reach.
144
+ *
145
+ * The one question two controls on an email's page turn on. It is stored as
146
+ * `scheduled` — the state the processor claims — so a surface reading the
147
+ * status alone offers "Send now" on a campaign that is already going out, and
148
+ * `sendNow` re-resolves the WHOLE audience rather than continuing: everyone
149
+ * already reached would receive a second copy under the same `cid`.
150
+ */ export function campaignSendIsMidFlight(send) {
151
+ return campaignSendProgress(send).state === 'sending';
152
+ }
153
+ /**
154
+ * The lists ONE SEND addressed — which can be narrower than the lists its
155
+ * campaign is aimed at.
156
+ *
157
+ * A campaign holds the lists a merchant plans to reach; each send inside it
158
+ * picks one audience, and that audience may be a segment or the site's leads
159
+ * rather than any of them. Answering from the send is therefore the only
160
+ * honest answer for a send's own detail page.
161
+ *
162
+ * An array for a document that stores one id, deliberately: the question
163
+ * "which lists did this reach" has a plural answer everywhere it is asked,
164
+ * and a caller that unwraps a single id today is a caller to revisit if a
165
+ * send ever addresses two.
166
+ */ export function campaignSendListIds(send) {
167
+ return send.audience === 'list' && send.listId ? [
168
+ send.listId
169
+ ] : [];
170
+ }
171
+ const aggregate = (sends, read)=>{
172
+ let total = 0;
173
+ let recorded = 0;
174
+ for (const send of sends){
175
+ var _send_stats;
176
+ const value = read((_send_stats = send.stats) != null ? _send_stats : {});
177
+ // `undefined` is "this send never recorded it"; a recorded 0 counts as
178
+ // measured, and moves `recorded` without moving the total.
179
+ if (value === undefined || value === null) continue;
180
+ const numeric = Number(value);
181
+ if (!Number.isFinite(numeric)) continue;
182
+ total += numeric;
183
+ recorded += 1;
184
+ }
185
+ return {
186
+ value: recorded ? total : null,
187
+ recorded,
188
+ sends: sends.length
189
+ };
190
+ };
191
+ /** When a send happened, in epoch milliseconds, or null. */ export function campaignSendAtMs(send) {
192
+ var _send_sentAt;
193
+ const seconds = (_send_sentAt = send.sentAt) == null ? void 0 : _send_sentAt.seconds;
194
+ if (typeof seconds === 'number') return seconds * 1000;
195
+ if (typeof send.sendAtMs === 'number') return send.sendAtMs;
196
+ return null;
197
+ }
198
+ /**
199
+ * Rolls a campaign's sends into one set of figures.
200
+ *
201
+ * Rates are taken over the sends that recorded BOTH sides, so a campaign
202
+ * whose first send predates the delivery webhook reports the open rate of the
203
+ * sends that can be measured rather than one deflated by a send with no
204
+ * denominator.
205
+ */ export function campaignRollup(sends) {
206
+ var _aggregate_value, _deliveredTotal_value, _aggregate_value1, _deliveredTotal_value1, _aggregate_value2, _deliveredTotal_value2;
207
+ /*==========================================
208
+ * ONLY MAIL THAT HAS GONE OUT IS A SEND.
209
+ *
210
+ * An email exists from the moment it is created, so this collection now
211
+ * holds records in three states that have mailed nobody — `draft`,
212
+ * `scheduled` and the `sending` claim — and every figure below is about
213
+ * mail that was delivered.
214
+ *
215
+ * Two separate faults if they are left in. The COUNT would report a
216
+ * campaign as having sent three emails when it has sent two and is still
217
+ * writing the third. And every aggregate carries `sends` as the denominator
218
+ * of its own "recorded by N of M" label, so an unsent record would enlarge
219
+ * the M — publishing a coverage figure that says the campaign is missing
220
+ * data it was never going to have.
221
+ *=========================================*/ /*
222
+ * A SEND THAT IS STILL GOING HAS STILL GONE.
223
+ *
224
+ * `emailIsUnsent` reads the stored status, and an email delivering an
225
+ * audience larger than one batch is stored as `scheduled` between runs —
226
+ * so reading it literally drops a send that has put five hundred messages
227
+ * in five hundred inboxes out of every total on the campaign, and counts it
228
+ * under "scheduled" as though nothing had happened. `campaignSendProgress`
229
+ * is what tells "waiting for its time" from "part way through", and only
230
+ * the first is genuinely unsent.
231
+ *
232
+ * A DRAFT is settled by the status because the progress states do not cover
233
+ * it — see `campaignSendDisplay`.
234
+ */ const notYet = (send)=>{
235
+ var _send_status;
236
+ return String((_send_status = send.status) != null ? _send_status : '') === 'draft' || campaignSendProgress(send).state === 'pending';
237
+ };
238
+ const gone = sends.filter((send)=>!notYet(send));
239
+ const measurable = sends.filter((send)=>{
240
+ var _send_stats;
241
+ return ((_send_stats = send.stats) == null ? void 0 : _send_stats.delivered) !== undefined;
242
+ });
243
+ const deliveredTotal = aggregate(measurable, (stats)=>stats.delivered);
244
+ return {
245
+ sends: gone.filter((send)=>send.status !== 'canceled').length,
246
+ // Waiting for its time with nothing delivered — which is what
247
+ // "scheduled" meant before an email could be delivered over several runs,
248
+ // and is now narrower than the stored status of that name.
249
+ scheduled: sends.filter((send)=>campaignSendProgress(send).state === 'pending').length,
250
+ sending: sends.filter((send)=>campaignSendProgress(send).state === 'sending').length,
251
+ drafts: sends.filter((send)=>send.status === 'draft').length,
252
+ addressed: aggregate(gone, (stats)=>stats.recipients),
253
+ sent: aggregate(gone, (stats)=>stats.sent),
254
+ delivered: aggregate(gone, (stats)=>stats.delivered),
255
+ opens: aggregate(gone, (stats)=>stats.opens),
256
+ uniqueOpens: aggregate(gone, (stats)=>stats.uniqueOpens),
257
+ clicks: aggregate(gone, (stats)=>stats.clicks),
258
+ uniqueClicks: aggregate(gone, (stats)=>stats.uniqueClicks),
259
+ bounced: aggregate(gone, (stats)=>stats.bounced),
260
+ complained: aggregate(gone, (stats)=>stats.complained),
261
+ unsubscribes: aggregate(gone, (stats)=>stats.unsubscribes),
262
+ openRate: campaignRate((_aggregate_value = aggregate(measurable, (stats)=>stats.uniqueOpens).value) != null ? _aggregate_value : undefined, (_deliveredTotal_value = deliveredTotal.value) != null ? _deliveredTotal_value : undefined, 'delivered'),
263
+ clickRate: campaignRate((_aggregate_value1 = aggregate(measurable, (stats)=>stats.uniqueClicks).value) != null ? _aggregate_value1 : undefined, (_deliveredTotal_value1 = deliveredTotal.value) != null ? _deliveredTotal_value1 : undefined, 'delivered'),
264
+ unsubscribeRate: campaignRate((_aggregate_value2 = aggregate(measurable, (stats)=>stats.unsubscribes).value) != null ? _aggregate_value2 : undefined, (_deliveredTotal_value2 = deliveredTotal.value) != null ? _deliveredTotal_value2 : undefined, 'delivered'),
265
+ lastSentAtMs: sends.reduce((latest, send)=>{
266
+ const at = campaignSendAtMs(send);
267
+ return at !== null && (latest === null || at > latest) ? at : latest;
268
+ }, null)
269
+ };
270
+ }
271
+ export function campaignWindowState(campaign, nowMs) {
272
+ var _campaign_startAtMs, _campaign_endAtMs;
273
+ const start = (_campaign_startAtMs = campaign.startAtMs) != null ? _campaign_startAtMs : null;
274
+ const end = (_campaign_endAtMs = campaign.endAtMs) != null ? _campaign_endAtMs : null;
275
+ if (start === null && end === null) return 'undated';
276
+ if (start !== null && nowMs < start) return 'upcoming';
277
+ if (end !== null && nowMs > end) return 'ended';
278
+ return 'running';
279
+ }
280
+ /**
281
+ * The campaigns table's rows: every container, plus every send that belongs
282
+ * to none.
283
+ *
284
+ * The second half is what makes the container additive. A merchant's history
285
+ * is the sends they already have; a list that showed only containers would
286
+ * read as an empty product on the day this shipped, and a backfill that
287
+ * adopted each old send into a container of one would rewrite documents whose
288
+ * ids are cited by mail already delivered. Adopting them AT READ TIME costs a
289
+ * pass over a list already in memory and rewrites nothing.
290
+ *
291
+ * Newest first, on the start date where there is one and the last send
292
+ * otherwise. Rows with no date at all sort last: they are campaigns nobody
293
+ * has scheduled or sent, and dating them from nothing would be an invention.
294
+ */ export function campaignListRows(campaigns, sends, nowMs) {
295
+ const byCampaign = new Map();
296
+ const orphans = [];
297
+ for (const send of sends){
298
+ const containerId = send.emailCampaignId;
299
+ if (!containerId) {
300
+ orphans.push(send);
301
+ continue;
302
+ }
303
+ const existing = byCampaign.get(containerId);
304
+ if (existing) existing.push(send);
305
+ else byCampaign.set(containerId, [
306
+ send
307
+ ]);
308
+ }
309
+ const rows = campaigns.map((campaign)=>{
310
+ var _byCampaign_get, _campaign_startAtMs, _campaign_endAtMs, _campaign_listIds, _ref, _campaign_startAtMs1;
311
+ const own = (_byCampaign_get = byCampaign.get(campaign.$id)) != null ? _byCampaign_get : [];
312
+ const rollup = campaignRollup(own);
313
+ return {
314
+ id: campaign.$id,
315
+ name: campaign.name || 'Untitled campaign',
316
+ legacy: false,
317
+ startAtMs: (_campaign_startAtMs = campaign.startAtMs) != null ? _campaign_startAtMs : null,
318
+ endAtMs: (_campaign_endAtMs = campaign.endAtMs) != null ? _campaign_endAtMs : null,
319
+ listIds: (_campaign_listIds = campaign.listIds) != null ? _campaign_listIds : [],
320
+ sends: own,
321
+ rollup,
322
+ windowState: campaignWindowState(campaign, nowMs),
323
+ atMs: (_ref = (_campaign_startAtMs1 = campaign.startAtMs) != null ? _campaign_startAtMs1 : rollup.lastSentAtMs) != null ? _ref : null
324
+ };
325
+ });
326
+ for (const send of orphans){
327
+ const rollup = campaignRollup([
328
+ send
329
+ ]);
330
+ const at = campaignSendAtMs(send);
331
+ rows.push({
332
+ id: send.$id,
333
+ name: send.subject || 'Untitled campaign',
334
+ legacy: true,
335
+ startAtMs: at,
336
+ endAtMs: at,
337
+ listIds: [],
338
+ sends: [
339
+ send
340
+ ],
341
+ rollup,
342
+ // A send has no window of its own; it happened, or it is going to.
343
+ windowState: send.status === 'scheduled' ? 'upcoming' : at === null ? 'undated' : 'ended',
344
+ atMs: at
345
+ });
346
+ }
347
+ return rows.sort((a, b)=>{
348
+ if (a.atMs === null && b.atMs === null) return a.name.localeCompare(b.name);
349
+ if (a.atMs === null) return 1;
350
+ if (b.atMs === null) return -1;
351
+ return b.atMs - a.atMs;
352
+ });
353
+ }
354
+
355
+ //# sourceMappingURL=campaign-container.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/shared/ui/email-campaigns/src/lib/model/campaign-container.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * A CAMPAIGN IS A CONTAINER; A SEND IS ONE MESSAGE INSIDE IT.\n *\n * ## The two collections, and why there are two\n *\n * `hosts/{hostId}/campaigns/{sendId}` holds a SEND: one subject, one body,\n * one audience, one set of counters. That is what the collection has always\n * held, and it is why the container could not simply be that document grown\n * new fields.\n *\n * **Its ids are load-bearing outside this repo.** Every unsubscribe link that\n * has ever gone out carries `cid={sendId}`, those emails sit in inboxes\n * forever, and the `cid` is inside the link's HMAC — so a send id that stops\n * resolving is an opt-out that stops working, which is a compliance failure\n * rather than a broken page. `/marketing/campaigns/{sendId}` is likewise\n * linkable by design: a merchant pastes it into a message about last week's\n * send.\n *\n * So the send collection is left exactly where it is, under exactly its\n * existing ids, and the container is a new collection above it:\n * `hosts/{hostId}/emailCampaigns/{campaignId}`. A send joins one by carrying\n * {@link CAMPAIGN_SEND_CONTAINER_FIELD}; a send written before containers\n * existed carries nothing, and {@link campaignListRows} presents it as a\n * container of one rather than hiding it.\n *\n * That last property is what makes this migration-free. There is no backfill\n * to run, no window in which a merchant's history is missing, and no id\n * rewritten anywhere.\n *\n * ## Why the arithmetic is here\n *\n * The same reason `campaign-report.ts` gives for the per-send rates: a\n * denominator chosen in JSX is a denominator nobody tests. Aggregating across\n * sends adds one problem the single-send report does not have — some sends\n * recorded a field and others never did — and summing those into one number\n * silently reports a partial total as a complete one. Every aggregate here\n * therefore reports how many sends it could measure.\n */\n\nimport {\n campaignRate,\n type CampaignRate,\n type CampaignStats,\n} from './campaign-report'\n\n/**\n * The field on a SEND naming the campaign it belongs to.\n *\n * Not `campaignId`: on a send document that name already means the send's own\n * id — it is what `cid` carries and what the report route addresses — and one\n * word meaning both would be read into the other on the first edit.\n */\nexport const CAMPAIGN_SEND_CONTAINER_FIELD = 'emailCampaignId'\n\n/**\n * A campaign: the container, not a message.\n *\n * Stored at `hosts/{hostId}/emailCampaigns/{campaignId}`.\n */\nexport interface EmailCampaign {\n $id: string\n /** What the merchant called it. */\n name: string\n /** When the campaign window opens. Null for a campaign with no dates. */\n startAtMs?: number | null\n /** When it closes. Null for an open-ended campaign. */\n endAtMs?: number | null\n /** Org email lists this campaign is aimed at, by list id. */\n listIds?: string[]\n /**\n * The stream this campaign's emails open on.\n *\n * A DEFAULT, not a constraint. The topic decides who a send skips, what the\n * preference page linked from the footer highlights, and which stream a\n * resulting opt-out is recorded against — all facts about one MESSAGE, and\n * one campaign may legitimately carry a newsletter and a promotion. So the\n * composer's picker is what the send records; this is what the picker opens\n * on, which is what stops a \"Sales\" campaign quietly mailing under\n * `marketing`.\n */\n topicId?: string\n createdAtMs?: number\n createdBy?: string\n deletedAt?: unknown\n}\n\n/** One send, as much of it as a list or a rollup needs. */\nexport interface CampaignSend {\n $id: string\n subject?: string\n /** The audience KIND: `'leads'`, `'members'`, `'segment'`, `'list'`. */\n audience?: string\n /** The list this send addressed, when the kind is `'list'`. */\n listId?: string\n /** The segment this send addressed, when the kind is `'segment'`. */\n segmentId?: string\n /** Which container it belongs to, absent on a send written before them. */\n emailCampaignId?: string\n status?: string\n sentAt?: { seconds?: number } | null\n sendAtMs?: number\n /**\n * When the record was minted, stamped by every writer that creates one.\n *\n * Absent on a send written before the stamp existed. The lists that draw\n * drafts beside sends order on it through `emailListTimeMs`, which is why\n * it is here rather than only on the loose record shape: a draft carries\n * neither `sentAt` nor `sendAtMs`, so this is the only time it has.\n */\n createdAtMs?: number\n stats?: CampaignStats\n /** How far a send that goes out over several batches has got. */\n resume?: CampaignResume\n}\n\n/**\n * The batch state the sender writes on an email that is still going out.\n *\n * Absent on every send that finished in one batch, and on every send that\n * predates batching — which is why {@link campaignSendProgress} treats a\n * missing record as \"this is not a batched send\" rather than as zero.\n */\nexport interface CampaignResume {\n /** People the email has resolved and not yet addressed. */\n remaining?: number\n /** Batches it has run. */\n batch?: number\n /** When the next batch may go, ms. Zero when there is not going to be one. */\n nextAtMs?: number\n /** Why it stopped short, when it did. */\n stop?: string\n}\n\n/**\n * WHAT A SEND IS ACTUALLY DOING, for a row that would otherwise lie.\n *\n * An email larger than one send may carry is delivered over several batches,\n * and between them it is stored as `scheduled` — the state the processor\n * claims, and the only one that resumes it without a second index and a\n * second query. Read literally, that is a row saying \"not sent yet\" about an\n * email that has already put five hundred messages in five hundred inboxes.\n *\n * So the stored fields are not the sentence. This is: it takes the status,\n * the delivered count and the batch record, and answers what a person needs\n * to see. Derived at read time and never persisted, exactly as\n * `campaignWindowState` beside it is, because it is a description of stored\n * facts and not a fact of its own.\n *\n * ## The four states, and which stored shape each one is\n *\n * - `pending` — `scheduled`, nothing delivered. A campaign waiting for its\n * time. This is what `scheduled` meant before batching and still does.\n * - `sending` — `scheduled` or `sending` with something delivered and more to\n * come. The state this function exists for.\n * - `sent` — finished, whether in one batch or six.\n * - `stopped` — finished with people it never addressed: canceled mid-flight,\n * failed mid-flight, or stopped by the batch guard. The count is what makes\n * this legible rather than alarming — an email that reached 2,400 of 3,000\n * and stopped is a different conversation from one that reached nobody.\n */\nexport type CampaignSendProgressState =\n | 'pending'\n | 'sending'\n | 'sent'\n | 'stopped'\n\nexport interface CampaignSendProgress {\n state: CampaignSendProgressState\n /** Messages this email has delivered. */\n reached: number\n /**\n * The audience it is working through, when one was recorded. Null when the\n * send never recorded an audience size, which is every send that predates\n * the figure — reported as null rather than as `reached` so a surface does\n * not present a floor as a total.\n */\n audience: number | null\n /** People it has resolved and not yet addressed. */\n remaining: number\n /** Batches it has run. Zero for a send that never batched. */\n batch: number\n /** When the next batch may go, ms. Zero unless {@link state} is `sending`. */\n nextAtMs: number\n /** One line a surface may show verbatim. */\n label: string\n}\n\n/** A stored count as a non-negative integer, or 0. */\nfunction progressCount(raw: unknown): number {\n const value = Math.floor(Number(raw))\n return Number.isFinite(value) && value > 0 ? value : 0\n}\n\nexport function campaignSendProgress(\n send: CampaignSend | null | undefined,\n): CampaignSendProgress {\n const status = String(send?.status ?? 'sent')\n const reached = progressCount(send?.stats?.sent)\n const rawAudience = send?.stats?.audienceSize\n const audience =\n rawAudience === undefined || rawAudience === null\n ? null\n : progressCount(rawAudience)\n const resume = send?.resume\n const remaining = progressCount(resume?.remaining)\n const batch = progressCount(resume?.batch)\n const nextAtMs = progressCount(resume?.nextAtMs)\n const of = audience !== null ? ` of ${audience.toLocaleString()}` : ''\n\n // Still going: more to address, and a run that will address it. A campaign\n // a merchant CANCELED is not still going however much is left, which is why\n // the status is read before the remainder.\n if (\n remaining > 0 &&\n nextAtMs > 0 &&\n (status === 'scheduled' || status === 'sending')\n ) {\n return {\n state: 'sending',\n reached,\n audience,\n remaining,\n batch,\n nextAtMs,\n label: `Sending — reached ${reached.toLocaleString()}${of}`,\n }\n }\n // Waiting for its time, with nothing delivered. The pre-batching meaning of\n // `scheduled`, and still the common one.\n if ((status === 'scheduled' || status === 'sending') && reached === 0) {\n return {\n state: 'pending',\n reached: 0,\n audience,\n remaining,\n batch,\n nextAtMs: 0,\n label: status === 'sending' ? 'Sending' : 'Scheduled',\n }\n }\n if (remaining > 0) {\n const why =\n status === 'canceled'\n ? 'canceled'\n : status === 'failed'\n ? 'stopped by an error'\n : 'stopped'\n return {\n state: 'stopped',\n reached,\n audience,\n remaining,\n batch,\n nextAtMs: 0,\n label:\n `Reached ${reached.toLocaleString()}${of} — ${why} with ` +\n `${remaining.toLocaleString()} not addressed`,\n }\n }\n return {\n state: 'sent',\n reached,\n audience,\n remaining: 0,\n batch,\n nextAtMs: 0,\n label:\n batch > 1\n ? `Sent to ${reached.toLocaleString()}${of} over ${batch} runs`\n : `Sent to ${reached.toLocaleString()}${of}`,\n }\n}\n\n/**\n * What a ROW says about one email, in one word and one line.\n *\n * {@link campaignSendProgress} answers what a send is DOING, and a draft is\n * not doing anything: it has no status the progress states cover, and — since\n * an absent status reads as `sent` and a draft has no counters — asking it\n * about one answers \"Sent to 0\", which is the worst available sentence about\n * an email nobody has written yet. So the draft is settled here and\n * everything else is deferred, unchanged, to the derivation that owns it.\n *\n * One helper rather than the same two-line branch on four surfaces. The\n * campaigns table, the emails list, an email's own page and a campaign's\n * emails table all draw this, and four copies is how three of them come to\n * say \"Scheduled\" about a campaign that has delivered five hundred messages.\n */\nexport type CampaignSendDisplayState = 'draft' | CampaignSendProgressState\n\nexport interface CampaignSendDisplay {\n state: CampaignSendDisplayState\n /** One line a surface may show verbatim. */\n label: string\n /** The progress underneath, for a surface that wants the figures. */\n progress: CampaignSendProgress\n}\n\nexport function campaignSendDisplay(\n send: CampaignSend | null | undefined,\n): CampaignSendDisplay {\n const progress = campaignSendProgress(send)\n if (String(send?.status ?? '') === 'draft') {\n return { state: 'draft', label: 'Draft', progress }\n }\n return { state: progress.state, label: progress.label, progress }\n}\n\n/**\n * Whether this email is between batches, with more of its audience to reach.\n *\n * The one question two controls on an email's page turn on. It is stored as\n * `scheduled` — the state the processor claims — so a surface reading the\n * status alone offers \"Send now\" on a campaign that is already going out, and\n * `sendNow` re-resolves the WHOLE audience rather than continuing: everyone\n * already reached would receive a second copy under the same `cid`.\n */\nexport function campaignSendIsMidFlight(\n send: CampaignSend | null | undefined,\n): boolean {\n return campaignSendProgress(send).state === 'sending'\n}\n\n/**\n * The lists ONE SEND addressed — which can be narrower than the lists its\n * campaign is aimed at.\n *\n * A campaign holds the lists a merchant plans to reach; each send inside it\n * picks one audience, and that audience may be a segment or the site's leads\n * rather than any of them. Answering from the send is therefore the only\n * honest answer for a send's own detail page.\n *\n * An array for a document that stores one id, deliberately: the question\n * \"which lists did this reach\" has a plural answer everywhere it is asked,\n * and a caller that unwraps a single id today is a caller to revisit if a\n * send ever addresses two.\n */\nexport function campaignSendListIds(send: CampaignSend): string[] {\n return send.audience === 'list' && send.listId ? [send.listId] : []\n}\n\n/**\n * A number summed across sends, with how much of the campaign it covers.\n *\n * `value` is `null` when NO send recorded the field — which is not zero, for\n * the reason `campaign-report.ts` gives at length: an unrecorded delivery\n * count and a delivery count of zero lead a merchant to opposite conclusions\n * about their sending domain.\n *\n * `recorded` below `sends` means the total is a floor. A campaign whose\n * older sends predate the delivery webhook has a real number that describes\n * part of itself, and saying which part is the difference between a total and\n * a guess.\n */\nexport interface CampaignAggregate {\n value: number | null\n /** Sends that recorded this field. */\n recorded: number\n /** Sends in the campaign. */\n sends: number\n}\n\n/** Every rolled-up figure for one campaign. */\nexport interface CampaignRollup {\n /** Sends that have actually gone out. */\n sends: number\n /** Sends still waiting for their send time, having delivered nothing. */\n scheduled: number\n /**\n * Sends part way through an audience larger than one batch.\n *\n * Counted apart from `scheduled` and from `sends`, because it is neither:\n * mail has gone out, and more is going to. Both of those are facts a\n * merchant reading a campaign row needs, and the stored status carries\n * only the first.\n */\n sending: number\n /**\n * Emails that have been created and not yet written or sent.\n *\n * Counted apart from `sends` and from `scheduled`, because a draft is\n * neither: it has mailed nobody, and it is not on the clock to.\n */\n drafts: number\n addressed: CampaignAggregate\n sent: CampaignAggregate\n delivered: CampaignAggregate\n opens: CampaignAggregate\n uniqueOpens: CampaignAggregate\n clicks: CampaignAggregate\n uniqueClicks: CampaignAggregate\n bounced: CampaignAggregate\n complained: CampaignAggregate\n unsubscribes: CampaignAggregate\n /** Distinct openers over delivered, across every send that recorded both. */\n openRate: CampaignRate | null\n /** Distinct clickers over delivered. */\n clickRate: CampaignRate | null\n /** Unsubscribes over delivered. */\n unsubscribeRate: CampaignRate | null\n /** The most recent send time in the campaign, for ordering a list. */\n lastSentAtMs: number | null\n}\n\nconst aggregate = (\n sends: CampaignSend[],\n read: (stats: CampaignStats) => number | undefined,\n): CampaignAggregate => {\n let total = 0\n let recorded = 0\n for (const send of sends) {\n const value = read(send.stats ?? {})\n // `undefined` is \"this send never recorded it\"; a recorded 0 counts as\n // measured, and moves `recorded` without moving the total.\n if (value === undefined || value === null) continue\n const numeric = Number(value)\n if (!Number.isFinite(numeric)) continue\n total += numeric\n recorded += 1\n }\n return { value: recorded ? total : null, recorded, sends: sends.length }\n}\n\n/** When a send happened, in epoch milliseconds, or null. */\nexport function campaignSendAtMs(send: CampaignSend): number | null {\n const seconds = send.sentAt?.seconds\n if (typeof seconds === 'number') return seconds * 1000\n if (typeof send.sendAtMs === 'number') return send.sendAtMs\n return null\n}\n\n/**\n * Rolls a campaign's sends into one set of figures.\n *\n * Rates are taken over the sends that recorded BOTH sides, so a campaign\n * whose first send predates the delivery webhook reports the open rate of the\n * sends that can be measured rather than one deflated by a send with no\n * denominator.\n */\nexport function campaignRollup(sends: CampaignSend[]): CampaignRollup {\n /*==========================================\n * ONLY MAIL THAT HAS GONE OUT IS A SEND.\n *\n * An email exists from the moment it is created, so this collection now\n * holds records in three states that have mailed nobody — `draft`,\n * `scheduled` and the `sending` claim — and every figure below is about\n * mail that was delivered.\n *\n * Two separate faults if they are left in. The COUNT would report a\n * campaign as having sent three emails when it has sent two and is still\n * writing the third. And every aggregate carries `sends` as the denominator\n * of its own \"recorded by N of M\" label, so an unsent record would enlarge\n * the M — publishing a coverage figure that says the campaign is missing\n * data it was never going to have.\n *=========================================*/\n /*\n * A SEND THAT IS STILL GOING HAS STILL GONE.\n *\n * `emailIsUnsent` reads the stored status, and an email delivering an\n * audience larger than one batch is stored as `scheduled` between runs —\n * so reading it literally drops a send that has put five hundred messages\n * in five hundred inboxes out of every total on the campaign, and counts it\n * under \"scheduled\" as though nothing had happened. `campaignSendProgress`\n * is what tells \"waiting for its time\" from \"part way through\", and only\n * the first is genuinely unsent.\n *\n * A DRAFT is settled by the status because the progress states do not cover\n * it — see `campaignSendDisplay`.\n */\n const notYet = (send: CampaignSend): boolean =>\n String(send.status ?? '') === 'draft' ||\n campaignSendProgress(send).state === 'pending'\n const gone = sends.filter((send) => !notYet(send))\n const measurable = sends.filter(\n (send) => send.stats?.delivered !== undefined,\n )\n const deliveredTotal = aggregate(measurable, (stats) => stats.delivered)\n return {\n sends: gone.filter((send) => send.status !== 'canceled').length,\n // Waiting for its time with nothing delivered — which is what\n // \"scheduled\" meant before an email could be delivered over several runs,\n // and is now narrower than the stored status of that name.\n scheduled: sends.filter(\n (send) => campaignSendProgress(send).state === 'pending',\n ).length,\n sending: sends.filter(\n (send) => campaignSendProgress(send).state === 'sending',\n ).length,\n drafts: sends.filter((send) => send.status === 'draft').length,\n addressed: aggregate(gone, (stats) => stats.recipients),\n sent: aggregate(gone, (stats) => stats.sent),\n delivered: aggregate(gone, (stats) => stats.delivered),\n opens: aggregate(gone, (stats) => stats.opens),\n uniqueOpens: aggregate(gone, (stats) => stats.uniqueOpens),\n clicks: aggregate(gone, (stats) => stats.clicks),\n uniqueClicks: aggregate(gone, (stats) => stats.uniqueClicks),\n bounced: aggregate(gone, (stats) => stats.bounced),\n complained: aggregate(gone, (stats) => stats.complained),\n unsubscribes: aggregate(gone, (stats) => stats.unsubscribes),\n openRate: campaignRate(\n aggregate(measurable, (stats) => stats.uniqueOpens).value ?? undefined,\n deliveredTotal.value ?? undefined,\n 'delivered',\n ),\n clickRate: campaignRate(\n aggregate(measurable, (stats) => stats.uniqueClicks).value ?? undefined,\n deliveredTotal.value ?? undefined,\n 'delivered',\n ),\n unsubscribeRate: campaignRate(\n aggregate(measurable, (stats) => stats.unsubscribes).value ?? undefined,\n deliveredTotal.value ?? undefined,\n 'delivered',\n ),\n lastSentAtMs: sends.reduce<number | null>((latest, send) => {\n const at = campaignSendAtMs(send)\n return at !== null && (latest === null || at > latest) ? at : latest\n }, null),\n }\n}\n\n/**\n * Where a campaign stands against its own window.\n *\n * Derived at read time and never persisted — the `status` values on a SEND\n * (`sent`, `scheduled`, `canceled`, `failed`) are stored strings that a\n * processor branches on, and a display state sharing those spellings would\n * eventually be written back.\n */\nexport type CampaignWindowState = 'undated' | 'upcoming' | 'running' | 'ended'\n\nexport function campaignWindowState(\n campaign: Pick<EmailCampaign, 'startAtMs' | 'endAtMs'>,\n nowMs: number,\n): CampaignWindowState {\n const start = campaign.startAtMs ?? null\n const end = campaign.endAtMs ?? null\n if (start === null && end === null) return 'undated'\n if (start !== null && nowMs < start) return 'upcoming'\n if (end !== null && nowMs > end) return 'ended'\n return 'running'\n}\n\n/** One row of the campaigns table. */\nexport interface CampaignListRow {\n /** The id the detail route resolves — a container id, or a send id. */\n id: string\n name: string\n /**\n * True when this row IS a send with no container.\n *\n * A campaign sent before containers existed is shown as a campaign of one\n * rather than dropped from the list, and the detail route falls back to the\n * send's own report for it. Nothing about that send is rewritten.\n */\n legacy: boolean\n startAtMs: number | null\n endAtMs: number | null\n listIds: string[]\n sends: CampaignSend[]\n rollup: CampaignRollup\n windowState: CampaignWindowState\n /** For ordering: the campaign's start, else its most recent send. */\n atMs: number | null\n}\n\n/**\n * The campaigns table's rows: every container, plus every send that belongs\n * to none.\n *\n * The second half is what makes the container additive. A merchant's history\n * is the sends they already have; a list that showed only containers would\n * read as an empty product on the day this shipped, and a backfill that\n * adopted each old send into a container of one would rewrite documents whose\n * ids are cited by mail already delivered. Adopting them AT READ TIME costs a\n * pass over a list already in memory and rewrites nothing.\n *\n * Newest first, on the start date where there is one and the last send\n * otherwise. Rows with no date at all sort last: they are campaigns nobody\n * has scheduled or sent, and dating them from nothing would be an invention.\n */\nexport function campaignListRows(\n campaigns: EmailCampaign[],\n sends: CampaignSend[],\n nowMs: number,\n): CampaignListRow[] {\n const byCampaign = new Map<string, CampaignSend[]>()\n const orphans: CampaignSend[] = []\n for (const send of sends) {\n const containerId = send.emailCampaignId\n if (!containerId) {\n orphans.push(send)\n continue\n }\n const existing = byCampaign.get(containerId)\n if (existing) existing.push(send)\n else byCampaign.set(containerId, [send])\n }\n\n const rows: CampaignListRow[] = campaigns.map((campaign) => {\n const own = byCampaign.get(campaign.$id) ?? []\n const rollup = campaignRollup(own)\n return {\n id: campaign.$id,\n name: campaign.name || 'Untitled campaign',\n legacy: false,\n startAtMs: campaign.startAtMs ?? null,\n endAtMs: campaign.endAtMs ?? null,\n listIds: campaign.listIds ?? [],\n sends: own,\n rollup,\n windowState: campaignWindowState(campaign, nowMs),\n atMs: campaign.startAtMs ?? rollup.lastSentAtMs ?? null,\n }\n })\n\n for (const send of orphans) {\n const rollup = campaignRollup([send])\n const at = campaignSendAtMs(send)\n rows.push({\n id: send.$id,\n name: send.subject || 'Untitled campaign',\n legacy: true,\n startAtMs: at,\n endAtMs: at,\n listIds: [],\n sends: [send],\n rollup,\n // A send has no window of its own; it happened, or it is going to.\n windowState:\n send.status === 'scheduled'\n ? 'upcoming'\n : at === null\n ? 'undated'\n : 'ended',\n atMs: at,\n })\n }\n\n return rows.sort((a, b) => {\n if (a.atMs === null && b.atMs === null) return a.name.localeCompare(b.name)\n if (a.atMs === null) return 1\n if (b.atMs === null) return -1\n return b.atMs - a.atMs\n })\n}\n"],"names":["campaignRate","CAMPAIGN_SEND_CONTAINER_FIELD","progressCount","raw","value","Math","floor","Number","isFinite","campaignSendProgress","send","status","String","reached","stats","sent","rawAudience","audienceSize","audience","undefined","resume","remaining","batch","nextAtMs","of","toLocaleString","state","label","why","campaignSendDisplay","progress","campaignSendIsMidFlight","campaignSendListIds","listId","aggregate","sends","read","total","recorded","numeric","length","campaignSendAtMs","seconds","sentAt","sendAtMs","campaignRollup","deliveredTotal","notYet","gone","filter","measurable","delivered","scheduled","sending","drafts","addressed","recipients","opens","uniqueOpens","clicks","uniqueClicks","bounced","complained","unsubscribes","openRate","clickRate","unsubscribeRate","lastSentAtMs","reduce","latest","at","campaignWindowState","campaign","nowMs","start","startAtMs","end","endAtMs","campaignListRows","campaigns","byCampaign","Map","orphans","containerId","emailCampaignId","push","existing","get","set","rows","map","own","$id","rollup","id","name","legacy","listIds","windowState","atMs","subject","sort","a","b","localeCompare"],"mappings":"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"}