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.
- package/README.md +62 -1
- package/dist/client.d.ts +24 -0
- package/dist/client.js +32 -5
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.js +20 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/bytes.d.ts +38 -0
- package/dist/internal/bytes.js +84 -0
- package/dist/internal/bytes.js.map +1 -0
- package/dist/internal/device.d.ts +24 -0
- package/dist/internal/device.js +91 -0
- package/dist/internal/device.js.map +1 -0
- package/dist/internal/md5.d.ts +4 -0
- package/dist/internal/md5.js +109 -0
- package/dist/internal/md5.js.map +1 -0
- package/dist/internal/receiver-file.d.ts +106 -0
- package/dist/internal/receiver-file.js +128 -0
- package/dist/internal/receiver-file.js.map +1 -0
- package/dist/resources/audience/notifications.d.ts +171 -0
- package/dist/resources/audience/notifications.js +215 -0
- package/dist/resources/audience/notifications.js.map +1 -0
- package/dist/resources/campaign-audience.d.ts +132 -0
- package/dist/resources/campaign-audience.js +224 -0
- package/dist/resources/campaign-audience.js.map +1 -0
- package/dist/resources/campaign-messages.d.ts +150 -0
- package/dist/resources/campaign-messages.js +172 -0
- package/dist/resources/campaign-messages.js.map +1 -0
- package/dist/resources/campaigns.d.ts +239 -0
- package/dist/resources/campaigns.js +300 -0
- package/dist/resources/campaigns.js.map +1 -0
- package/dist/types/audience.d.ts +82 -0
- package/dist/types/campaign.d.ts +319 -0
- package/dist/types/campaign.js +3 -0
- package/dist/types/campaign.js.map +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -1
- package/docs/audience/notifications.md +263 -0
- package/docs/campaign/campaign-messages.md +273 -0
- package/docs/campaign/campaigns.md +449 -0
- package/package.json +1 -1
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CampaignMessages = void 0;
|
|
4
|
+
const BASE = '/v1/campaign-messages';
|
|
5
|
+
/**
|
|
6
|
+
* Campaign reporting: what actually happened to each message.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* const stats = await crm.campaignMessages.stats({ campaign });
|
|
10
|
+
* // { total: 12480, sent: 12410, delivered: 11980, read: 7420, replied: 310, failed: 70 }
|
|
11
|
+
*
|
|
12
|
+
* const { data } = await crm.campaignMessages.list({ campaign, status: 'failed' });
|
|
13
|
+
* data[0].error; // why that one did not go
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* ## Where these numbers come from
|
|
17
|
+
*
|
|
18
|
+
* One row per attempted send, written by the campaign manager as it hands each
|
|
19
|
+
* message to a channel, with its `status` moved forward by the channel's
|
|
20
|
+
* delivery webhooks -- which are processed outside the CRM entirely. The report
|
|
21
|
+
* is **counted off those rows when you ask for it**, not rolled up on a
|
|
22
|
+
* schedule, so there is no "generated at" to reason about and no staleness
|
|
23
|
+
* window: the numbers are as current as the webhooks that have arrived.
|
|
24
|
+
*
|
|
25
|
+
* The flip side is that a campaign still going out has moving numbers. Reading
|
|
26
|
+
* `stats` seconds after launching under-counts everything, and that is correct
|
|
27
|
+
* rather than a lag.
|
|
28
|
+
*
|
|
29
|
+
* ## The funnel
|
|
30
|
+
*
|
|
31
|
+
* `status` is a single field holding the furthest stage reached, so the counts
|
|
32
|
+
* are cumulative and nest: `replied ⊆ read ⊆ delivered ⊆ sent`. For a stage's
|
|
33
|
+
* exclusive count, subtract the next one -- `delivered - read` is "delivered but
|
|
34
|
+
* unread". `failed` is outside the funnel and never inside `sent`.
|
|
35
|
+
*
|
|
36
|
+
* ## There is no export endpoint
|
|
37
|
+
*
|
|
38
|
+
* An export is {@link list} written to a file, so {@link iterate} does the
|
|
39
|
+
* paging and the caller formats the rows. Nothing is queued, nothing has to be
|
|
40
|
+
* polled, and no generated file expires before someone downloads it.
|
|
41
|
+
*/
|
|
42
|
+
class CampaignMessages {
|
|
43
|
+
http;
|
|
44
|
+
constructor(http) {
|
|
45
|
+
this.http = http;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The campaign's funnel counts.
|
|
49
|
+
*
|
|
50
|
+
* `GET /v1/campaign-messages/stats?campaign=<id>` · `campaigns:read` · **200**
|
|
51
|
+
*
|
|
52
|
+
* ### Query
|
|
53
|
+
*
|
|
54
|
+
* | Field | Type | Required | Notes |
|
|
55
|
+
* | --- | --- | --- | --- |
|
|
56
|
+
* | `campaign` | string | yes | The campaign id. |
|
|
57
|
+
* | `message` | number | no | One message of a multi-message campaign, zero-based. |
|
|
58
|
+
*
|
|
59
|
+
* ```ts
|
|
60
|
+
* const overall = await crm.campaignMessages.stats({ campaign });
|
|
61
|
+
* const second = await crm.campaignMessages.stats({ campaign, message: 1 });
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* No date window: a campaign is one send, so its whole life is the only
|
|
65
|
+
* meaningful window. A repeating campaign's runs are separate campaigns with
|
|
66
|
+
* their own ids.
|
|
67
|
+
*
|
|
68
|
+
* `404 not_found` if the campaign is not the caller's -- the same answer as a
|
|
69
|
+
* campaign that does not exist, so the error cannot be used to probe ids.
|
|
70
|
+
*/
|
|
71
|
+
stats(query) {
|
|
72
|
+
return this.http.request('GET', `${BASE}/stats`, { query });
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The rows behind the funnel. This is the report table.
|
|
76
|
+
*
|
|
77
|
+
* `GET /v1/campaign-messages?campaign=<id>` · `campaigns:read` · **200**
|
|
78
|
+
*
|
|
79
|
+
* ### Query
|
|
80
|
+
*
|
|
81
|
+
* | Field | Type | Default | Notes |
|
|
82
|
+
* | --- | --- | --- | --- |
|
|
83
|
+
* | `campaign` | string | — | Required. |
|
|
84
|
+
* | `message` | number | — | Zero-based index of one campaign message. |
|
|
85
|
+
* | `status` | `sent \| delivered \| read \| replied \| failed` | — | Cumulative; see below. Omit for every row. |
|
|
86
|
+
* | `exact` | boolean | `false` | Only rows sitting at `status` right now. |
|
|
87
|
+
* | `identifier` | string | — | Exact match on one recipient. Not a search. |
|
|
88
|
+
* | `reply_type` | string | — | Replied rows only; refused otherwise. |
|
|
89
|
+
* | `replied_within_hours` | number | — | Replied rows only; refused otherwise. |
|
|
90
|
+
* | `page` | number | `1` | 1-based. |
|
|
91
|
+
* | `rows` | number | `25` | 1–100. |
|
|
92
|
+
*
|
|
93
|
+
* Newest attempt first, ordered by when the row was written -- not by a stage
|
|
94
|
+
* timestamp, which would reorder the list as webhooks arrive and make paging
|
|
95
|
+
* skip and repeat rows.
|
|
96
|
+
*
|
|
97
|
+
* ```ts
|
|
98
|
+
* // the two queries a report screen actually makes
|
|
99
|
+
* const failures = await crm.campaignMessages.list({ campaign, status: 'failed', rows: 100 });
|
|
100
|
+
* const unread = await crm.campaignMessages.list({ campaign, status: 'delivered', exact: true });
|
|
101
|
+
* ```
|
|
102
|
+
*
|
|
103
|
+
* `reply_type` and `replied_within_hours` are **refused** with
|
|
104
|
+
* `400 validation_failed` unless `status` is `replied`, rather than quietly
|
|
105
|
+
* matching nothing.
|
|
106
|
+
*/
|
|
107
|
+
list(query) {
|
|
108
|
+
return this.http.request('GET', BASE, { query });
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Every matching row, paged through for you. What an export is.
|
|
112
|
+
*
|
|
113
|
+
* ```ts
|
|
114
|
+
* for await (const row of crm.campaignMessages.iterate({ campaign })) {
|
|
115
|
+
* csv.write([row.identifier, row.name, row.status, row.error ?? '']);
|
|
116
|
+
* }
|
|
117
|
+
* ```
|
|
118
|
+
*
|
|
119
|
+
* Pages at 100 rows unless told otherwise -- the server's maximum, and the
|
|
120
|
+
* fewest round trips. A campaign of a million recipients is ten thousand
|
|
121
|
+
* requests, so for exports that size prefer running it where the latency is
|
|
122
|
+
* low rather than from a browser.
|
|
123
|
+
*
|
|
124
|
+
* The collection is live: rows are written while a campaign is going out, and
|
|
125
|
+
* statuses change under an export that takes minutes. Offset paging over
|
|
126
|
+
* moving data can repeat or skip a row -- for a stable snapshot, export after
|
|
127
|
+
* the campaign completes.
|
|
128
|
+
*/
|
|
129
|
+
async *iterate(query) {
|
|
130
|
+
const rows = query.rows ?? 100;
|
|
131
|
+
let page = query.page ?? 1;
|
|
132
|
+
for (;;) {
|
|
133
|
+
const result = await this.list({ ...query, page, rows });
|
|
134
|
+
for (const record of result.data)
|
|
135
|
+
yield record;
|
|
136
|
+
if (result.data.length === 0 || page >= result.pagination.pages)
|
|
137
|
+
return;
|
|
138
|
+
page += 1;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* What people replied, grouped by the reply itself.
|
|
143
|
+
*
|
|
144
|
+
* `GET /v1/campaign-messages/replies?campaign=<id>` · `campaigns:read` · **200**
|
|
145
|
+
*
|
|
146
|
+
* ### Query
|
|
147
|
+
*
|
|
148
|
+
* | Field | Type | Default | Notes |
|
|
149
|
+
* | --- | --- | --- | --- |
|
|
150
|
+
* | `campaign` | string | — | Required. |
|
|
151
|
+
* | `message` | number | — | Zero-based index of one campaign message. |
|
|
152
|
+
* | `limit` | number | `50` | Distinct replies to return, 1–200. |
|
|
153
|
+
*
|
|
154
|
+
* ```ts
|
|
155
|
+
* const { groups, replied, distinct } = await crm.campaignMessages.replies({ campaign });
|
|
156
|
+
* groups; // [{ reply: 'Yes', replyType: 'button', count: 182 }, …]
|
|
157
|
+
* ```
|
|
158
|
+
*
|
|
159
|
+
* This is what a poll or a button message is sent to find out. Grouped on the
|
|
160
|
+
* reply **and** its type, so a tapped `Yes` and a typed `Yes` stay apart --
|
|
161
|
+
* they say different things about how engaged the recipient was.
|
|
162
|
+
*
|
|
163
|
+
* `distinct` against `groups.length` tells you whether the breakdown is
|
|
164
|
+
* complete. A campaign that asked an open question has as many groups as it
|
|
165
|
+
* has repliers, so a truncated answer there is normal rather than a problem.
|
|
166
|
+
*/
|
|
167
|
+
replies(query) {
|
|
168
|
+
return this.http.request('GET', `${BASE}/replies`, { query });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
exports.CampaignMessages = CampaignMessages;
|
|
172
|
+
//# sourceMappingURL=campaign-messages.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"campaign-messages.js","sourceRoot":"","sources":["../../src/resources/campaign-messages.ts"],"names":[],"mappings":";;;AAWA,MAAM,IAAI,GAAG,uBAAuB,CAAC;AAErC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,MAAa,gBAAgB;IACE;IAA7B,YAA6B,IAAgB;QAAhB,SAAI,GAAJ,IAAI,CAAY;IAAG,CAAC;IAEjD;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,KAAK,CAAC,KAAgC;QACpC,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAAuB,KAAK,EAAE,GAAG,IAAI,QAAQ,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;IACpF,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAgCG;IACH,IAAI,CAAC,KAAgC;QACnC,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAAmC,KAAK,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;IACrF,CAAC;IAED;;;;;;;;;;;;;;;;;;OAkBG;IACH,KAAK,CAAC,CAAC,OAAO,CAAC,KAAgC;QAC7C,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,IAAI,GAAG,CAAC;QAC/B,IAAI,IAAI,GAAG,KAAK,CAAC,IAAI,IAAI,CAAC,CAAC;QAE3B,SAAS,CAAC;YACR,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,EAAE,GAAG,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;YACzD,KAAK,MAAM,MAAM,IAAI,MAAM,CAAC,IAAI;gBAAE,MAAM,MAAM,CAAC;YAE/C,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,IAAI,MAAM,CAAC,UAAU,CAAC,KAAK;gBAAE,OAAO;YACxE,IAAI,IAAI,CAAC,CAAC;QACZ,CAAC;IACH,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACH,OAAO,CAAC,KAA2B;QACjC,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAAyB,KAAK,EAAE,GAAG,IAAI,UAAU,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;IACxF,CAAC;CACF;AAjID,4CAiIC"}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import type { HttpClient } from '../http';
|
|
2
|
+
import { InboxMedia } from './audience/inbox-media';
|
|
3
|
+
import { type CampaignReceiver, type ReceiverEncoding } from '../internal/receiver-file';
|
|
4
|
+
import { type BulkAudienceInput, type ResolvedAudience } from './campaign-audience';
|
|
5
|
+
import type { Campaign, CampaignNumbersFilter, CreateCampaignInput, ListCampaignsQuery, UpdateCampaignInput } from '../types/campaign';
|
|
6
|
+
import type { Page } from '../types/common';
|
|
7
|
+
/** Everything `createBulk` needs: a normal campaign, with an audience instead of `filters`. */
|
|
8
|
+
export interface CreateBulkCampaignInput extends Omit<CreateCampaignInput, 'filters' | 'type'> {
|
|
9
|
+
/** The recipients. Numbers, CSV text, parsed rows, or receivers you built. */
|
|
10
|
+
audience: BulkAudienceInput;
|
|
11
|
+
/** Overrides the dispatch route. Defaults to `bulk_campaign`; rarely needed. */
|
|
12
|
+
type?: string;
|
|
13
|
+
/** Aborts the upload. The campaign is not created if the upload is cancelled. */
|
|
14
|
+
signal?: AbortSignal;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* What `createBulk` did, beyond creating the campaign.
|
|
18
|
+
*
|
|
19
|
+
* `receivers` is a **count**, deliberately matching what the same field means on
|
|
20
|
+
* a {@link Campaign} rather than carrying the resolved list. Two reasons: the
|
|
21
|
+
* caller already holds the data they passed in, and a name that means a number in
|
|
22
|
+
* one place and an array in another is the kind of thing that reads fine and then
|
|
23
|
+
* renders `[object Object]` on someone's screen.
|
|
24
|
+
*/
|
|
25
|
+
export interface BulkCampaignResult extends Omit<ResolvedAudience, 'receivers'> {
|
|
26
|
+
campaign: Campaign;
|
|
27
|
+
/**
|
|
28
|
+
* Recipients actually written into the file.
|
|
29
|
+
*
|
|
30
|
+
* This -- not `campaign.receivers`, which is `0` until the campaign is sent --
|
|
31
|
+
* is the number to show a user now.
|
|
32
|
+
*/
|
|
33
|
+
receivers: number;
|
|
34
|
+
/** Public URL of the encrypted receiver file, as stored on the campaign. */
|
|
35
|
+
fileUrl: string;
|
|
36
|
+
}
|
|
37
|
+
/** An uploader, so the receiver file's destination can be swapped or faked in tests. */
|
|
38
|
+
export type ReceiverUploader = Pick<InboxMedia, 'upload'>;
|
|
39
|
+
/**
|
|
40
|
+
* One bucket of a group campaign: a slice of the recipients, and what should
|
|
41
|
+
* send it.
|
|
42
|
+
*
|
|
43
|
+
* Which of `channel` and `pairIndex` applies depends on the campaign's
|
|
44
|
+
* `distributeBy`. Neither is validated here -- a bucket with the wrong one is a
|
|
45
|
+
* mistake the sending service will report, and guessing which the caller meant
|
|
46
|
+
* would be worse than passing it along.
|
|
47
|
+
*/
|
|
48
|
+
export interface GroupBucket {
|
|
49
|
+
/** This bucket's recipients. Any of the four audience shapes. */
|
|
50
|
+
audience: BulkAudienceInput;
|
|
51
|
+
/** `distributeBy: 'channel'` -- the channel id this bucket is sent through. */
|
|
52
|
+
channel?: string;
|
|
53
|
+
/** `distributeBy: 'template'` -- which message in `messages` renders this bucket. */
|
|
54
|
+
pairIndex?: number;
|
|
55
|
+
/**
|
|
56
|
+
* Extra keys carried onto the filter entry verbatim, beside `numbers` and
|
|
57
|
+
* `hash`. The campaign contract passes anything it does not recognise straight
|
|
58
|
+
* through to the sending service, so this is the seam for distribution metadata
|
|
59
|
+
* this SDK does not model.
|
|
60
|
+
*/
|
|
61
|
+
extra?: Record<string, unknown>;
|
|
62
|
+
}
|
|
63
|
+
export interface CreateGroupCampaignInput extends Omit<CreateCampaignInput, 'filters' | 'type'> {
|
|
64
|
+
/**
|
|
65
|
+
* What the buckets are split by.
|
|
66
|
+
*
|
|
67
|
+
* `channel` sends each bucket through a different channel; `template` renders
|
|
68
|
+
* each with a different message. Every bucket is stamped with this, and the
|
|
69
|
+
* contract refuses a campaign whose buckets disagree.
|
|
70
|
+
*/
|
|
71
|
+
distributeBy: 'channel' | 'template';
|
|
72
|
+
buckets: readonly GroupBucket[];
|
|
73
|
+
/** Overrides the stored type. Defaults to `group_campaign`; rarely needed. */
|
|
74
|
+
type?: string;
|
|
75
|
+
signal?: AbortSignal;
|
|
76
|
+
}
|
|
77
|
+
/** One bucket's outcome. Same order as the buckets given. */
|
|
78
|
+
export interface GroupBucketResult {
|
|
79
|
+
receivers: number;
|
|
80
|
+
skipped: number;
|
|
81
|
+
duplicates: number;
|
|
82
|
+
/** Public URL of this bucket's encrypted file. */
|
|
83
|
+
fileUrl: string;
|
|
84
|
+
channel?: string;
|
|
85
|
+
pairIndex?: number;
|
|
86
|
+
}
|
|
87
|
+
export interface GroupCampaignResult {
|
|
88
|
+
campaign: Campaign;
|
|
89
|
+
/** Buckets that had recipients and were uploaded. Empty ones are not here. */
|
|
90
|
+
buckets: GroupBucketResult[];
|
|
91
|
+
/** Totals across every bucket. */
|
|
92
|
+
receivers: number;
|
|
93
|
+
skipped: number;
|
|
94
|
+
duplicates: number;
|
|
95
|
+
}
|
|
96
|
+
export declare class Campaigns {
|
|
97
|
+
private readonly http;
|
|
98
|
+
private readonly media;
|
|
99
|
+
constructor(http: HttpClient, media?: ReceiverUploader);
|
|
100
|
+
list(query?: Partial<ListCampaignsQuery>): Promise<Page<Campaign>>;
|
|
101
|
+
get(id: string): Promise<Campaign>;
|
|
102
|
+
/**
|
|
103
|
+
* Creates a campaign from a payload you have already assembled.
|
|
104
|
+
*
|
|
105
|
+
* For a bulk recipient list, prefer {@link createBulk} -- it does the
|
|
106
|
+
* encrypting and uploading that a file-backed audience requires, which is not
|
|
107
|
+
* something a caller should have to reproduce.
|
|
108
|
+
*/
|
|
109
|
+
create(input: CreateCampaignInput): Promise<Campaign>;
|
|
110
|
+
/**
|
|
111
|
+
* Creates a campaign addressed to a list of numbers you supply directly.
|
|
112
|
+
*
|
|
113
|
+
* This is the "bulk campaign" path, and the SDK does the whole of it: the
|
|
114
|
+
* recipient list never travels as part of the campaign, because a million
|
|
115
|
+
* numbers is not a request body. Instead it is encrypted into a file, uploaded
|
|
116
|
+
* to storage, and the campaign stores a pointer to it -- which the sending
|
|
117
|
+
* service fetches and decrypts when the campaign runs.
|
|
118
|
+
*
|
|
119
|
+
* ```ts
|
|
120
|
+
* // Pasted numbers, no variables
|
|
121
|
+
* const { campaign, receivers } = await crm.campaigns.createBulk({
|
|
122
|
+
* name: 'Diwali offer',
|
|
123
|
+
* audience: { numbers: '919000000001, 919000000002\n919000000003' },
|
|
124
|
+
* messages: [{ channel: { id: channelId }, message: template }],
|
|
125
|
+
* });
|
|
126
|
+
*
|
|
127
|
+
* // A CSV, with every column available as a template variable
|
|
128
|
+
* await crm.campaigns.createBulk({
|
|
129
|
+
* name: 'Renewal reminder',
|
|
130
|
+
* audience: { csv: await file.text(), recipientColumn: 'phone' },
|
|
131
|
+
* messages: [{ channel: { id: channelId }, message: template }],
|
|
132
|
+
* trigger: 'schedule',
|
|
133
|
+
* scheduledAt: '2026-11-01T04:30:00.000Z',
|
|
134
|
+
* });
|
|
135
|
+
* ```
|
|
136
|
+
*
|
|
137
|
+
* ## What it does, in order
|
|
138
|
+
*
|
|
139
|
+
* 1. **Resolves the audience.** Numbers are reduced to digits; CSV or TSV text
|
|
140
|
+
* is parsed with quoted fields honoured; every column becomes a per-recipient
|
|
141
|
+
* variable. Rows with no usable number are skipped and duplicates dropped,
|
|
142
|
+
* both counted in the result rather than passed over in silence.
|
|
143
|
+
* 2. **Encrypts the list.** AES-256-CBC under a secret generated fresh for this
|
|
144
|
+
* one file. See `internal/receiver-file.ts` for the exact scheme.
|
|
145
|
+
* 3. **Uploads it** as a `document` through the media endpoint: a presigned URL
|
|
146
|
+
* is issued and the bytes go straight to storage, so the file never passes
|
|
147
|
+
* through the API.
|
|
148
|
+
* 4. **Creates the campaign** with `type: 'bulk_campaign'` and
|
|
149
|
+
* `filters: [{numbers: <url>, hash: <secret>}]`.
|
|
150
|
+
*
|
|
151
|
+
* ## Two things worth knowing
|
|
152
|
+
*
|
|
153
|
+
* **The returned `campaign.receivers` is `0`, and that is correct.** The
|
|
154
|
+
* audience is resolved when the campaign is sent, not when it is created. Use
|
|
155
|
+
* the `receivers` on this result -- the number actually written into the file --
|
|
156
|
+
* for anything you show the user now.
|
|
157
|
+
*
|
|
158
|
+
* **The file is readable by anyone holding both the URL and the `hash`.** The
|
|
159
|
+
* URL is a public storage link, so the secret stored alongside it on the
|
|
160
|
+
* campaign is the only thing protecting the list. It is a credential: do not log
|
|
161
|
+
* it, and do not copy a `filters` entry from one campaign into another system.
|
|
162
|
+
*
|
|
163
|
+
* Throws `validation_failed` if the audience resolves to no recipients, or to
|
|
164
|
+
* more than a million.
|
|
165
|
+
*/
|
|
166
|
+
createBulk(input: CreateBulkCampaignInput): Promise<BulkCampaignResult>;
|
|
167
|
+
/**
|
|
168
|
+
* Creates a campaign whose recipients are split into buckets, each sent through
|
|
169
|
+
* a different channel or rendered with a different message.
|
|
170
|
+
*
|
|
171
|
+
* ```ts
|
|
172
|
+
* const { campaign, buckets, receivers } = await crm.campaigns.createGroup({
|
|
173
|
+
* name: 'Diwali offer',
|
|
174
|
+
* distributeBy: 'channel',
|
|
175
|
+
* buckets: [
|
|
176
|
+
* { channel: channelA, audience: { rows: north, recipientColumn: 'phone' } },
|
|
177
|
+
* { channel: channelB, audience: { rows: south, recipientColumn: 'phone' } },
|
|
178
|
+
* ],
|
|
179
|
+
* messages: [{ channel: { id: channelA }, message: template }],
|
|
180
|
+
* });
|
|
181
|
+
* ```
|
|
182
|
+
*
|
|
183
|
+
* ## How this differs from `createBulk`
|
|
184
|
+
*
|
|
185
|
+
* Three ways, and each of them is a thing that would be wrong if you built the
|
|
186
|
+
* payload by hand:
|
|
187
|
+
*
|
|
188
|
+
* - **One file per bucket, not one per campaign.** Each bucket is encrypted
|
|
189
|
+
* under its own secret and uploaded separately, and `filters` carries one
|
|
190
|
+
* entry per bucket. This is the only audience mode where `filters` has more
|
|
191
|
+
* than one element.
|
|
192
|
+
* - **A different file format.** A bucket file is a `||`-delimited table with a
|
|
193
|
+
* header row, not the JSON array a bulk file holds. The reading side chooses
|
|
194
|
+
* by campaign type, so the wrong encoding decrypts cleanly and then parses to
|
|
195
|
+
* nothing.
|
|
196
|
+
* - **Columns are fixed by the first recipient.** A table has one header, so
|
|
197
|
+
* every row carries the same variables in the same order. A bucket whose rows
|
|
198
|
+
* have differing columns will have the missing ones filled in as empty.
|
|
199
|
+
*
|
|
200
|
+
* Everything else is the same: numbers are reduced to digits, CSV is parsed
|
|
201
|
+
* with quoted fields honoured, duplicates and unusable rows are dropped and
|
|
202
|
+
* counted -- per bucket, and summed on the result.
|
|
203
|
+
*
|
|
204
|
+
* ## Empty buckets are dropped, not refused
|
|
205
|
+
*
|
|
206
|
+
* A bucket whose audience resolves to nobody is left out of `filters`
|
|
207
|
+
* altogether, matching the panel: distributing a list across six channels
|
|
208
|
+
* routinely leaves one or two with nothing in them, and that is not an error.
|
|
209
|
+
* The call fails only when **every** bucket is empty.
|
|
210
|
+
*
|
|
211
|
+
* De-duplication is per bucket. A number appearing in two buckets is sent to
|
|
212
|
+
* twice -- which is usually the intent, since the buckets are different
|
|
213
|
+
* channels, and is not something this can second-guess.
|
|
214
|
+
*/
|
|
215
|
+
createGroup(input: CreateGroupCampaignInput): Promise<GroupCampaignResult>;
|
|
216
|
+
/**
|
|
217
|
+
* Encrypts and uploads a receiver list, answering with the audience filter it
|
|
218
|
+
* produced.
|
|
219
|
+
*
|
|
220
|
+
* The file half of {@link createBulk}, exposed because the two halves can fail
|
|
221
|
+
* for unrelated reasons. Holding the filter lets you retry a failed campaign
|
|
222
|
+
* creation -- or create several campaigns against one uploaded list -- without
|
|
223
|
+
* re-uploading, which matters when the list is large.
|
|
224
|
+
*
|
|
225
|
+
* The returned `hash` is the decryption secret. Keep it with the URL or the
|
|
226
|
+
* file becomes unreadable; there is no copy of it anywhere else.
|
|
227
|
+
*/
|
|
228
|
+
uploadReceivers(receivers: readonly CampaignReceiver[], signal?: AbortSignal,
|
|
229
|
+
/**
|
|
230
|
+
* How the list is encoded inside the file. `json` for a bulk campaign's
|
|
231
|
+
* single file, `table` for a group campaign's bucket. The reading side picks
|
|
232
|
+
* by campaign type, so this has to match the campaign it is for.
|
|
233
|
+
*/
|
|
234
|
+
encoding?: ReceiverEncoding): Promise<CampaignNumbersFilter>;
|
|
235
|
+
update(id: string, input: UpdateCampaignInput): Promise<Campaign>;
|
|
236
|
+
schedule(id: string, scheduledAt: string): Promise<Campaign>;
|
|
237
|
+
cancel(id: string): Promise<Campaign>;
|
|
238
|
+
delete(id: string): Promise<void>;
|
|
239
|
+
}
|