ab-ecosystem-cpass 1.0.5 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +62 -1
  2. package/dist/client.d.ts +24 -0
  3. package/dist/client.js +32 -5
  4. package/dist/client.js.map +1 -1
  5. package/dist/index.d.ts +9 -0
  6. package/dist/index.js +20 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/internal/bytes.d.ts +38 -0
  9. package/dist/internal/bytes.js +84 -0
  10. package/dist/internal/bytes.js.map +1 -0
  11. package/dist/internal/device.d.ts +24 -0
  12. package/dist/internal/device.js +91 -0
  13. package/dist/internal/device.js.map +1 -0
  14. package/dist/internal/md5.d.ts +4 -0
  15. package/dist/internal/md5.js +109 -0
  16. package/dist/internal/md5.js.map +1 -0
  17. package/dist/internal/receiver-file.d.ts +106 -0
  18. package/dist/internal/receiver-file.js +128 -0
  19. package/dist/internal/receiver-file.js.map +1 -0
  20. package/dist/resources/audience/notifications.d.ts +171 -0
  21. package/dist/resources/audience/notifications.js +215 -0
  22. package/dist/resources/audience/notifications.js.map +1 -0
  23. package/dist/resources/campaign-audience.d.ts +132 -0
  24. package/dist/resources/campaign-audience.js +224 -0
  25. package/dist/resources/campaign-audience.js.map +1 -0
  26. package/dist/resources/campaign-messages.d.ts +150 -0
  27. package/dist/resources/campaign-messages.js +172 -0
  28. package/dist/resources/campaign-messages.js.map +1 -0
  29. package/dist/resources/campaigns.d.ts +239 -0
  30. package/dist/resources/campaigns.js +300 -0
  31. package/dist/resources/campaigns.js.map +1 -0
  32. package/dist/types/audience.d.ts +82 -0
  33. package/dist/types/campaign.d.ts +319 -0
  34. package/dist/types/campaign.js +3 -0
  35. package/dist/types/campaign.js.map +1 -0
  36. package/dist/types/index.d.ts +1 -0
  37. package/dist/types/index.js +2 -0
  38. package/dist/types/index.js.map +1 -1
  39. package/docs/audience/notifications.md +263 -0
  40. package/docs/campaign/campaign-messages.md +273 -0
  41. package/docs/campaign/campaigns.md +449 -0
  42. package/package.json +1 -1
@@ -0,0 +1,273 @@
1
+ # Campaign messages (reporting)
2
+
3
+ `crm.campaignMessages` — `/v1/campaign-messages`
4
+
5
+ What actually happened to each message a campaign sent. One row per attempted
6
+ send: who it went to, how far it got, what came back, and why it failed.
7
+
8
+ | Action | Route | Scope |
9
+ | --- | --- | --- |
10
+ | [`stats`](#stats) | `GET /v1/campaign-messages/stats` | `campaigns:read` |
11
+ | [`list`](#list) | `GET /v1/campaign-messages` | `campaigns:read` |
12
+ | [`iterate`](#iterate) | the same, paged for you | `campaigns:read` |
13
+ | [`replies`](#replies) | `GET /v1/campaign-messages/replies` | `campaigns:read` |
14
+
15
+ ## Where these numbers come from
16
+
17
+ The campaign manager writes one row as it hands each message to a channel. The
18
+ channel's delivery webhooks — processed **outside the CRM** — then move that
19
+ row's `status` forward as the message is delivered, read and replied to.
20
+
21
+ So reporting is a **read of those rows, counted when you ask**. There is no
22
+ report collection, no roll-up job, and nothing to regenerate: no `generatedAt` to
23
+ reason about, and no window in which a stored number is quietly wrong because a
24
+ webhook has arrived but the roll-up has not run.
25
+
26
+ The flip side is that a campaign still going out has moving numbers. Reading
27
+ `stats` seconds after launch under-counts everything, and that is the truth
28
+ rather than a lag.
29
+
30
+ Nothing in this module writes. There is no endpoint to correct a status: the
31
+ service that owns the lifecycle is the one receiving the webhooks.
32
+
33
+ ## The funnel
34
+
35
+ `status` is a **single field holding the furthest stage reached** — not a set of
36
+ flags. A row at `read` is also delivered and sent; a row at `replied` is all
37
+ three. So the counts nest:
38
+
39
+ ```
40
+ replied ⊆ read ⊆ delivered ⊆ sent failed ∥ (outside the funnel)
41
+ ```
42
+
43
+ Which means:
44
+
45
+ - **Cumulative by default.** `delivered` is everything that got at least as far
46
+ as delivered — the number a "delivered" tile shows.
47
+ - **Subtract for a stage's own count.** `delivered - read` is "delivered but
48
+ unread"; `read - replied` is "read but silent".
49
+ - **`failed` is never inside `sent`.** A failed message was never sent anywhere;
50
+ it is counted on its own.
51
+ - **There is no `queued`.** A row exists only once a send has been attempted, so
52
+ "waiting to go out" is not a state this collection can have. The campaign's own
53
+ `status` is where that lives.
54
+
55
+ `total` counts every row, failures included, and is the number of attempted
56
+ sends.
57
+
58
+ ## The row
59
+
60
+ ```ts
61
+ interface CampaignMessageRecord {
62
+ id: string; // the row's own id — what a `selection` retarget takes
63
+ campaignId: string;
64
+ messageIndex: number; // which message of the campaign, zero-based
65
+ providerMessageId: string; // '' when the send failed
66
+ channelId: string;
67
+ contactId: string; // the recipient's contact id
68
+ identifier: string; // the destination: a WhatsApp number, a Telegram id…
69
+ name: string; // '' when the contact had no name
70
+ status: 'sent' | 'delivered' | 'read' | 'replied' | 'failed';
71
+ sentAt?: string; // set per stage reached, absent for stages not reached
72
+ deliveredAt?: string;
73
+ readAt?: string;
74
+ repliedAt?: string;
75
+ failedAt?: string;
76
+ reply: string; // '' unless replied
77
+ replyType: string; // 'text', 'button', … '' unless replied
78
+ repliedIn: number; // seconds between the send and the reply; 0 unless replied
79
+ error?: string; // failed rows only
80
+ createdAt: string; // when the send was attempted
81
+ }
82
+ ```
83
+
84
+ `id` is worth singling out: it is the handle a **`selection` retarget** campaign
85
+ takes. Collect the ids of the rows you want and the campaign manager resolves
86
+ them back to recipients, which is how "re-send to everyone who did not read it"
87
+ is built.
88
+
89
+ `CampaignMessageRecord` is not `CampaignMessage`, despite the names. A
90
+ `CampaignMessage` is one of the messages a campaign is *defined* to send — a
91
+ channel, a payload, its polls. A `CampaignMessageRecord` is a record of one of
92
+ those having gone to one person, and there is one per recipient per message.
93
+
94
+ ---
95
+
96
+ ## stats
97
+
98
+ ```ts
99
+ crm.campaignMessages.stats(query: CampaignMessageStatsQuery): Promise<CampaignMessageStats>
100
+ ```
101
+
102
+ `GET /v1/campaign-messages/stats` · `campaigns:read` · **200**
103
+
104
+ ### Query
105
+
106
+ | Field | Type | Required | Notes |
107
+ | --- | --- | --- | --- |
108
+ | `campaign` | string | yes | The campaign id, as a 24-character ObjectId. |
109
+ | `message` | number | no | One message of a multi-message campaign, zero-based. |
110
+
111
+ ```ts
112
+ const overall = await crm.campaignMessages.stats({ campaign });
113
+ // { total: 12480, sent: 12410, delivered: 11980, read: 7420, replied: 310, failed: 70 }
114
+
115
+ const second = await crm.campaignMessages.stats({ campaign, message: 1 });
116
+ ```
117
+
118
+ **No date window**, deliberately. A campaign is a single send — its rows are
119
+ written over the minutes it takes to go out and then only change state — so "the
120
+ campaign's whole life" is the only window that means anything. A repeating
121
+ campaign's runs are separate campaigns, each with its own id.
122
+
123
+ One aggregation, not five counts. Legacy issued a `countDocuments` per stage,
124
+ which is five passes for numbers that all come from one grouping, and could
125
+ return a `read` higher than its own `delivered` when a webhook landed between
126
+ two of the passes.
127
+
128
+ `404 not_found` when the campaign is not the caller's — the same answer as a
129
+ campaign that does not exist, so the error cannot be used to probe whether
130
+ someone else's id is real.
131
+
132
+ ---
133
+
134
+ ## list
135
+
136
+ ```ts
137
+ crm.campaignMessages.list(query: ListCampaignMessagesQuery): Promise<Paginated<CampaignMessageRecord>>
138
+ ```
139
+
140
+ `GET /v1/campaign-messages` · `campaigns:read` · **200**
141
+
142
+ ### Query
143
+
144
+ | Field | Type | Default | Notes |
145
+ | --- | --- | --- | --- |
146
+ | `campaign` | string | — | Required. |
147
+ | `message` | number | — | Zero-based index of one campaign message. |
148
+ | `status` | `sent` `delivered` `read` `replied` `failed` | — | Cumulative; see below. Omit for every row. |
149
+ | `exact` | boolean | `false` | Only rows sitting at `status` right now. |
150
+ | `identifier` | string | — | Exact match on one recipient. Not a search. |
151
+ | `reply_type` | string | — | Replied rows only; refused otherwise. |
152
+ | `replied_within_hours` | number | — | Replied rows only; refused otherwise. |
153
+ | `page` | number | `1` | 1-based. |
154
+ | `rows` | number | `25` | 1–100. |
155
+
156
+ ```ts
157
+ // the two queries a report screen actually makes
158
+ const failures = await crm.campaignMessages.list({ campaign, status: 'failed', rows: 100 });
159
+ failures.data[0].error; // why that one did not go
160
+
161
+ const unread = await crm.campaignMessages.list({ campaign, status: 'delivered', exact: true });
162
+ unread.pagination.total; // equals stats.delivered - stats.read
163
+ ```
164
+
165
+ ### `status` and `exact`
166
+
167
+ `status` filters **cumulatively**, so a drill-down returns exactly the rows the
168
+ matching stat counted: `status: 'delivered'` gives you the `delivered` number's
169
+ rows. `exact: true` switches to the single stage — "delivered but never read" —
170
+ which is the drill-down for `delivered - read`.
171
+
172
+ `exact` has no effect without `status`, and none on `failed`, which has nothing
173
+ beyond it.
174
+
175
+ ### Reply filters
176
+
177
+ `reply_type` and `replied_within_hours` are **refused** with
178
+ `400 validation_failed` unless `status` is `replied`. They cannot match anything
179
+ on a non-replied row, and a filter that cannot match is a caller mistake worth
180
+ reporting rather than an empty page.
181
+
182
+ `replied_within_hours: 2` means the reply arrived within two hours of the send —
183
+ useful for separating people who answered immediately from people who came back
184
+ the next day.
185
+
186
+ ### Ordering
187
+
188
+ Newest attempt first, by when the row was **written**, not by a stage timestamp.
189
+ Sorting on `deliveredAt` would reorder the list as webhooks arrive, and offset
190
+ paging over a list that reorders itself skips and repeats rows.
191
+
192
+ ---
193
+
194
+ ## iterate
195
+
196
+ ```ts
197
+ crm.campaignMessages.iterate(query: ListCampaignMessagesQuery): AsyncGenerator<CampaignMessageRecord>
198
+ ```
199
+
200
+ Every matching row, paged through for you. **This is what an export is** — there
201
+ is no export endpoint, nothing to queue, nothing to poll, and no generated file
202
+ that expires before someone downloads it.
203
+
204
+ ```ts
205
+ for await (const row of crm.campaignMessages.iterate({ campaign })) {
206
+ csv.write([row.identifier, row.name, row.status, row.error ?? '']);
207
+ }
208
+ ```
209
+
210
+ Pages at 100 rows unless you pass `rows` — the server's maximum, and so the
211
+ fewest round trips. A million recipients is ten thousand requests, so run an
212
+ export that size somewhere with low latency to the gateway rather than from a
213
+ browser tab.
214
+
215
+ The collection is live. Rows are written while a campaign goes out and statuses
216
+ change under an export that takes minutes, and offset paging over moving data can
217
+ repeat or miss a row. For a stable snapshot, export after the campaign completes.
218
+
219
+ ---
220
+
221
+ ## replies
222
+
223
+ ```ts
224
+ crm.campaignMessages.replies(query: CampaignRepliesQuery): Promise<CampaignReplyBreakdown>
225
+ ```
226
+
227
+ `GET /v1/campaign-messages/replies` · `campaigns:read` · **200**
228
+
229
+ ### Query
230
+
231
+ | Field | Type | Default | Notes |
232
+ | --- | --- | --- | --- |
233
+ | `campaign` | string | — | Required. |
234
+ | `message` | number | — | Zero-based index of one campaign message. |
235
+ | `limit` | number | `50` | Distinct replies to return, 1–200. |
236
+
237
+ ```ts
238
+ const { groups, replied, distinct } = await crm.campaignMessages.replies({ campaign });
239
+
240
+ groups; // [{ reply: 'Yes', replyType: 'button', count: 182 }, { reply: 'No', … }]
241
+ replied; // 310 — every replied row, whether or not its group is listed
242
+ distinct; // 4 — distinct replies in total
243
+ ```
244
+
245
+ What a poll or a button message is sent to find out. Grouped on the reply **and**
246
+ its type, so a tapped `Yes` and a typed `Yes` stay apart — they say different
247
+ things about how engaged the recipient was. Most common first, ties broken by the
248
+ reply text so the order is stable between reads.
249
+
250
+ `distinct` against `groups.length` tells you whether the breakdown is complete.
251
+ A campaign that asked an open question has as many groups as it has repliers, so
252
+ a truncated answer there is normal rather than a problem — raise `limit`, or
253
+ treat the tail as "other".
254
+
255
+ ---
256
+
257
+ ## Operational note: the index this needs
258
+
259
+ Every read here matches on the campaign first, and the collection — which belongs
260
+ to the campaign manager — declares no index on it. Without one, each report is a
261
+ scan of every campaign message ever sent:
262
+
263
+ ```js
264
+ db.newcampaignmessages.createIndex(
265
+ { cid: 1, cmid: 1, status: 1 },
266
+ { name: 'Campaign Report', background: true },
267
+ );
268
+ ```
269
+
270
+ One compound index serves all three reads. This service deliberately does **not**
271
+ create it: building an index on a multi-million row collection owned by another
272
+ service, as a side effect of a process starting, is not something that should
273
+ happen by accident.