@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.
- package/LICENSE +201 -0
- package/README.md +73 -0
- package/package.json +39 -0
- package/src/index.d.ts +17 -0
- package/src/index.js +23 -0
- package/src/index.js.map +1 -0
- package/src/lib/components/campaign-picker.component.d.ts +54 -0
- package/src/lib/components/campaign-picker.component.js +121 -0
- package/src/lib/components/campaign-picker.component.js.map +1 -0
- package/src/lib/components/report-figures.d.ts +51 -0
- package/src/lib/components/report-figures.js +120 -0
- package/src/lib/components/report-figures.js.map +1 -0
- package/src/lib/model/campaign-container.d.ts +359 -0
- package/src/lib/model/campaign-container.js +355 -0
- package/src/lib/model/campaign-container.js.map +1 -0
- package/src/lib/model/campaign-conversions.d.ts +286 -0
- package/src/lib/model/campaign-conversions.js +249 -0
- package/src/lib/model/campaign-conversions.js.map +1 -0
- package/src/lib/model/campaign-report.d.ts +304 -0
- package/src/lib/model/campaign-report.js +326 -0
- package/src/lib/model/campaign-report.js.map +1 -0
- package/src/lib/model/campaign-revenue.d.ts +327 -0
- package/src/lib/model/campaign-revenue.js +332 -0
- package/src/lib/model/campaign-revenue.js.map +1 -0
- package/src/lib/model/campaign-send-time.d.ts +74 -0
- package/src/lib/model/campaign-send-time.js +120 -0
- package/src/lib/model/campaign-send-time.js.map +1 -0
- package/src/lib/model/email-record.d.ts +175 -0
- package/src/lib/model/email-record.js +198 -0
- package/src/lib/model/email-record.js.map +1 -0
- package/src/lib/model/index.d.ts +53 -0
- package/src/lib/model/index.js +48 -0
- package/src/lib/model/index.js.map +1 -0
|
@@ -0,0 +1,175 @@
|
|
|
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
|
+
* READING ONE MESSAGE RECORD.
|
|
19
|
+
*
|
|
20
|
+
* A message — an email that was or will be sent — is a document under
|
|
21
|
+
* `hosts/{hostId}/campaigns`. Two facts about that document are awkward
|
|
22
|
+
* enough that every surface reading it gets them wrong the same way, so they
|
|
23
|
+
* are decided here once.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* When a message went out, or is due to.
|
|
27
|
+
*
|
|
28
|
+
* **There is no one SEND date field.** The send path writes `{status:'sent',
|
|
29
|
+
* sentAt}` from one branch and `{status:'scheduled', sendAtMs}` from another.
|
|
30
|
+
* So a list ordered on either field in Firestore does not mis-sort —
|
|
31
|
+
* `orderBy` DROPS every document missing the ordered field — and half the
|
|
32
|
+
* messages simply vanish. Sorting on this instead keeps both kinds in one
|
|
33
|
+
* ordered list.
|
|
34
|
+
*
|
|
35
|
+
* `sentAt` is a Firestore `Timestamp`, which the client SDK gives as an
|
|
36
|
+
* object with `seconds` and `toMillis()`; a document read from cache while a
|
|
37
|
+
* write is pending can carry a sentinel with neither, which reads as 0 and
|
|
38
|
+
* sorts last rather than throwing.
|
|
39
|
+
*
|
|
40
|
+
* A DRAFT correctly answers 0 here: it has no send time, and inventing one
|
|
41
|
+
* from its creation would put an unsent message on the timeline of mail that
|
|
42
|
+
* went out. Ordering a list is {@link emailListTimeMs}'s job.
|
|
43
|
+
*
|
|
44
|
+
* @returns epoch ms, or 0 when the message has no time at all.
|
|
45
|
+
*/
|
|
46
|
+
export declare function emailSendTimeMs(record: Record<string, any> | null | undefined): number;
|
|
47
|
+
/**
|
|
48
|
+
* The field every writer of a message stamps when it MINTS the document.
|
|
49
|
+
*
|
|
50
|
+
* The one date on every message, which is what neither `sentAt` nor
|
|
51
|
+
* `sendAtMs` is — each is written by one branch of the send path and neither
|
|
52
|
+
* is on both kinds. A field carried by every record is the precondition for
|
|
53
|
+
* ordering this collection in Firestore rather than in the browser, because
|
|
54
|
+
* `orderBy` drops documents that lack the ordered field instead of
|
|
55
|
+
* mis-sorting them.
|
|
56
|
+
*
|
|
57
|
+
* Epoch milliseconds rather than a `Timestamp`, matching `sendAtMs` beside
|
|
58
|
+
* it: the two are compared against each other on every list that draws
|
|
59
|
+
* drafts and sends together, and a comparison that has to unwrap one side
|
|
60
|
+
* first is a comparison somebody writes wrong.
|
|
61
|
+
*/
|
|
62
|
+
export declare const EMAIL_CREATED_AT_FIELD = "createdAtMs";
|
|
63
|
+
/**
|
|
64
|
+
* When a message was created, for the records that carry it.
|
|
65
|
+
*
|
|
66
|
+
* `null` — not 0 — for a record carrying no {@link EMAIL_CREATED_AT_FIELD},
|
|
67
|
+
* because 0 is a real instant at the far end of the sort and "we do not know"
|
|
68
|
+
* is not "1970". A null orders by the record's send time instead, so the list
|
|
69
|
+
* is correct for a record from any era.
|
|
70
|
+
*/
|
|
71
|
+
export declare function emailCreatedAtMs(record: Record<string, any> | null | undefined): number | null;
|
|
72
|
+
/**
|
|
73
|
+
* WHERE A MESSAGE SITS IN A LIST OF MESSAGES.
|
|
74
|
+
*
|
|
75
|
+
* Its send time where it has one, and its creation time where it does not.
|
|
76
|
+
*
|
|
77
|
+
* A draft has neither `sentAt` nor `sendAtMs`, so ordering a list on the send
|
|
78
|
+
* time alone gives every draft the key 0 and files it below mail sent years
|
|
79
|
+
* ago — the newest thing a merchant did, at the bottom of the page, behind
|
|
80
|
+
* whatever paging the list has. Falling back to creation puts it where the
|
|
81
|
+
* merchant left it.
|
|
82
|
+
*
|
|
83
|
+
* The fallback is deliberately not the other way round. A SENT message orders
|
|
84
|
+
* by when it went out, never by when it was drafted: the list is a record of
|
|
85
|
+
* what happened, and a message drafted in March and sent in June belongs in
|
|
86
|
+
* June.
|
|
87
|
+
*
|
|
88
|
+
* @returns epoch ms, or 0 for a record with no time of any kind — a draft
|
|
89
|
+
* written before the created stamp existed, which sorts last exactly as it
|
|
90
|
+
* did before.
|
|
91
|
+
*/
|
|
92
|
+
export declare function emailListTimeMs(record: Record<string, any> | null | undefined): number;
|
|
93
|
+
/**
|
|
94
|
+
* Reader-facing text for a message's state.
|
|
95
|
+
*
|
|
96
|
+
* The KEYS are persisted values written by the send path and read by the
|
|
97
|
+
* scheduled-campaign processor — nothing here may change them, including
|
|
98
|
+
* `canceled`, which is already the American spelling and is a stored enum in
|
|
99
|
+
* either case. Only the right-hand side is display text, and an unrecognized
|
|
100
|
+
* state falls through as itself rather than being flattened into one of
|
|
101
|
+
* these: a state this list cannot name is worth seeing.
|
|
102
|
+
*
|
|
103
|
+
* `draft` is an email that has been created and not sent. `sending` is the
|
|
104
|
+
* claim the scheduled processor and the send-now route both take before they
|
|
105
|
+
* mail, and it is named here because a merchant who reloads mid-send would
|
|
106
|
+
* otherwise be shown the raw token.
|
|
107
|
+
*
|
|
108
|
+
* ⚠️ **Not what a console row should draw.** This names the STORED value, and
|
|
109
|
+
* an email delivering an audience larger than one batch is stored as
|
|
110
|
+
* `scheduled` between runs — so a row rendering this says "Scheduled" about a
|
|
111
|
+
* send that has already put five hundred messages in five hundred inboxes.
|
|
112
|
+
* `campaignSendDisplay` in `campaign-container.ts` reads the counters beside
|
|
113
|
+
* the status and is what every surface here draws. This remains the honest
|
|
114
|
+
* answer to the narrower question of what one stored token means.
|
|
115
|
+
*/
|
|
116
|
+
export declare function emailStateLabel(status: unknown): string;
|
|
117
|
+
/**
|
|
118
|
+
* Whether an email has yet been mailed to anybody.
|
|
119
|
+
*
|
|
120
|
+
* The surfaces that report on a send need this because an unsent email has no
|
|
121
|
+
* `stats` at all, and a report that divides into an absent denominator
|
|
122
|
+
* presents "not sent yet" as a delivery rate of 0% — the same class of fault
|
|
123
|
+
* as a rate rendered over a denominator it does not name.
|
|
124
|
+
*/
|
|
125
|
+
export declare function emailIsUnsent(record: Record<string, any> | null | undefined): boolean;
|
|
126
|
+
/**
|
|
127
|
+
* Reader-facing names for the built-in audience kinds.
|
|
128
|
+
*
|
|
129
|
+
* The KEYS are the persisted `audience` values written on every message since
|
|
130
|
+
* the composer shipped; only the values on the right are display text. A
|
|
131
|
+
* `list` audience is deliberately absent — it is not a kind of audience, it
|
|
132
|
+
* is a NAMED one, and naming it is {@link emailAudienceLabel}'s job.
|
|
133
|
+
*/
|
|
134
|
+
export declare const EMAIL_AUDIENCE_LABELS: Record<string, string>;
|
|
135
|
+
/**
|
|
136
|
+
* Which audience a message went to, named for a reader.
|
|
137
|
+
*
|
|
138
|
+
* A list send is named by the name the SEND recorded, never by the name the
|
|
139
|
+
* list carries today: resolving it now would let a rename rewrite the history
|
|
140
|
+
* of a message that went out months ago, and a deletion erase it. A send that
|
|
141
|
+
* recorded no name says so instead of printing a document id, which is not
|
|
142
|
+
* something a merchant recognises as a list.
|
|
143
|
+
*/
|
|
144
|
+
export declare function emailAudienceLabel(record: Record<string, any> | null | undefined): string;
|
|
145
|
+
/**
|
|
146
|
+
* The sender a message actually left with, as this record has it.
|
|
147
|
+
*
|
|
148
|
+
* `recorded` is the whole point of the shape. The address a send goes out on
|
|
149
|
+
* is stamped by the send — see `sentAsStamp` — and every message sent before
|
|
150
|
+
* that stamp existed carries nothing, so a surface has to be able to tell an
|
|
151
|
+
* unrecorded sender apart from one it could compose. Composing it is the
|
|
152
|
+
* thing that must not happen: a site's sending identity is a setting, and
|
|
153
|
+
* resolving it now would answer "what would this send as today" on a report
|
|
154
|
+
* whose subject is a message that went out months ago, under whatever address
|
|
155
|
+
* the site held then.
|
|
156
|
+
*
|
|
157
|
+
* The same rule {@link emailAudienceLabel} follows for a list name, and for
|
|
158
|
+
* the same reason: the report says what happened, not what would happen now.
|
|
159
|
+
*
|
|
160
|
+
* `replyTo` is null on a message that set none, which is not missing
|
|
161
|
+
* information — a message with no `Reply-To:` takes replies at its `from`,
|
|
162
|
+
* and that is a fact the record states by omission rather than one it lost.
|
|
163
|
+
* The difference only reads correctly when `recorded` is true, which is why
|
|
164
|
+
* a caller must consult that first.
|
|
165
|
+
*/
|
|
166
|
+
export interface EmailSentAs {
|
|
167
|
+
/** False on a message sent before the send began stamping its sender. */
|
|
168
|
+
recorded: boolean;
|
|
169
|
+
from: string | null;
|
|
170
|
+
/** The display name recipients saw, or null when the address stood alone. */
|
|
171
|
+
fromName: string | null;
|
|
172
|
+
/** Where replies were pointed, or null when they go to `from`. */
|
|
173
|
+
replyTo: string | null;
|
|
174
|
+
}
|
|
175
|
+
export declare function emailSentAs(record: Record<string, any> | null | undefined): EmailSentAs;
|
|
@@ -0,0 +1,198 @@
|
|
|
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
|
+
* READING ONE MESSAGE RECORD.
|
|
18
|
+
*
|
|
19
|
+
* A message — an email that was or will be sent — is a document under
|
|
20
|
+
* `hosts/{hostId}/campaigns`. Two facts about that document are awkward
|
|
21
|
+
* enough that every surface reading it gets them wrong the same way, so they
|
|
22
|
+
* are decided here once.
|
|
23
|
+
*/ /**
|
|
24
|
+
* When a message went out, or is due to.
|
|
25
|
+
*
|
|
26
|
+
* **There is no one SEND date field.** The send path writes `{status:'sent',
|
|
27
|
+
* sentAt}` from one branch and `{status:'scheduled', sendAtMs}` from another.
|
|
28
|
+
* So a list ordered on either field in Firestore does not mis-sort —
|
|
29
|
+
* `orderBy` DROPS every document missing the ordered field — and half the
|
|
30
|
+
* messages simply vanish. Sorting on this instead keeps both kinds in one
|
|
31
|
+
* ordered list.
|
|
32
|
+
*
|
|
33
|
+
* `sentAt` is a Firestore `Timestamp`, which the client SDK gives as an
|
|
34
|
+
* object with `seconds` and `toMillis()`; a document read from cache while a
|
|
35
|
+
* write is pending can carry a sentinel with neither, which reads as 0 and
|
|
36
|
+
* sorts last rather than throwing.
|
|
37
|
+
*
|
|
38
|
+
* A DRAFT correctly answers 0 here: it has no send time, and inventing one
|
|
39
|
+
* from its creation would put an unsent message on the timeline of mail that
|
|
40
|
+
* went out. Ordering a list is {@link emailListTimeMs}'s job.
|
|
41
|
+
*
|
|
42
|
+
* @returns epoch ms, or 0 when the message has no time at all.
|
|
43
|
+
*/ export function emailSendTimeMs(record) {
|
|
44
|
+
var _ref;
|
|
45
|
+
const sentAt = record == null ? void 0 : record['sentAt'];
|
|
46
|
+
if (typeof (sentAt == null ? void 0 : sentAt.toMillis) === 'function') return sentAt.toMillis();
|
|
47
|
+
if (typeof (sentAt == null ? void 0 : sentAt.seconds) === 'number') return sentAt.seconds * 1000;
|
|
48
|
+
const scheduled = Number((_ref = record == null ? void 0 : record['sendAtMs']) != null ? _ref : 0);
|
|
49
|
+
return Number.isFinite(scheduled) ? scheduled : 0;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The field every writer of a message stamps when it MINTS the document.
|
|
53
|
+
*
|
|
54
|
+
* The one date on every message, which is what neither `sentAt` nor
|
|
55
|
+
* `sendAtMs` is — each is written by one branch of the send path and neither
|
|
56
|
+
* is on both kinds. A field carried by every record is the precondition for
|
|
57
|
+
* ordering this collection in Firestore rather than in the browser, because
|
|
58
|
+
* `orderBy` drops documents that lack the ordered field instead of
|
|
59
|
+
* mis-sorting them.
|
|
60
|
+
*
|
|
61
|
+
* Epoch milliseconds rather than a `Timestamp`, matching `sendAtMs` beside
|
|
62
|
+
* it: the two are compared against each other on every list that draws
|
|
63
|
+
* drafts and sends together, and a comparison that has to unwrap one side
|
|
64
|
+
* first is a comparison somebody writes wrong.
|
|
65
|
+
*/ export const EMAIL_CREATED_AT_FIELD = 'createdAtMs';
|
|
66
|
+
/**
|
|
67
|
+
* When a message was created, for the records that carry it.
|
|
68
|
+
*
|
|
69
|
+
* `null` — not 0 — for a record carrying no {@link EMAIL_CREATED_AT_FIELD},
|
|
70
|
+
* because 0 is a real instant at the far end of the sort and "we do not know"
|
|
71
|
+
* is not "1970". A null orders by the record's send time instead, so the list
|
|
72
|
+
* is correct for a record from any era.
|
|
73
|
+
*/ export function emailCreatedAtMs(record) {
|
|
74
|
+
var _ref;
|
|
75
|
+
const created = Number((_ref = record == null ? void 0 : record[EMAIL_CREATED_AT_FIELD]) != null ? _ref : Number.NaN);
|
|
76
|
+
return Number.isFinite(created) && created > 0 ? created : null;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* WHERE A MESSAGE SITS IN A LIST OF MESSAGES.
|
|
80
|
+
*
|
|
81
|
+
* Its send time where it has one, and its creation time where it does not.
|
|
82
|
+
*
|
|
83
|
+
* A draft has neither `sentAt` nor `sendAtMs`, so ordering a list on the send
|
|
84
|
+
* time alone gives every draft the key 0 and files it below mail sent years
|
|
85
|
+
* ago — the newest thing a merchant did, at the bottom of the page, behind
|
|
86
|
+
* whatever paging the list has. Falling back to creation puts it where the
|
|
87
|
+
* merchant left it.
|
|
88
|
+
*
|
|
89
|
+
* The fallback is deliberately not the other way round. A SENT message orders
|
|
90
|
+
* by when it went out, never by when it was drafted: the list is a record of
|
|
91
|
+
* what happened, and a message drafted in March and sent in June belongs in
|
|
92
|
+
* June.
|
|
93
|
+
*
|
|
94
|
+
* @returns epoch ms, or 0 for a record with no time of any kind — a draft
|
|
95
|
+
* written before the created stamp existed, which sorts last exactly as it
|
|
96
|
+
* did before.
|
|
97
|
+
*/ export function emailListTimeMs(record) {
|
|
98
|
+
return emailSendTimeMs(record) || emailCreatedAtMs(record) || 0;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Reader-facing text for a message's state.
|
|
102
|
+
*
|
|
103
|
+
* The KEYS are persisted values written by the send path and read by the
|
|
104
|
+
* scheduled-campaign processor — nothing here may change them, including
|
|
105
|
+
* `canceled`, which is already the American spelling and is a stored enum in
|
|
106
|
+
* either case. Only the right-hand side is display text, and an unrecognized
|
|
107
|
+
* state falls through as itself rather than being flattened into one of
|
|
108
|
+
* these: a state this list cannot name is worth seeing.
|
|
109
|
+
*
|
|
110
|
+
* `draft` is an email that has been created and not sent. `sending` is the
|
|
111
|
+
* claim the scheduled processor and the send-now route both take before they
|
|
112
|
+
* mail, and it is named here because a merchant who reloads mid-send would
|
|
113
|
+
* otherwise be shown the raw token.
|
|
114
|
+
*
|
|
115
|
+
* ⚠️ **Not what a console row should draw.** This names the STORED value, and
|
|
116
|
+
* an email delivering an audience larger than one batch is stored as
|
|
117
|
+
* `scheduled` between runs — so a row rendering this says "Scheduled" about a
|
|
118
|
+
* send that has already put five hundred messages in five hundred inboxes.
|
|
119
|
+
* `campaignSendDisplay` in `campaign-container.ts` reads the counters beside
|
|
120
|
+
* the status and is what every surface here draws. This remains the honest
|
|
121
|
+
* answer to the narrower question of what one stored token means.
|
|
122
|
+
*/ export function emailStateLabel(status) {
|
|
123
|
+
const value = String(status != null ? status : '');
|
|
124
|
+
if (value === 'sent') return 'Sent';
|
|
125
|
+
if (value === 'scheduled') return 'Scheduled';
|
|
126
|
+
if (value === 'canceled') return 'Canceled';
|
|
127
|
+
if (value === 'draft') return 'Draft';
|
|
128
|
+
if (value === 'sending') return 'Sending';
|
|
129
|
+
return value || 'Unknown';
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Whether an email has yet been mailed to anybody.
|
|
133
|
+
*
|
|
134
|
+
* The surfaces that report on a send need this because an unsent email has no
|
|
135
|
+
* `stats` at all, and a report that divides into an absent denominator
|
|
136
|
+
* presents "not sent yet" as a delivery rate of 0% — the same class of fault
|
|
137
|
+
* as a rate rendered over a denominator it does not name.
|
|
138
|
+
*/ export function emailIsUnsent(record) {
|
|
139
|
+
var _ref;
|
|
140
|
+
const value = String((_ref = record == null ? void 0 : record['status']) != null ? _ref : '');
|
|
141
|
+
return value === 'draft' || value === 'scheduled' || value === 'sending';
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Reader-facing names for the built-in audience kinds.
|
|
145
|
+
*
|
|
146
|
+
* The KEYS are the persisted `audience` values written on every message since
|
|
147
|
+
* the composer shipped; only the values on the right are display text. A
|
|
148
|
+
* `list` audience is deliberately absent — it is not a kind of audience, it
|
|
149
|
+
* is a NAMED one, and naming it is {@link emailAudienceLabel}'s job.
|
|
150
|
+
*/ export const EMAIL_AUDIENCE_LABELS = {
|
|
151
|
+
leads: 'All leads',
|
|
152
|
+
members: 'All site members',
|
|
153
|
+
segment: 'A contact segment',
|
|
154
|
+
manual: 'Addresses typed into the composer'
|
|
155
|
+
};
|
|
156
|
+
/**
|
|
157
|
+
* Which audience a message went to, named for a reader.
|
|
158
|
+
*
|
|
159
|
+
* A list send is named by the name the SEND recorded, never by the name the
|
|
160
|
+
* list carries today: resolving it now would let a rename rewrite the history
|
|
161
|
+
* of a message that went out months ago, and a deletion erase it. A send that
|
|
162
|
+
* recorded no name says so instead of printing a document id, which is not
|
|
163
|
+
* something a merchant recognises as a list.
|
|
164
|
+
*/ export function emailAudienceLabel(record) {
|
|
165
|
+
var _ref, _ref1;
|
|
166
|
+
const audience = String((_ref = record == null ? void 0 : record['audience']) != null ? _ref : '');
|
|
167
|
+
if (audience !== 'list') {
|
|
168
|
+
var _ref2, _EMAIL_AUDIENCE_LABELS_audience;
|
|
169
|
+
return (_ref2 = (_EMAIL_AUDIENCE_LABELS_audience = EMAIL_AUDIENCE_LABELS[audience]) != null ? _EMAIL_AUDIENCE_LABELS_audience : audience) != null ? _ref2 : 'Not recorded';
|
|
170
|
+
}
|
|
171
|
+
const name = String((_ref1 = record == null ? void 0 : record['listName']) != null ? _ref1 : '');
|
|
172
|
+
if (name) return name;
|
|
173
|
+
return (record == null ? void 0 : record['listId']) ? 'A list this send did not name' : 'A list this send did not record';
|
|
174
|
+
}
|
|
175
|
+
export function emailSentAs(record) {
|
|
176
|
+
var _ref, _ref1, _ref2;
|
|
177
|
+
const stamp = record == null ? void 0 : record['sentAs'];
|
|
178
|
+
const from = String((_ref = stamp == null ? void 0 : stamp['from']) != null ? _ref : '').trim();
|
|
179
|
+
// The ADDRESS is what makes the stamp a record of a send. A `sentAs` map
|
|
180
|
+
// carrying only a display name is a partial write, and reporting it as
|
|
181
|
+
// recorded would put a name on screen beside an address nothing supplied.
|
|
182
|
+
if (!from) {
|
|
183
|
+
return {
|
|
184
|
+
recorded: false,
|
|
185
|
+
from: null,
|
|
186
|
+
fromName: null,
|
|
187
|
+
replyTo: null
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
recorded: true,
|
|
192
|
+
from,
|
|
193
|
+
fromName: String((_ref1 = stamp == null ? void 0 : stamp['fromName']) != null ? _ref1 : '').trim() || null,
|
|
194
|
+
replyTo: String((_ref2 = stamp == null ? void 0 : stamp['replyTo']) != null ? _ref2 : '').trim() || null
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
//# sourceMappingURL=email-record.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/shared/ui/email-campaigns/src/lib/model/email-record.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 * READING ONE MESSAGE RECORD.\n *\n * A message — an email that was or will be sent — is a document under\n * `hosts/{hostId}/campaigns`. Two facts about that document are awkward\n * enough that every surface reading it gets them wrong the same way, so they\n * are decided here once.\n */\n\n/**\n * When a message went out, or is due to.\n *\n * **There is no one SEND date field.** The send path writes `{status:'sent',\n * sentAt}` from one branch and `{status:'scheduled', sendAtMs}` from another.\n * So a list ordered on either field in Firestore does not mis-sort —\n * `orderBy` DROPS every document missing the ordered field — and half the\n * messages simply vanish. Sorting on this instead keeps both kinds in one\n * ordered list.\n *\n * `sentAt` is a Firestore `Timestamp`, which the client SDK gives as an\n * object with `seconds` and `toMillis()`; a document read from cache while a\n * write is pending can carry a sentinel with neither, which reads as 0 and\n * sorts last rather than throwing.\n *\n * A DRAFT correctly answers 0 here: it has no send time, and inventing one\n * from its creation would put an unsent message on the timeline of mail that\n * went out. Ordering a list is {@link emailListTimeMs}'s job.\n *\n * @returns epoch ms, or 0 when the message has no time at all.\n */\nexport function emailSendTimeMs(\n record: Record<string, any> | null | undefined,\n): number {\n const sentAt = record?.['sentAt'] as\n | { toMillis?: () => number; seconds?: number }\n | undefined\n if (typeof sentAt?.toMillis === 'function') return sentAt.toMillis()\n if (typeof sentAt?.seconds === 'number') return sentAt.seconds * 1000\n const scheduled = Number(record?.['sendAtMs'] ?? 0)\n return Number.isFinite(scheduled) ? scheduled : 0\n}\n\n/**\n * The field every writer of a message stamps when it MINTS the document.\n *\n * The one date on every message, which is what neither `sentAt` nor\n * `sendAtMs` is — each is written by one branch of the send path and neither\n * is on both kinds. A field carried by every record is the precondition for\n * ordering this collection in Firestore rather than in the browser, because\n * `orderBy` drops documents that lack the ordered field instead of\n * mis-sorting them.\n *\n * Epoch milliseconds rather than a `Timestamp`, matching `sendAtMs` beside\n * it: the two are compared against each other on every list that draws\n * drafts and sends together, and a comparison that has to unwrap one side\n * first is a comparison somebody writes wrong.\n */\nexport const EMAIL_CREATED_AT_FIELD = 'createdAtMs'\n\n/**\n * When a message was created, for the records that carry it.\n *\n * `null` — not 0 — for a record carrying no {@link EMAIL_CREATED_AT_FIELD},\n * because 0 is a real instant at the far end of the sort and \"we do not know\"\n * is not \"1970\". A null orders by the record's send time instead, so the list\n * is correct for a record from any era.\n */\nexport function emailCreatedAtMs(\n record: Record<string, any> | null | undefined,\n): number | null {\n const created = Number(record?.[EMAIL_CREATED_AT_FIELD] ?? Number.NaN)\n return Number.isFinite(created) && created > 0 ? created : null\n}\n\n/**\n * WHERE A MESSAGE SITS IN A LIST OF MESSAGES.\n *\n * Its send time where it has one, and its creation time where it does not.\n *\n * A draft has neither `sentAt` nor `sendAtMs`, so ordering a list on the send\n * time alone gives every draft the key 0 and files it below mail sent years\n * ago — the newest thing a merchant did, at the bottom of the page, behind\n * whatever paging the list has. Falling back to creation puts it where the\n * merchant left it.\n *\n * The fallback is deliberately not the other way round. A SENT message orders\n * by when it went out, never by when it was drafted: the list is a record of\n * what happened, and a message drafted in March and sent in June belongs in\n * June.\n *\n * @returns epoch ms, or 0 for a record with no time of any kind — a draft\n * written before the created stamp existed, which sorts last exactly as it\n * did before.\n */\nexport function emailListTimeMs(\n record: Record<string, any> | null | undefined,\n): number {\n return emailSendTimeMs(record) || emailCreatedAtMs(record) || 0\n}\n\n/**\n * Reader-facing text for a message's state.\n *\n * The KEYS are persisted values written by the send path and read by the\n * scheduled-campaign processor — nothing here may change them, including\n * `canceled`, which is already the American spelling and is a stored enum in\n * either case. Only the right-hand side is display text, and an unrecognized\n * state falls through as itself rather than being flattened into one of\n * these: a state this list cannot name is worth seeing.\n *\n * `draft` is an email that has been created and not sent. `sending` is the\n * claim the scheduled processor and the send-now route both take before they\n * mail, and it is named here because a merchant who reloads mid-send would\n * otherwise be shown the raw token.\n *\n * ⚠️ **Not what a console row should draw.** This names the STORED value, and\n * an email delivering an audience larger than one batch is stored as\n * `scheduled` between runs — so a row rendering this says \"Scheduled\" about a\n * send that has already put five hundred messages in five hundred inboxes.\n * `campaignSendDisplay` in `campaign-container.ts` reads the counters beside\n * the status and is what every surface here draws. This remains the honest\n * answer to the narrower question of what one stored token means.\n */\nexport function emailStateLabel(status: unknown): string {\n const value = String(status ?? '')\n if (value === 'sent') return 'Sent'\n if (value === 'scheduled') return 'Scheduled'\n if (value === 'canceled') return 'Canceled'\n if (value === 'draft') return 'Draft'\n if (value === 'sending') return 'Sending'\n return value || 'Unknown'\n}\n\n/**\n * Whether an email has yet been mailed to anybody.\n *\n * The surfaces that report on a send need this because an unsent email has no\n * `stats` at all, and a report that divides into an absent denominator\n * presents \"not sent yet\" as a delivery rate of 0% — the same class of fault\n * as a rate rendered over a denominator it does not name.\n */\nexport function emailIsUnsent(\n record: Record<string, any> | null | undefined,\n): boolean {\n const value = String(record?.['status'] ?? '')\n return value === 'draft' || value === 'scheduled' || value === 'sending'\n}\n\n/**\n * Reader-facing names for the built-in audience kinds.\n *\n * The KEYS are the persisted `audience` values written on every message since\n * the composer shipped; only the values on the right are display text. A\n * `list` audience is deliberately absent — it is not a kind of audience, it\n * is a NAMED one, and naming it is {@link emailAudienceLabel}'s job.\n */\nexport const EMAIL_AUDIENCE_LABELS: Record<string, string> = {\n leads: 'All leads',\n members: 'All site members',\n segment: 'A contact segment',\n manual: 'Addresses typed into the composer',\n}\n\n/**\n * Which audience a message went to, named for a reader.\n *\n * A list send is named by the name the SEND recorded, never by the name the\n * list carries today: resolving it now would let a rename rewrite the history\n * of a message that went out months ago, and a deletion erase it. A send that\n * recorded no name says so instead of printing a document id, which is not\n * something a merchant recognises as a list.\n */\nexport function emailAudienceLabel(\n record: Record<string, any> | null | undefined,\n): string {\n const audience = String(record?.['audience'] ?? '')\n if (audience !== 'list') {\n return EMAIL_AUDIENCE_LABELS[audience] ?? audience ?? 'Not recorded'\n }\n const name = String(record?.['listName'] ?? '')\n if (name) return name\n return record?.['listId']\n ? 'A list this send did not name'\n : 'A list this send did not record'\n}\n\n/**\n * The sender a message actually left with, as this record has it.\n *\n * `recorded` is the whole point of the shape. The address a send goes out on\n * is stamped by the send — see `sentAsStamp` — and every message sent before\n * that stamp existed carries nothing, so a surface has to be able to tell an\n * unrecorded sender apart from one it could compose. Composing it is the\n * thing that must not happen: a site's sending identity is a setting, and\n * resolving it now would answer \"what would this send as today\" on a report\n * whose subject is a message that went out months ago, under whatever address\n * the site held then.\n *\n * The same rule {@link emailAudienceLabel} follows for a list name, and for\n * the same reason: the report says what happened, not what would happen now.\n *\n * `replyTo` is null on a message that set none, which is not missing\n * information — a message with no `Reply-To:` takes replies at its `from`,\n * and that is a fact the record states by omission rather than one it lost.\n * The difference only reads correctly when `recorded` is true, which is why\n * a caller must consult that first.\n */\nexport interface EmailSentAs {\n /** False on a message sent before the send began stamping its sender. */\n recorded: boolean\n from: string | null\n /** The display name recipients saw, or null when the address stood alone. */\n fromName: string | null\n /** Where replies were pointed, or null when they go to `from`. */\n replyTo: string | null\n}\n\nexport function emailSentAs(\n record: Record<string, any> | null | undefined,\n): EmailSentAs {\n const stamp = record?.['sentAs'] as Record<string, any> | null | undefined\n const from = String(stamp?.['from'] ?? '').trim()\n // The ADDRESS is what makes the stamp a record of a send. A `sentAs` map\n // carrying only a display name is a partial write, and reporting it as\n // recorded would put a name on screen beside an address nothing supplied.\n if (!from) {\n return { recorded: false, from: null, fromName: null, replyTo: null }\n }\n return {\n recorded: true,\n from,\n fromName: String(stamp?.['fromName'] ?? '').trim() || null,\n replyTo: String(stamp?.['replyTo'] ?? '').trim() || null,\n }\n}\n"],"names":["emailSendTimeMs","record","sentAt","toMillis","seconds","scheduled","Number","isFinite","EMAIL_CREATED_AT_FIELD","emailCreatedAtMs","created","NaN","emailListTimeMs","emailStateLabel","status","value","String","emailIsUnsent","EMAIL_AUDIENCE_LABELS","leads","members","segment","manual","emailAudienceLabel","audience","name","emailSentAs","stamp","from","trim","recorded","fromName","replyTo"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;CAOC,GAED;;;;;;;;;;;;;;;;;;;;CAoBC,GACD,OAAO,SAASA,gBACdC,MAA8C;;IAE9C,MAAMC,SAASD,0BAAAA,MAAQ,CAAC,SAAS;IAGjC,IAAI,QAAOC,0BAAAA,OAAQC,QAAQ,MAAK,YAAY,OAAOD,OAAOC,QAAQ;IAClE,IAAI,QAAOD,0BAAAA,OAAQE,OAAO,MAAK,UAAU,OAAOF,OAAOE,OAAO,GAAG;IACjE,MAAMC,YAAYC,eAAOL,0BAAAA,MAAQ,CAAC,WAAW,mBAAI;IACjD,OAAOK,OAAOC,QAAQ,CAACF,aAAaA,YAAY;AAClD;AAEA;;;;;;;;;;;;;;CAcC,GACD,OAAO,MAAMG,yBAAyB,cAAa;AAEnD;;;;;;;CAOC,GACD,OAAO,SAASC,iBACdR,MAA8C;;IAE9C,MAAMS,UAAUJ,eAAOL,0BAAAA,MAAQ,CAACO,uBAAuB,mBAAIF,OAAOK,GAAG;IACrE,OAAOL,OAAOC,QAAQ,CAACG,YAAYA,UAAU,IAAIA,UAAU;AAC7D;AAEA;;;;;;;;;;;;;;;;;;;CAmBC,GACD,OAAO,SAASE,gBACdX,MAA8C;IAE9C,OAAOD,gBAAgBC,WAAWQ,iBAAiBR,WAAW;AAChE;AAEA;;;;;;;;;;;;;;;;;;;;;;CAsBC,GACD,OAAO,SAASY,gBAAgBC,MAAe;IAC7C,MAAMC,QAAQC,OAAOF,iBAAAA,SAAU;IAC/B,IAAIC,UAAU,QAAQ,OAAO;IAC7B,IAAIA,UAAU,aAAa,OAAO;IAClC,IAAIA,UAAU,YAAY,OAAO;IACjC,IAAIA,UAAU,SAAS,OAAO;IAC9B,IAAIA,UAAU,WAAW,OAAO;IAChC,OAAOA,SAAS;AAClB;AAEA;;;;;;;CAOC,GACD,OAAO,SAASE,cACdhB,MAA8C;;IAE9C,MAAMc,QAAQC,eAAOf,0BAAAA,MAAQ,CAAC,SAAS,mBAAI;IAC3C,OAAOc,UAAU,WAAWA,UAAU,eAAeA,UAAU;AACjE;AAEA;;;;;;;CAOC,GACD,OAAO,MAAMG,wBAAgD;IAC3DC,OAAO;IACPC,SAAS;IACTC,SAAS;IACTC,QAAQ;AACV,EAAC;AAED;;;;;;;;CAQC,GACD,OAAO,SAASC,mBACdtB,MAA8C;;IAE9C,MAAMuB,WAAWR,eAAOf,0BAAAA,MAAQ,CAAC,WAAW,mBAAI;IAChD,IAAIuB,aAAa,QAAQ;YAChBN,OAAAA;QAAP,QAAOA,SAAAA,kCAAAA,qBAAqB,CAACM,SAAS,YAA/BN,kCAAmCM,oBAAnCN,QAA+C;IACxD;IACA,MAAMO,OAAOT,gBAAOf,0BAAAA,MAAQ,CAAC,WAAW,oBAAI;IAC5C,IAAIwB,MAAM,OAAOA;IACjB,OAAOxB,CAAAA,0BAAAA,MAAQ,CAAC,SAAS,IACrB,kCACA;AACN;AAiCA,OAAO,SAASyB,YACdzB,MAA8C;;IAE9C,MAAM0B,QAAQ1B,0BAAAA,MAAQ,CAAC,SAAS;IAChC,MAAM2B,OAAOZ,eAAOW,yBAAAA,KAAO,CAAC,OAAO,mBAAI,IAAIE,IAAI;IAC/C,yEAAyE;IACzE,uEAAuE;IACvE,0EAA0E;IAC1E,IAAI,CAACD,MAAM;QACT,OAAO;YAAEE,UAAU;YAAOF,MAAM;YAAMG,UAAU;YAAMC,SAAS;QAAK;IACtE;IACA,OAAO;QACLF,UAAU;QACVF;QACAG,UAAUf,gBAAOW,yBAAAA,KAAO,CAAC,WAAW,oBAAI,IAAIE,IAAI,MAAM;QACtDG,SAAShB,gBAAOW,yBAAAA,KAAO,CAAC,UAAU,oBAAI,IAAIE,IAAI,MAAM;IACtD;AACF"}
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
* The email-campaign document model: the stored shapes and the pure
|
|
19
|
+
* arithmetic over them.
|
|
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
|
|
24
|
+
* `@aglyn/shared-ui-email-campaigns/components/report-figures`.
|
|
25
|
+
*/
|
|
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
|
+
/**
|
|
33
|
+
* What a campaign EARNED: the last-click window, the gross/refunded pair and
|
|
34
|
+
* the per-currency buckets that are never added together. Separate from the
|
|
35
|
+
* rate math because it reads a different document and answers the merchant's
|
|
36
|
+
* second question rather than their first.
|
|
37
|
+
*/
|
|
38
|
+
export * from './campaign-revenue';
|
|
39
|
+
/** Reading one message record — its state, and when it went out. */
|
|
40
|
+
export * from './email-record';
|
|
41
|
+
/**
|
|
42
|
+
* The campaign CONTAINER — its window, its lists, and the arithmetic that
|
|
43
|
+
* rolls its sends into one set of figures.
|
|
44
|
+
*/
|
|
45
|
+
export * from './campaign-container';
|
|
46
|
+
export * from './campaign-send-time';
|
|
47
|
+
/**
|
|
48
|
+
* What a campaign CAUSED: the four identify moments, kept apart because one
|
|
49
|
+
* visitor action writes several of them. Separate from the revenue module
|
|
50
|
+
* because it reads a different document and answers what happened to people
|
|
51
|
+
* who were anonymous until the moment being counted.
|
|
52
|
+
*/
|
|
53
|
+
export * from './campaign-conversions';
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
* The email-campaign document model: the stored shapes and the pure
|
|
18
|
+
* arithmetic over them.
|
|
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
|
|
23
|
+
* `@aglyn/shared-ui-email-campaigns/components/report-figures`.
|
|
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
|
+
/**
|
|
30
|
+
* What a campaign EARNED: the last-click window, the gross/refunded pair and
|
|
31
|
+
* the per-currency buckets that are never added together. Separate from the
|
|
32
|
+
* rate math because it reads a different document and answers the merchant's
|
|
33
|
+
* second question rather than their first.
|
|
34
|
+
*/ export * from "./campaign-revenue.js";
|
|
35
|
+
/** Reading one message record — its state, and when it went out. */ export * from "./email-record.js";
|
|
36
|
+
/**
|
|
37
|
+
* The campaign CONTAINER — its window, its lists, and the arithmetic that
|
|
38
|
+
* rolls its sends into one set of figures.
|
|
39
|
+
*/ export * from "./campaign-container.js";
|
|
40
|
+
export * from "./campaign-send-time.js";
|
|
41
|
+
/**
|
|
42
|
+
* What a campaign CAUSED: the four identify moments, kept apart because one
|
|
43
|
+
* visitor action writes several of them. Separate from the revenue module
|
|
44
|
+
* because it reads a different document and answers what happened to people
|
|
45
|
+
* who were anonymous until the moment being counted.
|
|
46
|
+
*/ export * from "./campaign-conversions.js";
|
|
47
|
+
|
|
48
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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-email-campaigns/components/report-figures`.\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/**\n * What a campaign EARNED: the last-click window, the gross/refunded pair and\n * the per-currency buckets that are never added together. Separate from the\n * rate math because it reads a different document and answers the merchant's\n * second question rather than their first.\n */\nexport * from './campaign-revenue'\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\nexport * from './campaign-send-time'\n\n/**\n * What a campaign CAUSED: the four identify moments, kept apart because one\n * visitor action writes several of them. Separate from the revenue module\n * because it reads a different document and answers what happened to people\n * who were anonymous until the moment being counted.\n */\nexport * from './campaign-conversions'\n"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;CAQC,GAED;;;;CAIC,GACD,cAAc,uBAAmB;AAEjC;;;;;CAKC,GACD,cAAc,wBAAoB;AAElC,kEAAkE,GAClE,cAAc,oBAAgB;AAE9B;;;CAGC,GACD,cAAc,0BAAsB;AAEpC,cAAc,0BAAsB;AAEpC;;;;;CAKC,GACD,cAAc,4BAAwB"}
|