@ikatec/digisac-api-sdk 4.7.0 → 4.9.0

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.
@@ -1,6 +1,7 @@
1
+ import type { ListQuery } from '../../core'
2
+ import type { Account } from '../accounts'
1
3
  import type { Department } from '../departments'
2
4
  import type { File } from '../files'
3
- import type { Message } from '../messages'
4
5
  import type { Service } from '../services'
5
6
  import type { Tag } from '../tags'
6
7
  import type { User } from '../users'
@@ -22,10 +23,13 @@ export type CampaignStatus =
22
23
  | 'preventive_break'
23
24
 
24
25
  export type CampaignConfig = {
25
- /** Minimum interval in ms between sends. */
26
- minInterval: number
27
- /** Maximum interval in ms between sends. */
28
- maxInterval: number
26
+ /**
27
+ * Minimum interval in ms between sends. Absent until set (a new campaign answers `config: {}`);
28
+ * `setIntervalById` stores a number, `updateById` stores what it is sent (a string stays a string).
29
+ */
30
+ minInterval?: number | string
31
+ /** Maximum interval in ms between sends. Same rules as `minInterval`. */
32
+ maxInterval?: number | string
29
33
  /** Column name in the import file used as the contact name. */
30
34
  nameField?: string
31
35
  /** Column name in the import file used as the phone number. */
@@ -50,11 +54,19 @@ export type Campaign = {
50
54
  totalContacts: number | null
51
55
  totalContactsImported: number | null
52
56
  totalValidContacts: number | null
53
- sentContacts: number | null
54
- viewedContacts: number | null
55
- contactCount: number | null
57
+ /** @deprecated Never answered: the campaign has no such column. */
58
+ sentContacts?: number | null
59
+ /** @deprecated Never answered: the campaign has no such column. */
60
+ viewedContacts?: number | null
61
+ /** @deprecated Never answered: read `getContactCount`. */
62
+ contactCount?: number | null
56
63
  mustOpenTicket: boolean
57
- marketingIntegration: boolean
64
+ /** @deprecated Never answered: the campaign transformer leaves the column out. */
65
+ marketingIntegration?: boolean
66
+ /** Always `null`: the campaign has no such column (answered by the transformer). */
67
+ userId?: null
68
+ /** Only in `getMany`: messages sent and viewed (ack 3). */
69
+ stats?: { sent: number; viewed: number }
58
70
  accountId: string
59
71
  serviceId: string
60
72
  defaultDepartmentId: string | null
@@ -71,10 +83,29 @@ export type Campaign = {
71
83
  createdBy?: User | null
72
84
  sentBy?: User | null
73
85
  tags?: Tag[]
74
- messages?: Message[]
86
+ messages?: CampaignMessage[]
75
87
  importFile?: File | null
76
88
  }
77
89
 
90
+ /** A message of the campaign (`campaign_messages`), as the `messages` include answers it. */
91
+ export type CampaignMessage = {
92
+ id: string
93
+ text: string | null
94
+ extraOptions: Record<string, unknown> | null
95
+ hsmId: string | null
96
+ hsmFileId: string | null
97
+ accountId: string
98
+ campaignId: string
99
+ createdAt: string
100
+ updatedAt: string
101
+ deletedAt: string | null
102
+ // relationships (includes `messages.file`, `messages.hsmFile` and `messages.hsm`)
103
+ account?: Account
104
+ file?: File | null
105
+ hsmFile?: File | null
106
+ hsm?: WhatsappBusinessTemplate | null
107
+ }
108
+
78
109
  export type CampaignRelationships =
79
110
  | 'service'
80
111
  | 'defaultDepartment'
@@ -129,6 +160,96 @@ export type CreateCampaignPayload = {
129
160
 
130
161
  export type UpdateCampaignPayload = Partial<CreateCampaignPayload>
131
162
 
163
+ /** The visible states the `status` filter of `getMany` takes (`'scheduled'` is derived from `isScheduled` + `sendsAt`). */
164
+ export type CampaignListStatus = CampaignStatus | 'scheduled'
165
+
166
+ /**
167
+ * `getMany` query: the `ListQuery` of a campaign plus the flat filters of the Campaigns screen.
168
+ * A flat `status` takes precedence over `where.status`. An invalid `status` or date answers 400
169
+ * `{ error: 'Invalid status' | 'Invalid date', code: 'FILTER_STATUS_INVALID' | 'FILTER_DATE_INVALID' }`.
170
+ */
171
+ export type CampaignListQuery = ListQuery<Campaign> & {
172
+ serviceId?: string
173
+ status?: CampaignListStatus
174
+ /**
175
+ * Quality of the HSM template of the messages. `'HIGH'`, `'MEDIUM'` and `'LOW'` only filter when
176
+ * `include` has `messages` (the filter rewrites that include); without it they are ignored.
177
+ * `'UNKNOWN'` keeps the campaigns without a rated template.
178
+ */
179
+ templateHealth?: 'UNKNOWN' | 'HIGH' | 'MEDIUM' | 'LOW'
180
+ /** Searches the title (accent-insensitive). */
181
+ q?: string
182
+ /**
183
+ * UTC ISO date-times ending in `Z` (a date alone answers 400 `FILTER_DATE_INVALID`); the days are
184
+ * taken in the timezone of the account. `sentAt*` filter `startedAt`.
185
+ */
186
+ sentAtFrom?: string
187
+ sentAtTo?: string
188
+ createdAtFrom?: string
189
+ createdAtTo?: string
190
+ }
191
+
192
+ /** The sending status of a contact of the campaign (`not_sent` in the filter also matches `pending`). */
193
+ export type CampaignContactStatus =
194
+ | 'pending'
195
+ | 'not_sent'
196
+ | 'sent'
197
+ | 'received'
198
+ | 'read'
199
+ | 'answered'
200
+ | 'quality_hold'
201
+ | 'quality_dropped'
202
+
203
+ export type CampaignContactsQuery = {
204
+ page?: number
205
+ perPage?: number
206
+ /** Searches the name, the number and the username of the contact. */
207
+ search?: string
208
+ contactStatus?: CampaignContactStatus[]
209
+ failReason?: string
210
+ sortBy?: 'contactName' | 'contactNumber' | 'contactUsername' | 'status' | 'sentAt' | 'failReason'
211
+ sortDir?: 'ASC' | 'DESC'
212
+ }
213
+
214
+ export type CampaignContact = {
215
+ id: string
216
+ sentAt: string | null
217
+ failed: boolean
218
+ failReason: string | null
219
+ parameters: unknown
220
+ contactName: string
221
+ /** `null` for a contact known only by its username. */
222
+ contactNumber: string | null
223
+ contactUsername: string | null
224
+ contactId: string
225
+ status: CampaignContactStatus
226
+ }
227
+
228
+ export type CampaignContactsPage = {
229
+ data: CampaignContact[]
230
+ total: number
231
+ limit: number
232
+ skip: number
233
+ currentPage: number
234
+ lastPage: number
235
+ from: number
236
+ to: number
237
+ }
238
+
239
+ export type CampaignTimelineEvent = {
240
+ /** `created`, `scheduled`, `started`, `finished` or `status-<status>`. */
241
+ id: string
242
+ type: 'created' | 'scheduled' | 'started' | 'finished' | CampaignStatus
243
+ timestamp: string
244
+ /** The name of the user who created or sent the campaign, or `'Sistema'`. */
245
+ actor: string
246
+ triggerType: 'manual' | 'automatic'
247
+ }
248
+
249
+ export type ExportCampaignImportErrorsQuery = {
250
+ delimiter?: ',' | ';' | ':'
251
+ }
252
+
132
253
  export type CampaignStatsInfo = {
133
254
  totalValidContacts: number
134
255
  totalContacts: number
@@ -136,29 +257,57 @@ export type CampaignStatsInfo = {
136
257
  firedSuccessCount: number
137
258
  firedFailedCount: number
138
259
  totalMessagesCount: number
260
+ sentMessagesCount: number
139
261
  totalFiredCount: number
140
262
  firedBlockedByMessageRuleCount: number
141
263
  }
142
264
 
265
+ /** Messages of the campaign by ack: `error`, `sending` (0), `sentToServer` (1), `received` (2), `viewed` (3), `played` (4). */
143
266
  export type CampaignStatsMessages = {
144
267
  error: number
268
+ sending: number
269
+ sentToServer: number
145
270
  received: number
146
271
  viewed: number
272
+ played: number
147
273
  answered: number
148
274
  }
149
275
 
150
276
  export type CampaignStats = {
151
277
  campaignInfo: CampaignStatsInfo
152
278
  messageInfo: CampaignStatsMessages
279
+ /** @deprecated Kept by the backend for backward compatibility: read `campaignInfo.totalMessagesCount`. */
280
+ total: number
281
+ /** @deprecated Read `campaignInfo.sentMessagesCount`. */
282
+ sent: number
283
+ /** @deprecated Read `campaignInfo.firedFailedCount`. */
284
+ failed: number
285
+ /** @deprecated Read `messageInfo.sending`. */
286
+ sending: number
287
+ /** @deprecated Read `messageInfo.sentToServer`. */
288
+ sentToServer: number
289
+ /** @deprecated Read `messageInfo.received`. */
290
+ received: number
291
+ /** @deprecated Read `messageInfo.viewed`. */
292
+ viewed: number
293
+ /** @deprecated Read `messageInfo.played`. */
294
+ played: number
295
+ /** @deprecated Read `messageInfo.answered`. */
296
+ answered: number
297
+ /** @deprecated Same as `messageInfo.sentToServer`. */
298
+ sentAndNotReceived: number
153
299
  }
154
300
 
301
+ /** Whole numbers, `0` or more (numeric strings are accepted). */
155
302
  export type SetIntervalPayload = {
156
- minInterval: string
157
- maxInterval: string
303
+ minInterval: number | string
304
+ maxInterval: number | string
158
305
  }
159
306
 
160
307
  export type ExportCampaignResultPayload = {
161
- campaignId: string
308
+ where: { campaignId: string; [key: string]: unknown }
309
+ /** `'semiColon'` separates the columns with `;` (default `,`). */
310
+ type?: 'semiColon'
162
311
  }
163
312
 
164
313
  export type ExportTemplateCsvPayload = {
@@ -166,10 +315,12 @@ export type ExportTemplateCsvPayload = {
166
315
  }
167
316
 
168
317
  export type AutoPauseModePayload = {
169
- mode: string
318
+ mode: 'low-health' | 'medium-health' | 'disabled'
170
319
  }
171
320
 
172
321
  export type HsmLimit = {
173
322
  hsmLimit: number
174
323
  hsmUsedLimit: number
324
+ /** The HSM messages still available (`0` when none or the plan has no WhatsApp Business); `true` when the limit is disabled. */
325
+ hasLimit: number | boolean
175
326
  }
@@ -10,6 +10,37 @@ type InteractiveMessageButton = {
10
10
 
11
11
  type InteractiveMessageBodyFooter = string | { text: string }
12
12
 
13
+ export type ChannelRedirectChannel = 'whatsapp' | 'telegram' | 'instagram' | 'webchat'
14
+
15
+ export type ChannelRedirectIdentifierType = 'phone' | 'botName'
16
+
17
+ /**
18
+ * Digisac-only setting of a `cta_url` message: the channel its URL was built for. It sits next to
19
+ * `action`, never inside `action.parameters`, and is stripped before the message reaches Meta. The
20
+ * backend reads it to add the ticket parameters to the URL on send. A missing key means
21
+ * `{ enabled: false }`.
22
+ */
23
+ export type ChannelRedirect = {
24
+ enabled: boolean
25
+ channel?: ChannelRedirectChannel
26
+ /** `'phone'` for WhatsApp; `'phone'` or `'botName'` for Telegram; unset for the others. */
27
+ identifierType?: ChannelRedirectIdentifierType
28
+ /** Phone as `+<DDI><number>` without mask, or the Telegram bot / Instagram user name. */
29
+ identifier?: string
30
+ /** Id of the Webchat service, when `channel` is `'webchat'`. */
31
+ serviceId?: string
32
+ }
33
+
34
+ type CtaUrlAction = {
35
+ name: 'cta_url'
36
+ parameters: {
37
+ /** 1–20 characters after trim, without `{{…}}` variables. */
38
+ display_text: string
39
+ /** 1–2000 characters after trim, starting with `http://` or `https://`. */
40
+ url: string
41
+ }
42
+ }
43
+
13
44
  // --- Response types ---
14
45
 
15
46
  type InteractiveMedia = {
@@ -60,9 +91,27 @@ type ButtonInteractiveMessageContent = {
60
91
  }
61
92
  }
62
93
 
94
+ export type CtaUrlInteractiveMessageContent = {
95
+ type: 'cta_url'
96
+ header?: ButtonInteractiveMessageContent['header']
97
+ body: InteractiveMessageBodyFooter
98
+ footer?: InteractiveMessageBodyFooter
99
+ action: CtaUrlAction
100
+ channelRedirect?: ChannelRedirect
101
+ }
102
+
103
+ /** Has no header or footer: WhatsApp refuses them on this type. */
104
+ export type RequestContactInfoInteractiveMessageContent = {
105
+ type: 'request_contact_info'
106
+ body: InteractiveMessageBodyFooter
107
+ action: { name?: string }
108
+ }
109
+
63
110
  export type InteractiveMessageContent =
64
111
  | ListInteractiveMessageContent
65
112
  | ButtonInteractiveMessageContent
113
+ | CtaUrlInteractiveMessageContent
114
+ | RequestContactInfoInteractiveMessageContent
66
115
 
67
116
  // --- Input types (create/update) ---
68
117
 
@@ -90,9 +139,26 @@ type ButtonInteractiveMessageContentInput = {
90
139
  }
91
140
  }
92
141
 
142
+ type CtaUrlInteractiveMessageContentInput = {
143
+ type: 'cta_url'
144
+ header?: ButtonInteractiveMessageContentInput['header']
145
+ body: InteractiveMessageBodyFooter
146
+ footer?: InteractiveMessageBodyFooter
147
+ action: CtaUrlAction
148
+ channelRedirect?: ChannelRedirect
149
+ }
150
+
151
+ type RequestContactInfoInteractiveMessageContentInput = {
152
+ type: 'request_contact_info'
153
+ body: InteractiveMessageBodyFooter
154
+ action: { name: 'request_contact_info' }
155
+ }
156
+
93
157
  type InteractiveMessageContentInput =
94
158
  | ListInteractiveMessageContentInput
95
159
  | ButtonInteractiveMessageContentInput
160
+ | CtaUrlInteractiveMessageContentInput
161
+ | RequestContactInfoInteractiveMessageContentInput
96
162
 
97
163
  type InteractiveMessageFilePayload =
98
164
  | { id: string }
@@ -103,7 +169,7 @@ type InteractiveMessageFilePayload =
103
169
  export type InteractiveMessage = {
104
170
  id: string
105
171
  name: string
106
- type: 'list' | 'button'
172
+ type: 'list' | 'button' | 'cta_url' | 'request_contact_info'
107
173
  interactive: InteractiveMessageContent
108
174
  archivedAt: string | null
109
175
  accountId: string
@@ -6,8 +6,24 @@ import type { AnswersOverview } from './answers/types'
6
6
  import { BotSimulatorsApi } from './botSimulators/BotSimulatorsApi'
7
7
  import { BotsApi } from './bots/BotsApi'
8
8
  import type { Bot, BotContextInput, BotParsedContext } from './bots/types'
9
+ import { CampaignsApi } from './campaigns/CampaignsApi'
10
+ import type {
11
+ Campaign,
12
+ CampaignContact,
13
+ CampaignContactsPage,
14
+ CampaignMessage,
15
+ CampaignStats,
16
+ CampaignTimelineEvent,
17
+ } from './campaigns/types'
9
18
  import { CardsApi } from './cards/CardsApi'
10
19
  import { ContactsApi } from './contacts/ContactsApi'
20
+ import { InteractiveMessagesApi } from './interactiveMessages/InteractiveMessagesApi'
21
+ import type {
22
+ ChannelRedirect,
23
+ CtaUrlInteractiveMessageContent,
24
+ InteractiveMessage,
25
+ RequestContactInfoInteractiveMessageContent,
26
+ } from './interactiveMessages'
11
27
  import { KanbanColumnsApi } from './kanbanColumns/KanbanColumnsApi'
12
28
  import { PipelineApi } from './pipeline/PipelineApi'
13
29
  import type { PermissionName } from './permissions/types'
@@ -17,6 +33,12 @@ import { QuickRepliesApi } from './quickReplies/QuickRepliesApi'
17
33
  import { RolesApi } from './roles/RolesApi'
18
34
  import { ScheduleApi } from './schedule/ScheduleApi'
19
35
  import type { ScheduleRecord } from './schedule/types'
36
+ import type {
37
+ CreateServicePayload,
38
+ Service,
39
+ UpdateServicePayload,
40
+ WebchatServiceData,
41
+ } from './services'
20
42
  import { TagsApi } from './tags/TagsApi'
21
43
  import { TicketTopicsApi } from './ticketTopics/TicketTopicsApi'
22
44
  import { TicketsApi } from './tickets/TicketsApi'
@@ -58,6 +80,154 @@ expectError(tokens.create({ name: 'CI', accessTokenExpiresAt: '2030-01-01T00:00:
58
80
  expectError(tokens.create({ name: 'CI', expiresIn: '7d' }))
59
81
  expectError(tokens.updateById('t-1', { scope: '*' }))
60
82
 
83
+ // ── interactive messages: cta_url and request_contact_info ──────────
84
+
85
+ const interactiveMessages = new InteractiveMessagesApi(client)
86
+ interactiveMessages.create({
87
+ name: 'Boleto',
88
+ interactive: {
89
+ type: 'cta_url',
90
+ header: { type: 'text', text: 'Seu boleto' },
91
+ body: { text: 'Clique para continuar' },
92
+ footer: { text: 'Digisac' },
93
+ action: {
94
+ name: 'cta_url',
95
+ parameters: { display_text: 'Continuar', url: 'https://wa.me/5541999999999' },
96
+ },
97
+ channelRedirect: {
98
+ enabled: true,
99
+ channel: 'whatsapp',
100
+ identifierType: 'phone',
101
+ identifier: '+5541999999999',
102
+ },
103
+ },
104
+ })
105
+ interactiveMessages.create({
106
+ name: 'Webchat',
107
+ interactive: {
108
+ type: 'cta_url',
109
+ header: { type: 'image' },
110
+ body: { text: 'Fale conosco' },
111
+ action: {
112
+ name: 'cta_url',
113
+ parameters: { display_text: 'Abrir', url: 'https://webchat.digisac.app/w-1' },
114
+ },
115
+ channelRedirect: { enabled: true, channel: 'webchat', serviceId: 's-1' },
116
+ },
117
+ })
118
+ interactiveMessages.updateById('im-1', {
119
+ interactive: {
120
+ type: 'cta_url',
121
+ body: { text: 'Site' },
122
+ action: { name: 'cta_url', parameters: { display_text: 'Abrir', url: 'https://exemplo.com' } },
123
+ },
124
+ })
125
+ interactiveMessages.create({
126
+ name: 'Dados',
127
+ interactive: {
128
+ type: 'request_contact_info',
129
+ body: { text: 'Compartilhe seu contato' },
130
+ action: { name: 'request_contact_info' },
131
+ },
132
+ })
133
+ // `channelRedirect` is a sibling of `action`, never inside `action.parameters`
134
+ expectError(
135
+ interactiveMessages.create({
136
+ name: 'Boleto',
137
+ interactive: {
138
+ type: 'cta_url',
139
+ body: { text: 'Clique' },
140
+ action: {
141
+ name: 'cta_url',
142
+ parameters: {
143
+ display_text: 'Continuar',
144
+ url: 'https://wa.me/5541999999999',
145
+ channelRedirect: { enabled: true },
146
+ },
147
+ },
148
+ },
149
+ }),
150
+ )
151
+ expectError(
152
+ interactiveMessages.create({
153
+ name: 'Boleto',
154
+ interactive: {
155
+ type: 'cta_url',
156
+ body: { text: 'Clique' },
157
+ action: { name: 'cta_url', parameters: { url: 'https://exemplo.com' } },
158
+ },
159
+ }),
160
+ )
161
+ expectError(
162
+ interactiveMessages.create({
163
+ name: 'Boleto',
164
+ interactive: {
165
+ type: 'cta_url',
166
+ body: { text: 'Clique' },
167
+ action: { name: 'cta_url', parameters: { display_text: 'Abrir', url: 'https://x.com' } },
168
+ channelRedirect: { enabled: true, channel: 'email' },
169
+ },
170
+ }),
171
+ )
172
+ // WhatsApp refuses header and footer on request_contact_info
173
+ expectError(
174
+ interactiveMessages.create({
175
+ name: 'Dados',
176
+ interactive: {
177
+ type: 'request_contact_info',
178
+ header: { type: 'text', text: 'Olá' },
179
+ body: { text: 'Compartilhe seu contato' },
180
+ action: { name: 'request_contact_info' },
181
+ },
182
+ }),
183
+ )
184
+
185
+ declare const interactiveMessage: InteractiveMessage
186
+ expectType<'list' | 'button' | 'cta_url' | 'request_contact_info'>(interactiveMessage.type)
187
+ if (interactiveMessage.interactive.type === 'cta_url') {
188
+ expectType<CtaUrlInteractiveMessageContent>(interactiveMessage.interactive)
189
+ expectType<string>(interactiveMessage.interactive.action.parameters.url)
190
+ expectType<ChannelRedirect | undefined>(interactiveMessage.interactive.channelRedirect)
191
+ }
192
+ if (interactiveMessage.interactive.type === 'request_contact_info') {
193
+ expectType<RequestContactInfoInteractiveMessageContent>(interactiveMessage.interactive)
194
+ }
195
+ expectAssignable<ChannelRedirect>({ enabled: false })
196
+ expectAssignable<ChannelRedirect>({
197
+ enabled: true,
198
+ channel: 'telegram',
199
+ identifierType: 'botName',
200
+ identifier: '@lojabot',
201
+ })
202
+ expectNotAssignable<ChannelRedirect>({ enabled: true, channel: 'telegram', identifierType: 'user' })
203
+ expectNotAssignable<ChannelRedirect>({ channel: 'whatsapp' })
204
+
205
+ // ── services: data.webchat of a Webchat service ─────────────────────
206
+
207
+ declare const service: Service
208
+ expectType<WebchatServiceData | undefined>(service.data.webchat)
209
+ expectType<string | undefined>(service.data.webchat?.id)
210
+ expectType<string | null | undefined>(service.data.webchat?.instagram)
211
+ expectAssignable<WebchatServiceData>({})
212
+ expectAssignable<WebchatServiceData>({ id: 'w-1', phone: '5541999999999', telegram: null })
213
+ expectNotAssignable<WebchatServiceData>({ id: 1 })
214
+ // values the web forms already store: the new form sends `null`, the legacy one `{}`
215
+ expectType<{ id?: string; name?: string } | null | undefined>(service.data.webchat?.customField)
216
+ expectAssignable<WebchatServiceData>({ customField: null })
217
+ expectAssignable<WebchatServiceData>({ customField: {} })
218
+ expectAssignable<WebchatServiceData>({ customField: { id: 'cf-1', name: 'CPF' } })
219
+ expectAssignable<WebchatServiceData>({ name: null, googleId: null, file: {} })
220
+ expectAssignable<WebchatServiceData>({
221
+ file: { base64Url: 'aGVsbG8=', mimetype: 'image/png', fileName: 'perfil.png' },
222
+ })
223
+ expectAssignable<WebchatServiceData>({ custom: { webchatBalloonText: null } })
224
+ expectNotAssignable<WebchatServiceData>({ customField: 'cf-1' })
225
+ // the same shape types the `data` of the create and update payloads
226
+ expectAssignable<CreateServicePayload['data']>({
227
+ webchat: { customField: null, googleId: null, name: null, custom: { webchatBalloonText: null } },
228
+ })
229
+ expectAssignable<UpdateServicePayload['data']>({ webchat: { customField: {}, file: {} } })
230
+
61
231
  // ── users: the dedicated archive route takes a boolean, not a date ──
62
232
 
63
233
  const users = new UsersApi(client)
@@ -292,3 +462,38 @@ const quickReplies = new QuickRepliesApi(client)
292
462
  quickReplies.updateById('r-1', { title: 'Oi', text: 'Olá', files: [] })
293
463
  expectError(quickReplies.updateById('r-1', { title: 'Oi', text: 'Olá' }))
294
464
  expectType<Promise<'OK'>>(quickReplies.deleteById('r-1'))
465
+
466
+ // ── campaigns: the actions answer booleans or 'OK'; the CSV export takes a where ──
467
+
468
+ const campaigns = new CampaignsApi(client)
469
+ expectType<Promise<Campaign>>(campaigns.sendById('c-1'))
470
+ expectType<Promise<boolean>>(campaigns.pauseById('c-1'))
471
+ expectType<Promise<boolean>>(campaigns.resumeById('c-1'))
472
+ expectType<Promise<'OK' | false>>(campaigns.retryById('c-1'))
473
+ expectType<Promise<boolean>>(campaigns.setIntervalById('c-1', { minInterval: 5, maxInterval: 10 }))
474
+ expectType<Promise<CampaignStats>>(campaigns.getStatsById('c-1'))
475
+ expectType<Promise<CampaignTimelineEvent[]>>(campaigns.getTimeline('c-1'))
476
+ expectType<Promise<CampaignContactsPage>>(
477
+ campaigns.getContacts('c-1', { contactStatus: ['not_sent', 'read'], sortDir: 'DESC' }),
478
+ )
479
+ expectType<Promise<number>>(campaigns.countReadyByService('s-1'))
480
+ expectType<Promise<'OK'>>(campaigns.deleteById('c-1'))
481
+ expectType<Promise<true>>(campaigns.deleteMany(['c-1']))
482
+ expectType<Promise<string>>(campaigns.exportCampaignResult({ where: { campaignId: 'c-1' } }))
483
+ expectType<Promise<string>>(campaigns.exportImportErrors('c-1', { delimiter: ';' }))
484
+ // the export takes the campaign in `where`: `{ campaignId }` answers 400
485
+ expectError(campaigns.exportCampaignResult({ campaignId: 'c-1' }))
486
+ expectError(campaigns.setAutoPauseMode({ mode: 'auto' }))
487
+ campaigns.getMany({
488
+ status: 'scheduled',
489
+ q: 'Black Friday',
490
+ createdAtFrom: '2026-10-01T00:00:00.000Z',
491
+ })
492
+ expectError(campaigns.getMany({ status: 'sending' }))
493
+ declare const listed: Campaign
494
+ expectType<{ sent: number; viewed: number } | undefined>(listed.stats)
495
+ // a new campaign answers `config: {}`; the include answers the campaign messages, not chat messages
496
+ expectType<number | string | undefined>(listed.config.minInterval)
497
+ expectType<CampaignMessage[] | undefined>(listed.messages)
498
+ declare const campaignContact: CampaignContact
499
+ expectType<string | null>(campaignContact.contactNumber)
@@ -1,5 +1,6 @@
1
1
  import { beforeEach, describe, expect, it, vi } from 'vitest'
2
2
  import type { ApiClient } from '../core/ApiClient'
3
+ import { InteractiveMessagesApi } from './interactiveMessages/InteractiveMessagesApi'
3
4
  import { PersonalAccessTokensApi } from './personalAccessTokens/PersonalAccessTokensApi'
4
5
  import { RolesApi } from './roles/RolesApi'
5
6
  import { TimetableApi } from './timetable/TimetableApi'
@@ -78,4 +79,44 @@ describe('payloads sent to the backend', () => {
78
79
  )
79
80
  })
80
81
  })
82
+
83
+ describe('InteractiveMessagesApi', () => {
84
+ it('create sends a cta_url with channelRedirect next to action, not inside parameters', async () => {
85
+ const body = {
86
+ name: 'Boleto',
87
+ departments: [{ id: 'd-1' }],
88
+ interactive: {
89
+ type: 'cta_url' as const,
90
+ header: { type: 'text' as const, text: 'Seu boleto' },
91
+ body: { text: 'Clique para continuar' },
92
+ footer: { text: 'Digisac' },
93
+ action: {
94
+ name: 'cta_url' as const,
95
+ parameters: { display_text: 'Continuar', url: 'https://wa.me/5541999999999' },
96
+ },
97
+ channelRedirect: {
98
+ enabled: true,
99
+ channel: 'whatsapp' as const,
100
+ identifierType: 'phone' as const,
101
+ identifier: '+5541999999999',
102
+ },
103
+ },
104
+ }
105
+ await new InteractiveMessagesApi(client).create(body)
106
+ expect(client.post).toHaveBeenCalledWith('/interactive-messages', body, undefined)
107
+ })
108
+
109
+ it('create sends a request_contact_info with only body and action', async () => {
110
+ const body = {
111
+ name: 'Dados',
112
+ interactive: {
113
+ type: 'request_contact_info' as const,
114
+ body: { text: 'Compartilhe seu contato' },
115
+ action: { name: 'request_contact_info' as const },
116
+ },
117
+ }
118
+ await new InteractiveMessagesApi(client).create(body)
119
+ expect(client.post).toHaveBeenCalledWith('/interactive-messages', body, undefined)
120
+ })
121
+ })
81
122
  })
@@ -4,6 +4,7 @@ import type { ApiClient } from '../core/ApiClient'
4
4
  import { BaseApiClient } from '../core/BaseApiClient'
5
5
  import type { QueryFormat } from '../core/queryString'
6
6
  import { AuthHistoryApi } from './authHistory/AuthHistoryApi'
7
+ import { CampaignsApi } from './campaigns/CampaignsApi'
7
8
  import { ContactsApi } from './contacts/ContactsApi'
8
9
  import { DashboardApi } from './dashboard/DashboardApi'
9
10
  import { NowApi } from './now/NowApi'
@@ -35,6 +36,12 @@ const listQuery = { where: { name: 'x' }, page: 2, perPage: 5 }
35
36
  const getQuery = { attributes: ['id'] }
36
37
  /** `Permission.name` is a union of the catalog names, so the permissions case filters by a real one. */
37
38
  const permissionsQuery = { where: { name: 'chat' as const }, page: 2, perPage: 5 }
39
+ const campaignsQuery = { status: 'scheduled' as const, q: 'Black Friday', page: 2, perPage: 5 }
40
+ const campaignContactsQuery = {
41
+ contactStatus: ['not_sent' as const],
42
+ sortBy: 'sentAt' as const,
43
+ page: 2,
44
+ }
38
45
  const ticketsQuery = {
39
46
  where: { isOpen: true },
40
47
  order: [['startedAt', 'DESC'] as ['startedAt', 'DESC']],
@@ -49,6 +56,24 @@ type Case = {
49
56
  }
50
57
 
51
58
  const cases: Case[] = [
59
+ {
60
+ name: 'CampaignsApi.getMany (flat filters)',
61
+ path: '/campaigns',
62
+ query: campaignsQuery,
63
+ call: (client, f) => {
64
+ const api = new CampaignsApi(client)
65
+ return (f ? api.withQueryFormat(f) : api).getMany(campaignsQuery)
66
+ },
67
+ },
68
+ {
69
+ name: 'CampaignsApi.getContacts',
70
+ path: '/campaigns/c1/contacts',
71
+ query: campaignContactsQuery,
72
+ call: (client, f) => {
73
+ const api = new CampaignsApi(client)
74
+ return (f ? api.withQueryFormat(f) : api).getContacts('c1', campaignContactsQuery)
75
+ },
76
+ },
52
77
  {
53
78
  name: 'ContactsApi.getMany (BaseCrudApi)',
54
79
  path: '/contacts',