@aglyn/plugins-marketing 1.0.0-beta.221 → 1.0.0-beta.222
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 +4 -1
- package/package.json +15 -15
- package/src/lib/components/campaign-conversions-card.js +53 -64
- package/src/lib/components/campaign-conversions-card.js.map +1 -1
- package/src/lib/components/campaign-detail-card.js +2 -1
- package/src/lib/components/campaign-detail-card.js.map +1 -1
- package/src/lib/components/campaign-edit-drawer.js +1 -1
- package/src/lib/components/campaign-edit-drawer.js.map +1 -1
- package/src/lib/components/campaign-queries.js +1 -1
- package/src/lib/components/campaign-queries.js.map +1 -1
- package/src/lib/components/campaign-reach-sections.js +1 -1
- package/src/lib/components/campaign-reach-sections.js.map +1 -1
- package/src/lib/components/campaign-report-card.js +5 -4
- package/src/lib/components/campaign-report-card.js.map +1 -1
- package/src/lib/components/campaigns-card.js +1 -1
- package/src/lib/components/campaigns-card.js.map +1 -1
- package/src/lib/components/email-compose-card.js +1 -1
- package/src/lib/components/email-compose-card.js.map +1 -1
- package/src/lib/components/email-detail.js +5 -4
- package/src/lib/components/email-detail.js.map +1 -1
- package/src/lib/components/email-template-report-card.d.ts +53 -0
- package/src/lib/components/email-template-report-card.js +469 -0
- package/src/lib/components/email-template-report-card.js.map +1 -0
- package/src/lib/components/emails-list-card.js +2 -2
- package/src/lib/components/emails-list-card.js.map +1 -1
- package/src/lib/components/host-experiments-card.component.js +3 -2
- package/src/lib/components/host-experiments-card.component.js.map +1 -1
- package/src/lib/components/site-runtime.js +3 -1
- package/src/lib/components/site-runtime.js.map +1 -1
- package/src/lib/declarations.server.d.ts +9 -0
- package/src/lib/declarations.server.js +62 -0
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/model/campaign-container.d.ts +503 -0
- package/src/lib/model/campaign-container.js +498 -0
- package/src/lib/model/campaign-container.js.map +1 -0
- package/src/lib/model/campaign-conversions.d.ts +8 -1
- package/src/lib/model/campaign-conversions.js +11 -0
- package/src/lib/model/campaign-conversions.js.map +1 -1
- package/src/lib/model/campaign-report.d.ts +158 -0
- package/src/lib/model/campaign-report.js +268 -0
- package/src/lib/model/campaign-report.js.map +1 -0
- package/src/lib/model/campaign-revenue.d.ts +3 -3
- package/src/lib/model/campaign-revenue.js +3 -3
- package/src/lib/model/campaign-revenue.js.map +1 -1
- package/src/lib/model/campaign-send-time.js +2 -2
- package/src/lib/model/campaign-send-time.js.map +1 -1
- package/src/lib/model/compile-client-automations.d.ts +2 -2
- package/src/lib/model/compile-client-automations.js +3 -3
- package/src/lib/model/compile-client-automations.js.map +1 -1
- package/src/lib/model/email-record.d.ts +175 -0
- package/src/lib/model/email-record.js +206 -0
- package/src/lib/model/email-record.js.map +1 -0
- package/src/lib/model/site-contract.d.ts +2 -2
- package/src/lib/model/site-contract.js.map +1 -1
- package/src/lib/model/template-report.d.ts +158 -0
- package/src/lib/model/template-report.js +249 -0
- package/src/lib/model/template-report.js.map +1 -0
- package/src/lib/plugin.js +16 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/preview-props.js.map +1 -1
- package/src/lib/server/campaign-attribution-store.d.ts +55 -0
- package/src/lib/server/campaign-attribution-store.js +98 -0
- package/src/lib/server/campaign-attribution-store.js.map +1 -0
- package/src/lib/server/campaign-conversion-attribution.d.ts +283 -0
- package/src/lib/server/campaign-conversion-attribution.js +447 -0
- package/src/lib/server/campaign-conversion-attribution.js.map +1 -0
- package/src/lib/server/campaign-manage.js +1 -1
- package/src/lib/server/campaign-manage.js.map +1 -1
- package/src/lib/server/campaign-org-refs.js +1 -1
- package/src/lib/server/campaign-org-refs.js.map +1 -1
- package/src/lib/server/campaign-recipients.js +7 -7
- package/src/lib/server/campaign-recipients.js.map +1 -1
- package/src/lib/server/campaign-send.js +25 -19
- package/src/lib/server/campaign-send.js.map +1 -1
- package/src/lib/server/conversion-creditor.d.ts +18 -0
- package/src/lib/server/conversion-creditor.js +131 -0
- package/src/lib/server/conversion-creditor.js.map +1 -0
- package/src/lib/server/email-campaign-touch.d.ts +81 -0
- package/src/lib/server/email-campaign-touch.js +227 -0
- package/src/lib/server/email-campaign-touch.js.map +1 -0
- package/src/lib/server/email-events.js +8 -6
- package/src/lib/server/email-events.js.map +1 -1
- package/src/lib/server/email-revenue-attribution.d.ts +186 -0
- package/src/lib/server/email-revenue-attribution.js +322 -0
- package/src/lib/server/email-revenue-attribution.js.map +1 -0
- package/src/lib/server/email-send-record-index.d.ts +34 -0
- package/src/lib/server/email-send-record-index.js +90 -0
- package/src/lib/server/email-send-record-index.js.map +1 -0
- package/src/lib/server/get-client-automations.js.map +1 -1
- package/src/lib/server/marketing-figures.js +3 -3
- package/src/lib/server/marketing-figures.js.map +1 -1
- package/src/lib/server/send-tally.d.ts +32 -0
- package/src/lib/server/send-tally.js +54 -0
- package/src/lib/server/send-tally.js.map +1 -0
- package/src/lib/testing/stand-in-person-records.d.ts +35 -0
- package/src/lib/testing/stand-in-person-records.js +67 -0
- package/src/lib/testing/stand-in-person-records.js.map +1 -0
- package/src/lib/testing/stand-in-record-counts.d.ts +26 -0
- package/src/lib/testing/stand-in-record-counts.js +70 -0
- package/src/lib/testing/stand-in-record-counts.js.map +1 -0
- package/src/lib/transfer/campaigns-package.d.ts +72 -0
- package/src/lib/transfer/campaigns-package.js +145 -0
- package/src/lib/transfer/campaigns-package.js.map +1 -0
- package/src/lib/transfer/campaigns-package.server.d.ts +33 -0
- package/src/lib/transfer/campaigns-package.server.js +305 -0
- package/src/lib/transfer/campaigns-package.server.js.map +1 -0
|
@@ -0,0 +1,503 @@
|
|
|
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
|
+
/**
|
|
18
|
+
* A CAMPAIGN IS A CONTAINER; A SEND IS ONE MESSAGE INSIDE IT.
|
|
19
|
+
*
|
|
20
|
+
* ## Both belong to the ORGANIZATION (AGL-3273)
|
|
21
|
+
*
|
|
22
|
+
* `orgs/{orgId}/emailCampaigns/{campaignId}` holds a CONTAINER: a name, a
|
|
23
|
+
* window, the lists it aims at. `orgs/{orgId}/campaigns/{sendId}` holds a
|
|
24
|
+
* SEND: one subject, one body, one audience, one set of counters.
|
|
25
|
+
*
|
|
26
|
+
* They lived under one site (`hosts/{hostId}/…`) until the audience side —
|
|
27
|
+
* contacts, lists, topics, sending domains — had long since become the
|
|
28
|
+
* org's. An organization's outbound sequences then had to be filed under a
|
|
29
|
+
* campaign that belonged to its marketing site, and the org-level Leads page
|
|
30
|
+
* could not name a campaign at all. Moving both up is what lets one campaign
|
|
31
|
+
* coordinate a push across every site, a sequence and the leads it produced.
|
|
32
|
+
*
|
|
33
|
+
* ## What stays a SITE fact, and where it is recorded
|
|
34
|
+
*
|
|
35
|
+
* A campaign's reach is {@link EmailCampaign.visibleTo}, the same scope
|
|
36
|
+
* tokens every org-owned resource carries: `['org']` is every site, and
|
|
37
|
+
* `['host:{id}', …]` is the sites it is placed on. It decides which sites'
|
|
38
|
+
* pickers offer the campaign and which scoped collaborators can read it —
|
|
39
|
+
* nothing else, because membership stays a field on the member.
|
|
40
|
+
*
|
|
41
|
+
* A send, by contrast, is always sent AS one site: that site's sender, the
|
|
42
|
+
* consent the person gave that site, the site's unsubscribe signature and
|
|
43
|
+
* its designs. {@link CAMPAIGN_SEND_HOST_FIELD} records which, and the
|
|
44
|
+
* send's `visibleTo` is that one site's token.
|
|
45
|
+
*
|
|
46
|
+
* ## Ids did not change, and that is load-bearing
|
|
47
|
+
*
|
|
48
|
+
* Every unsubscribe link that has ever gone out carries `hostId` and
|
|
49
|
+
* `cid={sendId}`, both inside the link's HMAC, and those emails sit in
|
|
50
|
+
* inboxes forever. So the move copied every document under its existing id,
|
|
51
|
+
* and every reader that starts from a link resolves the send through the
|
|
52
|
+
* site's org (`resolveCampaignSendRef` in the Marketing plugin), with the
|
|
53
|
+
* old site path as the fallback for a document the migration has not reached.
|
|
54
|
+
* A member's `campaignIds` names container ids, which likewise never moved.
|
|
55
|
+
*
|
|
56
|
+
* A send written before containers existed carries no
|
|
57
|
+
* {@link CAMPAIGN_SEND_CONTAINER_FIELD}, and {@link campaignListRows}
|
|
58
|
+
* presents it as a container of one rather than hiding it.
|
|
59
|
+
*
|
|
60
|
+
* ## Why the arithmetic is here
|
|
61
|
+
*
|
|
62
|
+
* The same reason `campaign-report.ts` gives for the per-send rates: a
|
|
63
|
+
* denominator chosen in JSX is a denominator nobody tests. Aggregating across
|
|
64
|
+
* sends adds one problem the single-send report does not have — some sends
|
|
65
|
+
* recorded a field and others never did — and summing those into one number
|
|
66
|
+
* silently reports a partial total as a complete one. Every aggregate here
|
|
67
|
+
* therefore reports how many sends it could measure.
|
|
68
|
+
*/
|
|
69
|
+
import { type ListUnsubscribeForcedReason } from '@aglyn/shared-util-email/list-unsubscribe';
|
|
70
|
+
import { type SendRate, type SendStats } from '@aglyn/shared-ui-email-campaigns/model/send-report';
|
|
71
|
+
/** The org collection holding campaign CONTAINERS. */
|
|
72
|
+
export declare const EMAIL_CAMPAIGNS_COLLECTION = "emailCampaigns";
|
|
73
|
+
/** The org collection holding SENDS — one document per email. */
|
|
74
|
+
export declare const CAMPAIGN_SENDS_COLLECTION = "campaigns";
|
|
75
|
+
/** `orgs/{orgId}/emailCampaigns`, as path segments. */
|
|
76
|
+
export declare function orgEmailCampaignsPath(orgId: string): [string, string, string];
|
|
77
|
+
/** `orgs/{orgId}/campaigns`, as path segments. */
|
|
78
|
+
export declare function orgCampaignSendsPath(orgId: string): [string, string, string];
|
|
79
|
+
/** `orgs/{orgId}/campaignSequenceReports`, as path segments. */
|
|
80
|
+
export declare function orgCampaignSequenceReportsPath(orgId: string): [string, string, string];
|
|
81
|
+
/**
|
|
82
|
+
* The field on a SEND naming the site it is sent AS.
|
|
83
|
+
*
|
|
84
|
+
* Required on every send written since AGL-3273. The sender, the consent
|
|
85
|
+
* group, the unsubscribe signature, the designs and the experiments all
|
|
86
|
+
* belong to that site, so a send with no site cannot be delivered.
|
|
87
|
+
*/
|
|
88
|
+
export declare const CAMPAIGN_SEND_HOST_FIELD = "hostId";
|
|
89
|
+
/** The scope token naming every site in the org, as `scope-tokens.ts` spells it. */
|
|
90
|
+
export declare const CAMPAIGN_ORG_SCOPE = "org";
|
|
91
|
+
/** `host:{hostId}`, the scope token naming one site. */
|
|
92
|
+
export declare function campaignHostScope(hostId: string): string;
|
|
93
|
+
/**
|
|
94
|
+
* The `visibleTo` a send is stamped with: the one site it sends as.
|
|
95
|
+
*
|
|
96
|
+
* Not `['org']`. An org-wide member reads every send regardless, and a
|
|
97
|
+
* collaborator invited to a sibling site has no business reading who this
|
|
98
|
+
* site mailed.
|
|
99
|
+
*/
|
|
100
|
+
export declare function campaignSendVisibleTo(hostId: string): string[];
|
|
101
|
+
/**
|
|
102
|
+
* The sites a container is placed on, or `null` when it is on every site.
|
|
103
|
+
*
|
|
104
|
+
* An absent or empty `visibleTo` reads as NO sites, for the reason
|
|
105
|
+
* `visibleToHost` in `scope-tokens.ts` gives: a document nobody scoped is
|
|
106
|
+
* not the same fact as one scoped to everyone, and hiding is the
|
|
107
|
+
* recoverable direction.
|
|
108
|
+
*/
|
|
109
|
+
export declare function campaignSiteIds(campaign: Pick<EmailCampaign, 'visibleTo'> | null | undefined): string[] | null;
|
|
110
|
+
/** Whether a container is offered on this site. */
|
|
111
|
+
export declare function campaignPlacedOnHost(campaign: Pick<EmailCampaign, 'visibleTo'> | null | undefined, hostId: string): boolean;
|
|
112
|
+
/**
|
|
113
|
+
* The `visibleTo` a container is stamped with, from a site selection.
|
|
114
|
+
*
|
|
115
|
+
* `null` (or an empty selection) is every site. A selection is deduped and
|
|
116
|
+
* capped at thirty, the ceiling `array-contains-any` and the rules' `hasAny`
|
|
117
|
+
* both impose.
|
|
118
|
+
*/
|
|
119
|
+
export declare function campaignVisibleTo(siteIds: readonly string[] | null): string[];
|
|
120
|
+
/**
|
|
121
|
+
* The field on a SEND naming the campaign it belongs to.
|
|
122
|
+
*
|
|
123
|
+
* Not `campaignId`: on a send document that name already means the send's own
|
|
124
|
+
* id — it is what `cid` carries and what the report route addresses — and one
|
|
125
|
+
* word meaning both would be read into the other on the first edit.
|
|
126
|
+
*/
|
|
127
|
+
export declare const CAMPAIGN_SEND_CONTAINER_FIELD = "emailCampaignId";
|
|
128
|
+
/**
|
|
129
|
+
* A campaign: the container, not a message.
|
|
130
|
+
*
|
|
131
|
+
* Stored at `orgs/{orgId}/emailCampaigns/{campaignId}`.
|
|
132
|
+
*/
|
|
133
|
+
export interface EmailCampaign {
|
|
134
|
+
$id: string;
|
|
135
|
+
/** What the merchant called it. */
|
|
136
|
+
name: string;
|
|
137
|
+
/** When the campaign window opens. Null for a campaign with no dates. */
|
|
138
|
+
startAtMs?: number | null;
|
|
139
|
+
/** When it closes. Null for an open-ended campaign. */
|
|
140
|
+
endAtMs?: number | null;
|
|
141
|
+
/** Org email lists this campaign is aimed at, by list id. */
|
|
142
|
+
listIds?: string[];
|
|
143
|
+
/**
|
|
144
|
+
* The stream this campaign's emails open on.
|
|
145
|
+
*
|
|
146
|
+
* A DEFAULT, not a constraint. The topic decides who a send skips, what the
|
|
147
|
+
* preference page linked from the footer highlights, and which stream a
|
|
148
|
+
* resulting opt-out is recorded against — all facts about one MESSAGE, and
|
|
149
|
+
* one campaign may legitimately carry a newsletter and a promotion. So the
|
|
150
|
+
* composer's picker is what the send records; this is what the picker opens
|
|
151
|
+
* on, which is what stops a "Sales" campaign quietly mailing under
|
|
152
|
+
* `marketing`.
|
|
153
|
+
*/
|
|
154
|
+
topicId?: string;
|
|
155
|
+
/**
|
|
156
|
+
* The sites the campaign is placed on, as scope tokens: `['org']` for
|
|
157
|
+
* every site, `['host:{id}', …]` for a set. See {@link campaignSiteIds}.
|
|
158
|
+
*/
|
|
159
|
+
visibleTo?: string[];
|
|
160
|
+
/**
|
|
161
|
+
* Whether its emails carry `List-Unsubscribe` and `List-Unsubscribe-Post`
|
|
162
|
+
* (RFC 8058), the header mail clients draw their own Unsubscribe button
|
|
163
|
+
* from (AGL-3307). ON unless stored `false` — absent on every campaign
|
|
164
|
+
* written before the setting existed, which read as on because that is
|
|
165
|
+
* what they always did. Read it through {@link campaignListUnsubscribe}.
|
|
166
|
+
*
|
|
167
|
+
* Off is overridden per send by the bulk guard; see
|
|
168
|
+
* {@link CampaignListUnsubscribeRecord}. The footer's unsubscribe link is
|
|
169
|
+
* in every email either way.
|
|
170
|
+
*/
|
|
171
|
+
listUnsubscribe?: boolean;
|
|
172
|
+
createdAtMs?: number;
|
|
173
|
+
createdBy?: string;
|
|
174
|
+
deletedAt?: unknown;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Whether a campaign's emails carry the mail-client unsubscribe button:
|
|
178
|
+
* the shared reader sequences use, with a campaign's default of ON.
|
|
179
|
+
*/
|
|
180
|
+
export declare function campaignListUnsubscribe(campaign: Pick<EmailCampaign, 'listUnsubscribe'> | null | undefined): boolean;
|
|
181
|
+
/**
|
|
182
|
+
* What one SEND did with the List-Unsubscribe header, written by the
|
|
183
|
+
* campaign sender on every batch (AGL-3307).
|
|
184
|
+
*/
|
|
185
|
+
export interface CampaignListUnsubscribeRecord {
|
|
186
|
+
/** The campaign's setting when it went out. */
|
|
187
|
+
requested?: boolean;
|
|
188
|
+
/** Whether the header was on the messages. */
|
|
189
|
+
on?: boolean;
|
|
190
|
+
/** True when the setting said off and the bulk guard turned it back on. */
|
|
191
|
+
forced?: boolean;
|
|
192
|
+
reason?: ListUnsubscribeForcedReason;
|
|
193
|
+
/** One sentence saying why, for the campaign page. */
|
|
194
|
+
detail?: string;
|
|
195
|
+
/** Campaign messages in the day, this email included, it was judged on. */
|
|
196
|
+
volume?: number;
|
|
197
|
+
/** The threshold it was judged against. */
|
|
198
|
+
threshold?: number;
|
|
199
|
+
atMs?: number;
|
|
200
|
+
}
|
|
201
|
+
/** The sends among these whose header the bulk guard forced back on. */
|
|
202
|
+
export declare function campaignForcedListUnsubscribeSends<T extends Pick<CampaignSend, 'listUnsubscribe'>>(sends: readonly T[] | null | undefined): T[];
|
|
203
|
+
/** One send, as much of it as a list or a rollup needs. */
|
|
204
|
+
export interface CampaignSend {
|
|
205
|
+
$id: string;
|
|
206
|
+
subject?: string;
|
|
207
|
+
/** The audience KIND: `'leads'`, `'members'`, `'segment'`, `'list'`. */
|
|
208
|
+
audience?: string;
|
|
209
|
+
/** The list this send addressed, when the kind is `'list'`. */
|
|
210
|
+
listId?: string;
|
|
211
|
+
/** The segment this send addressed, when the kind is `'segment'`. */
|
|
212
|
+
segmentId?: string;
|
|
213
|
+
/** Which container it belongs to, absent on a send written before them. */
|
|
214
|
+
emailCampaignId?: string;
|
|
215
|
+
/** The site it is sent as. See {@link CAMPAIGN_SEND_HOST_FIELD}. */
|
|
216
|
+
hostId?: string;
|
|
217
|
+
status?: string;
|
|
218
|
+
sentAt?: {
|
|
219
|
+
seconds?: number;
|
|
220
|
+
} | null;
|
|
221
|
+
sendAtMs?: number;
|
|
222
|
+
/**
|
|
223
|
+
* When the record was minted, stamped by every writer that creates one.
|
|
224
|
+
*
|
|
225
|
+
* Absent on a send written before the stamp existed. The lists that draw
|
|
226
|
+
* drafts beside sends order on it through `emailListTimeMs`, which is why
|
|
227
|
+
* it is here rather than only on the loose record shape: a draft carries
|
|
228
|
+
* neither `sentAt` nor `sendAtMs`, so this is the only time it has.
|
|
229
|
+
*/
|
|
230
|
+
createdAtMs?: number;
|
|
231
|
+
stats?: SendStats;
|
|
232
|
+
/** How far a send that goes out over several batches has got. */
|
|
233
|
+
resume?: CampaignResume;
|
|
234
|
+
/** What it did with the List-Unsubscribe header. Absent before AGL-3307. */
|
|
235
|
+
listUnsubscribe?: CampaignListUnsubscribeRecord;
|
|
236
|
+
/**
|
|
237
|
+
* Staff review of a send the outbound phishing screen held (AGL-3356).
|
|
238
|
+
* Written by the send core, decided only by staff. See
|
|
239
|
+
* {@link campaignSendHeldForReview}.
|
|
240
|
+
*/
|
|
241
|
+
staffReview?: CampaignStaffReview;
|
|
242
|
+
}
|
|
243
|
+
/** A held send's review, as the send core and the staff decision write it. */
|
|
244
|
+
export interface CampaignStaffReview {
|
|
245
|
+
/** `held` until staff decide; then `released` or `rejected`. */
|
|
246
|
+
state?: string;
|
|
247
|
+
/** `HS-…`, the reference the merchant quotes to support. */
|
|
248
|
+
reference?: string;
|
|
249
|
+
reviewId?: string;
|
|
250
|
+
heldAtMs?: number;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Is this send waiting on a staff review (AGL-3356)?
|
|
254
|
+
*
|
|
255
|
+
* A held send is STORED as `scheduled`, parked at a send time no processor
|
|
256
|
+
* run reaches, so the review can put it back on the clock without a second
|
|
257
|
+
* record. Every surface that read the status alone therefore called it
|
|
258
|
+
* "Scheduled" and showed a due date in the year 9999 — and offered "Send
|
|
259
|
+
* now", "Reschedule" and the composer, none of which a merchant may use to
|
|
260
|
+
* release it. This is the one test, so no surface restates it.
|
|
261
|
+
*/
|
|
262
|
+
export declare function campaignSendHeldForReview(send: Pick<CampaignSend, 'status' | 'staffReview'> | null | undefined): boolean;
|
|
263
|
+
/**
|
|
264
|
+
* What a held send's surfaces tell the merchant: that it is held, why in
|
|
265
|
+
* general terms, that they cannot release it, and what happens next. One
|
|
266
|
+
* sentence set, so the list, the page and the composer agree.
|
|
267
|
+
*/
|
|
268
|
+
export declare function campaignHeldForReviewNotice(send: Pick<CampaignSend, 'staffReview'> | null | undefined): string;
|
|
269
|
+
/**
|
|
270
|
+
* The batch state the sender writes on an email that is still going out.
|
|
271
|
+
*
|
|
272
|
+
* Absent on every send that finished in one batch, and on every send that
|
|
273
|
+
* predates batching — which is why {@link campaignSendProgress} treats a
|
|
274
|
+
* missing record as "this is not a batched send" rather than as zero.
|
|
275
|
+
*/
|
|
276
|
+
export interface CampaignResume {
|
|
277
|
+
/** People the email has resolved and not yet addressed. */
|
|
278
|
+
remaining?: number;
|
|
279
|
+
/** Batches it has run. */
|
|
280
|
+
batch?: number;
|
|
281
|
+
/** When the next batch may go, ms. Zero when there is not going to be one. */
|
|
282
|
+
nextAtMs?: number;
|
|
283
|
+
/** Why it stopped short, when it did. */
|
|
284
|
+
stop?: string;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* WHAT A SEND IS ACTUALLY DOING, for a row that would otherwise lie.
|
|
288
|
+
*
|
|
289
|
+
* An email larger than one send may carry is delivered over several batches,
|
|
290
|
+
* and between them it is stored as `scheduled` — the state the processor
|
|
291
|
+
* claims, and the only one that resumes it without a second index and a
|
|
292
|
+
* second query. Read literally, that is a row saying "not sent yet" about an
|
|
293
|
+
* email that has already put five hundred messages in five hundred inboxes.
|
|
294
|
+
*
|
|
295
|
+
* So the stored fields are not the sentence. This is: it takes the status,
|
|
296
|
+
* the delivered count and the batch record, and answers what a person needs
|
|
297
|
+
* to see. Derived at read time and never persisted, exactly as
|
|
298
|
+
* `campaignWindowState` beside it is, because it is a description of stored
|
|
299
|
+
* facts and not a fact of its own.
|
|
300
|
+
*
|
|
301
|
+
* ## The four states, and which stored shape each one is
|
|
302
|
+
*
|
|
303
|
+
* - `pending` — `scheduled`, nothing delivered. A campaign waiting for its
|
|
304
|
+
* time. This is what `scheduled` meant before batching and still does.
|
|
305
|
+
* - `sending` — `scheduled` or `sending` with something delivered and more to
|
|
306
|
+
* come. The state this function exists for.
|
|
307
|
+
* - `sent` — finished, whether in one batch or six.
|
|
308
|
+
* - `stopped` — finished with people it never addressed: canceled mid-flight,
|
|
309
|
+
* failed mid-flight, or stopped by the batch guard. The count is what makes
|
|
310
|
+
* this legible rather than alarming — an email that reached 2,400 of 3,000
|
|
311
|
+
* and stopped is a different conversation from one that reached nobody.
|
|
312
|
+
*/
|
|
313
|
+
export type CampaignSendProgressState = 'pending' | 'sending' | 'sent' | 'stopped';
|
|
314
|
+
export interface CampaignSendProgress {
|
|
315
|
+
state: CampaignSendProgressState;
|
|
316
|
+
/** Messages this email has delivered. */
|
|
317
|
+
reached: number;
|
|
318
|
+
/**
|
|
319
|
+
* The audience it is working through, when one was recorded. Null when the
|
|
320
|
+
* send never recorded an audience size, which is every send that predates
|
|
321
|
+
* the figure — reported as null rather than as `reached` so a surface does
|
|
322
|
+
* not present a floor as a total.
|
|
323
|
+
*/
|
|
324
|
+
audience: number | null;
|
|
325
|
+
/** People it has resolved and not yet addressed. */
|
|
326
|
+
remaining: number;
|
|
327
|
+
/** Batches it has run. Zero for a send that never batched. */
|
|
328
|
+
batch: number;
|
|
329
|
+
/** When the next batch may go, ms. Zero unless {@link state} is `sending`. */
|
|
330
|
+
nextAtMs: number;
|
|
331
|
+
/** One line a surface may show verbatim. */
|
|
332
|
+
label: string;
|
|
333
|
+
}
|
|
334
|
+
export declare function campaignSendProgress(send: CampaignSend | null | undefined): CampaignSendProgress;
|
|
335
|
+
/**
|
|
336
|
+
* What a ROW says about one email, in one word and one line.
|
|
337
|
+
*
|
|
338
|
+
* {@link campaignSendProgress} answers what a send is DOING, and a draft is
|
|
339
|
+
* not doing anything: it has no status the progress states cover, and — since
|
|
340
|
+
* an absent status reads as `sent` and a draft has no counters — asking it
|
|
341
|
+
* about one answers "Sent to 0", which is the worst available sentence about
|
|
342
|
+
* an email nobody has written yet. So the draft is settled here and
|
|
343
|
+
* everything else is deferred, unchanged, to the derivation that owns it.
|
|
344
|
+
*
|
|
345
|
+
* One helper rather than the same two-line branch on four surfaces. The
|
|
346
|
+
* campaigns table, the emails list, an email's own page and a campaign's
|
|
347
|
+
* emails table all draw this, and four copies is how three of them come to
|
|
348
|
+
* say "Scheduled" about a campaign that has delivered five hundred messages.
|
|
349
|
+
*/
|
|
350
|
+
export type CampaignSendDisplayState = 'draft' | 'held' | CampaignSendProgressState;
|
|
351
|
+
export interface CampaignSendDisplay {
|
|
352
|
+
state: CampaignSendDisplayState;
|
|
353
|
+
/** One line a surface may show verbatim. */
|
|
354
|
+
label: string;
|
|
355
|
+
/** The progress underneath, for a surface that wants the figures. */
|
|
356
|
+
progress: CampaignSendProgress;
|
|
357
|
+
}
|
|
358
|
+
export declare function campaignSendDisplay(send: CampaignSend | null | undefined): CampaignSendDisplay;
|
|
359
|
+
/**
|
|
360
|
+
* Whether this email is between batches, with more of its audience to reach.
|
|
361
|
+
*
|
|
362
|
+
* The one question two controls on an email's page turn on. It is stored as
|
|
363
|
+
* `scheduled` — the state the processor claims — so a surface reading the
|
|
364
|
+
* status alone offers "Send now" on a campaign that is already going out, and
|
|
365
|
+
* `sendNow` re-resolves the WHOLE audience rather than continuing: everyone
|
|
366
|
+
* already reached would receive a second copy under the same `cid`.
|
|
367
|
+
*/
|
|
368
|
+
export declare function campaignSendIsMidFlight(send: CampaignSend | null | undefined): boolean;
|
|
369
|
+
/**
|
|
370
|
+
* The lists ONE SEND addressed — which can be narrower than the lists its
|
|
371
|
+
* campaign is aimed at.
|
|
372
|
+
*
|
|
373
|
+
* A campaign holds the lists a merchant plans to reach; each send inside it
|
|
374
|
+
* picks one audience, and that audience may be a segment or the site's leads
|
|
375
|
+
* rather than any of them. Answering from the send is therefore the only
|
|
376
|
+
* honest answer for a send's own detail page.
|
|
377
|
+
*
|
|
378
|
+
* An array for a document that stores one id, deliberately: the question
|
|
379
|
+
* "which lists did this reach" has a plural answer everywhere it is asked,
|
|
380
|
+
* and a caller that unwraps a single id today is a caller to revisit if a
|
|
381
|
+
* send ever addresses two.
|
|
382
|
+
*/
|
|
383
|
+
export declare function campaignSendListIds(send: CampaignSend): string[];
|
|
384
|
+
/**
|
|
385
|
+
* A number summed across sends, with how much of the campaign it covers.
|
|
386
|
+
*
|
|
387
|
+
* `value` is `null` when NO send recorded the field — which is not zero, for
|
|
388
|
+
* the reason `campaign-report.ts` gives at length: an unrecorded delivery
|
|
389
|
+
* count and a delivery count of zero lead a merchant to opposite conclusions
|
|
390
|
+
* about their sending domain.
|
|
391
|
+
*
|
|
392
|
+
* `recorded` below `sends` means the total is a floor. A campaign whose
|
|
393
|
+
* older sends predate the delivery webhook has a real number that describes
|
|
394
|
+
* part of itself, and saying which part is the difference between a total and
|
|
395
|
+
* a guess.
|
|
396
|
+
*/
|
|
397
|
+
export interface CampaignAggregate {
|
|
398
|
+
value: number | null;
|
|
399
|
+
/** Sends that recorded this field. */
|
|
400
|
+
recorded: number;
|
|
401
|
+
/** Sends in the campaign. */
|
|
402
|
+
sends: number;
|
|
403
|
+
}
|
|
404
|
+
/** Every rolled-up figure for one campaign. */
|
|
405
|
+
export interface CampaignRollup {
|
|
406
|
+
/** Sends that have actually gone out. */
|
|
407
|
+
sends: number;
|
|
408
|
+
/** Sends still waiting for their send time, having delivered nothing. */
|
|
409
|
+
scheduled: number;
|
|
410
|
+
/**
|
|
411
|
+
* Sends part way through an audience larger than one batch.
|
|
412
|
+
*
|
|
413
|
+
* Counted apart from `scheduled` and from `sends`, because it is neither:
|
|
414
|
+
* mail has gone out, and more is going to. Both of those are facts a
|
|
415
|
+
* merchant reading a campaign row needs, and the stored status carries
|
|
416
|
+
* only the first.
|
|
417
|
+
*/
|
|
418
|
+
sending: number;
|
|
419
|
+
/**
|
|
420
|
+
* Emails that have been created and not yet written or sent.
|
|
421
|
+
*
|
|
422
|
+
* Counted apart from `sends` and from `scheduled`, because a draft is
|
|
423
|
+
* neither: it has mailed nobody, and it is not on the clock to.
|
|
424
|
+
*/
|
|
425
|
+
drafts: number;
|
|
426
|
+
addressed: CampaignAggregate;
|
|
427
|
+
sent: CampaignAggregate;
|
|
428
|
+
delivered: CampaignAggregate;
|
|
429
|
+
opens: CampaignAggregate;
|
|
430
|
+
uniqueOpens: CampaignAggregate;
|
|
431
|
+
clicks: CampaignAggregate;
|
|
432
|
+
uniqueClicks: CampaignAggregate;
|
|
433
|
+
bounced: CampaignAggregate;
|
|
434
|
+
complained: CampaignAggregate;
|
|
435
|
+
unsubscribes: CampaignAggregate;
|
|
436
|
+
/** Distinct openers over delivered, across every send that recorded both. */
|
|
437
|
+
openRate: SendRate | null;
|
|
438
|
+
/** Distinct clickers over delivered. */
|
|
439
|
+
clickRate: SendRate | null;
|
|
440
|
+
/** Unsubscribes over delivered. */
|
|
441
|
+
unsubscribeRate: SendRate | null;
|
|
442
|
+
/** The most recent send time in the campaign, for ordering a list. */
|
|
443
|
+
lastSentAtMs: number | null;
|
|
444
|
+
}
|
|
445
|
+
/** When a send happened, in epoch milliseconds, or null. */
|
|
446
|
+
export declare function campaignSendAtMs(send: CampaignSend): number | null;
|
|
447
|
+
/**
|
|
448
|
+
* Rolls a campaign's sends into one set of figures.
|
|
449
|
+
*
|
|
450
|
+
* Rates are taken over the sends that recorded BOTH sides, so a campaign
|
|
451
|
+
* whose first send predates the delivery webhook reports the open rate of the
|
|
452
|
+
* sends that can be measured rather than one deflated by a send with no
|
|
453
|
+
* denominator.
|
|
454
|
+
*/
|
|
455
|
+
export declare function campaignRollup(sends: CampaignSend[]): CampaignRollup;
|
|
456
|
+
/**
|
|
457
|
+
* Where a campaign stands against its own window.
|
|
458
|
+
*
|
|
459
|
+
* Derived at read time and never persisted — the `status` values on a SEND
|
|
460
|
+
* (`sent`, `scheduled`, `canceled`, `failed`) are stored strings that a
|
|
461
|
+
* processor branches on, and a display state sharing those spellings would
|
|
462
|
+
* eventually be written back.
|
|
463
|
+
*/
|
|
464
|
+
export type CampaignWindowState = 'undated' | 'upcoming' | 'running' | 'ended';
|
|
465
|
+
export declare function campaignWindowState(campaign: Pick<EmailCampaign, 'startAtMs' | 'endAtMs'>, nowMs: number): CampaignWindowState;
|
|
466
|
+
/** One row of the campaigns table. */
|
|
467
|
+
export interface CampaignListRow {
|
|
468
|
+
/** The id the detail route resolves — a container id, or a send id. */
|
|
469
|
+
id: string;
|
|
470
|
+
name: string;
|
|
471
|
+
/**
|
|
472
|
+
* True when this row IS a send with no container.
|
|
473
|
+
*
|
|
474
|
+
* A campaign sent before containers existed is shown as a campaign of one
|
|
475
|
+
* rather than dropped from the list, and the detail route falls back to the
|
|
476
|
+
* send's own report for it. Nothing about that send is rewritten.
|
|
477
|
+
*/
|
|
478
|
+
legacy: boolean;
|
|
479
|
+
startAtMs: number | null;
|
|
480
|
+
endAtMs: number | null;
|
|
481
|
+
listIds: string[];
|
|
482
|
+
sends: CampaignSend[];
|
|
483
|
+
rollup: CampaignRollup;
|
|
484
|
+
windowState: CampaignWindowState;
|
|
485
|
+
/** For ordering: the campaign's start, else its most recent send. */
|
|
486
|
+
atMs: number | null;
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* The campaigns table's rows: every container, plus every send that belongs
|
|
490
|
+
* to none.
|
|
491
|
+
*
|
|
492
|
+
* The second half is what makes the container additive. A merchant's history
|
|
493
|
+
* is the sends they already have; a list that showed only containers would
|
|
494
|
+
* read as an empty product on the day this shipped, and a backfill that
|
|
495
|
+
* adopted each old send into a container of one would rewrite documents whose
|
|
496
|
+
* ids are cited by mail already delivered. Adopting them AT READ TIME costs a
|
|
497
|
+
* pass over a list already in memory and rewrites nothing.
|
|
498
|
+
*
|
|
499
|
+
* Newest first, on the start date where there is one and the last send
|
|
500
|
+
* otherwise. Rows with no date at all sort last: they are campaigns nobody
|
|
501
|
+
* has scheduled or sent, and dating them from nothing would be an invention.
|
|
502
|
+
*/
|
|
503
|
+
export declare function campaignListRows(campaigns: EmailCampaign[], sends: CampaignSend[], nowMs: number): CampaignListRow[];
|