@ikatec/digisac-api-sdk 4.3.0 → 4.4.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/contacts/ContactsApi.cjs +70 -0
- package/dist/apis/contacts/ContactsApi.cjs.map +1 -1
- package/dist/apis/contacts/ContactsApi.d.ts +51 -1
- package/dist/apis/contacts/ContactsApi.d.ts.map +1 -1
- package/dist/apis/contacts/ContactsApi.mjs +70 -0
- package/dist/apis/contacts/ContactsApi.mjs.map +1 -1
- package/dist/apis/contacts/types.d.ts +141 -1
- package/dist/apis/contacts/types.d.ts.map +1 -1
- package/dist/apis/services/ServicesApi.cjs +8 -0
- package/dist/apis/services/ServicesApi.cjs.map +1 -1
- package/dist/apis/services/ServicesApi.d.ts +7 -1
- package/dist/apis/services/ServicesApi.d.ts.map +1 -1
- package/dist/apis/services/ServicesApi.mjs +8 -0
- package/dist/apis/services/ServicesApi.mjs.map +1 -1
- package/dist/apis/services/types.d.ts +7 -0
- package/dist/apis/services/types.d.ts.map +1 -1
- package/dist/apis/tags/TagsApi.cjs +23 -0
- package/dist/apis/tags/TagsApi.cjs.map +1 -1
- package/dist/apis/tags/TagsApi.d.ts +19 -1
- package/dist/apis/tags/TagsApi.d.ts.map +1 -1
- package/dist/apis/tags/TagsApi.mjs +23 -0
- package/dist/apis/tags/TagsApi.mjs.map +1 -1
- package/dist/apis/tags/types.d.ts +8 -0
- package/dist/apis/tags/types.d.ts.map +1 -1
- package/dist/apis/users/UsersApi.cjs +77 -0
- package/dist/apis/users/UsersApi.cjs.map +1 -1
- package/dist/apis/users/UsersApi.d.ts +59 -1
- package/dist/apis/users/UsersApi.d.ts.map +1 -1
- package/dist/apis/users/UsersApi.mjs +77 -0
- package/dist/apis/users/UsersApi.mjs.map +1 -1
- package/dist/apis/users/types.d.ts +102 -0
- package/dist/apis/users/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/apis/acceptanceTerms/AcceptanceTermsApi.test.ts +1 -1
- package/src/apis/authHistory/AuthHistoryApi.test.ts +1 -1
- package/src/apis/campaigns/CampaignsApi.test.ts +1 -1
- package/src/apis/contacts/ContactsApi.test.ts +182 -5
- package/src/apis/contacts/ContactsApi.ts +128 -0
- package/src/apis/contacts/types.ts +154 -1
- package/src/apis/messages/MessagesApi.test.ts +3 -3
- package/src/apis/payloads.test-d.ts +26 -0
- package/src/apis/queryFormat.test.ts +27 -3
- package/src/apis/serviceAccessManagement/ServiceAccessManagementApi.test.ts +2 -2
- package/src/apis/services/ServicesApi.test.ts +49 -0
- package/src/apis/services/ServicesApi.ts +10 -1
- package/src/apis/services/types.ts +9 -0
- package/src/apis/stickerUsers/StickerUsersApi.test.ts +1 -1
- package/src/apis/tags/TagsApi.test.ts +79 -0
- package/src/apis/tags/TagsApi.ts +57 -1
- package/src/apis/tags/types.ts +9 -0
- package/src/apis/terms/TermsApi.test.ts +1 -1
- package/src/apis/users/UsersApi.test.ts +228 -0
- package/src/apis/users/UsersApi.ts +152 -1
- package/src/apis/users/types.ts +121 -0
|
@@ -2,12 +2,20 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
2
2
|
import { ContactsApi } from './ContactsApi'
|
|
3
3
|
import type { ApiClient } from '../../core/ApiClient'
|
|
4
4
|
import type {
|
|
5
|
+
BlockContactPayload,
|
|
6
|
+
BulkTransferTicketsPayload,
|
|
7
|
+
BulkTransferTicketsResult,
|
|
5
8
|
Contact,
|
|
9
|
+
ContactMediaPage,
|
|
10
|
+
ContactsSelection,
|
|
6
11
|
CreateContactPayload,
|
|
12
|
+
CreateManyContactsItem,
|
|
13
|
+
ExportCsvPayload,
|
|
7
14
|
ExportTemplatePayload,
|
|
8
15
|
CountContactsQuery,
|
|
9
16
|
ExistsContactsParams,
|
|
10
17
|
CountMediaResult,
|
|
18
|
+
ImportContactsPayload,
|
|
11
19
|
} from './types'
|
|
12
20
|
import type { Paginated } from '../../core/types'
|
|
13
21
|
|
|
@@ -101,7 +109,7 @@ describe('ContactsApi', () => {
|
|
|
101
109
|
it('serializes query parameters', async () => {
|
|
102
110
|
vi.mocked(client.get).mockResolvedValue(paginated)
|
|
103
111
|
await api.getMany({ page: 2, perPage: 5 })
|
|
104
|
-
const url = vi.mocked(client.get).mock.calls[0][0] as string
|
|
112
|
+
const url = vi.mocked(client.get).mock.calls[0]?.[0] as string
|
|
105
113
|
expect(url).toContain('/contacts?')
|
|
106
114
|
expect(decodeURIComponent(url)).toContain('"page":2')
|
|
107
115
|
})
|
|
@@ -225,7 +233,7 @@ describe('ContactsApi', () => {
|
|
|
225
233
|
vi.mocked(client.get).mockResolvedValue({ count: 10 })
|
|
226
234
|
const query: CountContactsQuery = { where: { name: 'Alice' } }
|
|
227
235
|
await api.count(query)
|
|
228
|
-
const url = vi.mocked(client.get).mock.calls[0][0] as string
|
|
236
|
+
const url = vi.mocked(client.get).mock.calls[0]?.[0] as string
|
|
229
237
|
expect(url).toContain('/contacts/count?')
|
|
230
238
|
expect(url).toContain('where')
|
|
231
239
|
})
|
|
@@ -270,7 +278,7 @@ describe('ContactsApi', () => {
|
|
|
270
278
|
it('serializes query parameters', async () => {
|
|
271
279
|
vi.mocked(client.get).mockResolvedValue(paginated)
|
|
272
280
|
await api.forward({ page: 1, perPage: 10 })
|
|
273
|
-
const url = vi.mocked(client.get).mock.calls[0][0] as string
|
|
281
|
+
const url = vi.mocked(client.get).mock.calls[0]?.[0] as string
|
|
274
282
|
expect(url).toContain('/contacts/forward?')
|
|
275
283
|
})
|
|
276
284
|
|
|
@@ -289,7 +297,7 @@ describe('ContactsApi', () => {
|
|
|
289
297
|
vi.mocked(client.get).mockResolvedValue({ '1234567890': true })
|
|
290
298
|
const params: ExistsContactsParams = { serviceId: 'svc-1', numbers: ['1234567890'] }
|
|
291
299
|
const result = await api.exists(params)
|
|
292
|
-
const url = vi.mocked(client.get).mock.calls[0][0] as string
|
|
300
|
+
const url = vi.mocked(client.get).mock.calls[0]?.[0] as string
|
|
293
301
|
expect(url).toContain('/contacts/exists?')
|
|
294
302
|
expect(url).toContain('serviceId=svc-1')
|
|
295
303
|
expect(result).toEqual({ '1234567890': true })
|
|
@@ -299,7 +307,7 @@ describe('ContactsApi', () => {
|
|
|
299
307
|
vi.mocked(client.get).mockResolvedValue({})
|
|
300
308
|
const headers = { 'X-Custom': 'h' }
|
|
301
309
|
await api.exists({ serviceId: 'svc-1', numbers: [] }, headers)
|
|
302
|
-
expect(vi.mocked(client.get).mock.calls[0][1]).toEqual(headers)
|
|
310
|
+
expect(vi.mocked(client.get).mock.calls[0]?.[1]).toEqual(headers)
|
|
303
311
|
})
|
|
304
312
|
})
|
|
305
313
|
|
|
@@ -365,6 +373,175 @@ describe('ContactsApi', () => {
|
|
|
365
373
|
})
|
|
366
374
|
})
|
|
367
375
|
|
|
376
|
+
// ── createMany ───────────────────────────────────────────────────
|
|
377
|
+
|
|
378
|
+
describe('createMany', () => {
|
|
379
|
+
const items: CreateManyContactsItem[] = [
|
|
380
|
+
{ serviceId: 'svc-1', name: 'Alice', number: '5511999990001' },
|
|
381
|
+
{ serviceId: 'svc-1', name: 'Bob', number: '5511999990002', tagIds: ['t1'] },
|
|
382
|
+
]
|
|
383
|
+
|
|
384
|
+
it('calls client.post with /contacts/many and the array body', async () => {
|
|
385
|
+
vi.mocked(client.post).mockResolvedValue([contact])
|
|
386
|
+
const result = await api.createMany(items)
|
|
387
|
+
expect(client.post).toHaveBeenCalledWith('/contacts/many', items, undefined)
|
|
388
|
+
expect(result).toEqual([contact])
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
it('serializes the query and forwards custom headers', async () => {
|
|
392
|
+
const headers = { 'X-Custom': 'value' }
|
|
393
|
+
await api.createMany(items, { include: ['tags'] }, headers)
|
|
394
|
+
const [url, body, sentHeaders] = vi.mocked(client.post).mock.calls[0] ?? []
|
|
395
|
+
expect(decodeURIComponent(url as string)).toBe('/contacts/many?query={"include":["tags"]}')
|
|
396
|
+
expect(body).toBe(items)
|
|
397
|
+
expect(sentHeaders).toBe(headers)
|
|
398
|
+
})
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
// ── deleteMany ───────────────────────────────────────────────────
|
|
402
|
+
|
|
403
|
+
describe('deleteMany', () => {
|
|
404
|
+
it('calls client.request with DELETE /contacts/many and the selection', async () => {
|
|
405
|
+
vi.mocked(client.request).mockResolvedValue(true)
|
|
406
|
+
const body: ContactsSelection = { where: { id: { $in: ['c1', 'c2'] } } }
|
|
407
|
+
const result = await api.deleteMany(body)
|
|
408
|
+
expect(client.request).toHaveBeenCalledWith('DELETE', '/contacts/many', body, undefined)
|
|
409
|
+
expect(result).toBe(true)
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
it('forwards custom headers', async () => {
|
|
413
|
+
const headers = { 'X-Custom': 'value' }
|
|
414
|
+
await api.deleteMany({ where: { id: 'c1' } }, headers)
|
|
415
|
+
expect(client.request).toHaveBeenCalledWith(
|
|
416
|
+
'DELETE',
|
|
417
|
+
'/contacts/many',
|
|
418
|
+
{ where: { id: 'c1' } },
|
|
419
|
+
headers,
|
|
420
|
+
)
|
|
421
|
+
})
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
// ── exportCsv ────────────────────────────────────────────────────
|
|
425
|
+
|
|
426
|
+
describe('exportCsv', () => {
|
|
427
|
+
it('calls client.post with /contacts/export/csv and resolves to the CSV text', async () => {
|
|
428
|
+
const csv = 'Nome,DDI,Número\nAlice,55,11999990001\n'
|
|
429
|
+
vi.mocked(client.post).mockResolvedValue(csv)
|
|
430
|
+
const body: ExportCsvPayload = {
|
|
431
|
+
serviceType: 'whatsapp',
|
|
432
|
+
type: 'comma',
|
|
433
|
+
where: { id: 'cnt-1' },
|
|
434
|
+
}
|
|
435
|
+
const result = await api.exportCsv(body)
|
|
436
|
+
expect(client.post).toHaveBeenCalledWith('/contacts/export/csv', body, undefined)
|
|
437
|
+
expect(result).toBe(csv)
|
|
438
|
+
})
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
// ── importCsv ────────────────────────────────────────────────────
|
|
442
|
+
|
|
443
|
+
describe('importCsv', () => {
|
|
444
|
+
it('calls client.post with /contacts/import-contacts and body', async () => {
|
|
445
|
+
const body: ImportContactsPayload = {
|
|
446
|
+
service: { id: 'svc-1', accountId: 'acc-1', type: 'whatsapp' },
|
|
447
|
+
file: { id: 'f1', name: 'contacts.csv', mimetype: 'text/csv', urlToUpload: 'https://s3' },
|
|
448
|
+
csvHeaderLine: ['Nome', 'DDI', 'Número'],
|
|
449
|
+
type: 'comma',
|
|
450
|
+
tagsIds: ['t1'],
|
|
451
|
+
}
|
|
452
|
+
vi.mocked(client.post).mockResolvedValue('OK')
|
|
453
|
+
const headers = { 'X-Custom': 'value' }
|
|
454
|
+
const result = await api.importCsv(body, headers)
|
|
455
|
+
expect(client.post).toHaveBeenCalledWith('/contacts/import-contacts', body, headers)
|
|
456
|
+
expect(result).toBe('OK')
|
|
457
|
+
})
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
// ── sync ─────────────────────────────────────────────────────────
|
|
461
|
+
|
|
462
|
+
describe('sync', () => {
|
|
463
|
+
it('calls client.post with /contacts/:id/sync and an empty body by default', async () => {
|
|
464
|
+
vi.mocked(client.post).mockResolvedValue('OK')
|
|
465
|
+
const result = await api.sync('c1')
|
|
466
|
+
expect(client.post).toHaveBeenCalledWith('/contacts/c1/sync', {}, undefined)
|
|
467
|
+
expect(result).toBe('OK')
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
it('sends forceSync and forwards custom headers', async () => {
|
|
471
|
+
const headers = { 'X-Custom': 'value' }
|
|
472
|
+
await api.sync('c1', { forceSync: true }, headers)
|
|
473
|
+
expect(client.post).toHaveBeenCalledWith('/contacts/c1/sync', { forceSync: true }, headers)
|
|
474
|
+
})
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
// ── block ────────────────────────────────────────────────────────
|
|
478
|
+
|
|
479
|
+
describe('block', () => {
|
|
480
|
+
it('calls client.post with /contacts/:id/block and body', async () => {
|
|
481
|
+
vi.mocked(client.post).mockResolvedValue({ ...contact, block: true })
|
|
482
|
+
const body: BlockContactPayload = { block: true, description: 'spam' }
|
|
483
|
+
const result = await api.block('c1', body)
|
|
484
|
+
expect(client.post).toHaveBeenCalledWith('/contacts/c1/block', body, undefined)
|
|
485
|
+
expect(result.block).toBe(true)
|
|
486
|
+
})
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
// ── markRead ─────────────────────────────────────────────────────
|
|
490
|
+
|
|
491
|
+
describe('markRead', () => {
|
|
492
|
+
it('calls client.post with /contacts/:id/mark-read and no body', async () => {
|
|
493
|
+
vi.mocked(client.post).mockResolvedValue('OK')
|
|
494
|
+
const headers = { 'X-Custom': 'value' }
|
|
495
|
+
const result = await api.markRead('c1', headers)
|
|
496
|
+
expect(client.post).toHaveBeenCalledWith('/contacts/c1/mark-read', undefined, headers)
|
|
497
|
+
expect(result).toBe('OK')
|
|
498
|
+
})
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
// ── getMedia ─────────────────────────────────────────────────────
|
|
502
|
+
|
|
503
|
+
describe('getMedia', () => {
|
|
504
|
+
const page: ContactMediaPage = { data: [], limit: 50, hasNextPage: false, nextCursor: null }
|
|
505
|
+
|
|
506
|
+
it('calls client.get with /contacts/:id/media when no query is provided', async () => {
|
|
507
|
+
vi.mocked(client.get).mockResolvedValue(page)
|
|
508
|
+
const result = await api.getMedia('c1')
|
|
509
|
+
expect(client.get).toHaveBeenCalledWith('/contacts/c1/media', undefined)
|
|
510
|
+
expect(result).toEqual(page)
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
it('serializes the type, cursor and perPage', async () => {
|
|
514
|
+
await api.getMedia('c1', {
|
|
515
|
+
where: { type: 'document', cursor: '2026-01-01T00:00:00Z' },
|
|
516
|
+
perPage: 10,
|
|
517
|
+
})
|
|
518
|
+
const url = decodeURIComponent(vi.mocked(client.get).mock.calls[0]?.[0] as string)
|
|
519
|
+
expect(url).toBe(
|
|
520
|
+
'/contacts/c1/media?query={"where":{"type":"document","cursor":"2026-01-01T00:00:00Z"},"perPage":10}',
|
|
521
|
+
)
|
|
522
|
+
})
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
// ── bulkTransferTickets ──────────────────────────────────────────
|
|
526
|
+
|
|
527
|
+
describe('bulkTransferTickets', () => {
|
|
528
|
+
it('calls client.post with /contacts/ticket/bulk-transfer and body', async () => {
|
|
529
|
+
const response: BulkTransferTicketsResult = {
|
|
530
|
+
message: 'Transferência realizada com sucesso',
|
|
531
|
+
ticketsSelectedId: ['t1'],
|
|
532
|
+
}
|
|
533
|
+
vi.mocked(client.post).mockResolvedValue(response)
|
|
534
|
+
const body: BulkTransferTicketsPayload = {
|
|
535
|
+
departmentId: 'd1',
|
|
536
|
+
userId: null,
|
|
537
|
+
ticketsSelectedId: ['t1'],
|
|
538
|
+
}
|
|
539
|
+
const result = await api.bulkTransferTickets(body)
|
|
540
|
+
expect(client.post).toHaveBeenCalledWith('/contacts/ticket/bulk-transfer', body, undefined)
|
|
541
|
+
expect(result).toEqual(response)
|
|
542
|
+
})
|
|
543
|
+
})
|
|
544
|
+
|
|
368
545
|
// ── Error propagation ────────────────────────────────────────────
|
|
369
546
|
|
|
370
547
|
describe('error propagation', () => {
|
|
@@ -3,12 +3,22 @@ import type { ApiClient } from '../../core/ApiClient'
|
|
|
3
3
|
import { BaseCrudApi } from '../../core/BaseCrudApi'
|
|
4
4
|
import type { ListQuery, Paginated } from '../../core/types'
|
|
5
5
|
import type {
|
|
6
|
+
BlockContactPayload,
|
|
7
|
+
BulkTransferTicketsPayload,
|
|
8
|
+
BulkTransferTicketsResult,
|
|
6
9
|
Contact,
|
|
10
|
+
ContactMediaPage,
|
|
11
|
+
ContactMediaQuery,
|
|
12
|
+
ContactsSelection,
|
|
7
13
|
CountContactsQuery,
|
|
8
14
|
CountMediaResult,
|
|
9
15
|
CreateContactPayload,
|
|
16
|
+
CreateManyContactsItem,
|
|
10
17
|
ExistsContactsParams,
|
|
18
|
+
ExportCsvPayload,
|
|
11
19
|
ExportTemplatePayload,
|
|
20
|
+
ImportContactsPayload,
|
|
21
|
+
SyncContactPayload,
|
|
12
22
|
TransferTicketPayload,
|
|
13
23
|
UpdateContactPayload,
|
|
14
24
|
} from './types'
|
|
@@ -120,4 +130,122 @@ export class ContactsApi extends BaseCrudApi<Contact, CreateContactPayload, Upda
|
|
|
120
130
|
headers,
|
|
121
131
|
)
|
|
122
132
|
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Creates or updates several contacts in one call. Existing contacts (same service and
|
|
136
|
+
* number or e-mail) are updated; the others are created. Returns the resulting contacts.
|
|
137
|
+
* @permissions contacts.view OR contacts.update OR contacts.create
|
|
138
|
+
*/
|
|
139
|
+
createMany(
|
|
140
|
+
body: CreateManyContactsItem[],
|
|
141
|
+
query?: ListQuery<Contact>,
|
|
142
|
+
headers?: Record<string, string>,
|
|
143
|
+
): Promise<Contact[]> {
|
|
144
|
+
const queryString = this.toQueryString(query)
|
|
145
|
+
return this.client.post<Contact[], CreateManyContactsItem[]>(
|
|
146
|
+
`${this.basePath}/many${queryString}`,
|
|
147
|
+
body,
|
|
148
|
+
headers,
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Deletes every contact of the account that matches the selection.
|
|
154
|
+
* @permissions contacts.view OR contacts.destroy
|
|
155
|
+
*/
|
|
156
|
+
deleteMany(body: ContactsSelection, headers?: Record<string, string>): Promise<boolean> {
|
|
157
|
+
return this.client.request<boolean, ContactsSelection>(
|
|
158
|
+
'DELETE',
|
|
159
|
+
`${this.basePath}/many`,
|
|
160
|
+
body,
|
|
161
|
+
headers,
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Exports the contacts that match the filter as CSV. Resolves to the CSV text.
|
|
167
|
+
* @permissions contacts.view AND contacts.export
|
|
168
|
+
*/
|
|
169
|
+
exportCsv(body: ExportCsvPayload, headers?: Record<string, string>): Promise<string> {
|
|
170
|
+
return this.client.post<string, ExportCsvPayload>(`${this.basePath}/export/csv`, body, headers)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Starts the import of a CSV file previously uploaded through `FilesApi`. The import runs
|
|
175
|
+
* in the background; the header line is validated against the service type template.
|
|
176
|
+
* Resolves to `'OK'`.
|
|
177
|
+
* @permissions contacts.view OR contacts.update OR contacts.create
|
|
178
|
+
*/
|
|
179
|
+
importCsv(body: ImportContactsPayload, headers?: Record<string, string>): Promise<string> {
|
|
180
|
+
return this.client.post<string, ImportContactsPayload>(
|
|
181
|
+
`${this.basePath}/import-contacts`,
|
|
182
|
+
body,
|
|
183
|
+
headers,
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Marks the contact as read and, when needed, syncs it with the messaging service.
|
|
189
|
+
* Resolves to `'OK'`.
|
|
190
|
+
* @permissions contacts.view OR contacts.update
|
|
191
|
+
*/
|
|
192
|
+
sync(id: string, body?: SyncContactPayload, headers?: Record<string, string>): Promise<string> {
|
|
193
|
+
return this.client.post<string, SyncContactPayload>(
|
|
194
|
+
`${this.basePath}/${id}/sync`,
|
|
195
|
+
body ?? {},
|
|
196
|
+
headers,
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Blocks or unblocks a contact. Blocking closes the contact's open ticket. Requires the
|
|
202
|
+
* block list to be active on the account.
|
|
203
|
+
* @permissions contacts.block
|
|
204
|
+
*/
|
|
205
|
+
block(id: string, body: BlockContactPayload, headers?: Record<string, string>): Promise<Contact> {
|
|
206
|
+
return this.client.post<Contact, BlockContactPayload>(
|
|
207
|
+
`${this.basePath}/${id}/block`,
|
|
208
|
+
body,
|
|
209
|
+
headers,
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Resets the contact's unread counter. Resolves to `'OK'`.
|
|
215
|
+
* @permissions contacts.view OR contacts.update
|
|
216
|
+
*/
|
|
217
|
+
markRead(id: string, headers?: Record<string, string>): Promise<string> {
|
|
218
|
+
return this.client.post<string>(`${this.basePath}/${id}/mark-read`, undefined, headers)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Returns the files and links exchanged with a contact, cursor-paginated.
|
|
223
|
+
* @permissions contacts.view
|
|
224
|
+
*/
|
|
225
|
+
getMedia(
|
|
226
|
+
contactId: string,
|
|
227
|
+
query?: ContactMediaQuery,
|
|
228
|
+
headers?: Record<string, string>,
|
|
229
|
+
): Promise<ContactMediaPage> {
|
|
230
|
+
const queryString = this.toQueryString(query)
|
|
231
|
+
return this.client.get<ContactMediaPage>(
|
|
232
|
+
`${this.basePath}/${contactId}/media${queryString}`,
|
|
233
|
+
headers,
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Transfers several tickets at once to a department and/or user.
|
|
239
|
+
* @permissions tickets.transfer.all.bulk
|
|
240
|
+
*/
|
|
241
|
+
bulkTransferTickets(
|
|
242
|
+
body: BulkTransferTicketsPayload,
|
|
243
|
+
headers?: Record<string, string>,
|
|
244
|
+
): Promise<BulkTransferTicketsResult> {
|
|
245
|
+
return this.client.post<BulkTransferTicketsResult, BulkTransferTicketsPayload>(
|
|
246
|
+
`${this.basePath}/ticket/bulk-transfer`,
|
|
247
|
+
body,
|
|
248
|
+
headers,
|
|
249
|
+
)
|
|
250
|
+
}
|
|
123
251
|
}
|
|
@@ -4,7 +4,7 @@ import type { Service } from '../services/types'
|
|
|
4
4
|
import type { Tag } from '../tags/types'
|
|
5
5
|
import type { Ticket } from '../tickets/types'
|
|
6
6
|
import type { User } from '../users/types'
|
|
7
|
-
import type { ListQuery } from '../../core/types'
|
|
7
|
+
import type { IncludeItem, ListQuery, WhereClause } from '../../core/types'
|
|
8
8
|
|
|
9
9
|
export type ContactStatus = 'online' | 'offline' | 'absent'
|
|
10
10
|
export type ContactOrigin = 'api' | 'app' | 'campaign' | 'driver' | 'import' | 'web'
|
|
@@ -204,3 +204,156 @@ export type TransferTicketPayload = {
|
|
|
204
204
|
kanbanCardSubject?: string
|
|
205
205
|
initialMessage?: string
|
|
206
206
|
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* One item of `POST /contacts/many`. The backend matches existing contacts by service and
|
|
210
|
+
* number (or e-mail) and updates them; the others are created. Items repeating a number of
|
|
211
|
+
* the same batch are dropped (the first one wins).
|
|
212
|
+
*/
|
|
213
|
+
export type CreateManyContactsItem = {
|
|
214
|
+
serviceId: string
|
|
215
|
+
name: string
|
|
216
|
+
/** Phone number with country code, or the e-mail address for e-mail connections. */
|
|
217
|
+
number: string
|
|
218
|
+
internalName?: string
|
|
219
|
+
personId?: string
|
|
220
|
+
defaultDepartmentId?: string
|
|
221
|
+
defaultUserId?: string
|
|
222
|
+
tagIds?: string[]
|
|
223
|
+
/** When `true`, the given `tagIds` are merged into the existing ones instead of replacing them. */
|
|
224
|
+
mergeRelations?: boolean
|
|
225
|
+
/**
|
|
226
|
+
* Only `data.number` is read, as a fallback when `number` is empty; the backend rebuilds
|
|
227
|
+
* `data` from the number.
|
|
228
|
+
*/
|
|
229
|
+
data?: {
|
|
230
|
+
number: string
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Selects a set of contacts by filter. Used by `DELETE /contacts/many` and by the tag
|
|
236
|
+
* attach/detach routes. The backend always scopes the filter to the current account.
|
|
237
|
+
*
|
|
238
|
+
* `where` is required on purpose: the backend applies the operation to every contact the
|
|
239
|
+
* filter matches, so an empty filter would reach every contact of the account.
|
|
240
|
+
*/
|
|
241
|
+
export type ContactsSelection = {
|
|
242
|
+
where: WhereClause<Contact>
|
|
243
|
+
include?: IncludeItem<Contact>[]
|
|
244
|
+
customFilter?: Record<string, unknown>
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* CSV layout of the contact export and import, as in `exportTemplate`:
|
|
249
|
+
* - `'colon'`: `;` as the column delimiter (any other value uses `,`);
|
|
250
|
+
* - `'google'`: phone columns as DDI + Phone ("Telefone") instead of DDI + Number.
|
|
251
|
+
*
|
|
252
|
+
* E-mail and webchat connections ignore it for the contact columns (Email / Email + Number).
|
|
253
|
+
*/
|
|
254
|
+
export type ContactsCsvType = 'colon' | 'google' | (string & {})
|
|
255
|
+
|
|
256
|
+
/** Body for `POST /contacts/export/csv`. */
|
|
257
|
+
export type ExportCsvPayload = Omit<ListQuery<Contact>, 'page' | 'perPage'> & {
|
|
258
|
+
serviceType: string
|
|
259
|
+
type: ContactsCsvType
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Body for `POST /contacts/import-contacts`. The CSV must be uploaded first (see `FilesApi`);
|
|
264
|
+
* the header line is checked against the template of the service type and language.
|
|
265
|
+
*/
|
|
266
|
+
export type ImportContactsPayload = {
|
|
267
|
+
service: Pick<Service, 'id' | 'accountId' | 'type'>
|
|
268
|
+
file: {
|
|
269
|
+
id: string
|
|
270
|
+
name: string
|
|
271
|
+
mimetype: string
|
|
272
|
+
/** Sent by the web app; the backend reads only `id`. */
|
|
273
|
+
urlToUpload?: string
|
|
274
|
+
}
|
|
275
|
+
csvHeaderLine: string[]
|
|
276
|
+
type: ContactsCsvType
|
|
277
|
+
tagsIds?: string[]
|
|
278
|
+
defaultDepartmentId?: string
|
|
279
|
+
defaultUserId?: string
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Body for `POST /contacts/:id/block`. Blocking a contact closes its open ticket. */
|
|
283
|
+
export type BlockContactPayload = {
|
|
284
|
+
block: boolean
|
|
285
|
+
byUserId?: string
|
|
286
|
+
description?: string
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Body for `POST /contacts/:id/sync`. */
|
|
290
|
+
export type SyncContactPayload = {
|
|
291
|
+
forceSync?: boolean
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export type ContactMediaType = 'media' | 'document' | 'link'
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Query for `GET /contacts/:id/media`. Cursor-based: pass the previous page's `nextCursor`
|
|
298
|
+
* as `where.cursor`.
|
|
299
|
+
*/
|
|
300
|
+
export type ContactMediaQuery = {
|
|
301
|
+
where?: {
|
|
302
|
+
/** `'media'` = images and videos. Omitted = images, videos and documents. */
|
|
303
|
+
type?: ContactMediaType
|
|
304
|
+
/** Case-insensitive filter by file name (ignored for links). */
|
|
305
|
+
name?: string
|
|
306
|
+
cursor?: string
|
|
307
|
+
}
|
|
308
|
+
perPage?: number
|
|
309
|
+
order?: [string, 'ASC' | 'DESC' | 'asc' | 'desc'][]
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* One item of `GET /contacts/:id/media`. When the user cannot see the message, the
|
|
314
|
+
* identifying fields come as `'UNAUTHORIZED_MESSAGE'` and `isAuthorized` is `false`.
|
|
315
|
+
*/
|
|
316
|
+
export type ContactMediaItem = {
|
|
317
|
+
id: string
|
|
318
|
+
/** ID of the message that carries the file or link. */
|
|
319
|
+
attachedId: string
|
|
320
|
+
/** File name; absent for links. */
|
|
321
|
+
name?: string | null
|
|
322
|
+
/** Only for files. */
|
|
323
|
+
mimetype?: string
|
|
324
|
+
createdAt: string
|
|
325
|
+
updatedAt: string
|
|
326
|
+
accountId: string
|
|
327
|
+
url: string | null
|
|
328
|
+
/** Only for files (filled for videos). */
|
|
329
|
+
previewUrl?: string | null
|
|
330
|
+
isAuthorized: boolean
|
|
331
|
+
/** Only for `type: 'link'`. */
|
|
332
|
+
links?: string[]
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export type ContactMediaPage = {
|
|
336
|
+
data: ContactMediaItem[]
|
|
337
|
+
limit: number
|
|
338
|
+
hasNextPage: boolean
|
|
339
|
+
nextCursor: string | null
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Body for `POST /contacts/ticket/bulk-transfer`. */
|
|
343
|
+
export type BulkTransferTicketsPayload = {
|
|
344
|
+
departmentId?: string
|
|
345
|
+
/** `null` removes the assigned user. */
|
|
346
|
+
userId?: string | null
|
|
347
|
+
comments?: string
|
|
348
|
+
/** IDs of the tickets to transfer. Ignored when `allContactsSelected` is `true`. */
|
|
349
|
+
ticketsSelectedId?: string[]
|
|
350
|
+
allContactsSelected?: boolean
|
|
351
|
+
/** Contact filter used with `allContactsSelected`. */
|
|
352
|
+
query?: ListQuery<Contact>
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export type BulkTransferTicketsResult = {
|
|
356
|
+
message: string
|
|
357
|
+
/** IDs of the tickets actually transferred (the ones the target user can access). */
|
|
358
|
+
ticketsSelectedId: string[]
|
|
359
|
+
}
|
|
@@ -85,7 +85,7 @@ describe('MessagesApi', () => {
|
|
|
85
85
|
it('serializes query parameters', async () => {
|
|
86
86
|
vi.mocked(client.get).mockResolvedValue(paginated)
|
|
87
87
|
await api.getMany({ page: 2, perPage: 5 })
|
|
88
|
-
const url = vi.mocked(client.get).mock.calls[0][0] as string
|
|
88
|
+
const url = vi.mocked(client.get).mock.calls[0]?.[0] as string
|
|
89
89
|
expect(url).toContain('/messages?')
|
|
90
90
|
expect(decodeURIComponent(url)).toContain('"page":2')
|
|
91
91
|
expect(decodeURIComponent(url)).toContain('"perPage":5')
|
|
@@ -382,7 +382,7 @@ describe('MessagesApi', () => {
|
|
|
382
382
|
it('appends limit query parameter when provided', async () => {
|
|
383
383
|
vi.mocked(client.get).mockResolvedValue({ outOfRange: true })
|
|
384
384
|
await api.outOfRange('msg-1', 50)
|
|
385
|
-
const url = vi.mocked(client.get).mock.calls[0][0] as string
|
|
385
|
+
const url = vi.mocked(client.get).mock.calls[0]?.[0] as string
|
|
386
386
|
expect(url).toContain('/messages/msg-1/out-of-range?')
|
|
387
387
|
expect(url).toContain('limit=50')
|
|
388
388
|
})
|
|
@@ -426,7 +426,7 @@ describe('MessagesApi', () => {
|
|
|
426
426
|
it('appends limit query parameter when provided', async () => {
|
|
427
427
|
vi.mocked(client.get).mockResolvedValue([])
|
|
428
428
|
await api.around('msg-1', { limit: 10 })
|
|
429
|
-
const url = vi.mocked(client.get).mock.calls[0][0] as string
|
|
429
|
+
const url = vi.mocked(client.get).mock.calls[0]?.[0] as string
|
|
430
430
|
expect(url).toContain('/messages/msg-1/around?')
|
|
431
431
|
expect(url).toContain('limit=10')
|
|
432
432
|
})
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { expectError } from 'tsd'
|
|
2
2
|
import type { ApiClient } from '../core/ApiClient'
|
|
3
|
+
import { ContactsApi } from './contacts/ContactsApi'
|
|
3
4
|
import { PersonalAccessTokensApi } from './personalAccessTokens/PersonalAccessTokensApi'
|
|
4
5
|
import { RolesApi } from './roles/RolesApi'
|
|
6
|
+
import { TagsApi } from './tags/TagsApi'
|
|
5
7
|
import { TimetableApi } from './timetable/TimetableApi'
|
|
8
|
+
import { UsersApi } from './users/UsersApi'
|
|
6
9
|
|
|
7
10
|
declare const client: ApiClient
|
|
8
11
|
|
|
@@ -37,3 +40,26 @@ tokens.create({ name: 'CI', expiresIn: '90d' })
|
|
|
37
40
|
expectError(tokens.create({ name: 'CI', accessTokenExpiresAt: '2030-01-01T00:00:00Z' }))
|
|
38
41
|
expectError(tokens.create({ name: 'CI', expiresIn: '7d' }))
|
|
39
42
|
expectError(tokens.updateById('t-1', { scope: '*' }))
|
|
43
|
+
|
|
44
|
+
// ── users: the dedicated archive route takes a boolean, not a date ──
|
|
45
|
+
|
|
46
|
+
const users = new UsersApi(client)
|
|
47
|
+
users.archiveWithValidation('u-1', { archive: true })
|
|
48
|
+
expectError(users.archiveWithValidation('u-1', { archivedAt: '2030-01-01T00:00:00Z' }))
|
|
49
|
+
expectError(users.exportCsv({ filter: 'deleted' }))
|
|
50
|
+
|
|
51
|
+
// ── contacts: /many takes an array, the bulk delete a selection ─────
|
|
52
|
+
|
|
53
|
+
const contacts = new ContactsApi(client)
|
|
54
|
+
contacts.createMany([{ serviceId: 's-1', name: 'Alice', number: '5511999990001' }])
|
|
55
|
+
expectError(contacts.createMany({ serviceId: 's-1', name: 'Alice', number: '5511999990001' }))
|
|
56
|
+
expectError(contacts.getMedia('c-1', { where: { type: 'audio' } }))
|
|
57
|
+
|
|
58
|
+
// ── contact selections: an empty filter would reach every contact of the account ─
|
|
59
|
+
|
|
60
|
+
contacts.deleteMany({ where: { id: 'c-1' } })
|
|
61
|
+
expectError(contacts.deleteMany({}))
|
|
62
|
+
const tags = new TagsApi(client)
|
|
63
|
+
tags.attachContacts('t-1', { where: { id: { $in: ['c-1', 'c-2'] } } })
|
|
64
|
+
expectError(tags.attachContacts('t-1', {}))
|
|
65
|
+
expectError(tags.detachContacts('t-1', { include: ['tags'] }))
|
|
@@ -32,6 +32,8 @@ function createMockClient(queryFormat?: QueryFormat): ApiClient {
|
|
|
32
32
|
|
|
33
33
|
const listQuery = { where: { name: 'x' }, page: 2, perPage: 5 }
|
|
34
34
|
const getQuery = { attributes: ['id'] }
|
|
35
|
+
/** `Permission.name` is a union of the catalog names, so the permissions case filters by a real one. */
|
|
36
|
+
const permissionsQuery = { where: { name: 'chat' as const }, page: 2, perPage: 5 }
|
|
35
37
|
|
|
36
38
|
type Case = {
|
|
37
39
|
name: string
|
|
@@ -69,6 +71,27 @@ const cases: Case[] = [
|
|
|
69
71
|
return (f ? api.withQueryFormat(f) : api).count(listQuery)
|
|
70
72
|
},
|
|
71
73
|
},
|
|
74
|
+
{
|
|
75
|
+
name: 'ContactsApi.getMedia',
|
|
76
|
+
path: '/contacts/c1/media',
|
|
77
|
+
query: { where: { type: 'document' }, perPage: 10 },
|
|
78
|
+
call: (client, f) => {
|
|
79
|
+
const api = new ContactsApi(client)
|
|
80
|
+
return (f ? api.withQueryFormat(f) : api).getMedia('c1', {
|
|
81
|
+
where: { type: 'document' },
|
|
82
|
+
perPage: 10,
|
|
83
|
+
})
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: 'ContactsApi.createMany',
|
|
88
|
+
path: '/contacts/many',
|
|
89
|
+
query: { include: ['tags'] },
|
|
90
|
+
call: (client, f) => {
|
|
91
|
+
const api = new ContactsApi(client)
|
|
92
|
+
return (f ? api.withQueryFormat(f) : api).createMany([], { include: ['tags'] })
|
|
93
|
+
},
|
|
94
|
+
},
|
|
72
95
|
{
|
|
73
96
|
name: 'ContactsApi.forward',
|
|
74
97
|
path: '/contacts/forward',
|
|
@@ -199,10 +222,10 @@ const cases: Case[] = [
|
|
|
199
222
|
{
|
|
200
223
|
name: 'PermissionsApi.getMany',
|
|
201
224
|
path: '/permissions',
|
|
202
|
-
query:
|
|
225
|
+
query: permissionsQuery,
|
|
203
226
|
call: (client, f) => {
|
|
204
227
|
const api = new PermissionsApi(client)
|
|
205
|
-
return (f ? api.withQueryFormat(f) : api).getMany(
|
|
228
|
+
return (f ? api.withQueryFormat(f) : api).getMany(permissionsQuery)
|
|
206
229
|
},
|
|
207
230
|
},
|
|
208
231
|
{
|
|
@@ -255,7 +278,8 @@ const cases: Case[] = [
|
|
|
255
278
|
const jsonUrl = (path: string, query: object) =>
|
|
256
279
|
`${path}?query=${encodeURIComponent(JSON.stringify(query))}`
|
|
257
280
|
const qsUrl = (path: string, query: object) => `${path}?${qs.stringify(query)}`
|
|
258
|
-
const calledUrl = (client: ApiClient) =>
|
|
281
|
+
const calledUrl = (client: ApiClient) =>
|
|
282
|
+
vi.mocked(client.get).mock.calls.at(0)?.at(0) ?? vi.mocked(client.post).mock.calls.at(0)?.at(0)
|
|
259
283
|
|
|
260
284
|
describe.each(cases)('$name', ({ path, query, call }) => {
|
|
261
285
|
it("sends JSON with the default client format ('json')", async () => {
|