@aglyn/shared-ui-email-campaigns 1.0.0-beta.221 → 1.0.0-beta.223

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @aglyn/shared-ui-email-campaigns
2
2
 
3
- The email-campaign document model, owned by none of the plugins that read it.
3
+ The bulk-send reporting math, owned by none of the plugins that send in bulk.
4
4
 
5
5
  > Beta. Published from the Aglyn monorepo under the `beta` dist-tag; APIs can change between beta releases.
6
6
 
@@ -12,59 +12,38 @@ npm install @aglyn/shared-ui-email-campaigns@beta
12
12
 
13
13
  ## Why it is not in a plugin
14
14
 
15
- An email campaign is written by one plugin and reported by another. The
16
- Marketing plugin owns the `campaigns` console section; the Email plugin owns
17
- the message, template and list surfaces. Both are independently toggleable —
18
- `release_marketing` and `release_email` are separate flags in
19
- `libs/aglyn/src/lib/plugin-manager/enabled-plugins.ts` — so an organization
20
- can have either one without the other.
21
-
22
- A shape that lives inside one of them is therefore a shape the other reaches
23
- past a switch to get. Because every plugin compiles into the same console
24
- bundle, such an import resolves at runtime whether or not the owning plugin is
25
- enabled, and the plugin switchboard is bypassed silently rather than failing.
26
- Putting the shared half here makes both plugins peers of it: each depends on
27
- this library, neither depends on the other for a rate, a field name or a
28
- `<Figure>`.
29
-
30
- The boundary is enforced in
31
- `libs/plugins/marketing/src/lib/plugin-email-boundary.spec.ts`, which fails if
32
- Marketing re-acquires an import of anything this library owns from
33
- `@aglyn/plugins-email`.
15
+ Two plugins mail in bulk and report what their mail did: the Marketing plugin
16
+ sends campaigns, and the Outreach plugin runs one-to-one sequences. Each send
17
+ records the same counters — sent, delivered, opens, clicks, bounces — and each
18
+ report divides them. A rate taken in two places is a rate that drifts: one
19
+ reader divides opens by `sent`, the other by `delivered`, and the same mail
20
+ reads two ways. So the division lives here, once, and both plugins are its
21
+ peers; neither imports the other.
34
22
 
35
23
  ## The model only
36
24
 
37
25
  ```typescript
38
26
  // Pure. No React, no MUI — safe in a /server handler.
39
- import {
40
- campaignReport,
41
- CAMPAIGN_SEND_CONTAINER_FIELD,
42
- } from '@aglyn/shared-ui-email-campaigns/model'
27
+ import { sendRate, sendLinkReport, type SendStats } from '@aglyn/shared-ui-email-campaigns/model'
43
28
  ```
44
29
 
45
30
  The library renders nothing. The figure renderers live in
46
- `@aglyn/shared-ui-jsx`, and the control that files a record under a campaign
47
- is the core's generic `ContainerPicker` (`@aglyn/tenant-feature-instance`),
48
- handed the `campaign` kind the Marketing plugin declares.
31
+ `@aglyn/shared-ui-jsx`.
49
32
 
50
33
  ## What is here
51
34
 
52
- | Module | Answers |
53
- | --------------------------- | --------------------------------------------------------------------------------- |
54
- | `model/campaign-container` | The campaign's window, its lists, its sends, and the rollup across them |
55
- | `model/campaign-report` | The rate math, the population each rate describes, and the link rollup |
56
- | `model/email-record` | One message: its state, its audience, and when it went out |
35
+ | Module | Answers |
36
+ | ------------------- | ----------------------------------------------------------------------------------------- |
37
+ | `model/send-report` | A send's stored counters, a rate with its denominator named, and the per-link click rollup |
57
38
 
58
39
  ## What is not here
59
40
 
60
- What a campaign caused and earned — the conversions it is credited with and
61
- its revenue per currency. Only the Marketing plugin reads them, so they are
62
- its model (`libs/plugins/marketing/src/lib/model/`), not a shape two plugins
63
- share.
64
-
65
- Sending. The send loop, the composer, the topic subscriptions and the
66
- send-time API all stay in `@aglyn/plugins-email`: they are behavior that the
67
- Email plugin's switch is supposed to govern, not shapes that two plugins share.
41
+ A campaign's own model (AGL-3080): its container and window, a send's report
42
+ and the populations it measured, a message's record, what a design's sends
43
+ did, and what a campaign caused and earned. Only the Marketing plugin reads
44
+ them, so they are its model (`libs/plugins/marketing/src/lib/model/`), not a
45
+ shape two plugins share. The Email plugin's template page shows what a
46
+ design's sends did through a zone the Marketing plugin fills.
68
47
 
69
48
  ## License
70
49
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/shared-ui-email-campaigns",
3
- "version": "1.0.0-beta.221",
3
+ "version": "1.0.0-beta.223",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -29,8 +29,8 @@
29
29
  "./package.json": "./package.json"
30
30
  },
31
31
  "dependencies": {
32
- "@aglyn/shared-ui-jsx": "1.0.0-beta.221",
33
- "@aglyn/shared-util-email": "1.0.0-beta.221",
32
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.223",
33
+ "@aglyn/shared-util-email": "1.0.0-beta.223",
34
34
  "@swc/helpers": "0.5.23"
35
35
  },
36
36
  "peerDependencies": {
@@ -15,24 +15,14 @@
15
15
  * limitations under the License.
16
16
  */
17
17
  /**
18
- * The email-campaign document model: the stored shapes and the pure
19
- * arithmetic over them.
18
+ * The bulk-send reporting math: a send's stored counters, the rate a figure
19
+ * is taken as (always with its denominator named), and the link rollup.
20
20
  *
21
- * Nothing here imports React or MUI, so a server handler that needs a field
22
- * name or a rate can take this barrel without dragging a component graph
23
- * into its bundle. The renderers live one directory over, behind
21
+ * Generic to every plugin that mails in bulk — the campaign sender, the
22
+ * sequence runner — and owned by none of them (AGL-3080). Nothing here
23
+ * imports React or MUI, so a server handler that needs a rate can take this
24
+ * barrel without dragging a component graph into its bundle. The renderers
25
+ * live one directory over, behind
24
26
  * `@aglyn/shared-ui-jsx/components/measured-figures.component`.
25
27
  */
26
- /**
27
- * Campaign reporting: the rate math, the populations the send recorded, and
28
- * the link rollup — pure, so every denominator is named once and provable
29
- * rather than chosen in JSX.
30
- */
31
- export * from './campaign-report';
32
- /** Reading one message record — its state, and when it went out. */
33
- export * from './email-record';
34
- /**
35
- * The campaign CONTAINER — its window, its lists, and the arithmetic that
36
- * rolls its sends into one set of figures.
37
- */
38
- export * from './campaign-container';
28
+ export * from './send-report';
@@ -14,22 +14,15 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */ /**
17
- * The email-campaign document model: the stored shapes and the pure
18
- * arithmetic over them.
17
+ * The bulk-send reporting math: a send's stored counters, the rate a figure
18
+ * is taken as (always with its denominator named), and the link rollup.
19
19
  *
20
- * Nothing here imports React or MUI, so a server handler that needs a field
21
- * name or a rate can take this barrel without dragging a component graph
22
- * into its bundle. The renderers live one directory over, behind
20
+ * Generic to every plugin that mails in bulk — the campaign sender, the
21
+ * sequence runner — and owned by none of them (AGL-3080). Nothing here
22
+ * imports React or MUI, so a server handler that needs a rate can take this
23
+ * barrel without dragging a component graph into its bundle. The renderers
24
+ * live one directory over, behind
23
25
  * `@aglyn/shared-ui-jsx/components/measured-figures.component`.
24
- */ /**
25
- * Campaign reporting: the rate math, the populations the send recorded, and
26
- * the link rollup — pure, so every denominator is named once and provable
27
- * rather than chosen in JSX.
28
- */ export * from "./campaign-report.js";
29
- /** Reading one message record — its state, and when it went out. */ export * from "./email-record.js";
30
- /**
31
- * The campaign CONTAINER — its window, its lists, and the arithmetic that
32
- * rolls its sends into one set of figures.
33
- */ export * from "./campaign-container.js";
26
+ */ export * from "./send-report.js";
34
27
 
35
28
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/shared/ui/email-campaigns/src/lib/model/index.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 * The email-campaign document model: the stored shapes and the pure\n * arithmetic over them.\n *\n * Nothing here imports React or MUI, so a server handler that needs a field\n * name or a rate can take this barrel without dragging a component graph\n * into its bundle. The renderers live one directory over, behind\n * `@aglyn/shared-ui-jsx/components/measured-figures.component`.\n */\n\n/**\n * Campaign reporting: the rate math, the populations the send recorded, and\n * the link rollup — pure, so every denominator is named once and provable\n * rather than chosen in JSX.\n */\nexport * from './campaign-report'\n\n/** Reading one message record — its state, and when it went out. */\nexport * from './email-record'\n\n/**\n * The campaign CONTAINER — its window, its lists, and the arithmetic that\n * rolls its sends into one set of figures.\n */\nexport * from './campaign-container'\n"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;CAQC,GAED;;;;CAIC,GACD,cAAc,uBAAmB;AAEjC,kEAAkE,GAClE,cAAc,oBAAgB;AAE9B;;;CAGC,GACD,cAAc,0BAAsB"}
1
+ {"version":3,"sources":["../../../../../../../../libs/shared/ui/email-campaigns/src/lib/model/index.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 * The bulk-send reporting math: a send's stored counters, the rate a figure\n * is taken as (always with its denominator named), and the link rollup.\n *\n * Generic to every plugin that mails in bulk — the campaign sender, the\n * sequence runner — and owned by none of them (AGL-3080). Nothing here\n * imports React or MUI, so a server handler that needs a rate can take this\n * barrel without dragging a component graph into its bundle. The renderers\n * live one directory over, behind\n * `@aglyn/shared-ui-jsx/components/measured-figures.component`.\n */\nexport * from './send-report'\n"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;CAUC,GACD,cAAc,mBAAe"}
@@ -15,11 +15,17 @@
15
15
  * limitations under the License.
16
16
  */
17
17
  /**
18
- * CAMPAIGN REPORTING MATH — the only place a rate is computed.
18
+ * BULK SEND REPORTING MATH — the only place a rate is computed.
19
+ *
20
+ * Owned by none of the plugins that read it: the plugin that sends a
21
+ * campaign, the one that keeps the email templates a send is built from and
22
+ * the one that runs one-to-one sequences all report what their mail did, and
23
+ * each divides the same counters a send records. Putting the division here
24
+ * makes them peers of one rule rather than three copies of it.
19
25
  *
20
26
  * ## Why a pure module and not a component
21
27
  *
22
- * Every number on the report screen is a division, and a division is where
28
+ * Every number on a report screen is a division, and a division is where
23
29
  * email reporting goes wrong. Putting the arithmetic in JSX means the
24
30
  * denominator is chosen by whoever writes the next card, in a file nobody
25
31
  * tests for arithmetic; putting it here means each rate is named once,
@@ -32,47 +38,47 @@
32
38
  * rate over `delivered` are different numbers with the same label, and the
33
39
  * gap between them is exactly the mail that bounced. The industry convention
34
40
  * is over `delivered`, and a report that quietly used `sent` would read
35
- * higher than the same campaign measured anywhere else.
41
+ * higher than the same send measured anywhere else.
36
42
  *
37
- * So {@link CampaignRate} carries `denominatorLabel`, and the screen is
38
- * required to render it. There is no overload that omits it.
43
+ * So {@link SendRate} carries `denominatorLabel`, and the screen is required
44
+ * to render it. There is no overload that omits it.
39
45
  *
40
46
  * ## Why some rates are deliberately absent
41
47
  *
42
- * {@link campaignRate} answers `null`, not zero, when it cannot divide:
48
+ * {@link sendRate} answers `null`, not zero, when it cannot divide:
43
49
  *
44
50
  * - **A zero denominator.** 0 opens out of 0 delivered is not a 0% open
45
51
  * rate, it is no open rate. Rendering 0% invites the reader to compare it
46
- * with a campaign that really did fail.
52
+ * with a send that really did fail.
47
53
  * - **An UNKNOWN denominator.** `delivered` is counted by the delivery
48
- * webhook, which was connected after some campaigns were sent. A campaign
49
- * with 400 sends and no delivery events has an unknown denominator, not a
54
+ * webhook, which was connected after some sends went out. A send with 400
55
+ * messages and no delivery events has an unknown denominator, not a
50
56
  * denominator of zero — and dividing by `sent` instead is precisely the
51
57
  * flattering substitution above.
52
58
  *
53
59
  * ## The structural-zero window
54
60
  *
55
61
  * Click tracking rewrites links in the HTML part. Sends that carried no HTML
56
- * part were therefore untrackable, and every one of those campaigns reports 0
57
- * clicks whatever the recipients actually did — a real 0 and a structural 0
62
+ * part were therefore untrackable, and every one of them reports 0 clicks
63
+ * whatever the recipients actually did — a real 0 and a structural 0
58
64
  * rendered identically. `send-email.ts` now synthesises an HTML part for a
59
- * text-only send, so every send after that carries one, and
60
- * `campaign-send.ts` records {@link CampaignStats.clickTracked} to say so.
61
- *
62
- * A campaign with no such marker predates the record. Its click COUNT is
63
- * still shown — it is a real count of real events — but no click RATE is
64
- * computed from it, because a rate presents the number as a measurement of
65
- * the audience and for those campaigns it is a measurement of the sender.
65
+ * text-only send, so every send after that carries one, and the sender
66
+ * records {@link SendStats.clickTracked} to say so. A send with no such
67
+ * marker predates the record: its click COUNT is still shown — it is a real
68
+ * count of real events — but no click RATE is computed from it, because a
69
+ * rate presents the number as a measurement of the audience and for those
70
+ * sends it is a measurement of the sender.
66
71
  */
67
72
  /**
68
- * The `stats` map on `hosts/{hostId}/campaigns/{campaignId}`.
73
+ * The `stats` map one bulk send keeps — a campaign send's on
74
+ * `orgs/{orgId}/campaigns/{sendId}`.
69
75
  *
70
76
  * Every field is optional and every reader defaults it, because these are
71
77
  * written by three different writers at three different times — the send, the
72
78
  * delivery webhook, the unsubscribe handler — and a campaign is a legitimate,
73
79
  * readable document from the instant the first of them lands.
74
80
  */
75
- export interface CampaignStats {
81
+ export interface SendStats {
76
82
  /** The whole audience the send was taken from, before the per-send cap. */
77
83
  audienceSize?: number;
78
84
  /** `audienceSize` is a FLOOR: audience resolution hit its read ceiling. */
@@ -139,7 +145,7 @@ export interface CampaignStats {
139
145
  * numbers called "open rate" distinguishable, and the screen renders it
140
146
  * beside the percentage for that reason.
141
147
  */
142
- export interface CampaignRate {
148
+ export interface SendRate {
143
149
  /** 0–1. Multiply for display; the model never formats. */
144
150
  value: number;
145
151
  numerator: number;
@@ -153,98 +159,16 @@ export interface CampaignRate {
153
159
  * `null` on a zero or unknown denominator — see the module header for why
154
160
  * that is not the same as 0%.
155
161
  */
156
- export declare function campaignRate(numerator: number | undefined, denominator: number | undefined, denominatorLabel: string): CampaignRate | null;
157
- /** Why a number the report would otherwise show is being withheld. */
158
- export interface CampaignCaveat {
159
- /** Stable id, so a spec can assert on the caveat rather than its prose. */
160
- id: 'delivery-unrecorded' | 'click-tracking-unrecorded' | 'audience-truncated' | 'send-deferred' | 'revenue-denominator-unrecorded' | 'revenue-multi-currency' | 'revenue-mid-flight' | 'revenue-mixed-model' | 'conversions-kinds-overlap' | 'conversions-web-not-rolled-up' | 'conversions-unattributed-is-a-ceiling' | 'conversions-total-crosses-hosts' | 'sequences-funnel-not-summed';
161
- message: string;
162
- }
163
- /** One population the send measured, for the audience breakdown. */
164
- export interface CampaignPopulation {
165
- id: string;
166
- label: string;
167
- count: number;
168
- /** What this count is a part OF, named. */
169
- ofLabel: string;
170
- of: number;
171
- }
172
- /** Everything the report screen renders, decided here rather than in JSX. */
173
- export interface CampaignReport {
174
- sent: number;
175
- recipients: number;
176
- delivered: number | null;
177
- opens: number;
178
- clicks: number;
179
- uniqueOpens: number | null;
180
- uniqueClicks: number | null;
181
- bounced: number;
182
- complained: number;
183
- unsubscribes: number;
184
- /** Rates, each `null` when its denominator is zero or unrecorded. */
185
- rates: {
186
- /** Accepted by the receiving server, over what the provider accepted. */
187
- delivery: CampaignRate | null;
188
- /** Distinct readers who opened, over delivered. */
189
- open: CampaignRate | null;
190
- /** Distinct readers who clicked, over delivered. */
191
- click: CampaignRate | null;
192
- /**
193
- * Distinct clickers over distinct OPENERS — a different question from
194
- * `click`, and the one the two get confused for. It answers "of the
195
- * people who read it, how many acted", not "of the people who received
196
- * it". Reported separately and labelled separately, never as "click
197
- * rate".
198
- */
199
- clickToOpen: CampaignRate | null;
200
- /** Bounced over what the provider accepted. */
201
- bounce: CampaignRate | null;
202
- /** Complaints over delivered — the number mailbox providers judge on. */
203
- complaint: CampaignRate | null;
204
- /** Unsubscribes through this campaign's link, over delivered. */
205
- unsubscribe: CampaignRate | null;
206
- };
207
- populations: CampaignPopulation[];
208
- caveats: CampaignCaveat[];
209
- }
210
- /**
211
- * Turns a stored `stats` map into the report.
212
- *
213
- * ## The denominator decisions, in one place
214
- *
215
- * - **`delivered`** carries the engagement rates — open, click, complaint,
216
- * unsubscribe. Mail that bounced was never in front of a human, so
217
- * including it in the denominator of an open rate depresses a number that
218
- * describes the audience with a fact about the address list. This is also
219
- * the convention every other tool reports, which matters: a merchant
220
- * comparing our figure with their previous ESP's must be comparing the same
221
- * quantity.
222
- * - **`sent`** carries the delivery and bounce rates, because those describe
223
- * what happened to what we handed the provider, and `delivered` is the
224
- * numerator of one of them — a rate cannot be over itself.
225
- * - **`uniqueOpens`** is the open-rate numerator, not `opens`. Open EVENTS
226
- * over recipients can exceed 100% the moment one person opens twice, and a
227
- * percentage above 100 is how a reader learns the number means something
228
- * other than what it says. Both are shown; only the distinct count is
229
- * divided.
230
- *
231
- * ## `delivered` unknown vs. zero
232
- *
233
- * A campaign predating the delivery webhook records no `delivered` at all.
234
- * That is reported as `null` and every rate over it is withheld, with a
235
- * caveat naming the reason — rather than substituting `sent`, which would
236
- * silently publish the flattered number this module exists to refuse.
237
- */
238
- export declare function campaignReport(stats: CampaignStats | undefined): CampaignReport;
162
+ export declare function sendRate(numerator: number | undefined, denominator: number | undefined, denominatorLabel: string): SendRate | null;
239
163
  /**
240
164
  * How many distinct destinations one campaign's rollup keeps.
241
165
  *
242
166
  * A CAP rather than a page size, and it exists because the map lives in a
243
167
  * single document with a 1 MiB ceiling. Clicks past the cap are counted in
244
- * {@link CampaignLinkRollup.overflowClicks} rather than dropped, so the
168
+ * {@link SendLinkRollup.overflowClicks} rather than dropped, so the
245
169
  * table's total still reconciles with `stats.clicks`.
246
170
  */
247
- export declare const CAMPAIGN_LINK_ROLLUP_MAX = 50;
171
+ export declare const SEND_LINK_ROLLUP_MAX = 50;
248
172
  /**
249
173
  * Reduces a clicked URL to the key the rollup counts under.
250
174
  *
@@ -269,29 +193,29 @@ export declare const CAMPAIGN_LINK_ROLLUP_MAX = 50;
269
193
  * @returns the normalized URL, or `null` for anything unparseable or not
270
194
  * http(s). A rollup key must be a URL a merchant recognises.
271
195
  */
272
- export declare function campaignLinkKey(link: string | null | undefined): string | null;
196
+ export declare function sendLinkKey(link: string | null | undefined): string | null;
273
197
  /** One destination in the rollup. */
274
- export interface CampaignLinkRow {
198
+ export interface SendLinkRow {
275
199
  url: string;
276
200
  /** Click EVENTS on this destination. One reader clicking twice counts two. */
277
201
  clicks: number;
278
202
  /** Share of the campaign's counted link clicks. */
279
- share: CampaignRate | null;
203
+ share: SendRate | null;
280
204
  }
281
205
  /** The stored shape of `campaigns/{campaignId}/reports/links`. */
282
- export interface CampaignLinkRollup {
206
+ export interface SendLinkRollup {
283
207
  links?: Record<string, {
284
208
  url?: string;
285
209
  clicks?: number;
286
210
  }>;
287
- /** Clicks on destinations past {@link CAMPAIGN_LINK_ROLLUP_MAX}. */
211
+ /** Clicks on destinations past {@link SEND_LINK_ROLLUP_MAX}. */
288
212
  overflowClicks?: number;
289
213
  /** Click events that arrived carrying no destination at all. */
290
214
  unattributedClicks?: number;
291
215
  }
292
216
  /** What the link table renders. */
293
- export interface CampaignLinkReport {
294
- rows: CampaignLinkRow[];
217
+ export interface SendLinkReport {
218
+ rows: SendLinkRow[];
295
219
  /** Clicks counted against a named destination — the table's total. */
296
220
  attributedClicks: number;
297
221
  overflowClicks: number;
@@ -308,53 +232,4 @@ export interface CampaignLinkReport {
308
232
  * that are not on screen is the kind of arithmetic a reader cannot check.
309
233
  * Both excluded figures are returned so the screen can state them.
310
234
  */
311
- export declare function campaignLinkReport(rollup: CampaignLinkRollup | undefined): CampaignLinkReport;
312
- /**
313
- * The outcomes the runtime credits, in funnel order. An array first, because
314
- * the ORDER is the reading order and a second list is a second chance to
315
- * leave one out; the union is derived from it.
316
- */
317
- export declare const CAMPAIGN_SEQUENCE_OUTCOMES: readonly ["enrolled", "sent", "replied", "meetings", "converted"];
318
- export type CampaignSequenceOutcome = (typeof CAMPAIGN_SEQUENCE_OUTCOMES)[number];
319
- /**
320
- * The org collection holding one document per campaign container,
321
- * `orgs/{orgId}/campaignSequenceReports/{campaignId}`.
322
- */
323
- export declare const CAMPAIGN_SEQUENCE_REPORTS_COLLECTION = "campaignSequenceReports";
324
- /** What a reader calls each outcome, and what the count means. */
325
- export declare const CAMPAIGN_SEQUENCE_OUTCOME_COPY: Readonly<Record<CampaignSequenceOutcome, {
326
- label: string;
327
- note: string;
328
- }>>;
329
- /**
330
- * The stored shape of `campaignSequenceReports/{campaignId}`.
331
- *
332
- * Read-side only, every field optional: another plugin's runtime wrote it,
333
- * and a reader that assumed a field was present would throw on the first
334
- * document written before the field existed.
335
- */
336
- export interface CampaignSequencesRollup {
337
- byOutcome?: Partial<Record<CampaignSequenceOutcome, number>>;
338
- /** When the runtime last credited anything, epoch ms. */
339
- updatedAtMs?: number;
340
- }
341
- /** One outcome's figure on screen. */
342
- export interface CampaignSequenceFigure {
343
- outcome: CampaignSequenceOutcome;
344
- label: string;
345
- /** The count, or `null` when the rollup holds no entry for the outcome. */
346
- value: number | null;
347
- note: string;
348
- }
349
- /** Everything the Sequences section renders. */
350
- export interface CampaignSequencesReport {
351
- /** Always all five, always in {@link CAMPAIGN_SEQUENCE_OUTCOMES} order. */
352
- figures: CampaignSequenceFigure[];
353
- /** Whether the rollup document exists at all — see the block header. */
354
- recorded: boolean;
355
- /** At least one outcome holds a figure. */
356
- any: boolean;
357
- caveats: CampaignCaveat[];
358
- }
359
- /** Turns the stored rollup into the Sequences section. */
360
- export declare function campaignSequencesReport(rollup: CampaignSequencesRollup | undefined): CampaignSequencesReport;
235
+ export declare function sendLinkReport(rollup: SendLinkRollup | undefined): SendLinkReport;