@ikatec/digisac-api-sdk 4.6.0 → 4.8.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/README.md +11 -8
- 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/tickets/TicketsApi.cjs +74 -1
- package/dist/apis/tickets/TicketsApi.cjs.map +1 -1
- package/dist/apis/tickets/TicketsApi.d.ts +68 -3
- package/dist/apis/tickets/TicketsApi.d.ts.map +1 -1
- package/dist/apis/tickets/TicketsApi.mjs +74 -1
- package/dist/apis/tickets/TicketsApi.mjs.map +1 -1
- package/dist/apis/tickets/types.d.ts +69 -8
- package/dist/apis/tickets/types.d.ts.map +1 -1
- package/dist/incommingWebhooks/index.d.ts +10 -2
- package/dist/incommingWebhooks/index.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/payloads.test-d.ts +74 -1
- package/src/apis/queryFormat.test.ts +48 -0
- package/src/apis/tickets/TicketsApi.test.ts +142 -5
- package/src/apis/tickets/TicketsApi.ts +135 -3
- package/src/apis/tickets/types.ts +76 -6
- package/src/incommingWebhooks/index.test-d.ts +14 -1
- package/src/incommingWebhooks/index.ts +11 -2
|
@@ -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'
|
|
@@ -11,6 +12,7 @@ import { PermissionsApi } from './permissions/PermissionsApi'
|
|
|
11
12
|
import { ServiceAccessManagementApi } from './serviceAccessManagement/ServiceAccessManagementApi'
|
|
12
13
|
import { StickerUsersApi } from './stickerUsers/StickerUsersApi'
|
|
13
14
|
import { TermsApi } from './terms/TermsApi'
|
|
15
|
+
import { TicketsApi } from './tickets/TicketsApi'
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* Every method that sends a list/get query must serialize it in the client's `queryFormat` and
|
|
@@ -34,6 +36,16 @@ const listQuery = { where: { name: 'x' }, page: 2, perPage: 5 }
|
|
|
34
36
|
const getQuery = { attributes: ['id'] }
|
|
35
37
|
/** `Permission.name` is a union of the catalog names, so the permissions case filters by a real one. */
|
|
36
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
|
+
}
|
|
45
|
+
const ticketsQuery = {
|
|
46
|
+
where: { isOpen: true },
|
|
47
|
+
order: [['startedAt', 'DESC'] as ['startedAt', 'DESC']],
|
|
48
|
+
}
|
|
37
49
|
|
|
38
50
|
type Case = {
|
|
39
51
|
name: string
|
|
@@ -44,6 +56,24 @@ type Case = {
|
|
|
44
56
|
}
|
|
45
57
|
|
|
46
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
|
+
},
|
|
47
77
|
{
|
|
48
78
|
name: 'ContactsApi.getMany (BaseCrudApi)',
|
|
49
79
|
path: '/contacts',
|
|
@@ -273,6 +303,24 @@ const cases: Case[] = [
|
|
|
273
303
|
return (f ? api.withQueryFormat(f) : api).getById('t1', { attributes: ['id'] })
|
|
274
304
|
},
|
|
275
305
|
},
|
|
306
|
+
{
|
|
307
|
+
name: 'TicketsApi.count',
|
|
308
|
+
path: '/tickets/count',
|
|
309
|
+
query: ticketsQuery,
|
|
310
|
+
call: (client, f) => {
|
|
311
|
+
const api = new TicketsApi(client)
|
|
312
|
+
return (f ? api.withQueryFormat(f) : api).count(ticketsQuery)
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
name: 'TicketsApi.exportHistory',
|
|
317
|
+
path: '/tickets/export-history',
|
|
318
|
+
query: ticketsQuery,
|
|
319
|
+
call: (client, f) => {
|
|
320
|
+
const api = new TicketsApi(client)
|
|
321
|
+
return (f ? api.withQueryFormat(f) : api).exportHistory(ticketsQuery)
|
|
322
|
+
},
|
|
323
|
+
},
|
|
276
324
|
]
|
|
277
325
|
|
|
278
326
|
const jsonUrl = (path: string, query: object) =>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
2
|
import { TicketsApi } from './TicketsApi'
|
|
3
3
|
import type { ApiClient } from '../../core/ApiClient'
|
|
4
|
+
import type { TicketsListQuery } from './types'
|
|
4
5
|
|
|
5
6
|
// ── Mock client factory ──────────────────────────────────────────────
|
|
6
7
|
|
|
@@ -18,6 +19,8 @@ function createMockClient(): ApiClient {
|
|
|
18
19
|
|
|
19
20
|
// ── Tests ────────────────────────────────────────────────────────────
|
|
20
21
|
|
|
22
|
+
const headers = { 'X-Custom': 'value' }
|
|
23
|
+
|
|
21
24
|
describe('TicketsApi', () => {
|
|
22
25
|
let client: ReturnType<typeof createMockClient>
|
|
23
26
|
let api: TicketsApi
|
|
@@ -29,18 +32,28 @@ describe('TicketsApi', () => {
|
|
|
29
32
|
|
|
30
33
|
describe('closeMany', () => {
|
|
31
34
|
it('PUTs /tickets/close-many with explicit ids', async () => {
|
|
32
|
-
vi.mocked(client.put).mockResolvedValue(
|
|
35
|
+
vi.mocked(client.put).mockResolvedValue(undefined)
|
|
33
36
|
const result = await api.closeMany({ ids: ['t1', 't2'] })
|
|
34
37
|
expect(client.put).toHaveBeenCalledWith(
|
|
35
38
|
'/tickets/close-many',
|
|
36
39
|
{ ids: ['t1', 't2'] },
|
|
37
40
|
undefined,
|
|
38
41
|
)
|
|
39
|
-
expect(result).
|
|
42
|
+
expect(result).toBeUndefined()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('sends the subjects and the summary of the closing', async () => {
|
|
46
|
+
vi.mocked(client.put).mockResolvedValue(undefined)
|
|
47
|
+
await api.closeMany({ ids: ['t1'], ticketTopicIds: ['tt1'], comments: 'Resolved' })
|
|
48
|
+
expect(client.put).toHaveBeenCalledWith(
|
|
49
|
+
'/tickets/close-many',
|
|
50
|
+
{ ids: ['t1'], ticketTopicIds: ['tt1'], comments: 'Resolved' },
|
|
51
|
+
undefined,
|
|
52
|
+
)
|
|
40
53
|
})
|
|
41
54
|
|
|
42
55
|
it('PUTs /tickets/close-many with a JSON-stringified query for the select-all variant', async () => {
|
|
43
|
-
vi.mocked(client.put).mockResolvedValue(
|
|
56
|
+
vi.mocked(client.put).mockResolvedValue(undefined)
|
|
44
57
|
await api.closeMany({ allContactsSelected: true, query: { query: '{"where":{}}' } })
|
|
45
58
|
expect(client.put).toHaveBeenCalledWith(
|
|
46
59
|
'/tickets/close-many',
|
|
@@ -50,8 +63,7 @@ describe('TicketsApi', () => {
|
|
|
50
63
|
})
|
|
51
64
|
|
|
52
65
|
it('forwards custom headers', async () => {
|
|
53
|
-
vi.mocked(client.put).mockResolvedValue(
|
|
54
|
-
const headers = { 'X-Custom': 'value' }
|
|
66
|
+
vi.mocked(client.put).mockResolvedValue(undefined)
|
|
55
67
|
await api.closeMany({ ids: ['t1'] }, headers)
|
|
56
68
|
expect(client.put).toHaveBeenCalledWith('/tickets/close-many', { ids: ['t1'] }, headers)
|
|
57
69
|
})
|
|
@@ -61,4 +73,129 @@ describe('TicketsApi', () => {
|
|
|
61
73
|
await expect(api.closeMany({ ids: ['t1'] })).rejects.toThrow('Forbidden')
|
|
62
74
|
})
|
|
63
75
|
})
|
|
76
|
+
|
|
77
|
+
describe('deprecated CRUD', () => {
|
|
78
|
+
it('keeps create, updateById and deleteById on the generic routes', async () => {
|
|
79
|
+
vi.mocked(client.post).mockResolvedValue({})
|
|
80
|
+
vi.mocked(client.put).mockResolvedValue({})
|
|
81
|
+
vi.mocked(client.delete).mockResolvedValue({})
|
|
82
|
+
await api.create({ contactId: 'c1', departmentId: 'd1' })
|
|
83
|
+
await api.updateById('t1', { comments: 'x' })
|
|
84
|
+
await api.deleteById('t1')
|
|
85
|
+
expect(client.post).toHaveBeenCalledWith(
|
|
86
|
+
'/tickets',
|
|
87
|
+
{ contactId: 'c1', departmentId: 'd1' },
|
|
88
|
+
undefined,
|
|
89
|
+
)
|
|
90
|
+
expect(client.put).toHaveBeenCalledWith('/tickets/t1', { comments: 'x' }, undefined)
|
|
91
|
+
expect(client.delete).toHaveBeenCalledWith('/tickets/t1', undefined)
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
describe('count', () => {
|
|
96
|
+
it('GETs /tickets/count with the query', async () => {
|
|
97
|
+
vi.mocked(client.get).mockResolvedValue({ count: 3 })
|
|
98
|
+
const query = { where: { isOpen: true } }
|
|
99
|
+
const result = await api.count(query)
|
|
100
|
+
expect(client.get).toHaveBeenCalledWith(
|
|
101
|
+
`/tickets/count?query=${encodeURIComponent(JSON.stringify(query))}`,
|
|
102
|
+
undefined,
|
|
103
|
+
)
|
|
104
|
+
expect(result).toEqual({ count: 3 })
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('GETs /tickets/count without a query', async () => {
|
|
108
|
+
vi.mocked(client.get).mockResolvedValue({ count: 10 })
|
|
109
|
+
await api.count()
|
|
110
|
+
expect(client.get).toHaveBeenCalledWith('/tickets/count', undefined)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('sends the queries for one count per query', async () => {
|
|
114
|
+
vi.mocked(client.get).mockResolvedValue({ counts: [1, 2] })
|
|
115
|
+
const query = { queries: [{ where: { isOpen: true } }, { where: { isOpen: false } }] }
|
|
116
|
+
const result = await api.count(query, headers)
|
|
117
|
+
expect(client.get).toHaveBeenCalledWith(
|
|
118
|
+
`/tickets/count?query=${encodeURIComponent(JSON.stringify(query))}`,
|
|
119
|
+
headers,
|
|
120
|
+
)
|
|
121
|
+
expect(result).toEqual({ counts: [1, 2] })
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
describe('countPost', () => {
|
|
126
|
+
it('POSTs /tickets/count with the filters and the query', async () => {
|
|
127
|
+
vi.mocked(client.post).mockResolvedValue({ count: 3 })
|
|
128
|
+
const body = { where: { isOpen: true }, query: {} }
|
|
129
|
+
const result = await api.countPost(body, headers)
|
|
130
|
+
expect(client.post).toHaveBeenCalledWith('/tickets/count', body, headers)
|
|
131
|
+
expect(result).toEqual({ count: 3 })
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('POSTs the queries for one count per query', async () => {
|
|
135
|
+
vi.mocked(client.post).mockResolvedValue({ counts: [3, 0] })
|
|
136
|
+
const body = {
|
|
137
|
+
query: { queries: [{ where: { isOpen: true } }, { where: { isOpen: false } }] },
|
|
138
|
+
}
|
|
139
|
+
const result = await api.countPost(body)
|
|
140
|
+
expect(client.post).toHaveBeenCalledWith('/tickets/count', body, undefined)
|
|
141
|
+
expect(result).toEqual({ counts: [3, 0] })
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
describe('open ticket counts', () => {
|
|
146
|
+
it('GETs /tickets/count-open/{serviceId}', async () => {
|
|
147
|
+
vi.mocked(client.get).mockResolvedValue(3)
|
|
148
|
+
expect(await api.countOpenByService('s1', headers)).toBe(3)
|
|
149
|
+
expect(client.get).toHaveBeenCalledWith('/tickets/count-open/s1', headers)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('GETs /tickets/count-open-by-department/{departmentId}', async () => {
|
|
153
|
+
vi.mocked(client.get).mockResolvedValue(2)
|
|
154
|
+
expect(await api.countOpenByDepartment('d1')).toBe(2)
|
|
155
|
+
expect(client.get).toHaveBeenCalledWith('/tickets/count-open-by-department/d1', undefined)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('GETs /tickets/count-open-by-user/{userId}', async () => {
|
|
159
|
+
vi.mocked(client.get).mockResolvedValue(0)
|
|
160
|
+
expect(await api.countOpenByUser('u1')).toBe(0)
|
|
161
|
+
expect(client.get).toHaveBeenCalledWith('/tickets/count-open-by-user/u1', undefined)
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
describe('getFirstWaitingTime', () => {
|
|
166
|
+
it('GETs /tickets/{id}/first-waiting-time', async () => {
|
|
167
|
+
vi.mocked(client.get).mockResolvedValue('00:01:30')
|
|
168
|
+
expect(await api.getFirstWaitingTime('t1', headers)).toBe('00:01:30')
|
|
169
|
+
expect(client.get).toHaveBeenCalledWith('/tickets/t1/first-waiting-time', headers)
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
describe('exportTxt', () => {
|
|
174
|
+
it('POSTs /tickets/export with the protocol', async () => {
|
|
175
|
+
vi.mocked(client.post).mockResolvedValue('Nome do contato: A')
|
|
176
|
+
expect(await api.exportTxt('202610061675', headers)).toBe('Nome do contato: A')
|
|
177
|
+
expect(client.post).toHaveBeenCalledWith(
|
|
178
|
+
'/tickets/export',
|
|
179
|
+
{ protocol: '202610061675' },
|
|
180
|
+
headers,
|
|
181
|
+
)
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
describe('exportHistory', () => {
|
|
186
|
+
it('POSTs /tickets/export-history with the query in the URL and no body', async () => {
|
|
187
|
+
vi.mocked(client.post).mockResolvedValue('Protocolo;Nome\n')
|
|
188
|
+
const query: TicketsListQuery = {
|
|
189
|
+
where: { isOpen: true },
|
|
190
|
+
include: [{ model: 'contact', include: ['service'] }],
|
|
191
|
+
order: [['startedAt', 'DESC']],
|
|
192
|
+
}
|
|
193
|
+
expect(await api.exportHistory(query)).toBe('Protocolo;Nome\n')
|
|
194
|
+
expect(client.post).toHaveBeenCalledWith(
|
|
195
|
+
`/tickets/export-history?query=${encodeURIComponent(JSON.stringify(query))}`,
|
|
196
|
+
undefined,
|
|
197
|
+
undefined,
|
|
198
|
+
)
|
|
199
|
+
})
|
|
200
|
+
})
|
|
64
201
|
})
|
|
@@ -5,26 +5,158 @@ import type {
|
|
|
5
5
|
CreateTicketPayload,
|
|
6
6
|
UpdateTicketPayload,
|
|
7
7
|
CloseManyTicketsPayload,
|
|
8
|
+
TicketsCount,
|
|
9
|
+
TicketsCountManyQuery,
|
|
10
|
+
TicketsCountPostPayload,
|
|
11
|
+
TicketsCounts,
|
|
12
|
+
TicketsListQuery,
|
|
8
13
|
} from './types'
|
|
9
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Tickets. The backend only serves them for reading (`getMany`, `getOne`, `getById`, the counts,
|
|
17
|
+
* the first waiting time and the exports) plus `closeMany`: a ticket is opened and transferred by
|
|
18
|
+
* `ContactsApi.transferTicket`. The reads check no permission (any authenticated user): the
|
|
19
|
+
* `@permissions` of the inherited `getMany`, `getOne` and `getById` do not apply here. `getById`
|
|
20
|
+
* of an unknown id resolves `null`. For a user who is not an admin, `getMany` leaves out the
|
|
21
|
+
* tickets of the contacts of the connections restricted to that user; the counts and `getById`
|
|
22
|
+
* do not.
|
|
23
|
+
*/
|
|
10
24
|
export class TicketsApi extends BaseCrudApi<Ticket, CreateTicketPayload, UpdateTicketPayload> {
|
|
11
25
|
constructor(client: ApiClient) {
|
|
12
26
|
super(client, '/tickets')
|
|
13
27
|
}
|
|
14
28
|
|
|
29
|
+
/** @deprecated Not served: `POST /tickets` answers 404 `Route not found.`. Open a ticket through `ContactsApi.transferTicket`. */
|
|
30
|
+
override create(body: CreateTicketPayload, headers?: Record<string, string>): Promise<Ticket> {
|
|
31
|
+
return super.create(body, headers)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** @deprecated Not served: `PUT /tickets/{id}` answers 404 `Route not found.`. Transfer through `ContactsApi.transferTicket`, close through `closeMany`. */
|
|
35
|
+
override updateById(
|
|
36
|
+
id: string,
|
|
37
|
+
body: UpdateTicketPayload,
|
|
38
|
+
headers?: Record<string, string>,
|
|
39
|
+
): Promise<Ticket> {
|
|
40
|
+
return super.updateById(id, body, headers)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** @deprecated Not served: `DELETE /tickets/{id}` answers 404 `Route not found.`. */
|
|
44
|
+
override deleteById(id: string, headers?: Record<string, string>): Promise<Ticket> {
|
|
45
|
+
return super.deleteById(id, headers)
|
|
46
|
+
}
|
|
47
|
+
|
|
15
48
|
/**
|
|
16
49
|
* Closes multiple tickets in a single request. This is the only ticket-close
|
|
17
50
|
* endpoint exposed by the API — there is no per-ticket close/update route.
|
|
18
51
|
*
|
|
19
52
|
* Pass `{ ids }` to close specific tickets, or `{ allContactsSelected: true, query }`
|
|
20
|
-
* to close every OPEN ticket matching `query` (the backend forces `isOpen: true`)
|
|
53
|
+
* to close every OPEN ticket matching `query` (the backend forces `isOpen: true`): an empty
|
|
54
|
+
* `where` closes every open ticket of the account. The answer has no body. Ids of closed,
|
|
55
|
+
* unknown or other accounts' tickets are skipped in silence; no id and no
|
|
56
|
+
* `allContactsSelected` answers 400 `ids not found`.
|
|
21
57
|
* @permissions tickets.close.all OR tickets.close.me
|
|
22
58
|
*/
|
|
23
|
-
closeMany(payload: CloseManyTicketsPayload, headers?: Record<string, string>): Promise<
|
|
24
|
-
return this.client.put<
|
|
59
|
+
closeMany(payload: CloseManyTicketsPayload, headers?: Record<string, string>): Promise<void> {
|
|
60
|
+
return this.client.put<void, CloseManyTicketsPayload>(
|
|
25
61
|
`${this.basePath}/close-many`,
|
|
26
62
|
payload,
|
|
27
63
|
headers,
|
|
28
64
|
)
|
|
29
65
|
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Counts the tickets matching the query. With `queries`, answers one count per query, in the
|
|
69
|
+
* same order (the root filters are then ignored).
|
|
70
|
+
*/
|
|
71
|
+
count(query: TicketsCountManyQuery, headers?: Record<string, string>): Promise<TicketsCounts>
|
|
72
|
+
count(query?: TicketsListQuery, headers?: Record<string, string>): Promise<TicketsCount>
|
|
73
|
+
count(
|
|
74
|
+
query?: TicketsListQuery | TicketsCountManyQuery,
|
|
75
|
+
headers?: Record<string, string>,
|
|
76
|
+
): Promise<TicketsCount | TicketsCounts> {
|
|
77
|
+
return this.client.get<TicketsCount | TicketsCounts>(
|
|
78
|
+
`${this.basePath}/count${this.toQueryString(query)}`,
|
|
79
|
+
headers,
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* POST variant of `count`, for queries too long for the URL. See {@link TicketsCountPostPayload}:
|
|
85
|
+
* `query` is required.
|
|
86
|
+
*/
|
|
87
|
+
countPost(
|
|
88
|
+
body: TicketsCountPostPayload & { query: { queries: TicketsListQuery[] } },
|
|
89
|
+
headers?: Record<string, string>,
|
|
90
|
+
): Promise<TicketsCounts>
|
|
91
|
+
countPost(body: TicketsCountPostPayload, headers?: Record<string, string>): Promise<TicketsCount>
|
|
92
|
+
countPost(
|
|
93
|
+
body: TicketsCountPostPayload,
|
|
94
|
+
headers?: Record<string, string>,
|
|
95
|
+
): Promise<TicketsCount | TicketsCounts> {
|
|
96
|
+
return this.client.post<TicketsCount | TicketsCounts, TicketsCountPostPayload>(
|
|
97
|
+
`${this.basePath}/count`,
|
|
98
|
+
body,
|
|
99
|
+
headers,
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Counts the open tickets of the contacts of a connection. An unknown id counts 0. */
|
|
104
|
+
countOpenByService(serviceId: string, headers?: Record<string, string>): Promise<number> {
|
|
105
|
+
return this.client.get<number>(`${this.basePath}/count-open/${serviceId}`, headers)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Counts the open tickets of a department. An unknown id counts 0. */
|
|
109
|
+
countOpenByDepartment(departmentId: string, headers?: Record<string, string>): Promise<number> {
|
|
110
|
+
return this.client.get<number>(
|
|
111
|
+
`${this.basePath}/count-open-by-department/${departmentId}`,
|
|
112
|
+
headers,
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Counts the open tickets assigned to a user. An unknown id counts 0. */
|
|
117
|
+
countOpenByUser(userId: string, headers?: Record<string, string>): Promise<number> {
|
|
118
|
+
return this.client.get<number>(`${this.basePath}/count-open-by-user/${userId}`, headers)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The first waiting time of the ticket (`metrics.waitingTime`) as `HH:mm:ss`; `'00:00:00'` while
|
|
123
|
+
* it was not measured. The hours wrap every 24 h (a wait of 25 h answers `'01:00:00'`): read
|
|
124
|
+
* `metrics.waitingTime` (seconds) for the full value. An unknown id answers 500.
|
|
125
|
+
*/
|
|
126
|
+
getFirstWaitingTime(id: string, headers?: Record<string, string>): Promise<string> {
|
|
127
|
+
return this.client.get<string>(`${this.basePath}/${id}/first-waiting-time`, headers)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Exports the conversation of a ticket as text, by its protocol (contact, attendants, subjects,
|
|
132
|
+
* summary, dates and the messages without the internal comments), sent as the attachment
|
|
133
|
+
* `<protocol>.txt` (Content-Type `plain/text`). Without a protocol the body is empty
|
|
134
|
+
* (`undefined`); an unknown protocol never answers.
|
|
135
|
+
*/
|
|
136
|
+
exportTxt(protocol: string, headers?: Record<string, string>): Promise<string> {
|
|
137
|
+
return this.client.post<string, { protocol: string }>(
|
|
138
|
+
`${this.basePath}/export`,
|
|
139
|
+
{ protocol },
|
|
140
|
+
headers,
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Exports the tickets matching the query as CSV (`;`, a header of 20 columns in the language of
|
|
146
|
+
* the user, dates in the timezone of the account), streamed without a Content-Type.
|
|
147
|
+
*
|
|
148
|
+
* The query must include `contact` with its `service` (the History screen also includes
|
|
149
|
+
* `contact.tags`, `contact.person`, `user`, `department`, `ticketTopics` and `firstMessage`),
|
|
150
|
+
* and `order` must name unambiguous columns (e.g. `startedAt`): when a row fails after the header
|
|
151
|
+
* is sent, the answer never ends. Leave `page` out: the route pages through every match itself
|
|
152
|
+
* (`perPage` only sets the size of each batch), and a `page` in the query is read on every
|
|
153
|
+
* batch, so its rows are repeated or the CSV comes with the header only.
|
|
154
|
+
*/
|
|
155
|
+
exportHistory(query: TicketsListQuery, headers?: Record<string, string>): Promise<string> {
|
|
156
|
+
return this.client.post<string>(
|
|
157
|
+
`${this.basePath}/export-history${this.toQueryString(query)}`,
|
|
158
|
+
undefined,
|
|
159
|
+
headers,
|
|
160
|
+
)
|
|
161
|
+
}
|
|
30
162
|
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import type { ListQuery } from '../../core/types'
|
|
2
|
+
import type { Account } from '../accounts/types'
|
|
1
3
|
import type { Contact } from '../contacts/types'
|
|
2
4
|
import type { Department } from '../departments/types'
|
|
3
5
|
import type { Message } from '../messages/types'
|
|
4
6
|
import type { Service } from '../services/types'
|
|
7
|
+
import type { TicketTopic } from '../ticketTopics/types'
|
|
5
8
|
import type { User } from '../users/types'
|
|
6
9
|
|
|
7
10
|
export type TicketOrigin = 'automatic' | 'manual' | 'widget'
|
|
@@ -22,6 +25,14 @@ export type TicketMetrics = {
|
|
|
22
25
|
/** Average waiting time per transfer. */
|
|
23
26
|
waitingTimeTransfersAvg?: number
|
|
24
27
|
isActiveTicket?: boolean
|
|
28
|
+
/** Longest waiting time in seconds, among the opening and the transfers. */
|
|
29
|
+
maxWaitingTime?: number
|
|
30
|
+
/** Department where the longest wait happened. */
|
|
31
|
+
maxWaitingTimeDepartmentId?: string
|
|
32
|
+
/** Name of that department when the wait was measured (the history export prefers the current name). */
|
|
33
|
+
maxWaitingTimeDepartmentName?: string
|
|
34
|
+
maxWaitingTimeStartedAt?: string
|
|
35
|
+
maxWaitingTimeEndedAt?: string
|
|
25
36
|
}
|
|
26
37
|
|
|
27
38
|
export type TicketTransferAction = 'opened' | 'transferred' | 'closed'
|
|
@@ -61,6 +72,16 @@ export type TicketTransfer = {
|
|
|
61
72
|
lastMessage?: Message | null
|
|
62
73
|
}
|
|
63
74
|
|
|
75
|
+
/**
|
|
76
|
+
* A ticket as `GET /tickets` and `GET /tickets/{id}` answer it. The relationships come only when
|
|
77
|
+
* asked in `include`, except `firstMessage` and `lastMessage`, which always come: `null` when not
|
|
78
|
+
* included (or when the ticket has no message).
|
|
79
|
+
*
|
|
80
|
+
* The `include` whitelist of the route takes `contact` (and `contact.avatar`, `.thumbAvatar`,
|
|
81
|
+
* `.lastMessage`, `.tags`, `.service`, `.person`), `department`, `user`, `account`, `ticketTopics`,
|
|
82
|
+
* `firstMessage` and `ticketTransfers` (with `fromUser`, `toUser`, `fromDepartment`,
|
|
83
|
+
* `toDepartment`, `byUser`); anything else is dropped in silence.
|
|
84
|
+
*/
|
|
64
85
|
export type Ticket = {
|
|
65
86
|
id: string
|
|
66
87
|
isOpen: boolean
|
|
@@ -70,13 +91,18 @@ export type Ticket = {
|
|
|
70
91
|
metrics: TicketMetrics
|
|
71
92
|
startedAt: string | null
|
|
72
93
|
endedAt: string | null
|
|
73
|
-
|
|
74
|
-
|
|
94
|
+
/** Only in the `ticket.*` webhooks (the stored ticket): the routes of `/tickets` never send it. */
|
|
95
|
+
isDistributing?: boolean
|
|
96
|
+
/** Only in the `ticket.*` webhooks (the stored ticket): the routes of `/tickets` never send it. */
|
|
97
|
+
distributingAt?: string | null
|
|
98
|
+
/** Only in the `ticket.*` webhooks (the stored ticket): the routes of `/tickets` never send it. */
|
|
99
|
+
count?: number | null
|
|
75
100
|
accountId: string
|
|
76
101
|
departmentId: string
|
|
77
102
|
contactId: string
|
|
78
103
|
userId: string | null
|
|
79
|
-
|
|
104
|
+
/** @deprecated Never comes: a ticket has no connection of its own. Read `contact.serviceId`. */
|
|
105
|
+
serviceId?: string | null
|
|
80
106
|
firstMessageId: string | null
|
|
81
107
|
lastMessageId: string | null
|
|
82
108
|
currentTicketTransferId: string | null
|
|
@@ -86,12 +112,28 @@ export type Ticket = {
|
|
|
86
112
|
contact?: Contact
|
|
87
113
|
department?: Department
|
|
88
114
|
user?: User | null
|
|
115
|
+
account?: Account
|
|
116
|
+
ticketTopics?: TicketTopic[]
|
|
117
|
+
firstMessage?: Message | null
|
|
118
|
+
/** @deprecated Always `null`: `lastMessage` is out of the include whitelist. Include `contact.lastMessage`. */
|
|
119
|
+
lastMessage?: Message | null
|
|
120
|
+
/** @deprecated Never comes: `service` is out of the include whitelist. Include `contact.service`. */
|
|
89
121
|
service?: Service | null
|
|
90
122
|
ticketTransfers?: TicketTransfer[]
|
|
91
123
|
}
|
|
92
124
|
|
|
93
|
-
export type TicketRelationships =
|
|
125
|
+
export type TicketRelationships =
|
|
126
|
+
| 'contact'
|
|
127
|
+
| 'department'
|
|
128
|
+
| 'user'
|
|
129
|
+
| 'account'
|
|
130
|
+
| 'ticketTopics'
|
|
131
|
+
| 'firstMessage'
|
|
132
|
+
| 'lastMessage'
|
|
133
|
+
| 'service'
|
|
134
|
+
| 'ticketTransfers'
|
|
94
135
|
|
|
136
|
+
/** @deprecated Not served: the backend has no `POST /tickets`. A ticket is opened by `ContactsApi.transferTicket`. */
|
|
95
137
|
export type CreateTicketPayload = {
|
|
96
138
|
contactId: string
|
|
97
139
|
departmentId: string
|
|
@@ -100,6 +142,7 @@ export type CreateTicketPayload = {
|
|
|
100
142
|
origin?: TicketOrigin
|
|
101
143
|
}
|
|
102
144
|
|
|
145
|
+
/** @deprecated Not served: the backend has no `PUT /tickets/{id}`. Transfer through `ContactsApi.transferTicket`, close through `TicketsApi.closeMany`. */
|
|
103
146
|
export type UpdateTicketPayload = {
|
|
104
147
|
departmentId?: string
|
|
105
148
|
userId?: string | null
|
|
@@ -107,6 +150,14 @@ export type UpdateTicketPayload = {
|
|
|
107
150
|
isOpen?: boolean
|
|
108
151
|
}
|
|
109
152
|
|
|
153
|
+
/** What `closeMany` records on every ticket it closes. */
|
|
154
|
+
export type CloseTicketsData = {
|
|
155
|
+
/** Subjects of the closing (one id or a list). */
|
|
156
|
+
ticketTopicIds?: string[] | string
|
|
157
|
+
/** Summary of the closing, stored in `comments`. */
|
|
158
|
+
comments?: string
|
|
159
|
+
}
|
|
160
|
+
|
|
110
161
|
/**
|
|
111
162
|
* Body for `PUT /tickets/close-many`.
|
|
112
163
|
*
|
|
@@ -116,5 +167,24 @@ export type UpdateTicketPayload = {
|
|
|
116
167
|
* the backend always forces `where.isOpen = true`.
|
|
117
168
|
*/
|
|
118
169
|
export type CloseManyTicketsPayload =
|
|
119
|
-
| { ids: string[]
|
|
120
|
-
| { allContactsSelected: true; query: { query: string } }
|
|
170
|
+
| ({ ids: string[] } & CloseTicketsData)
|
|
171
|
+
| ({ allContactsSelected: true; query: { query: string } } & CloseTicketsData)
|
|
172
|
+
|
|
173
|
+
/** A list query of tickets, as `count` and `exportHistory` take it. */
|
|
174
|
+
export type TicketsListQuery = ListQuery<Ticket>
|
|
175
|
+
|
|
176
|
+
/** `count` with `queries`: one count per query, in the same order. */
|
|
177
|
+
export type TicketsCountManyQuery = TicketsListQuery & { queries: TicketsListQuery[] }
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Body of `POST /tickets/count`. The filters go at the root, as in a list query, and `query` is
|
|
181
|
+
* required: without it the route answers 500. `query: {}` counts the root filters into
|
|
182
|
+
* `{ count }`; `query: { queries }` ignores them and answers `{ counts }`, one per query.
|
|
183
|
+
*/
|
|
184
|
+
export type TicketsCountPostPayload = TicketsListQuery & {
|
|
185
|
+
query: { queries?: TicketsListQuery[] }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export type TicketsCount = { count: number }
|
|
189
|
+
|
|
190
|
+
export type TicketsCounts = { counts: number[] }
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { expectAssignable, expectNotAssignable, expectType } from 'tsd'
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
WebhookPayload,
|
|
4
|
+
WebhookEvent,
|
|
5
|
+
WebhookEventPayloadMap,
|
|
6
|
+
BotCommandData,
|
|
7
|
+
TicketWebhookData,
|
|
8
|
+
} from './index'
|
|
9
|
+
import type { Contact, ContactRelationships } from '../apis/contacts'
|
|
3
10
|
import type { Message, MessageRelationships } from '../apis/messages'
|
|
4
11
|
import type { Ticket, TicketRelationships } from '../apis/tickets'
|
|
5
12
|
import type { BotApiSignalData, BotContexts } from '../apis/bots'
|
|
@@ -27,6 +34,12 @@ expectNotAssignable<WebhookPayload<'ticket.updated'>['data']>(
|
|
|
27
34
|
{} as Pick<Ticket, TicketRelationships>,
|
|
28
35
|
)
|
|
29
36
|
|
|
37
|
+
// ...except the contact, which the sender always attaches (stored contact, no relationships)
|
|
38
|
+
declare const ticketUpdated: WebhookPayload<'ticket.updated'>
|
|
39
|
+
expectType<TicketWebhookData>(ticketUpdated.data)
|
|
40
|
+
expectType<Omit<Contact, ContactRelationships>>(ticketUpdated.data.contact)
|
|
41
|
+
expectType<string>(ticketUpdated.data.contact.serviceId)
|
|
42
|
+
|
|
30
43
|
// ── event narrowing tightens data type ───────────────────────────────────────
|
|
31
44
|
|
|
32
45
|
declare const ticketPayload: WebhookPayload<'ticket.created'>
|
|
@@ -66,6 +66,15 @@ export type WebhookEvent =
|
|
|
66
66
|
|
|
67
67
|
// ─── Event → Payload map ─────────────────────────────────────────────────────
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* `data` of `ticket.created` and `ticket.updated`: the stored ticket (so `isDistributing`,
|
|
71
|
+
* `distributingAt` and `count` come, and `serviceId` does not) plus its stored contact, which the
|
|
72
|
+
* sender always attaches. Read the connection of the ticket in `contact.serviceId`.
|
|
73
|
+
*/
|
|
74
|
+
export type TicketWebhookData = Omit<Ticket, TicketRelationships> & {
|
|
75
|
+
contact: Omit<Contact, ContactRelationships>
|
|
76
|
+
}
|
|
77
|
+
|
|
69
78
|
export type BotCommandData = {
|
|
70
79
|
id: string
|
|
71
80
|
contactId: string
|
|
@@ -77,8 +86,8 @@ export type BotCommandData = {
|
|
|
77
86
|
export interface WebhookEventPayloadMap {
|
|
78
87
|
'message.created': Omit<Message, MessageRelationships>
|
|
79
88
|
'message.updated': Omit<Message, MessageRelationships>
|
|
80
|
-
'ticket.created':
|
|
81
|
-
'ticket.updated':
|
|
89
|
+
'ticket.created': TicketWebhookData
|
|
90
|
+
'ticket.updated': TicketWebhookData
|
|
82
91
|
'service.created': Omit<Service, ServiceRelationships>
|
|
83
92
|
'service.updated': Omit<Service, ServiceRelationships>
|
|
84
93
|
'service.destroyed': Omit<Service, ServiceRelationships>
|