@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.
- package/dist/apis/campaigns/CampaignsApi.cjs +122 -55
- package/dist/apis/campaigns/CampaignsApi.cjs.map +1 -1
- package/dist/apis/campaigns/CampaignsApi.d.ts +86 -24
- package/dist/apis/campaigns/CampaignsApi.d.ts.map +1 -1
- package/dist/apis/campaigns/CampaignsApi.mjs +120 -55
- package/dist/apis/campaigns/CampaignsApi.mjs.map +1 -1
- package/dist/apis/campaigns/types.d.ts +153 -14
- package/dist/apis/campaigns/types.d.ts.map +1 -1
- package/dist/apis/interactiveMessages/types.d.ts +61 -3
- package/dist/apis/interactiveMessages/types.d.ts.map +1 -1
- package/dist/apis/services/types.d.ts +53 -1
- package/dist/apis/services/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/apis/campaigns/CampaignsApi.test.ts +177 -44
- package/src/apis/campaigns/CampaignsApi.ts +167 -45
- package/src/apis/campaigns/types.ts +165 -14
- package/src/apis/interactiveMessages/types.ts +67 -1
- package/src/apis/payloads.test-d.ts +205 -0
- package/src/apis/payloads.test.ts +41 -0
- package/src/apis/queryFormat.test.ts +25 -0
- package/src/apis/services/types.ts +47 -1
|
@@ -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
|
-
/**
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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?:
|
|
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:
|
|
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',
|