ab-ecosystem-cpass 1.0.0 → 1.0.6

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 (86) hide show
  1. package/README.md +121 -21
  2. package/dist/client.d.ts +38 -0
  3. package/dist/client.js +49 -5
  4. package/dist/client.js.map +1 -1
  5. package/dist/http.d.ts +15 -0
  6. package/dist/http.js +21 -1
  7. package/dist/http.js.map +1 -1
  8. package/dist/index.d.ts +11 -0
  9. package/dist/index.js +24 -1
  10. package/dist/index.js.map +1 -1
  11. package/dist/internal/bytes.d.ts +38 -0
  12. package/dist/internal/bytes.js +84 -0
  13. package/dist/internal/bytes.js.map +1 -0
  14. package/dist/internal/device.d.ts +24 -0
  15. package/dist/internal/device.js +91 -0
  16. package/dist/internal/device.js.map +1 -0
  17. package/dist/internal/md5.d.ts +4 -0
  18. package/dist/internal/md5.js +109 -0
  19. package/dist/internal/md5.js.map +1 -0
  20. package/dist/internal/receiver-file.d.ts +106 -0
  21. package/dist/internal/receiver-file.js +128 -0
  22. package/dist/internal/receiver-file.js.map +1 -0
  23. package/dist/realtime/client.d.ts +85 -0
  24. package/dist/realtime/client.js +126 -0
  25. package/dist/realtime/client.js.map +1 -0
  26. package/dist/realtime/events.d.ts +147 -0
  27. package/dist/realtime/events.js +3 -0
  28. package/dist/realtime/events.js.map +1 -0
  29. package/dist/realtime/inbox.d.ts +173 -0
  30. package/dist/realtime/inbox.js +263 -0
  31. package/dist/realtime/inbox.js.map +1 -0
  32. package/dist/realtime/index.d.ts +16 -0
  33. package/dist/realtime/index.js +37 -0
  34. package/dist/realtime/index.js.map +1 -0
  35. package/dist/resources/audience/contacts.d.ts +19 -3
  36. package/dist/resources/audience/contacts.js +19 -3
  37. package/dist/resources/audience/contacts.js.map +1 -1
  38. package/dist/resources/audience/inbox-media.d.ts +92 -0
  39. package/dist/resources/audience/inbox-media.js +127 -0
  40. package/dist/resources/audience/inbox-media.js.map +1 -0
  41. package/dist/resources/audience/notifications.d.ts +171 -0
  42. package/dist/resources/audience/notifications.js +215 -0
  43. package/dist/resources/audience/notifications.js.map +1 -0
  44. package/dist/resources/audience/scheduled-messages.d.ts +234 -0
  45. package/dist/resources/audience/scheduled-messages.js +259 -0
  46. package/dist/resources/audience/scheduled-messages.js.map +1 -0
  47. package/dist/resources/campaign-audience.d.ts +132 -0
  48. package/dist/resources/campaign-audience.js +224 -0
  49. package/dist/resources/campaign-audience.js.map +1 -0
  50. package/dist/resources/campaign-report-exports.d.ts +14 -0
  51. package/dist/resources/campaign-report-exports.js +28 -0
  52. package/dist/resources/campaign-report-exports.js.map +1 -0
  53. package/dist/resources/campaign-reports.d.ts +15 -0
  54. package/dist/resources/campaign-reports.js +25 -0
  55. package/dist/resources/campaign-reports.js.map +1 -0
  56. package/dist/resources/campaigns.d.ts +239 -0
  57. package/dist/resources/campaigns.js +300 -0
  58. package/dist/resources/campaigns.js.map +1 -0
  59. package/dist/types/audience.d.ts +93 -1
  60. package/dist/types/campaign.d.ts +229 -0
  61. package/dist/types/campaign.js +3 -0
  62. package/dist/types/campaign.js.map +1 -0
  63. package/dist/types/contact-filter.d.ts +3 -2
  64. package/dist/types/contact-filter.js +3 -2
  65. package/dist/types/contact-filter.js.map +1 -1
  66. package/dist/types/inbox.d.ts +391 -0
  67. package/dist/types/inbox.js +16 -0
  68. package/dist/types/inbox.js.map +1 -0
  69. package/dist/types/index.d.ts +3 -0
  70. package/dist/types/index.js +5 -0
  71. package/dist/types/index.js.map +1 -1
  72. package/dist/types/scheduled-message.d.ts +221 -0
  73. package/dist/types/scheduled-message.js +16 -0
  74. package/dist/types/scheduled-message.js.map +1 -0
  75. package/docs/audience/contacts.md +34 -4
  76. package/docs/audience/notifications.md +263 -0
  77. package/docs/audience/scheduled-messages.md +557 -0
  78. package/docs/campaign/campaigns.md +489 -0
  79. package/docs/contact-filter.md +2 -1
  80. package/docs/inbox/chats.md +356 -0
  81. package/docs/inbox/connection.md +171 -0
  82. package/docs/inbox/media.md +202 -0
  83. package/docs/inbox/messages.md +338 -0
  84. package/docs/inbox/referrals.md +72 -0
  85. package/docs/inbox/sessions.md +74 -0
  86. package/package.json +28 -2
@@ -0,0 +1,391 @@
1
+ import type { JsonObject } from './common';
2
+ import type { Contact, ContactChatMetadata, ContactName, ContactProfile, ContactTag } from './audience';
3
+ import type { ContactFilterEntry } from './contact-filter';
4
+ /**
5
+ * Delivery state, shared by every channel kind. Providers report more than these
6
+ * -- email bounces, carrier rejections -- and each maps onto one of the five, so
7
+ * the inbox, the campaign reports and the billing logs all read one field. The
8
+ * provider's own wording survives in `error`.
9
+ */
10
+ export type MessageStatus = 'received' | 'sent' | 'delivered' | 'read' | 'failed';
11
+ /** `received` is the only inbound state, so direction is derived, not stored. */
12
+ export type MessageDirection = 'inbound' | 'outbound';
13
+ /** Who put the message on the wire, derived from the `sent_by_*` flags. */
14
+ export type MessageSource = 'contact' | 'agent' | 'bot' | 'campaign' | 'api' | 'business_app';
15
+ /**
16
+ * The message body, as the channel describes it.
17
+ *
18
+ * Open by design -- a WhatsApp interactive reply, an email part tree and an RCS
19
+ * card share no structure worth forcing into one schema. Only `type` is promised,
20
+ * and by convention the body for that kind sits under a key of the same name:
21
+ * `{ type: 'image', image: { id, link } }`.
22
+ *
23
+ * Narrow it yourself where you know the channel: `Message<MyWhatsAppContent>`.
24
+ */
25
+ export interface MessageContent extends JsonObject {
26
+ type: string;
27
+ }
28
+ /** Per-status unix seconds. `0` means the message never reached that state. */
29
+ export interface MessageReceipts {
30
+ sent: number;
31
+ read: number;
32
+ failed: number;
33
+ replied: number;
34
+ received: number;
35
+ delivered: number;
36
+ }
37
+ export interface Message<C extends MessageContent = MessageContent, X = JsonObject> {
38
+ id: string;
39
+ contact: string;
40
+ /** Channel it travelled over. Lives in the channels service, so an id only. */
41
+ channel: string;
42
+ content: C;
43
+ /** Provider context: the replied-to id, forwarding flags, the CTWA referral. */
44
+ context: X;
45
+ /** The contact's identity on this channel -- phone number, handle, address. */
46
+ identifier: string;
47
+ /**
48
+ * The provider's id (a `wamid` for WhatsApp), unique across the account. An
49
+ * outbound message carries its queue id here until the provider answers.
50
+ */
51
+ message_id: string;
52
+ status: MessageStatus;
53
+ /** Unix seconds as the provider reported it. Thread order is by this. */
54
+ timestamp: number;
55
+ direction: MessageDirection;
56
+ source: MessageSource;
57
+ sent_by_api: boolean;
58
+ sent_by_biz_app: boolean;
59
+ sent_by_campaign: boolean;
60
+ sent_by_bot: boolean;
61
+ agent: string | null;
62
+ receipts: MessageReceipts;
63
+ meta: {
64
+ bookmarked: boolean;
65
+ hide: boolean;
66
+ };
67
+ campaign: {
68
+ id: string | null;
69
+ cmid: number;
70
+ };
71
+ bot: {
72
+ id: string | null;
73
+ node: string;
74
+ };
75
+ error: string;
76
+ createdAt: string;
77
+ updatedAt: string;
78
+ }
79
+ /** Media kinds whose bytes the platform fetches from the provider and re-hosts. */
80
+ export type MediaContentType = 'image' | 'video' | 'audio' | 'document' | 'voice' | 'sticker';
81
+ export interface ListMessagesPayload {
82
+ /** Contact whose thread to read. */
83
+ id: string;
84
+ /** Default 20, max 100. */
85
+ limit?: number;
86
+ /** `id` of the oldest message already held; older ones follow. */
87
+ last_message_id?: string;
88
+ }
89
+ export interface FetchMessagePayload {
90
+ /** The provider id, not the document id. */
91
+ message_id: string;
92
+ }
93
+ export interface SendMessagePayload {
94
+ channel: string;
95
+ contact: {
96
+ id: string;
97
+ identifier: string;
98
+ };
99
+ content: MessageContent;
100
+ context?: JsonObject;
101
+ /** Attribute to another member. Needs `live_chat.send.AS_OTHER`. */
102
+ agent?: string;
103
+ }
104
+ export interface DeleteMessagePayload {
105
+ /** Contact whose thread the message belongs to. */
106
+ id: string;
107
+ /** Either the document id or the provider id. */
108
+ message_id: string;
109
+ }
110
+ export interface RetryDownloadPayload {
111
+ /** The document id -- a retry targets a row the platform already stored. */
112
+ message_id: string;
113
+ }
114
+ export interface MessageDeleteResult {
115
+ success: boolean;
116
+ id: string;
117
+ }
118
+ /**
119
+ * The inbox's view of a contact: the chat-relevant projection, not the whole
120
+ * contact. Every chat event answers with this, so one shape updates the list row
121
+ * and the conversation header together.
122
+ */
123
+ export interface Chat {
124
+ id: string;
125
+ name: ContactName;
126
+ profile: Record<string, ContactProfile>;
127
+ chat_metadata: ContactChatMetadata;
128
+ /** Unix seconds of the last inbound message. The list is ordered by this. */
129
+ last_seen: number;
130
+ /** Flow state. Open-ended on the stored schema, carried through as-is. */
131
+ bot: JsonObject;
132
+ /** Last-message preview, so a list of chats needs no thread reads. */
133
+ last_interaction_props: JsonObject;
134
+ agent: string | null;
135
+ team: string | null;
136
+ tags: ContactTag[];
137
+ /** Present on list rows; the chat header renders them. */
138
+ custom_field_hash?: JsonObject;
139
+ }
140
+ /**
141
+ * The inbox's own list filter. Every key is independent and omitting one means
142
+ * "don't care", so an empty filter is every live chat.
143
+ */
144
+ export interface ChatListFilter {
145
+ /** `all` every chat, `me` the caller's own, `none` the unassigned queue. */
146
+ assignment?: 'all' | 'me' | 'none';
147
+ /** `unread` has unseen messages or a by-hand flag; `replied` is caught up. */
148
+ unread?: 'unread' | 'replied';
149
+ new?: 'new' | 'old';
150
+ /** `closed` covers closed and marked-done alike; `open` is neither. */
151
+ closed?: 'closed' | 'open';
152
+ archived?: 'archived' | 'unarchived';
153
+ /** `active_no_bot` is running with no flow attached. */
154
+ automation?: 'active' | 'paused' | 'active_no_bot';
155
+ block?: 'ic' | 'og' | 'api' | 'bot' | 'unblocked';
156
+ /** `active` means seen inside the last 24 hours -- the provider reply window. */
157
+ session?: 'active' | 'expired';
158
+ }
159
+ export interface ListChatsPayload {
160
+ filter?: ChatListFilter;
161
+ /** Conditions in the same DSL the contact list and segments use. */
162
+ conditions?: ContactFilterEntry[];
163
+ search?: string;
164
+ /** `last_seen` of the oldest row already held; omit for the first page. */
165
+ nextPage?: number;
166
+ /** Default 20, max 100. */
167
+ limit?: number;
168
+ department?: string;
169
+ team_member?: string;
170
+ }
171
+ export interface ChatList {
172
+ contacts: Chat[];
173
+ /**
174
+ * The filter after permission narrowing. A team member without the unassigned
175
+ * permission asking for `all` gets `me` back, so the UI can show what it got.
176
+ */
177
+ filter: ChatListFilter;
178
+ /** Send as `nextPage` for the following page; `null` at the end. */
179
+ nextPage: number | null;
180
+ }
181
+ /** Every action `chat.action` accepts. */
182
+ export type ChatActionName = 'delete' | 'transfer' | 'bulk_transfer' | 'archive' | 'unarchive' | 'block' | 'mark_as_read' | 'mark_as_unread' | 'pause_automation' | 'resume_automation' | 'close' | 'done' | 'pause_timer' | 'update_tags';
183
+ /** The blocked flags; send only the ones to change. */
184
+ export interface ChatBlockUpdate {
185
+ incoming?: boolean;
186
+ outgoing?: boolean;
187
+ bot?: boolean;
188
+ api?: boolean;
189
+ }
190
+ /** `chat` names the target. `id` is accepted as an alias on every action. */
191
+ interface ChatTarget {
192
+ chat?: string;
193
+ id?: string;
194
+ }
195
+ export type ChatActionPayload = (ChatTarget & {
196
+ action: 'delete';
197
+ }) | (ChatTarget & {
198
+ action: 'transfer';
199
+ member: string;
200
+ }) | (ChatTarget & {
201
+ action: 'archive';
202
+ }) | (ChatTarget & {
203
+ action: 'unarchive';
204
+ }) | (ChatTarget & {
205
+ action: 'block';
206
+ update: ChatBlockUpdate;
207
+ }) | (ChatTarget & {
208
+ action: 'mark_as_read';
209
+ }) | (ChatTarget & {
210
+ action: 'mark_as_unread';
211
+ }) | (ChatTarget & {
212
+ action: 'pause_automation';
213
+ }) | (ChatTarget & {
214
+ action: 'resume_automation';
215
+ }) | (ChatTarget & {
216
+ action: 'close';
217
+ }) | (ChatTarget & {
218
+ action: 'done';
219
+ isEnded?: boolean;
220
+ }) | (ChatTarget & {
221
+ action: 'pause_timer';
222
+ duration: number;
223
+ }) | (ChatTarget & {
224
+ action: 'update_tags';
225
+ tags: string[];
226
+ }) | {
227
+ action: 'bulk_transfer';
228
+ /** Member to take chats from, or `UNASSIGN` for the unassigned queue. */
229
+ source: string | 'UNASSIGN';
230
+ /** Member or team to hand them to, or `UNASSIGN` to clear it. */
231
+ target: string | 'UNASSIGN';
232
+ target_type?: 'member' | 'department';
233
+ };
234
+ /** The envelope every chat action answers and broadcasts with. */
235
+ export interface ChatActionResult {
236
+ action: ChatActionName;
237
+ id: string;
238
+ /** `null` for `delete`: the row has left the list, so there is nothing to draw. */
239
+ chat?: Chat | null;
240
+ }
241
+ export interface BulkTransferResult {
242
+ action: 'bulk_transfer';
243
+ count: number;
244
+ chats: ChatActionResult[];
245
+ }
246
+ export interface UpdateChatPayload {
247
+ id: string;
248
+ action: 'done' | 'archive';
249
+ }
250
+ /** `contact.update` answers with the metadata alone. */
251
+ export interface UpdateChatResult {
252
+ id: string;
253
+ action: 'done' | 'archive';
254
+ chat_metadata?: ContactChatMetadata;
255
+ }
256
+ export interface AssignBotPayload {
257
+ chat: string;
258
+ channel: string;
259
+ bot: string;
260
+ node?: string;
261
+ /**
262
+ * `assign` attaches the flow and resumes automation. `trigger` hands the chat
263
+ * to the bot server, which decides where in the flow it belongs.
264
+ */
265
+ action?: 'assign' | 'trigger';
266
+ }
267
+ /** A click-to-WhatsApp referral: the ad payload behind a conversation. */
268
+ /**
269
+ * A delivery state moved on a message already on screen.
270
+ *
271
+ * Its own push rather than another `message.received`, because a client updates
272
+ * the ticks in place and does not want to re-render the bubble. The row is the
273
+ * stored document as the sender service wrote it, so `_id` is a string and
274
+ * `receipts` carries the unix second each state was reached.
275
+ */
276
+ export interface MessageStatusUpdate extends JsonObject {
277
+ /** The provider's message id -- what an echoed bubble's queue id is replaced by. */
278
+ message_id: string;
279
+ status: MessageStatus;
280
+ /** Owner the row belongs to. */
281
+ user: string;
282
+ contact?: string;
283
+ receipts?: Partial<MessageReceipts>;
284
+ /** Provider text when `status` is `failed`. */
285
+ error?: string;
286
+ }
287
+ /** Media kinds a chat message can carry. */
288
+ export type MediaKind = 'image' | 'video' | 'audio' | 'voice' | 'sticker' | 'document';
289
+ /**
290
+ * Per-kind ceilings, in bytes. WhatsApp's limits, enforced when the upload is
291
+ * requested rather than when the message is sent.
292
+ */
293
+ export declare const MEDIA_LIMITS: Record<MediaKind, number>;
294
+ /** What `POST /v1/audience/inbox/media/upload-url` takes. Metadata, not bytes. */
295
+ export interface CreateMediaUploadPayload {
296
+ kind: MediaKind;
297
+ /** Only its extension and display name survive; the stored key is a uuid. */
298
+ filename: string;
299
+ content_type: string;
300
+ /** Exact byte length. Signed into the ticket, so the upload must match. */
301
+ size: number;
302
+ }
303
+ /** A presigned upload. `PUT` the bytes to `upload_url` with exactly `headers`. */
304
+ export interface MediaUpload {
305
+ upload_url: string;
306
+ /** Every header the signature covers. Anything else fails the upload. */
307
+ headers: Record<string, string>;
308
+ /**
309
+ * Where the object is readable once the upload succeeds. Permanent -- only
310
+ * `upload_url` expires, and only if unused.
311
+ */
312
+ link: string;
313
+ key: string;
314
+ kind: MediaKind;
315
+ filename: string;
316
+ content_type: string;
317
+ size: number;
318
+ /** Unix seconds after which `upload_url` stops working. */
319
+ expires_at: number;
320
+ expires_in: number;
321
+ }
322
+ /**
323
+ * The bytes `sendMedia` can take.
324
+ *
325
+ * A browser `File` or `Blob`, or a description of bytes for Node, where neither
326
+ * exists. Structurally the same thing `crm.inboxMedia.upload()` accepts, declared
327
+ * here so the realtime entrypoint does not have to import the REST one.
328
+ */
329
+ export type MediaFile = Blob | {
330
+ body: unknown;
331
+ filename: string;
332
+ contentType: string;
333
+ size: number;
334
+ };
335
+ /**
336
+ * Uploads a file and answers with its permanent link.
337
+ *
338
+ * Satisfied by `crm.inboxMedia.upload`. Declared as a function so the socket
339
+ * client takes a capability rather than a dependency on the REST client: an app
340
+ * that only ever sends a `link` pulls none of it in.
341
+ */
342
+ export type MediaUploader = (file: MediaFile, options: {
343
+ kind: MediaKind;
344
+ filename?: string;
345
+ }) => Promise<string>;
346
+ /**
347
+ * What `sendMedia` takes: a chat, a kind, and either a link or a file.
348
+ *
349
+ * `link` and `file` are alternatives -- a link is sent as it is, a file is
350
+ * uploaded first and needs the `Inbox` to have been given an uploader.
351
+ */
352
+ export interface SendMediaPayload {
353
+ channel: string;
354
+ contact: {
355
+ id: string;
356
+ identifier: string;
357
+ };
358
+ kind: MediaKind;
359
+ /** Already uploaded. Takes precedence over `file`. */
360
+ link?: string;
361
+ /** Uploaded before sending. Requires `uploadMedia` on the Inbox. */
362
+ file?: MediaFile;
363
+ /** Shown under the media. Ignored for a sticker, which takes none. */
364
+ caption?: string;
365
+ /** What the recipient sees for a document. Also names the uploaded object. */
366
+ filename?: string;
367
+ context?: JsonObject;
368
+ /** Attribute to another member. Needs `live_chat.send.AS_OTHER`. */
369
+ agent?: string;
370
+ }
371
+ export interface CtwaReferral {
372
+ id: string;
373
+ contact: string;
374
+ channel: string | null;
375
+ /** The provider's referral object. `payload.source_id` is the ad id. */
376
+ payload: JsonObject;
377
+ createdAt: string;
378
+ updatedAt: string;
379
+ }
380
+ /**
381
+ * When a contact was last seen on one channel. Providers only allow unprompted
382
+ * replies inside 24 hours of the last inbound message, and a contact reachable on
383
+ * several channels has one such window per channel.
384
+ */
385
+ export interface ChatSession {
386
+ id: string;
387
+ channel: string;
388
+ last_seen: number;
389
+ }
390
+ /** Re-exported so a consumer importing `Chat` need not also reach for `Contact`. */
391
+ export type { Contact };
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MEDIA_LIMITS = void 0;
4
+ /**
5
+ * Per-kind ceilings, in bytes. WhatsApp's limits, enforced when the upload is
6
+ * requested rather than when the message is sent.
7
+ */
8
+ exports.MEDIA_LIMITS = {
9
+ image: 5 * 1024 * 1024,
10
+ video: 16 * 1024 * 1024,
11
+ audio: 16 * 1024 * 1024,
12
+ voice: 16 * 1024 * 1024,
13
+ sticker: 100 * 1024,
14
+ document: 100 * 1024 * 1024,
15
+ };
16
+ //# sourceMappingURL=inbox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inbox.js","sourceRoot":"","sources":["../../src/types/inbox.ts"],"names":[],"mappings":";;;AAoTA;;;GAGG;AACU,QAAA,YAAY,GAA8B;IACrD,KAAK,EAAE,CAAC,GAAG,IAAI,GAAG,IAAI;IACtB,KAAK,EAAE,EAAE,GAAG,IAAI,GAAG,IAAI;IACvB,KAAK,EAAE,EAAE,GAAG,IAAI,GAAG,IAAI;IACvB,KAAK,EAAE,EAAE,GAAG,IAAI,GAAG,IAAI;IACvB,OAAO,EAAE,GAAG,GAAG,IAAI;IACnB,QAAQ,EAAE,GAAG,GAAG,IAAI,GAAG,IAAI;CAC5B,CAAC"}
@@ -2,3 +2,6 @@ export * from './common';
2
2
  export * from './contact-filter';
3
3
  export * from './audience';
4
4
  export * from './channels';
5
+ export * from './inbox';
6
+ export * from './scheduled-message';
7
+ export * from './campaign';
@@ -18,4 +18,9 @@ __exportStar(require("./common"), exports);
18
18
  __exportStar(require("./contact-filter"), exports);
19
19
  __exportStar(require("./audience"), exports);
20
20
  __exportStar(require("./channels"), exports);
21
+ __exportStar(require("./inbox"), exports);
22
+ // After './inbox': a scheduled message's body is a `MessageContent`.
23
+ __exportStar(require("./scheduled-message"), exports);
24
+ // After './contact-filter': a campaign's audience may be a list of its entries.
25
+ __exportStar(require("./campaign"), exports);
21
26
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,2CAAyB;AACzB,mDAAiC;AACjC,6CAA2B;AAC3B,6CAA2B"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,2CAAyB;AACzB,mDAAiC;AACjC,6CAA2B;AAC3B,6CAA2B;AAC3B,0CAAwB;AACxB,qEAAqE;AACrE,sDAAoC;AACpC,gFAAgF;AAChF,6CAA2B"}
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Scheduled messages: a send with a time, and optionally a condition checked
3
+ * immediately before it goes out.
4
+ *
5
+ * The condition is what makes a drip safe to set and forget. A sequence of five
6
+ * messages over two weeks is five rows; each one re-reads the contact a moment
7
+ * before sending and stands down if the contact no longer matches. So "stop
8
+ * messaging a lead once they convert" is a field on the contact changing, not a
9
+ * job anybody has to remember to cancel.
10
+ *
11
+ * Declared here rather than imported from `@crm/contracts` so the published
12
+ * package has no private dependencies; kept in step with the zod schemas.
13
+ */
14
+ import type { ContactFilter } from './contact-filter';
15
+ import type { IsoDate, JsonObject, ListQuery, ObjectId } from './common';
16
+ import type { MessageContent } from './inbox';
17
+ /** Units a schedule's spacing is expressed in. */
18
+ export type ScheduleUnit = 'minutes' | 'hours' | 'days' | 'weeks' | 'months';
19
+ /**
20
+ * Where one occurrence ended up.
21
+ *
22
+ * | Status | Meaning |
23
+ * | --- | --- |
24
+ * | `pending` | Waiting for its moment. The only status that can be rescheduled. |
25
+ * | `processing` | Claimed by the scheduler, mid-send. |
26
+ * | `sent` | Handed to the channel. Delivery is then a message status, not this. |
27
+ * | `skipped` | Its condition did not match. `result` says what was expected and what was found. |
28
+ * | `cancelled` | Stood down -- by a caller, or by an earlier occurrence failing its condition under `cancel_series`. |
29
+ * | `failed` | Could not be sent: the contact is gone, trashed, blocked for outbound, or has no channel identity. `result.reason` says which. |
30
+ */
31
+ export type ScheduledMessageStatus = 'pending' | 'processing' | 'sent' | 'skipped' | 'failed' | 'cancelled';
32
+ /**
33
+ * What a failed condition does to the rest of the series.
34
+ *
35
+ * - `skip` -- this occurrence only. Later ones still run and are checked again,
36
+ * so a contact who leaves and re-enters the condition resumes receiving.
37
+ * - `cancel_series` -- this occurrence and every later one. The default for a
38
+ * series, because a converted lead should drop out of the sequence for good
39
+ * rather than re-enter it if someone edits their stage back.
40
+ */
41
+ export type OnConditionFail = 'skip' | 'cancel_series';
42
+ /** A step's distance from the series start. Cumulative, never relative to the previous step. */
43
+ export interface ScheduleOffset {
44
+ value: number;
45
+ unit: ScheduleUnit;
46
+ }
47
+ /**
48
+ * One message in a sequence, with its own copy.
49
+ *
50
+ * `after` is measured from the series' `starts_at`, so editing one step's delay
51
+ * does not shift the steps behind it.
52
+ */
53
+ export interface ScheduledMessageStep {
54
+ after: ScheduleOffset;
55
+ content: MessageContent;
56
+ context?: JsonObject;
57
+ /**
58
+ * Narrows this step beyond the series condition. Both must pass. Use it for a
59
+ * step that needs an extra guard -- a final discount offer only if the contact
60
+ * has not already bought -- and leave it out to inherit the series condition
61
+ * alone.
62
+ */
63
+ conditions?: ContactFilter | null;
64
+ }
65
+ /** Expands into evenly spaced occurrences, all carrying the same message. */
66
+ export interface ScheduleRepeat {
67
+ /** Spacing between occurrences. Must be at least 1. */
68
+ every: number;
69
+ unit: ScheduleUnit;
70
+ /**
71
+ * Inclusive end. The last occurrence is the final one at or before this
72
+ * instant, so `starts_at` + `every: 3, unit: 'days'` + `until` two weeks out
73
+ * produces five sends, not four or six.
74
+ */
75
+ until: IsoDate;
76
+ }
77
+ /**
78
+ * Why an occurrence did not send, or how it did.
79
+ *
80
+ * Only the first failing condition is named: listing every one of them would be
81
+ * noise when the first already explains the skip.
82
+ */
83
+ export interface ScheduledMessageResult {
84
+ /**
85
+ * `queued` | `condition_not_met` | `series_cancelled_by_condition` |
86
+ * `cancelled_by_user` | `contact_missing` | `contact_trashed` |
87
+ * `outgoing_blocked` | `no_channel_identity`
88
+ */
89
+ reason?: string;
90
+ /** For `condition_not_met`: which condition refused -- the series' or the step's. */
91
+ scope?: 'series' | 'step';
92
+ /** For `condition_not_met`: the first condition that failed, and what it wanted. */
93
+ field_id?: string;
94
+ condition?: string;
95
+ expected?: unknown;
96
+ /** For `series_cancelled_by_condition`: the occurrence whose condition failed. */
97
+ at_occurrence?: number;
98
+ /** Anything else the server recorded. */
99
+ error?: string;
100
+ }
101
+ export interface ScheduledMessage {
102
+ id: ObjectId;
103
+ /** The series this belongs to, or `null` for a one-off. */
104
+ series: ObjectId | null;
105
+ /** 1-based position in the series. `1` for a one-off. */
106
+ occurrence: number;
107
+ title: string;
108
+ contact: ObjectId;
109
+ /** Channel it will go out on. Lives in the channels service, so an id only. */
110
+ channel: ObjectId;
111
+ content: MessageContent;
112
+ context: JsonObject;
113
+ /** IANA zone the schedule was expressed in, e.g. `Asia/Kolkata`. */
114
+ timezone: string;
115
+ /** The resolved instant, always UTC. This is what the scheduler fires on. */
116
+ scheduled_at: IsoDate;
117
+ /** The same moment as wall-clock in `timezone`, with no offset -- what the user chose. */
118
+ scheduled_at_local: string;
119
+ /** The series condition. `null` means send unconditionally. */
120
+ conditions: ContactFilter | null;
121
+ /**
122
+ * This occurrence's own extra narrowing, from its step. Both this and
123
+ * `conditions` have to pass.
124
+ */
125
+ step_conditions: ContactFilter | null;
126
+ on_condition_fail: OnConditionFail;
127
+ status: ScheduledMessageStatus;
128
+ result: ScheduledMessageResult | null;
129
+ /** When the scheduler claimed it. */
130
+ processed_at: IsoDate | null;
131
+ /** When it reached a terminal status. */
132
+ completed_at: IsoDate | null;
133
+ createdAt: IsoDate;
134
+ updatedAt: IsoDate;
135
+ }
136
+ /** A series and the occurrences it expanded into. */
137
+ export interface ScheduledMessageSeries {
138
+ series: ObjectId;
139
+ title: string;
140
+ contact: ObjectId;
141
+ channel: ObjectId;
142
+ timezone: string;
143
+ conditions: ContactFilter | null;
144
+ on_condition_fail: OnConditionFail;
145
+ /** In send order. One row per message, each independently cancellable. */
146
+ occurrences: ScheduledMessage[];
147
+ }
148
+ /** What every create shares. */
149
+ interface ScheduledMessageBase {
150
+ /** Shown in the schedule list. Defaults to `'Untitled'`. */
151
+ title?: string;
152
+ channel: ObjectId;
153
+ contact: {
154
+ id: ObjectId;
155
+ };
156
+ /**
157
+ * IANA zone name -- `Asia/Kolkata`, `Europe/London`. Not an abbreviation and
158
+ * not an offset: `IST` and `+05:30` are rejected.
159
+ *
160
+ * Required. When the time below carries no offset it is read as wall-clock in
161
+ * this zone; when it does, the instant is already pinned and this is kept for
162
+ * display and for later edits.
163
+ */
164
+ timezone: string;
165
+ /**
166
+ * Checked against the contact immediately before each send. Omit to always
167
+ * send.
168
+ *
169
+ * Same DSL as `contacts.list({ filter })` and a saved `Segment` -- see the
170
+ * contact filter page. A custom field is addressed by its **slug**.
171
+ */
172
+ conditions?: ContactFilter | null;
173
+ /** Default `skip` for a one-off, `cancel_series` for a series. */
174
+ on_condition_fail?: OnConditionFail;
175
+ }
176
+ /** One message, one time. */
177
+ export interface CreateScheduledMessageInput extends ScheduledMessageBase {
178
+ /**
179
+ * `2026-10-15T10:00:00` is 10:00 in `timezone`. `2026-10-15T04:30:00Z` is that
180
+ * instant regardless of it. Must be in the future.
181
+ */
182
+ scheduled_at: IsoDate;
183
+ content: MessageContent;
184
+ context?: JsonObject;
185
+ }
186
+ /** A sequence with different copy per step. */
187
+ export interface CreateScheduledSeriesInput extends ScheduledMessageBase {
188
+ /** The first step's moment; every `after` is measured from here. */
189
+ starts_at: IsoDate;
190
+ /** 1..365 steps, in any order -- they are sorted by `after` before expansion. */
191
+ steps: ScheduledMessageStep[];
192
+ }
193
+ /** A sequence repeating one message on a fixed cadence. */
194
+ export interface CreateRepeatingScheduleInput extends ScheduledMessageBase {
195
+ starts_at: IsoDate;
196
+ repeat: ScheduleRepeat;
197
+ content: MessageContent;
198
+ context?: JsonObject;
199
+ }
200
+ /** Moves a `pending` occurrence. Anything else is a 409. */
201
+ export interface RescheduleMessageInput {
202
+ scheduled_at: IsoDate;
203
+ /** Re-reads `scheduled_at` in a different zone. Defaults to the one already stored. */
204
+ timezone?: string;
205
+ title?: string;
206
+ }
207
+ export interface ListScheduledMessagesQuery extends ListQuery {
208
+ /** Everything queued for one contact. */
209
+ contact?: ObjectId;
210
+ /** One drip's occurrences. */
211
+ series?: ObjectId;
212
+ status?: ScheduledMessageStatus;
213
+ channel?: ObjectId;
214
+ }
215
+ /** What `cancelSeries` answers with. */
216
+ export interface CancelSeriesResult {
217
+ series: ObjectId;
218
+ /** How many `pending` occurrences were stood down. Already-sent ones are untouched. */
219
+ cancelled: number;
220
+ }
221
+ export {};
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ /**
3
+ * Scheduled messages: a send with a time, and optionally a condition checked
4
+ * immediately before it goes out.
5
+ *
6
+ * The condition is what makes a drip safe to set and forget. A sequence of five
7
+ * messages over two weeks is five rows; each one re-reads the contact a moment
8
+ * before sending and stands down if the contact no longer matches. So "stop
9
+ * messaging a lead once they convert" is a field on the contact changing, not a
10
+ * job anybody has to remember to cancel.
11
+ *
12
+ * Declared here rather than imported from `@crm/contracts` so the published
13
+ * package has no private dependencies; kept in step with the zod schemas.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ //# sourceMappingURL=scheduled-message.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scheduled-message.js","sourceRoot":"","sources":["../../src/types/scheduled-message.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;GAYG"}