@ikatec/digisac-api-sdk 4.5.1 → 4.7.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.
Files changed (129) hide show
  1. package/README.md +16 -11
  2. package/dist/apis/answers/AnswersApi.cjs +38 -0
  3. package/dist/apis/answers/AnswersApi.cjs.map +1 -1
  4. package/dist/apis/answers/AnswersApi.d.ts +33 -2
  5. package/dist/apis/answers/AnswersApi.d.ts.map +1 -1
  6. package/dist/apis/answers/AnswersApi.mjs +38 -0
  7. package/dist/apis/answers/AnswersApi.mjs.map +1 -1
  8. package/dist/apis/answers/types.d.ts +64 -1
  9. package/dist/apis/answers/types.d.ts.map +1 -1
  10. package/dist/apis/botSimulators/BotSimulatorsApi.cjs +42 -0
  11. package/dist/apis/botSimulators/BotSimulatorsApi.cjs.map +1 -0
  12. package/dist/apis/botSimulators/BotSimulatorsApi.d.ts +31 -0
  13. package/dist/apis/botSimulators/BotSimulatorsApi.d.ts.map +1 -0
  14. package/dist/apis/botSimulators/BotSimulatorsApi.mjs +41 -0
  15. package/dist/apis/botSimulators/BotSimulatorsApi.mjs.map +1 -0
  16. package/dist/apis/botSimulators/index.cjs +4 -0
  17. package/dist/apis/botSimulators/index.d.ts +3 -0
  18. package/dist/apis/botSimulators/index.d.ts.map +1 -0
  19. package/dist/apis/botSimulators/index.mjs +3 -0
  20. package/dist/apis/botSimulators/types.cjs +0 -0
  21. package/dist/apis/botSimulators/types.d.ts +73 -0
  22. package/dist/apis/botSimulators/types.d.ts.map +1 -0
  23. package/dist/apis/botSimulators/types.mjs +0 -0
  24. package/dist/apis/botVersions/BotVersionsApi.cjs +25 -0
  25. package/dist/apis/botVersions/BotVersionsApi.cjs.map +1 -1
  26. package/dist/apis/botVersions/BotVersionsApi.d.ts +18 -0
  27. package/dist/apis/botVersions/BotVersionsApi.d.ts.map +1 -1
  28. package/dist/apis/botVersions/BotVersionsApi.mjs +25 -0
  29. package/dist/apis/botVersions/BotVersionsApi.mjs.map +1 -1
  30. package/dist/apis/botVersions/types.d.ts +4 -0
  31. package/dist/apis/botVersions/types.d.ts.map +1 -1
  32. package/dist/apis/bots/BotsApi.cjs +99 -4
  33. package/dist/apis/bots/BotsApi.cjs.map +1 -1
  34. package/dist/apis/bots/BotsApi.d.ts +78 -5
  35. package/dist/apis/bots/BotsApi.d.ts.map +1 -1
  36. package/dist/apis/bots/BotsApi.mjs +99 -4
  37. package/dist/apis/bots/BotsApi.mjs.map +1 -1
  38. package/dist/apis/bots/types.d.ts +266 -16
  39. package/dist/apis/bots/types.d.ts.map +1 -1
  40. package/dist/apis/index.cjs +3 -0
  41. package/dist/apis/index.d.ts +1 -0
  42. package/dist/apis/index.d.ts.map +1 -1
  43. package/dist/apis/index.mjs +3 -1
  44. package/dist/apis/questions/QuestionsApi.cjs +15 -0
  45. package/dist/apis/questions/QuestionsApi.cjs.map +1 -1
  46. package/dist/apis/questions/QuestionsApi.d.ts +15 -1
  47. package/dist/apis/questions/QuestionsApi.d.ts.map +1 -1
  48. package/dist/apis/questions/QuestionsApi.mjs +15 -0
  49. package/dist/apis/questions/QuestionsApi.mjs.map +1 -1
  50. package/dist/apis/questions/types.d.ts +25 -5
  51. package/dist/apis/questions/types.d.ts.map +1 -1
  52. package/dist/apis/quickReplies/QuickRepliesApi.cjs +16 -0
  53. package/dist/apis/quickReplies/QuickRepliesApi.cjs.map +1 -1
  54. package/dist/apis/quickReplies/QuickRepliesApi.d.ts +15 -2
  55. package/dist/apis/quickReplies/QuickRepliesApi.d.ts.map +1 -1
  56. package/dist/apis/quickReplies/QuickRepliesApi.mjs +16 -0
  57. package/dist/apis/quickReplies/QuickRepliesApi.mjs.map +1 -1
  58. package/dist/apis/quickReplies/types.d.ts +30 -2
  59. package/dist/apis/quickReplies/types.d.ts.map +1 -1
  60. package/dist/apis/schedule/ScheduleApi.cjs +37 -2
  61. package/dist/apis/schedule/ScheduleApi.cjs.map +1 -1
  62. package/dist/apis/schedule/ScheduleApi.d.ts +30 -3
  63. package/dist/apis/schedule/ScheduleApi.d.ts.map +1 -1
  64. package/dist/apis/schedule/ScheduleApi.mjs +37 -2
  65. package/dist/apis/schedule/ScheduleApi.mjs.map +1 -1
  66. package/dist/apis/schedule/types.d.ts +58 -11
  67. package/dist/apis/schedule/types.d.ts.map +1 -1
  68. package/dist/apis/ticketTopics/TicketTopicsApi.cjs +25 -2
  69. package/dist/apis/ticketTopics/TicketTopicsApi.cjs.map +1 -1
  70. package/dist/apis/ticketTopics/TicketTopicsApi.d.ts +18 -4
  71. package/dist/apis/ticketTopics/TicketTopicsApi.d.ts.map +1 -1
  72. package/dist/apis/ticketTopics/TicketTopicsApi.mjs +25 -2
  73. package/dist/apis/ticketTopics/TicketTopicsApi.mjs.map +1 -1
  74. package/dist/apis/ticketTopics/types.d.ts +22 -1
  75. package/dist/apis/ticketTopics/types.d.ts.map +1 -1
  76. package/dist/apis/tickets/TicketsApi.cjs +74 -1
  77. package/dist/apis/tickets/TicketsApi.cjs.map +1 -1
  78. package/dist/apis/tickets/TicketsApi.d.ts +68 -3
  79. package/dist/apis/tickets/TicketsApi.d.ts.map +1 -1
  80. package/dist/apis/tickets/TicketsApi.mjs +74 -1
  81. package/dist/apis/tickets/TicketsApi.mjs.map +1 -1
  82. package/dist/apis/tickets/types.d.ts +69 -8
  83. package/dist/apis/tickets/types.d.ts.map +1 -1
  84. package/dist/core/BaseCrudApi.cjs +5 -2
  85. package/dist/core/BaseCrudApi.cjs.map +1 -1
  86. package/dist/core/BaseCrudApi.d.ts +7 -4
  87. package/dist/core/BaseCrudApi.d.ts.map +1 -1
  88. package/dist/core/BaseCrudApi.mjs +5 -2
  89. package/dist/core/BaseCrudApi.mjs.map +1 -1
  90. package/dist/core/types.d.ts +1 -1
  91. package/dist/core/types.d.ts.map +1 -1
  92. package/dist/incommingWebhooks/index.d.ts +17 -9
  93. package/dist/incommingWebhooks/index.d.ts.map +1 -1
  94. package/package.json +1 -1
  95. package/src/apis/answers/AnswersApi.test.ts +67 -0
  96. package/src/apis/answers/AnswersApi.ts +66 -2
  97. package/src/apis/answers/types.ts +64 -2
  98. package/src/apis/botSimulators/BotSimulatorsApi.test.ts +58 -0
  99. package/src/apis/botSimulators/BotSimulatorsApi.ts +57 -0
  100. package/src/apis/botSimulators/index.ts +2 -0
  101. package/src/apis/botSimulators/types.ts +72 -0
  102. package/src/apis/botVersions/BotVersionsApi.test.ts +51 -0
  103. package/src/apis/botVersions/BotVersionsApi.ts +41 -0
  104. package/src/apis/botVersions/types.ts +4 -0
  105. package/src/apis/bots/BotsApi.test.ts +177 -0
  106. package/src/apis/bots/BotsApi.ts +162 -6
  107. package/src/apis/bots/types.ts +267 -15
  108. package/src/apis/index.ts +1 -0
  109. package/src/apis/payloads.test-d.ts +178 -1
  110. package/src/apis/queryFormat.test.ts +23 -0
  111. package/src/apis/questions/QuestionsApi.test.ts +49 -0
  112. package/src/apis/questions/QuestionsApi.ts +20 -1
  113. package/src/apis/questions/types.ts +25 -6
  114. package/src/apis/quickReplies/QuickRepliesApi.test.ts +69 -0
  115. package/src/apis/quickReplies/QuickRepliesApi.ts +35 -2
  116. package/src/apis/quickReplies/types.ts +34 -2
  117. package/src/apis/schedule/ScheduleApi.test.ts +80 -0
  118. package/src/apis/schedule/ScheduleApi.ts +59 -6
  119. package/src/apis/schedule/types.ts +66 -11
  120. package/src/apis/ticketTopics/TicketTopicsApi.test.ts +40 -0
  121. package/src/apis/ticketTopics/TicketTopicsApi.ts +35 -4
  122. package/src/apis/ticketTopics/types.ts +26 -1
  123. package/src/apis/tickets/TicketsApi.test.ts +142 -5
  124. package/src/apis/tickets/TicketsApi.ts +135 -3
  125. package/src/apis/tickets/types.ts +76 -6
  126. package/src/core/BaseCrudApi.ts +9 -5
  127. package/src/core/types.ts +2 -1
  128. package/src/incommingWebhooks/index.test-d.ts +23 -1
  129. package/src/incommingWebhooks/index.ts +18 -9
package/src/apis/index.ts CHANGED
@@ -4,6 +4,7 @@ export * from './accounts'
4
4
  export * from './activityLog'
5
5
  export * from './answers'
6
6
  export * from './authHistory'
7
+ export * from './botSimulators'
7
8
  export * from './botVersions'
8
9
  export * from './bots'
9
10
  export * from './campaigns'
@@ -1,15 +1,26 @@
1
1
  import { expectAssignable, expectError, expectNotAssignable, expectType } from 'tsd'
2
2
  import type { ApiClient } from '../core/ApiClient'
3
+ import type { Paginated } from '../core/types'
4
+ import { AnswersApi } from './answers/AnswersApi'
5
+ import type { AnswersOverview } from './answers/types'
6
+ import { BotSimulatorsApi } from './botSimulators/BotSimulatorsApi'
7
+ import { BotsApi } from './bots/BotsApi'
8
+ import type { Bot, BotContextInput, BotParsedContext } from './bots/types'
3
9
  import { CardsApi } from './cards/CardsApi'
4
10
  import { ContactsApi } from './contacts/ContactsApi'
5
11
  import { KanbanColumnsApi } from './kanbanColumns/KanbanColumnsApi'
6
12
  import { PipelineApi } from './pipeline/PipelineApi'
7
13
  import type { PermissionName } from './permissions/types'
8
14
  import { PersonalAccessTokensApi } from './personalAccessTokens/PersonalAccessTokensApi'
15
+ import { QuestionsApi } from './questions/QuestionsApi'
16
+ import { QuickRepliesApi } from './quickReplies/QuickRepliesApi'
9
17
  import { RolesApi } from './roles/RolesApi'
18
+ import { ScheduleApi } from './schedule/ScheduleApi'
19
+ import type { ScheduleRecord } from './schedule/types'
10
20
  import { TagsApi } from './tags/TagsApi'
21
+ import { TicketTopicsApi } from './ticketTopics/TicketTopicsApi'
11
22
  import { TicketsApi } from './tickets/TicketsApi'
12
- import type { Ticket, TicketTransfer } from './tickets/types'
23
+ import type { Ticket, TicketTransfer, TicketsCount, TicketsCounts } from './tickets/types'
13
24
  import { TimetableApi } from './timetable/TimetableApi'
14
25
  import { UsersApi } from './users/UsersApi'
15
26
 
@@ -115,3 +126,169 @@ expectType<TicketTransfer[] | undefined>(ticket.ticketTransfers)
115
126
  declare const transfer: TicketTransfer
116
127
  expectType<string | null>(transfer.byUserId)
117
128
  expectType<'opened' | 'transferred' | 'closed' | null>(transfer.action)
129
+
130
+ // ── tickets: the counts, the first waiting time and the exports ─────
131
+
132
+ expectType<Promise<TicketsCount>>(tickets.count({ where: { isOpen: true } }))
133
+ expectType<Promise<TicketsCounts>>(tickets.count({ queries: [{ where: { isOpen: true } }] }))
134
+ expectType<Promise<TicketsCount>>(tickets.countPost({ where: { isOpen: true }, query: {} }))
135
+ expectType<Promise<TicketsCounts>>(
136
+ tickets.countPost({ query: { queries: [{ where: { isOpen: false } }] } }),
137
+ )
138
+ // `query` is required: without it the route answers 500
139
+ expectError(tickets.countPost({ where: { isOpen: true } }))
140
+ expectType<Promise<number>>(tickets.countOpenByService('s-1'))
141
+ expectType<Promise<number>>(tickets.countOpenByDepartment('d-1'))
142
+ expectType<Promise<number>>(tickets.countOpenByUser('u-1'))
143
+ expectType<Promise<string>>(tickets.getFirstWaitingTime('t-1'))
144
+ expectType<Promise<string>>(tickets.exportTxt('202610061675'))
145
+ expectType<Promise<string>>(
146
+ tickets.exportHistory({ include: [{ model: 'contact', include: ['service'] }] }),
147
+ )
148
+ expectType<Promise<void>>(tickets.closeMany({ ids: ['t-1'], comments: 'Resolved' }))
149
+ expectType<Promise<void>>(
150
+ tickets.closeMany({
151
+ allContactsSelected: true,
152
+ query: { query: '{}' },
153
+ ticketTopicIds: ['tt-1'],
154
+ }),
155
+ )
156
+ tickets.getById('t-1', { include: ['ticketTopics', 'firstMessage', 'account'] })
157
+ expectType<string | undefined>(ticket.metrics.maxWaitingTimeDepartmentId)
158
+
159
+ // ── bots: the /bots routes take and return the contexts as a list ───
160
+
161
+ const bots = new BotsApi(client)
162
+ const botContexts: BotContextInput[] = [
163
+ {
164
+ id: '@INIT',
165
+ triggers: [
166
+ {
167
+ trigger: { type: 'MESSAGE_RECEIVED' },
168
+ rules: [
169
+ { conditions: [], actions: [{ type: { value: 'SEND_MESSAGE' }, data: { text: 'Oi' } }] },
170
+ ],
171
+ },
172
+ ],
173
+ },
174
+ ]
175
+ bots.create({ name: 'Bot', contexts: botContexts, flowJson: { nodes: [], edges: [] } })
176
+ expectError(bots.create({ name: 'Bot', contexts: botContexts, flowJson: { nodes: [] } }))
177
+ expectError(bots.create({ name: 'Bot' }))
178
+ expectError(bots.create({ name: 'Bot', contexts: { '@INIT': { name: 'Início', triggers: {} } } }))
179
+ expectError(
180
+ bots.create({
181
+ name: 'Bot',
182
+ contexts: [
183
+ {
184
+ id: '@INIT',
185
+ triggers: [
186
+ {
187
+ trigger: { type: 'MESSAGE_RECEIVED' },
188
+ rules: [
189
+ {
190
+ conditions: [],
191
+ actions: [{ type: { value: 'SET_VAR' }, data: { key: 'k', value: 'v' } }],
192
+ },
193
+ ],
194
+ },
195
+ ],
196
+ },
197
+ ],
198
+ }),
199
+ )
200
+ bots.updateById('b-1', { name: 'Bot', contexts: botContexts, isDraft: true })
201
+ bots.updateById('b-1', { name: 'Bot', onlyUpdateBotName: true })
202
+ bots.create({
203
+ name: 'Bot',
204
+ contexts: [
205
+ {
206
+ id: '@INIT',
207
+ triggers: [
208
+ {
209
+ trigger: { type: 'TICKET_OPENED' },
210
+ rules: [
211
+ {
212
+ conditions: [],
213
+ actions: [
214
+ { type: { value: 'SEND_TERM' }, data: { acceptanceTerms: { id: 'term-1' } } },
215
+ ],
216
+ },
217
+ ],
218
+ },
219
+ ],
220
+ },
221
+ ],
222
+ })
223
+ expectError(bots.updateById('b-1', { contexts: botContexts }))
224
+ expectError(bots.updateById('b-1', { name: 'Bot' }))
225
+ expectType<Promise<Paginated<Bot>>>(bots.getMany({ paginate: false }))
226
+ expectType<Promise<'OK' | false>>(bots.deleteById('b-1'))
227
+ declare const bot: Bot
228
+ expectType<BotParsedContext[]>(bot.contexts)
229
+
230
+ // ── bot simulator: the trigger pick needs the id of the selection message ──
231
+
232
+ const simulator = new BotSimulatorsApi(client)
233
+ simulator.sendEvent('b-1', { type: 'text_contact', text: 'oi' })
234
+ simulator.sendEvent('b-1', { type: 'select_trigger', id: 'm-1', selectedValue: 'TICKET_OPENED' })
235
+ expectError(simulator.sendEvent('b-1', { type: 'select_trigger', selectedValue: 'TICKET_OPENED' }))
236
+ expectError(
237
+ simulator.sendEvent('b-1', { type: 'select_condition', id: 'm-1', selectedValue: 'yes' }),
238
+ )
239
+
240
+ // ── questions: duration goes as a string; update is not partial ─────
241
+
242
+ const questions = new QuestionsApi(client)
243
+ const question = {
244
+ name: 'NPS',
245
+ type: 'nps',
246
+ questionMessage: 'De 0 a 10?',
247
+ duration: '60',
248
+ tries: 3,
249
+ successMessage: 'Obrigado!',
250
+ invalidMessage: 'De 0 a 10, por favor',
251
+ } as const
252
+ questions.create(question)
253
+ expectType<Promise<'OK'>>(questions.deleteById('q-1'))
254
+ expectError(questions.create({ ...question, duration: 60 }))
255
+ questions.updateById('q-1', { type: 'nps', questionMessage: 'x', duration: 60, tries: 3 })
256
+ expectError(questions.updateById('q-1', { name: 'NPS' }))
257
+
258
+ // ── answers: the overview NPS is positional ─────────────────────────
259
+
260
+ const answers = new AnswersApi(client)
261
+ expectType<Promise<AnswersOverview>>(answers.overview({ type: 'nps', periodType: 'closeDate' }))
262
+ expectError(answers.overview({ type: 'custom' }))
263
+ expectType<Promise<'OK'>>(answers.deleteById('a-1'))
264
+
265
+ // ── schedule: create answers no body; update is not partial ─────────
266
+
267
+ const schedule = new ScheduleApi(client)
268
+ const scheduled = {
269
+ scheduledAt: '2026-10-02T12:00:00Z',
270
+ contactId: 'c-1',
271
+ departmentId: 'd-1',
272
+ notes: '',
273
+ }
274
+ expectType<Promise<void>>(schedule.create({ ...scheduled, message: 'Oi' }))
275
+ expectType<Promise<ScheduleRecord>>(schedule.updateById('s-1', scheduled))
276
+ expectError(schedule.updateById('s-1', { message: 'Oi' }))
277
+ expectError(schedule.create({ ...scheduled, files: [{ name: 'a.png' }] }))
278
+ expectError(schedule.create({ ...scheduled, files: [{ id: 'f-1' }] }))
279
+ schedule.updateById('s-1', { ...scheduled, files: [{ id: 'f-1' }] })
280
+
281
+ // ── ticket topics: create needs the departments ─────────────────────
282
+
283
+ const topics = new TicketTopicsApi(client)
284
+ topics.create({ name: 'Dúvida', departments: [] })
285
+ expectError(topics.create({ name: 'Dúvida' }))
286
+ expectError(topics.updateById('t-1', { departments: [] }))
287
+ expectType<Promise<'OK'>>(topics.deleteById('t-1'))
288
+
289
+ // ── quick replies: update fails without the files ───────────────────
290
+
291
+ const quickReplies = new QuickRepliesApi(client)
292
+ quickReplies.updateById('r-1', { title: 'Oi', text: 'Olá', files: [] })
293
+ expectError(quickReplies.updateById('r-1', { title: 'Oi', text: 'Olá' }))
294
+ expectType<Promise<'OK'>>(quickReplies.deleteById('r-1'))
@@ -11,6 +11,7 @@ import { PermissionsApi } from './permissions/PermissionsApi'
11
11
  import { ServiceAccessManagementApi } from './serviceAccessManagement/ServiceAccessManagementApi'
12
12
  import { StickerUsersApi } from './stickerUsers/StickerUsersApi'
13
13
  import { TermsApi } from './terms/TermsApi'
14
+ import { TicketsApi } from './tickets/TicketsApi'
14
15
 
15
16
  /**
16
17
  * Every method that sends a list/get query must serialize it in the client's `queryFormat` and
@@ -34,6 +35,10 @@ const listQuery = { where: { name: 'x' }, page: 2, perPage: 5 }
34
35
  const getQuery = { attributes: ['id'] }
35
36
  /** `Permission.name` is a union of the catalog names, so the permissions case filters by a real one. */
36
37
  const permissionsQuery = { where: { name: 'chat' as const }, page: 2, perPage: 5 }
38
+ const ticketsQuery = {
39
+ where: { isOpen: true },
40
+ order: [['startedAt', 'DESC'] as ['startedAt', 'DESC']],
41
+ }
37
42
 
38
43
  type Case = {
39
44
  name: string
@@ -273,6 +278,24 @@ const cases: Case[] = [
273
278
  return (f ? api.withQueryFormat(f) : api).getById('t1', { attributes: ['id'] })
274
279
  },
275
280
  },
281
+ {
282
+ name: 'TicketsApi.count',
283
+ path: '/tickets/count',
284
+ query: ticketsQuery,
285
+ call: (client, f) => {
286
+ const api = new TicketsApi(client)
287
+ return (f ? api.withQueryFormat(f) : api).count(ticketsQuery)
288
+ },
289
+ },
290
+ {
291
+ name: 'TicketsApi.exportHistory',
292
+ path: '/tickets/export-history',
293
+ query: ticketsQuery,
294
+ call: (client, f) => {
295
+ const api = new TicketsApi(client)
296
+ return (f ? api.withQueryFormat(f) : api).exportHistory(ticketsQuery)
297
+ },
298
+ },
276
299
  ]
277
300
 
278
301
  const jsonUrl = (path: string, query: object) =>
@@ -0,0 +1,49 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import type { ApiClient } from '../../core/ApiClient'
3
+ import { QuestionsApi } from './QuestionsApi'
4
+
5
+ // ── Mock client factory ──────────────────────────────────────────────
6
+
7
+ function createMockClient(): ApiClient {
8
+ return {
9
+ setAccessToken: vi.fn().mockReturnThis(),
10
+ request: vi.fn(),
11
+ get: vi.fn(),
12
+ post: vi.fn(),
13
+ put: vi.fn(),
14
+ patch: vi.fn(),
15
+ delete: vi.fn(),
16
+ }
17
+ }
18
+
19
+ // ── Tests ────────────────────────────────────────────────────────────
20
+
21
+ describe('QuestionsApi', () => {
22
+ let client: ReturnType<typeof createMockClient>
23
+ let api: QuestionsApi
24
+
25
+ beforeEach(() => {
26
+ client = createMockClient()
27
+ api = new QuestionsApi(client)
28
+ })
29
+
30
+ it('create sends duration as a string', async () => {
31
+ const body = {
32
+ name: 'NPS',
33
+ type: 'nps',
34
+ questionMessage: 'De 0 a 10?',
35
+ duration: '60',
36
+ tries: 3,
37
+ successMessage: 'Obrigado!',
38
+ invalidMessage: 'De 0 a 10',
39
+ } as const
40
+ await api.create(body)
41
+ expect(client.post).toHaveBeenCalledWith('/questions', body, undefined)
42
+ })
43
+
44
+ it('deleteById resolves to the OK of the route', async () => {
45
+ vi.mocked(client.delete).mockResolvedValue('OK')
46
+ expect(await api.deleteById('q1')).toBe('OK')
47
+ expect(client.delete).toHaveBeenCalledWith('/questions/q1', undefined)
48
+ })
49
+ })
@@ -1,13 +1,32 @@
1
1
  import type { ApiClient } from '../../core/ApiClient'
2
2
  import { BaseCrudApi } from '../../core/BaseCrudApi'
3
+ import type { ListQuery } from '../../core/types'
3
4
  import type { Question, CreateQuestionPayload, UpdateQuestionPayload } from './types'
4
5
 
6
+ /**
7
+ * Evaluation questions (NPS/CSAT), sent to the contact by the `SEND_QUESTION` bot action. The
8
+ * backend also serves the same routes under the singular `/question` (kept for compatibility).
9
+ *
10
+ * Permissions: `evaluation.view` to read; `evaluation.view` OR `evaluation.create`/`update`/
11
+ * `destroy` to write.
12
+ */
5
13
  export class QuestionsApi extends BaseCrudApi<
6
14
  Question,
7
15
  CreateQuestionPayload,
8
- UpdateQuestionPayload
16
+ UpdateQuestionPayload,
17
+ ListQuery<Question>,
18
+ 'OK'
9
19
  > {
10
20
  constructor(client: ApiClient) {
11
21
  super(client, '/questions')
12
22
  }
23
+
24
+ /**
25
+ * Deletes a question (soft delete). Resolves to `'OK'`. A question used in a bot answers 403;
26
+ * the answers of a deleted question leave the overview and the exports.
27
+ * @permissions evaluation.view OR evaluation.destroy
28
+ */
29
+ override deleteById(id: string, headers?: Record<string, string>): Promise<'OK'> {
30
+ return super.deleteById(id, headers)
31
+ }
13
32
  }
@@ -1,5 +1,6 @@
1
1
  import type { Answer } from '../answers/types'
2
2
 
3
+ /** `custom` exists in the database, but it never takes an answer (the UI hides it). */
3
4
  export type QuestionType = 'nps' | 'csat' | 'custom'
4
5
 
5
6
  export type Question = {
@@ -7,39 +8,57 @@ export type Question = {
7
8
  name: string
8
9
  type: QuestionType
9
10
  questionMessage: string
11
+ /** Minutes the contact has to answer. Comes back as a number, although it is sent as a string. */
10
12
  duration: number
11
13
  tries: number
12
14
  successMessage: string
13
15
  invalidMessage: string
14
16
  reasonMessage: string | null
17
+ /** Sends `closureMessage` after the evaluation ends. */
18
+ sendClosureMessage: boolean
19
+ closureMessage: string | null
15
20
  accountId: string
16
21
  createdAt: string
17
22
  updatedAt: string
18
23
  deletedAt: string | null
19
- // relationships
24
+ /** @deprecated Never comes: the route drops every `include` (empty whitelist). */
20
25
  answers?: Answer[]
21
26
  }
22
27
 
28
+ /** @deprecated The route drops every `include` (empty whitelist). */
23
29
  export type QuestionRelationships = 'answers'
24
30
 
25
31
  export type CreateQuestionPayload = {
32
+ /** Unique in the account (409, unless the account allows duplicate names). */
26
33
  name: string
34
+ /** An unknown type answers 500 (no validation before the database). */
27
35
  type: QuestionType
28
36
  questionMessage: string
29
- duration: number
37
+ /** Minutes, AS A STRING: a number answers 400 (`Should be of type string.`). */
38
+ duration: string
39
+ /** Greater than 0. */
30
40
  tries: number
41
+ /** Not validated by the route: missing answers 500. */
31
42
  successMessage: string
43
+ /** Not validated by the route: missing answers 500. */
32
44
  invalidMessage: string
33
45
  reasonMessage?: string
46
+ sendClosureMessage?: boolean
47
+ /** Required when `sendClosureMessage` is true (400); up to 500 characters. */
48
+ closureMessage?: string
34
49
  }
35
50
 
51
+ /** Not partial: `type`, `questionMessage`, `duration` and `tries` are required on update. */
36
52
  export type UpdateQuestionPayload = {
53
+ type: QuestionType
54
+ questionMessage: string
55
+ /** Minutes; on update a number is also taken. */
56
+ duration: string | number
57
+ tries: number
37
58
  name?: string
38
- type?: QuestionType
39
- questionMessage?: string
40
- duration?: number
41
- tries?: number
42
59
  successMessage?: string
43
60
  invalidMessage?: string
44
61
  reasonMessage?: string | null
62
+ sendClosureMessage?: boolean
63
+ closureMessage?: string | null
45
64
  }
@@ -0,0 +1,69 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import type { ApiClient } from '../../core/ApiClient'
3
+ import { QuickRepliesApi } from './QuickRepliesApi'
4
+ import type { QuickRepliesLoadOnDemandQuery } from './types'
5
+
6
+ // ── Mock client factory ──────────────────────────────────────────────
7
+
8
+ function createMockClient(): ApiClient {
9
+ return {
10
+ setAccessToken: vi.fn().mockReturnThis(),
11
+ request: vi.fn(),
12
+ get: vi.fn(),
13
+ post: vi.fn(),
14
+ put: vi.fn(),
15
+ patch: vi.fn(),
16
+ delete: vi.fn(),
17
+ }
18
+ }
19
+
20
+ const headers = { 'X-Custom': 'value' }
21
+
22
+ // ── Tests ────────────────────────────────────────────────────────────
23
+
24
+ describe('QuickRepliesApi', () => {
25
+ let client: ReturnType<typeof createMockClient>
26
+ let api: QuickRepliesApi
27
+
28
+ beforeEach(() => {
29
+ client = createMockClient()
30
+ api = new QuickRepliesApi(client)
31
+ })
32
+
33
+ it('loadOnDemand calls client.get with /quick-replies/load-on-demand and the query', async () => {
34
+ const query: QuickRepliesLoadOnDemandQuery = { order: [['title', 'ASC']], limit: 20, offset: 0 }
35
+ vi.mocked(client.get).mockResolvedValue({ data: [], total: 0 })
36
+ expect(await api.loadOnDemand(query, headers)).toEqual({
37
+ data: [],
38
+ total: 0,
39
+ })
40
+ expect(client.get).toHaveBeenCalledWith(
41
+ `/quick-replies/load-on-demand?query=${encodeURIComponent(JSON.stringify(query))}`,
42
+ headers,
43
+ )
44
+ })
45
+
46
+ it('create sends the departments, categories and files', async () => {
47
+ const body = {
48
+ title: 'Saudação',
49
+ text: 'Olá!',
50
+ departmentIds: ['d1'],
51
+ categoryIds: ['c1'],
52
+ files: [{ id: 'f1' }],
53
+ }
54
+ await api.create(body)
55
+ expect(client.post).toHaveBeenCalledWith('/quick-replies', body, undefined)
56
+ })
57
+
58
+ it('updateById sends the whole reply', async () => {
59
+ const body = { title: 'Saudação', text: 'Oi!', files: [] }
60
+ await api.updateById('q1', body, headers)
61
+ expect(client.put).toHaveBeenCalledWith('/quick-replies/q1', body, headers)
62
+ })
63
+
64
+ it('deleteById resolves to the OK of the route', async () => {
65
+ vi.mocked(client.delete).mockResolvedValue('OK')
66
+ expect(await api.deleteById('q1')).toBe('OK')
67
+ expect(client.delete).toHaveBeenCalledWith('/quick-replies/q1', undefined)
68
+ })
69
+ })
@@ -1,13 +1,46 @@
1
1
  import type { ApiClient } from '../../core/ApiClient'
2
2
  import { BaseCrudApi } from '../../core/BaseCrudApi'
3
- import type { QuickReply, CreateQuickReplyPayload, UpdateQuickReplyPayload } from './types'
3
+ import type { ListQuery } from '../../core/types'
4
+ import type {
5
+ QuickReply,
6
+ QuickRepliesLoadOnDemandQuery,
7
+ QuickRepliesLoadOnDemandResult,
8
+ CreateQuickReplyPayload,
9
+ UpdateQuickReplyPayload,
10
+ } from './types'
4
11
 
5
12
  export class QuickRepliesApi extends BaseCrudApi<
6
13
  QuickReply,
7
14
  CreateQuickReplyPayload,
8
- UpdateQuickReplyPayload
15
+ UpdateQuickReplyPayload,
16
+ ListQuery<QuickReply>,
17
+ 'OK'
9
18
  > {
10
19
  constructor(client: ApiClient) {
11
20
  super(client, '/quick-replies')
12
21
  }
22
+
23
+ /**
24
+ * Deletes a quick reply (soft delete). Resolves to `'OK'`; an unknown id answers 500.
25
+ * @permissions quickReplies.view OR quickReplies.destroy
26
+ */
27
+ override deleteById(id: string, headers?: Record<string, string>): Promise<'OK'> {
28
+ return super.deleteById(id, headers)
29
+ }
30
+
31
+ /**
32
+ * Lists the replies the user can use (no department, or one of the departments of the user),
33
+ * the way the chat loads them: `limit`/`offset` instead of pages, and `{ data, total }`
34
+ * instead of the pagination envelope.
35
+ * @permissions quickReplies.view
36
+ */
37
+ loadOnDemand(
38
+ query?: QuickRepliesLoadOnDemandQuery,
39
+ headers?: Record<string, string>,
40
+ ): Promise<QuickRepliesLoadOnDemandResult> {
41
+ return this.client.get<QuickRepliesLoadOnDemandResult>(
42
+ `${this.basePath}/load-on-demand${this.toQueryString(query)}`,
43
+ headers,
44
+ )
45
+ }
13
46
  }
@@ -1,3 +1,4 @@
1
+ import type { ListQuery } from '../../core/types'
1
2
  import type { Category } from '../categories/types'
2
3
  import type { Department } from '../departments/types'
3
4
  import type { File } from '../files/types'
@@ -11,6 +12,7 @@ export type QuickReply = {
11
12
  updatedAt: string
12
13
  deletedAt: string | null
13
14
  // relationships
15
+ /** Empty = all departments. */
14
16
  departments?: Department[]
15
17
  categories?: Category[]
16
18
  files?: File[]
@@ -18,12 +20,42 @@ export type QuickReply = {
18
20
 
19
21
  export type QuickReplyRelationships = 'departments' | 'categories' | 'files'
20
22
 
23
+ /**
24
+ * Files already uploaded through `FilesApi` with `attachedType: 'quickreply.file'` — uploaded
25
+ * with another type they get the `attachedId` but never show in `files`.
26
+ */
27
+ export type QuickReplyFilePayload = { id: string }
28
+
29
+ /** `title` and `text` are each unique in the account (409, unless the account allows duplicate names). */
21
30
  export type CreateQuickReplyPayload = {
22
31
  title: string
23
32
  text: string
33
+ /** Department ids; omitted = all departments. */
34
+ departmentIds?: string[]
35
+ categoryIds?: string[]
36
+ files?: QuickReplyFilePayload[]
24
37
  }
25
38
 
39
+ /**
40
+ * Not partial: `title` and `text` are required, the departments and categories are replaced
41
+ * (omitted = none), and so are the files.
42
+ */
26
43
  export type UpdateQuickReplyPayload = {
27
- title?: string
28
- text?: string
44
+ title: string
45
+ text: string
46
+ departmentIds?: string[]
47
+ categoryIds?: string[]
48
+ /** The files to keep or attach; `[]` removes all. Required: omitted, the update fails (500). */
49
+ files: QuickReplyFilePayload[]
50
+ }
51
+
52
+ /** The query of `loadOnDemand`, passed to the database as is. */
53
+ export type QuickRepliesLoadOnDemandQuery = Pick<
54
+ ListQuery<QuickReply>,
55
+ 'where' | 'include' | 'order' | 'attributes' | 'limit' | 'offset'
56
+ >
57
+
58
+ export type QuickRepliesLoadOnDemandResult = {
59
+ data: QuickReply[]
60
+ total: number
29
61
  }
@@ -0,0 +1,80 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import type { ApiClient } from '../../core/ApiClient'
3
+ import { ScheduleApi } from './ScheduleApi'
4
+
5
+ // ── Mock client factory ──────────────────────────────────────────────
6
+
7
+ function createMockClient(): ApiClient {
8
+ return {
9
+ setAccessToken: vi.fn().mockReturnThis(),
10
+ request: vi.fn(),
11
+ get: vi.fn(),
12
+ post: vi.fn(),
13
+ put: vi.fn(),
14
+ patch: vi.fn(),
15
+ delete: vi.fn(),
16
+ }
17
+ }
18
+
19
+ const headers = { 'X-Custom': 'value' }
20
+ const body = {
21
+ scheduledAt: '2026-10-02T12:00:00.000Z',
22
+ contactId: 'c1',
23
+ departmentId: 'd1',
24
+ message: 'Lembrete',
25
+ notes: '',
26
+ }
27
+
28
+ // ── Tests ────────────────────────────────────────────────────────────
29
+
30
+ describe('ScheduleApi', () => {
31
+ let client: ReturnType<typeof createMockClient>
32
+ let api: ScheduleApi
33
+
34
+ beforeEach(() => {
35
+ client = createMockClient()
36
+ api = new ScheduleApi(client)
37
+ })
38
+
39
+ it('create calls client.post with /schedule and resolves with no content', async () => {
40
+ vi.mocked(client.post).mockResolvedValue(undefined)
41
+ expect(await api.create(body, headers)).toBeUndefined()
42
+ expect(client.post).toHaveBeenCalledWith('/schedule', body, headers)
43
+ })
44
+
45
+ it('updateById calls client.put with /schedule/:id', async () => {
46
+ await api.updateById('s1', { ...body, files: [{ id: 'f1' }] })
47
+ expect(client.put).toHaveBeenCalledWith(
48
+ '/schedule/s1',
49
+ { ...body, files: [{ id: 'f1' }] },
50
+ undefined,
51
+ )
52
+ })
53
+
54
+ it('deleteById calls client.delete with /schedule/:id', async () => {
55
+ vi.mocked(client.delete).mockResolvedValue('OK')
56
+ expect(await api.deleteById('s1', headers)).toBe('OK')
57
+ expect(client.delete).toHaveBeenCalledWith('/schedule/s1', headers)
58
+ })
59
+
60
+ it('deleteMany sends the ids in where.id of a DELETE body', async () => {
61
+ vi.mocked(client.request).mockResolvedValue('OK')
62
+ expect(await api.deleteMany(['s1', 's2'], headers)).toBe('OK')
63
+ expect(client.request).toHaveBeenCalledWith(
64
+ 'DELETE',
65
+ '/schedule/many',
66
+ { where: { id: ['s1', 's2'] } },
67
+ headers,
68
+ )
69
+ })
70
+
71
+ it('reads through the inherited getMany', async () => {
72
+ await api.getMany({ where: { contactId: 'c1' }, paginate: false })
73
+ expect(client.get).toHaveBeenCalledWith(
74
+ `/schedule?query=${encodeURIComponent(
75
+ JSON.stringify({ where: { contactId: 'c1' }, paginate: false }),
76
+ )}`,
77
+ undefined,
78
+ )
79
+ })
80
+ })